Skip to content
This repository was archived by the owner on Apr 3, 2025. It is now read-only.
This repository was archived by the owner on Apr 3, 2025. It is now read-only.

Just another suggestion (with Symbol) #66

Open
@MAZ01001

Description

@MAZ01001

I think of a possible implementation like Symbol.toPrimitive, similar to #65 but slightly different...

Click to collapse
/**
 * possible new JSDoc for classes: <at>operator [RESULT_TYPE] [OTHER_TYPE] OPERATOR [DESCRIPTION]
 * ~ RESULT_TYPE takes priority over OTHER_TYPE if only one of the two is specified; both are optional
 *
 * @operator {boolean}          == → returns a boolean for `self==other` (other can be anything)
 * @operator {number}           +U → returns a number  for `+self`
 * @operator {number}  {number} *  → returns a number  for `self*other` when other is a number
 * @operator {number}  {number} /  → returns a number  for `self/other` when other is a number
 */
class Example(){
    /** @type {unknown} example property */test;

    /**
     * overrides all operator behaviour with this object (called just before `Symbol.toPrimitive`/type coercion)
     * @param {string} operator - JS operator
     * @param {unknown} other - other object, `undefined` is used for unary operators
     */
    [Symbol.oOverload](operator, other){
        switch(operator){
            // numerics
            case "+U": return +this.test;       // use `Symbol.toPrimitive` with hint number if not implemented here
            case "-U": return -this.test;       // use `Symbol.invert` if not implemented here
            case "+": return this.test+other;   // use `Symbol.oAdd` if not implemented here
            case "-": return this.test-other;   // use `Symbol.oSub` if not implemented here
            case "*": return this.test*other;   // use `Symbol.oMul` if not implemented here
            case "/": return this.test/other;   // use `Symbol.oDiv` if not implemented here
            case "**": return this.test**other; // use `Symbol.oPow` if not implemented here
            case "%": return this.test%other;   // use `Symbol.oMod` if not implemented here
            case "++": return ++this.test;      // use `Symbol.increment` if not implemented here
            case "--": return --this.test;      // use `Symbol.decrement` if not implemented here
            // bitwise
            case "~": return ~this.test;          // use `Symbol.binNOT` if not implemented here
            case "&": return this.test&other;     // use `Symbol.binAND` if not implemented here
            case "|": return this.test|other;     // use `Symbol.binOR` if not implemented here
            case "^": return this.test^other;     // use `Symbol.binXOR` if not implemented here
            case "<<": return this.test<<other;   // use `Symbol.binLShift` if not implemented here
            case ">>": return this.test>>other;   // use `Symbol.binRShift` if not implemented here
            case ">>>": return this.test>>>other; // use `Symbol.binURShift` if not implemented here
            // comparison
            case "==": return this.test==other; // use `Symbol.comparison` if not implemented here
            case "<": return this.test<other;   // use `Symbol.comparison` if not implemented here
            case ">": return this.test>other;   // use `Symbol.comparison` if not implemented here
            case "<=": return this.test<=other; // use boolean inverse of `>` if not implemented here
            case ">=": return this.test>=other; // use boolean inverse of `<` if not implemented here
            // assign
            case "=": return this.test=other; // use `Symbol.assign` if not implemented here
            // assign numerics
            case "+=": return this.test+=other;   // use `+`  then `=` if not implemented here
            case "-=": return this.test-=other;   // use `-`  then `=` if not implemented here
            case "*=": return this.test*=other;   // use `*`  then `=` if not implemented here
            case "/=": return this.test/=other;   // use `/`  then `=` if not implemented here
            case "**=": return this.test**=other; // use `**` then `=` if not implemented here
            case "%=": return this.test%=other;   // use `%`  then `=` if not implemented here
            // assign bitwise
            case "&=": return this.test&=other;     // use `&`   then `=` if not implemented here
            case "|=": return this.test|=other;     // use `|`   then `=` if not implemented here
            case "^=": return this.test^=other;     // use `^`   then `=` if not implemented here
            case "<<=": return this.test<<=other;   // use `<<`  then `=` if not implemented here
            case ">>=": return this.test>>=other;   // use `>>`  then `=` if not implemented here
            case ">>>=": return this.test>>>=other; // use `>>>` then `=` if not implemented here
        }
        return Symbol.oOverloadAbort; // override not implemented ~ continue with `Symbol.toPrimitive`/type coercion
        // ! without explicit return, `undefined` is used as if it were intentionally returned
    }

    // more specific overrides can be used if only some functionality needs to be custom-tailored

    // --- bitwise ---

    /**
     * used if `~` operator is not overridden by `Symbol.oOverload`
     * @returns {unknown}
     */
    [Symbol.binNOT](){ return ~this.test; }

    /**
     * used if `&` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.binAND](other){ return this.test&other; }

    /**
     * used if `|` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.binOR](other){ return this.test|other; }

    /**
     * used if `^` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.binXOR](other){ return this.test^other; }

    /**
     * used if `<<` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.binLShift](other){ return this.test<<other; }

    /**
     * used if `>>` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.binRShift](other){ return this.test>>other; }

    /**
     * used if `>>>` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.binURShift](other){ return this.test>>>other; }

    // --- numeric ---

    /**
     * used if (unary) `-` operator is not overridden by `Symbol.oOverload`
     * @returns {unknown}
     */
    [Symbol.invert](){ return -this.test; }

    /**
     * used if `+` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.oAdd](other){ return this.test+other; }

    /**
     * used if `-` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.oSub](other){ return this.test-other; }

    /**
     * used if `*` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.oMul](other){ return this.test*other; }

    /**
     * used if `/` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.oDiv](other){ return this.test/other; }

    /**
     * used if `**` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.oPow](other){ return this.test**other; }

    /**
     * used if `%` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} other
     * @returns {unknown}
     */
    [Symbol.oMod](other){ return this.test%other; }

    /**
     * used if `++` operator is not overridden by `Symbol.oOverload`
     * @returns {unknown}
     */
    [Symbol.increment](){ return ++this.test; }

    /**
     * used if `--` operator is not overridden by `Symbol.oOverload`
     * @returns {unknown}
     */
    [Symbol.decrement](){ return --this.test; }

    // --- other ---

    /**
     * used if `=` operator is not overridden by `Symbol.oOverload`
     * @param {unknown} value
     * @returns {unknown}
     */
    [Symbol.assign](value){ return this.test = value; }

    /**
     * used for internal comparisons like {@linkcode Array.sort} and fallback if comparison operators are not overridden in `Symbol.oOverload`
     * @param {unknown} other
     * @returns {-1|0|1} if {@linkcode other} is larger: `-1`, is smaller: `1`, is equal: `0`
     */
    [Symbol.comparison](other){
        if(this.test===other)return 0;
        if(this.test<other)return -1;
        return 1;
    }

    /**
     * used for coercion to boolean (before `Symbol.toPrimitive`)
     * @returns {boolean} if this obj is truthy or not
     */
    [Symbol.toBoolean](){ return Boolean(this.test); }

    /**
     * used for numeric property access/assign (after testing if that property exists on this object and failing)
     * @param {number|bigint} index - indexed property
     * @param {boolean} assign - `true` when assigning to that property
     * @param {unknown} other - value for assignment
     * @returns {unknown}
     */
    [Symbol.indexAccess](index, assign, other){
        if(assign)return this.test[index] = other;
        return this.test[index];
    }
}

The unary + and - operators are specified as +U and -U, so no other parameter is needed to distinguish them from + and - in Symbol.oOverload and JSDoc.


So, for any A # B where A is an object and # is an operator, do:

  1. if A is an Object
    1. if A[Symbol.oOverload] is a function that exists in A
      1. set tmp to A[Symbol.oOverload]("#", B)
      2. if tmp is not Symbol.oOverloadAbort
        • use tmp

    2. if a more specific method like Symbol.comparison exists in A
      • use the result of that specific method

    3. if changing operator is possible (like += to + and =)
      • go back to step 1.1 with new operator/s

  2. continue with Symbol.toPrimitve/type coercion

And, for any # A where # is an operato,r do:

  1. if A is an Object
    1. if A[Symbol.oOverload] is a function that exists in A
      1. set tmp to A[Symbol.oOverload]("#", undefined)
      2. if tmp is not Symbol.oOverloadAbort
        • use tmp

    2. if a more specific method like Symbol.increment exists in A
      • use the result of that specific method

    3. if changing operator is possible (like ++ to += with 1)
      • go back to step 1.1 with new operator/s

  2. continue with Symbol.toPrimitve/type coercion

Also, A++ is the same as ++A but evaluated after the expression A is in (if any), same for A--.
So, for example, A++*B => A*B,++A (exec in that order, but this expression itself returns the result of A*B not ++A).


IMO, instead of making boolean operators overrideable, a Symbol.toBoolean method that evaluates the (current) truthiness of the object would be better.
And the boolean assignments &&= and ||= would use Symbol.toBoolean and assign (via override from Symbol.oOverload or Symbol.assign) accordingly.

Also, the Symbol.comparison can be used internally without needing to give Array.sort a compare function.

I agree that strict equals and the nullish coalescing operators/assignment should not be overrideable.
Along with the others listed like , and ().


The main benefit is that it does not break older code, can be easily type-checked (especially when using JSDoc), and should not be too hard to implement.
It also does not add more syntax to the language, except the suggested JSDoc tag.

PS: I don't have deep knowledge of JS engines, so I don't truly know how hard it would be to add support for this :P

PPS: Of course, the symbol names can be changed; after all, naming things is hard ^^

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions