One approach could be to translate code from the inline operator syntax to method syntax. For this you could use a function that runs once on the functions that you want to have this behaviour.
For the case in your question, you would run it as follows:
const main = Overload.enable(function () {
var x = new Vector2(10, 10);
var y = new Vector2(10, 10);
x += y;
console.log(x);
});
main();
Here is how Overload could be defined -- it would be a library to include in your project:
const Overload = {
// Default for how operators should work:
binaryOperators: {
"+"(a, b) { return a + b },
"-"(a, b) { return a - b },
"*"(a, b) { return a * b },
"/"(a, b) { return a / b },
"%"(a, b) { return a % b },
},
unaryOperators: {
"-"(a) { return -a },
},
// Functions that perform operations, being aware of potential overloads
binary(operator, left, right) {
return Object(left)[operator] ? Object(left)[operator](right)
: Object(right)[operator] ? Object(right)[operator](left, true)
: operators[operator](left, right);
},
unary(operator, operand) {
return Object(operand)[operator] ? Object(operand)[operator]()
: operators[operator](operand);
},
// Create a new version of a given function
// replacing the operators in its source code with calls of the above functions.
// This has a dependency on the esprima parser
enable(f) {
const code = "(" + f + ")";
const splices = [];
esprima.parseScript(code, {range:true}, function(node) {
if (node.type === "BinaryExpression") {
splices.push([node.range[0], 0, `Overload.binary("${node.operator}",`]);
splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, ","]);
} else if (node.type === "AssignmentExpression") {
splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length,
`= Overload.binary("${node.operator.slice(0, -1)}",${code.slice(...node.left.range)},`]);
} else if (node.type === "UnaryExpression") {
splices.push([node.range[0], node.operator.length, `Overload.unary("${node.operator}",`]);
} else return;
splices.push([node.range[1], 0, ")"]);
});
// Apply all collected code changes:
const arr = [...code];
for (const [a, b, c] of splices.sort(([a, c], [b, d]) => b - a || d - c)) {
arr.splice(a, b, c);
}
// Perform an indirect(!!) eval, so that it runs in the global scope (not local)
// If the input function is a named function, the name is reused for the new function
return eval?.(arr.join(""));
}
};
Here is all of that combined:
const Overload = {binaryOperators: {"+"(a, b) { return a + b },"-"(a, b) { return a - b },"*"(a, b) { return a * b },"/"(a, b) { return a / b },"%"(a, b) { return a % b },},unaryOperators: {"-"(a) { return -a }},binary(operator, left, right) {return Object(left)[operator] ? Object(left)[operator](right): Object(right)[operator] ? Object(right)[operator](left, true): operators[operator](left, right);},unary(operator, operand) {return Object(operand)[operator] ? Object(operand)[operator]() : operators[operator](operand);},enable(f) {const code = "(" + f + ")";const splices = [];esprima.parseScript(code, {range:true}, function(node) {if (node.type === "BinaryExpression") {splices.push([node.range[0], 0, `Overload.binary("${node.operator}",`]);splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, ","]);} else if (node.type === "AssignmentExpression") {splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, `= Overload.binary("${node.operator.slice(0, -1)}",${code.slice(...node.left.range)},`]);} else if (node.type === "UnaryExpression") {splices.push([node.range[0], node.operator.length, `Overload.unary("${node.operator}",`]);} else return;splices.push([node.range[1], 0, ")"]);});const arr = [...code];for (const [a, b, c] of splices.sort(([a, c], [b, d]) => b - a || d - c)) {arr.splice(a, b, c);}return eval?.(arr.join(""));}};
// Demo
class Vector2 extends Array {
constructor(x, y) {
super(x, y);
}
// The following prototype method intends to overload operator functionality:
["+"](other) {
return new Vector2(this[0] + other[0], this[1] + other[1]);
}
}
const example = Overload.enable(function () {
var x = new Vector2(10,10);
var y = new Vector2(10,10);
x += y;
console.log(x);
});
example();
<script src="https://unpkg.com/esprima@~4.0/dist/esprima.js"></script>
Here is an example with an implementation of complex numbers:
const Overload = {binaryOperators: {"+"(a, b) { return a + b },"-"(a, b) { return a - b },"*"(a, b) { return a * b },"/"(a, b) { return a / b },"%"(a, b) { return a % b },},unaryOperators: {"-"(a) { return -a }},binary(operator, left, right) {return Object(left)[operator] ? Object(left)[operator](right): Object(right)[operator] ? Object(right)[operator](left, true): operators[operator](left, right);},unary(operator, operand) {return Object(operand)[operator] ? Object(operand)[operator]() : operators[operator](operand);},enable(f) {const code = "(" + f + ")";const splices = [];esprima.parseScript(code, {range:true}, function(node) {if (node.type === "BinaryExpression") {splices.push([node.range[0], 0, `Overload.binary("${node.operator}",`]);splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, ","]);} else if (node.type === "AssignmentExpression") {splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, `= Overload.binary("${node.operator.slice(0, -1)}",${code.slice(...node.left.range)},`]);} else if (node.type === "UnaryExpression") {splices.push([node.range[0], node.operator.length, `Overload.unary("${node.operator}",`]);} else return;splices.push([node.range[1], 0, ")"]);});const arr = [...code];for (const [a, b, c] of splices.sort(([a, c], [b, d]) => b - a || d - c)) {arr.splice(a, b, c);}return eval?.(arr.join(""));}};
class Complex {
constructor(real, img) {
this.real = real;
this.img = img;
}
toString() {
if (!this.img) return `${this.real}`;
if (!this.real) return `${this.img}i`;
return `${this.real}${this.img < 0 ? "" : "+"}${this.img}i`;
}
static cast(other) {
if (other instanceof Complex || other == null) return other; // Pass-thru
if (Object(other) !== other) return new Complex(Number(other), 0); // Primitive
if (other.length === 2) return new Complex(other[0], other[1]); // Array-like
throw "Cannot cast this to Complex";
}
// The following prototype methods will override operator functionality:
["+"](other) {
other = Complex.cast(other);
return new Complex(this.real + other.real, this.img + other.img);
}
["-"](other, reversedOperands=false) {
// Unary minus?
if (arguments.length === 0) return new Complex(-this.real, -this.img);
// Binary minus:
other = Complex.cast(other);
return reversedOperands ? this["-"]()["+"](other) : other["-"]()["+"](this);
}
["*"](other) {
other = Complex.cast(other);
return new Complex(this.real * other.real - this.img * other.img, this.real * other.img + this.img * other.real);
}
["/"](other, reversedOperands=false) {
other = Complex.cast(other);
if (reversedOperands) return other["/"](this);
const denom = other.real * other.real + other.img * other.img;
return new Complex((this.real * other.real + this.img * other.img) / denom,
(this.img * other.real - this.real * other.img) / denom);
}
}
// Decorate the function(s) that need operator overrides:
const example = Overload.enable(function (a, b) {
return 8 * (a - b) / (a + b);
});
const result = example(new Complex(10, 6), new Complex(4, 8))
console.log("result: " + result);
<script src="https://unpkg.com/esprima@~4.0/dist/esprima.js"></script>
Remarks:
This approach has no limitation on the number of operations that are performed in one expression.
For assignment operators, such as +=, this solution silently assumes that there are no side-effects being performed in the left-hand side of the assignment. If this is the case, the converted code would execute that side effect twice, like in this example:
a[i++] += a[j];
This would be executed as:
a[i++] = Overload.binary("+", a[i++], a[j]);
...and so you see i gets incremented twice. To avoid this, make sure that the left-hand side has no side effects. If the accessed property of a is not a getter, then instead do:
a[i] += a[j];
i++;