@leawind/rop
Version:
Evaluate expression with operator overloading and Python-style array slicing using tagged template literal.
1,007 lines (992 loc) • 32.3 kB
JavaScript
// src/utils/TokenWalker.ts
class TokenWalker {
tokens;
position = 0;
constructor(tokens) {
this.tokens = tokens;
}
getSource() {
return this.tokens;
}
isFinished() {
return this.position >= this.tokens.length;
}
getCurrentPosition() {
return this.position;
}
hasRemaining() {
return this.position < this.tokens.length;
}
getRemaining() {
return this.tokens.slice(this.position);
}
peek(offset = 0) {
const index = this.position + offset;
if (index >= 0 && index < this.tokens.length) {
return this.tokens[index] ?? null;
}
return null;
}
next(count) {
if (count === undefined) {
if (this.position < this.tokens.length) {
return this.tokens[this.position++] ?? null;
}
return null;
} else {
const result = [];
for (let i = 0;i < count; i++) {
if (this.position < this.tokens.length) {
const token = this.tokens[this.position++];
if (token !== undefined) {
result.push(token);
}
} else {
break;
}
}
return result.length > 0 ? result : null;
}
}
consume(count = 1) {
this.position = Math.min(this.position + count, this.tokens.length);
}
skip(count = 1) {
this.position = Math.min(this.position + count, this.tokens.length);
}
}
// src/compiler/Operators.ts
var UNARY_OPERATOR_NAMES = ["!", "~", "-x", "+x"];
var BINARY_OPERATOR_NAMES = [
"+",
"-",
"*",
"/",
"%",
"**",
"&",
"|",
"^",
"<<",
">>",
">>>",
"&&",
"||",
"==",
"===",
"!=",
"!==",
"<",
">",
"<=",
">="
];
var OPERATOR_NAMES = ["[i]", "[:]", ...UNARY_OPERATOR_NAMES, ...BINARY_OPERATOR_NAMES];
var OPERATION_REGISTRY = ((obj) => {
for (const [name, meta] of Object.entries(obj)) {
meta.name = name;
meta.symbol = Symbol(name);
obj[meta.symbol] = meta;
}
return obj;
})({
"[i]": { type: "other" },
"[:]": { type: "other" },
"!": { type: "unary", literal: "!", native: (self) => !self, precedence: 10 },
"~": { type: "unary", literal: "~", native: (self) => ~self, precedence: 10 },
"-x": { type: "unary", literal: "-", native: (self) => -self, precedence: 10 },
"+x": { type: "unary", literal: "+", native: (self) => +self, precedence: 10 },
"||": { type: "binary", literal: "||", precedence: 1, native: (self, other) => self || other },
"&&": { type: "binary", literal: "&&", precedence: 2, native: (self, other) => self && other },
"|": { type: "binary", literal: "|", precedence: 3, native: (self, other) => self | other },
"^": { type: "binary", literal: "^", precedence: 4, native: (self, other) => self ^ other },
"&": { type: "binary", literal: "&", precedence: 5, native: (self, other) => self & other },
"==": { type: "binary", literal: "==", precedence: 6, native: (self, other) => self == other },
"===": { type: "binary", literal: "===", precedence: 6, native: (self, other) => self === other },
"!=": { type: "binary", literal: "!=", precedence: 6, native: (self, other) => self != other },
"!==": { type: "binary", literal: "!==", precedence: 6, native: (self, other) => self !== other },
"<": { type: "binary", literal: "<", precedence: 7, native: (self, other) => self < other },
">": { type: "binary", literal: ">", precedence: 7, native: (self, other) => self > other },
"<=": { type: "binary", literal: "<=", precedence: 7, native: (self, other) => self <= other },
">=": { type: "binary", literal: ">=", precedence: 7, native: (self, other) => self >= other },
"<<": { type: "binary", literal: "<<", precedence: 8, native: (self, other) => self << other },
">>": { type: "binary", literal: ">>", precedence: 8, native: (self, other) => self >> other },
">>>": { type: "binary", literal: ">>>", precedence: 8, native: (self, other) => self >>> other },
"+": { type: "binary", literal: "+", precedence: 9, native: (self, other) => self + other },
"-": { type: "binary", literal: "-", precedence: 9, native: (self, other) => self - other },
"*": { type: "binary", literal: "*", precedence: 10, native: (self, other) => self * other },
"/": { type: "binary", literal: "/", precedence: 10, native: (self, other) => self / other },
"%": { type: "binary", literal: "%", precedence: 10, native: (self, other) => self % other },
"**": { type: "binary", literal: "**", precedence: 11, native: (self, other) => self ** other }
});
var OPERATOR_LITERAL_TO_NAME_MAP = (() => {
const map = new Map;
for (const [name, meta] of Object.entries(OPERATION_REGISTRY)) {
switch (meta.type) {
case "unary":
case "binary": {
const literal = meta.literal;
if (!map.has(meta.literal)) {
map.set(literal, {});
}
map.get(literal)[meta.type] = name;
}
}
}
return map;
})();
class Operations {
constructor() {}
static isKnownOperation(op) {
return op in OPERATION_REGISTRY;
}
static unaryFromLiteral(literal) {
return OPERATOR_LITERAL_TO_NAME_MAP.get(literal)?.unary || null;
}
static binaryFromLiteral(literal) {
return OPERATOR_LITERAL_TO_NAME_MAP.get(literal)?.binary || null;
}
static meta(op) {
return OPERATION_REGISTRY[op] ?? null;
}
static symbol(op) {
return OPERATION_REGISTRY[op]?.symbol ?? null;
}
}
// src/compiler/ast-parser/AstParser.ts
class AstParser extends TokenWalker {
constructor(tokens) {
super(tokens);
}
parse() {
this.skipWhitespace();
if (this.isFinished()) {
throw new Error("Empty expression");
}
const result = this.parseExpression();
this.skipWhitespace();
if (!this.isFinished()) {
const remaining = this.getRemaining();
const t = `[
` + remaining.map((x) => "\t" + JSON.stringify(x)).join(`,
`) + `
]`;
throw new Error(`Unexpected token at end of expression:
${t}`);
}
return result;
}
skipWhitespace() {
while (this.peek()?.type === "Whitespace" /* Whitespace */) {
this.skip();
}
}
parseExpression(precedence = 0) {
let left = this.parseAtom();
loop_parse_exp:
while (true) {
this.skipWhitespace();
const token = this.peek();
if (token === null) {
break loop_parse_exp;
}
branch_token_type:
switch (token.type) {
case "Operator" /* Operator */: {
const operator = Operations.binaryFromLiteral(token.literal);
if (operator === null) {
throw new Error(`Unexpected token: ${token}, binary operator expected`);
}
const meta = Operations.meta(operator);
if (meta.type !== "binary") {
throw new Error(`Never!`);
}
if (meta.precedence < precedence) {
break loop_parse_exp;
}
this.consume();
const isRightAssociative = operator === "**";
const right = this.parseExpression(isRightAssociative ? meta.precedence : meta.precedence + 1);
left = { type: "Binary" /* Binary */, left, operation: operator, right };
break branch_token_type;
}
case "Punctuation" /* Punctuation */: {
switch (token.literal) {
case ".": {
this.consume();
this.skipWhitespace();
const prop = this.peek();
if (prop !== null && prop.type === "Identifier" /* Identifier */) {
left = { type: "AccessProperty" /* AccessProperty */, left, name: prop.literal };
this.consume();
break branch_token_type;
}
throw new Error("Expected identifier after dot");
}
case "(": {
this.consume();
const args = [];
while (true) {
this.skipWhitespace();
if (this.tryConsumePunctuation(")")) {
break;
}
const arg = this.parseExpression();
args.push(arg);
this.skipWhitespace();
this.tryConsumePunctuation(",");
}
left = { type: "Invoke" /* Invoke */, target: left, args };
break branch_token_type;
}
case "[": {
this.consume();
this.skipWhitespace();
if (this.peekPunctuation("]")) {
this.consume();
throw new Error("Empty subscript is not allowed");
}
const slices = [];
let isSlicing = false;
let elementCount = 0;
while (true) {
this.skipWhitespace();
const slice = (() => {
const willBeExpression = () => {
const p = this.peek();
return !(p !== null && p.type === "Punctuation" /* Punctuation */ && (p.literal === "]" || p.literal === "," || p.literal === ":"));
};
const tryParseExpressionInSlice = () => {
return willBeExpression() ? this.parseExpression() : null;
};
const slice2 = [];
let colons = 0;
let hasExpression = false;
while (true) {
if (this.peekPunctuation(",") || this.peekPunctuation("]")) {
break;
}
if (this.peekPunctuation(":")) {
this.consume();
colons++;
isSlicing = true;
} else {
const s = tryParseExpressionInSlice();
if (s !== null) {
slice2[colons] = s;
hasExpression = true;
}
}
}
if (hasExpression || colons > 0) {
elementCount++;
return { start: slice2[0], end: slice2[1], step: slice2[2] };
}
return null;
})();
if (slice !== null) {
slices.push(slice);
}
this.skipWhitespace();
if (this.tryConsumePunctuation(",")) {
isSlicing = true;
} else if (this.tryConsumePunctuation("]")) {
break;
}
}
if (elementCount === 0) {
throw new Error("Empty indexing or slicing expression");
}
if (isSlicing || slices.length > 1) {
left = { type: "Slicing" /* Slicing */, target: left, slices };
} else if (slices.length === 1) {
const slice = slices[0];
if (slice.start && slice.end === undefined && slice.step === undefined) {
left = { type: "Indexing" /* Indexing */, target: left, index: slice.start };
} else {
left = { type: "Slicing" /* Slicing */, target: left, slices };
}
}
break branch_token_type;
}
default:
break loop_parse_exp;
}
}
default:
break loop_parse_exp;
}
}
return left;
}
parseAtom() {
this.skipWhitespace();
const token = this.next();
if (token === null) {
throw new Error("Unexpected end of expression");
}
switch (token.type) {
case "Interpolation" /* Interpolation */:
case "Constant" /* Constant */:
return { type: "Value" /* Value */, token };
case "Identifier" /* Identifier */:
return { type: "Identifier" /* Identifier */, name: token.literal };
case "Operator" /* Operator */: {
const unaryOperatorName = Operations.unaryFromLiteral(token.literal);
if (!unaryOperatorName) {
throw new Error(`Unexpected operator: '${token.literal}'`);
}
const meta = Operations.meta(unaryOperatorName);
if (meta.type !== "unary") {
throw new Error(`Never!`);
}
return {
type: "Unary" /* Unary */,
operation: unaryOperatorName,
operand: this.parseExpression(meta.precedence)
};
}
case "Punctuation" /* Punctuation */:
if (token.literal === "(") {
const expr = this.parseExpression();
this.skipWhitespace();
if (this.tryConsumePunctuation(")") === null) {
throw new Error("Expected closing parenthesis");
}
return expr;
}
throw new Error(`Unexpected punctuation: ${token.literal}`);
case "Whitespace" /* Whitespace */:
throw new Error(`Unexpected whitespace token: '${token.literal}'`);
default:
throw new Error(`Unknown token type: ${token}`);
}
}
peekPunctuation(literal) {
const p = this.peek();
return p !== null && p.type === "Punctuation" /* Punctuation */ && (literal === undefined || p.literal === literal) ? p : null;
}
tryConsumePunctuation(punctuation) {
const p = this.peek();
if (p !== null && p.type === "Punctuation" /* Punctuation */ && p.literal === punctuation) {
return this.next();
} else {
return null;
}
}
}
// src/compiler/evaluater/Evaluater.ts
class Evaluater {
ast;
rop;
constructor(ast, rop = Rop.INST) {
this.ast = ast;
this.rop = rop;
}
evaluate() {
return this.evaluateNode(this.ast);
}
evaluateNode(node) {
switch (node.type) {
case "Value" /* Value */:
return this.evaluateValueNode(node);
case "Identifier" /* Identifier */:
return this.evaluateIdentifierNode(node);
case "Unary" /* Unary */:
return this.evaluateUnaryNode(node);
case "Binary" /* Binary */:
return this.evaluateBinaryNode(node);
case "AccessProperty" /* AccessProperty */:
return this.evaluateAccessPropertyNode(node);
case "Indexing" /* Indexing */:
return this.evaluateIndexingNode(node);
case "Slicing" /* Slicing */:
return this.evaluateSlicingNode(node);
case "Invoke" /* Invoke */:
return this.evaluateInvokeNode(node);
default:
throw new Error(`Unknown node type: ${node.type}`);
}
}
evaluateValueNode(node) {
const token = node.token;
if (token.type === "Interpolation" /* Interpolation */) {
return token.value;
} else if (token.type === "Constant" /* Constant */) {
return token.value;
}
throw new Error(`Unknown value token type: ${token.type}`);
}
evaluateIdentifierNode(node) {
const bindings = this.rop.bindings;
if (!bindings.has(node.name)) {
throw new Error(`Unknown identifier: ${node.name}`);
}
return bindings.get(node.name);
}
evaluateUnaryNode(node) {
const operandValue = this.evaluateNode(node.operand);
const meta = Operations.meta(node.operation);
if (meta.type !== "unary") {
throw new Error(`Invalid node: ${node}`);
}
const overload = this.rop.getOverloadOnInstance(operandValue, meta.symbol);
if (typeof overload === "function") {
return overload.call(operandValue);
}
return meta.native(operandValue, undefined);
}
evaluateBinaryNode(node) {
const leftValue = this.evaluateNode(node.left);
const rightValue = this.evaluateNode(node.right);
const meta = Operations.meta(node.operation);
if (meta.type !== "binary") {
throw new Error(`Invalid node: ${node}`);
}
const leftOverload = this.rop.getOverloadOnInstance(leftValue, meta.symbol);
if (typeof leftOverload === "function") {
return leftOverload.call(leftValue, rightValue);
}
const rightOverload = this.rop.getOverloadOnInstance(rightValue, meta.symbol);
if (typeof rightOverload === "function") {
return rightOverload.call(rightValue, leftValue);
}
return meta.native(leftValue, rightValue);
}
evaluateAccessPropertyNode(node) {
const leftValue = this.evaluateNode(node.left);
return leftValue[node.name];
}
evaluateInvokeNode(node) {
const target = this.evaluateNode(node.target);
const args = node.args.map((arg) => this.evaluateNode(arg));
if (typeof target !== "function") {
throw new Error(`Cannot invoke non-function: ${typeof target}`);
}
return target(...args);
}
evaluateIndexingNode(node) {
const target = this.evaluateNode(node.target);
const fn = this.rop.getOverloadOnInstance(target, Operations.symbol("[i]"));
if (typeof fn === "function") {
return fn.call(target, this.evaluateNode(node.index));
} else {
return target[this.evaluateNode(node.index)];
}
}
evaluateSlicingNode(node) {
const target = this.evaluateNode(node.target);
const fn = this.rop.getOverloadOnInstance(target, Operations.symbol("[:]"));
if (typeof fn === "function") {
return fn.call(target, node.slices.map((ns) => this.calculateSlice(ns)));
} else {
if (node.slices.length !== 1) {
throw new Error("Target does not support slicing");
}
const slice = node.slices[0];
if (slice.end !== undefined || slice.step !== undefined) {
throw new Error("Target does not support slicing with end or step");
}
return target[this.calculateSlice(slice).start];
}
}
calculateSlice(ns) {
return {
start: ns.start ? this.evaluateNode(ns.start) : undefined,
end: ns.end ? this.evaluateNode(ns.end) : undefined,
step: ns.step ? this.evaluateNode(ns.step) : undefined
};
}
}
// src/error.ts
class RopNeverError extends Error {
constructor(message) {
super(`RopNeverError: ${message}
This error is caused by a bug in Rop!`);
}
}
class CodeContext {
source;
begin;
end;
lines;
constructor(source, begin, end = begin + 1) {
this.source = source;
this.begin = begin;
this.end = end;
if (begin < 0) {
throw new RopNeverError("begin < 0");
}
if (end <= begin) {
throw new RopNeverError("end <= begin");
}
if (end > source.length) {
throw new RopNeverError("end > source.length");
}
this.lines = source.split(`
`).reduce((lines, content, row) => {
lines.push({
content,
row,
offset: lines.reduce((ofs, li) => ofs + li.content.length + 1, 0)
});
return lines;
}, []);
}
toIndex(row, col) {
let index = 0;
for (let i = 0;i < row - 1; i++) {
index += this.lines[i].content.length + 1;
}
return index + col;
}
toRowCol(index) {
index = Math.min(Math.max(index, 0), this.source.length - 1);
let row = 0;
let col = 0;
for (let i = 0;i < index; i++) {
if (this.source[i] === `
`) {
row++;
col = 0;
} else {
col++;
}
}
return [row, col];
}
render(message = "", previousLineCount = 2) {
const [beginRow, beginCol] = this.toRowCol(this.begin);
const [endRow, endCol] = this.toRowCol(this.end);
const lineNumberWidth = endRow.toString().length;
const renderedLines = this.lines.slice(beginRow - previousLineCount, endRow + 1);
let result = "\x1B[0m";
for (const line of renderedLines) {
result += `\x1B[1m`;
const lineNumberStr = `${(1 + line.row).toString().padStart(lineNumberWidth, " ")} | `;
result += lineNumberStr;
result += `\x1B[0m`;
result += line.content;
result += `
`;
if (beginRow <= line.row && line.row <= endRow) {
result += " ".repeat(lineNumberStr.length);
const left = line.row === beginRow ? beginCol : 0;
const right = line.row === endRow ? endCol : line.content.length;
result += line.content.substring(0, left).replace(/[^\t]/g, " ");
result += "\x1B[31m\x1B[1m";
result += line.content.substring(left, right).replace(/[^\t]/g, "^");
result += "\x1B[0m";
result += `
`;
}
}
if (message) {
result += `\x1B[31m${message}\x1B[0m
`;
}
return result;
}
}
class RopSyntaxError extends Error {
context;
constructor(context, reason) {
super(`\x1B[91mROP Syntax Error:
${context.render(reason)}`);
this.context = context;
}
}
class TokenizingError extends RopSyntaxError {
constructor(context, reason) {
super(context, reason);
}
}
// src/utils/StringWalker.ts
class StringWalker {
source;
position = 0;
constructor(source) {
this.source = source;
}
getSource() {
return this.source;
}
isFinished() {
return this.position >= this.source.length;
}
hasRemaining() {
return this.position < this.source.length;
}
getRemaining() {
return this.source.substring(this.position);
}
peek(by) {
switch (typeof by) {
case "number": {
return this.source.substring(this.position, this.position + by);
}
case "string": {
return this.peek(by.length) === by ? by : null;
}
case "object": {
return by instanceof RegExp ? this.getRemaining().match(by) : null;
}
}
}
next(by) {
const result = this.peek(by);
if (typeof result === "string") {
this.position += result.length;
return result;
}
if (Array.isArray(result)) {
this.position += result[0].length;
return result;
}
return null;
}
consume(len) {
this.position = Math.min(this.position + len, this.source.length);
}
}
// src/compiler/tokenizer/TokenFactory.ts
class TokenFactory {
constructor() {}
static whitespace(literal) {
return { type: "Whitespace" /* Whitespace */, literal };
}
static operator(literal) {
return { type: "Operator" /* Operator */, literal };
}
static interpolation(value) {
return { type: "Interpolation" /* Interpolation */, literal: "${}", value };
}
static constant(literal, value) {
return { type: "Constant" /* Constant */, literal, value };
}
static punctuation(literal) {
return { type: "Punctuation" /* Punctuation */, literal };
}
static identifier(literal) {
return { type: "Identifier" /* Identifier */, literal };
}
}
// src/compiler/tokenizer/Tokenizer.ts
class Tokenizer extends StringWalker {
ignoreWhitespace;
constructor(input, ignoreWhitespace = true) {
super(input);
this.ignoreWhitespace = ignoreWhitespace;
}
tokenize() {
this.source = Tokenizer.parseUnicodeEscapes(this.source);
const tokens = [];
while (this.hasRemaining()) {
{
const m = this.next(/^(\s|\n)+/);
if (m !== null) {
if (!this.ignoreWhitespace) {
tokens.push({ type: "Whitespace" /* Whitespace */, literal: m[0] });
}
continue;
}
}
{
const ch2 = this.peek(1);
switch (ch2) {
case "(":
case ")":
case "[":
case "]":
case "{":
case "}":
case ",":
case ":":
case ".":
tokens.push(TokenFactory.punctuation(ch2));
this.consume(1);
continue;
}
}
{
const m = this.next(/^(<=|>=|===|!==|==|!=|\*\*|>>>|>>|<<|&&|\|\||[+\-*/%&|^<>!~])/);
if (m) {
tokens.push(TokenFactory.operator(m[0]));
continue;
}
}
{
{
const m = this.next(/^[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?[n]?/);
if (m) {
const literal = m[0];
let value = 0;
if (literal.endsWith("n")) {
value = BigInt(literal.slice(0, -1));
} else if (literal.includes(".") || literal.includes("e") || literal.includes("E")) {
value = parseFloat(literal);
} else {
value = parseInt(literal, 10);
}
tokens.push(TokenFactory.constant(literal, value));
continue;
}
}
{
const m = this.next(/^'([^'\\]|\\.)*'/);
if (m) {
const literal = m[0];
const value = literal.slice(1, -1).replace(/\\'/g, "'").replace(/\\\\/g, "\\");
tokens.push(TokenFactory.constant(literal, value));
continue;
}
}
{
const m = this.next(/^"([^"\\]|\\.)*"/);
if (m) {
const literal = m[0];
const value = literal.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
tokens.push(TokenFactory.constant(literal, value));
continue;
}
}
}
{
const m = this.next(/^([$_\p{ID_Start}][$_\p{ID_Continue}]*)/u);
if (m) {
const literal = m[0];
tokens.push(TokenFactory.identifier(literal));
continue;
}
}
const ctx = new CodeContext(this.source, this.position);
const ch = this.peek(1);
const hex = ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0");
throw new TokenizingError(ctx, `Unexpected character '${ch}', code is \\u${hex}`);
}
return tokens;
}
static tokenize(s, ...args) {
if (typeof s === "string") {
return new Tokenizer(s).tokenize();
} else {
let tokens = [];
for (let i = 0;i < args.length; i++) {
tokens.push(...new Tokenizer(s.raw[i]).tokenize());
tokens.push(TokenFactory.interpolation(args[i]));
}
tokens.push(...this.tokenize(s.raw.at(-1)));
return tokens;
}
}
static parseUnicodeEscapes(str) {
return str.replace(/\\u([0-9A-Fa-f]{4,})/g, (_, hex) => {
return String.fromCodePoint(parseInt(hex, 16));
}).replace(/\\u\{([0-9A-Fa-f]{4,})\}/g, (_, hex) => {
return String.fromCodePoint(parseInt(hex, 16));
});
}
}
// src/utils/index.ts
function detectFunctionType(fn) {
if (fn.hasOwnProperty("prototype")) {
return "normal";
}
const str = Function.prototype.toString.call(fn);
if (str.startsWith("(") || /^[^(),.=>{}[]]+\s*=>\s*\{/.test(str)) {
return "arrow";
}
return "method";
}
function normalizeIndex(index, length) {
return index < 0 ? index + length : index;
}
// src/Rop.ts
class Rop {
overloadings = new Map;
bindings = new Map;
constructor() {}
o(strs, ...args) {
const tokens = Tokenizer.tokenize(strs, ...args);
const ast = new AstParser(tokens).parse();
const result = new Evaluater(ast, this).evaluate();
return result;
}
bind(...args) {
if (typeof args[0] === "string") {
const [key, value] = args;
this.bindings.set(key, value);
} else if (args[0] instanceof Map) {
for (const [key, value] of args[0].entries()) {
this.bindings.set(key, value);
}
} else {
for (const [key, value] of Object.entries(args[0])) {
this.bindings.set(key, value);
}
}
return this;
}
unbind(...keys) {
for (const k of keys) {
this.bindings.delete(k);
}
return this;
}
static op(name) {
if (!Operations.isKnownOperation(name)) {
throw new Error(`Unknown operation name: '${name}'`);
}
return Operations.symbol(name);
}
setOverload(prototype, symbol, operationFn) {
if (!this.overloadings.has(prototype)) {
this.overloadings.set(prototype, new Map);
}
const classOverloads = this.overloadings.get(prototype);
switch (detectFunctionType(operationFn)) {
case "normal":
case "method":
classOverloads.set(symbol, operationFn);
break;
case "arrow":
classOverloads.set(symbol, function(...args) {
return operationFn(this, ...args);
});
break;
}
}
overload(clazz, op, operationFn) {
if (clazz.prototype === undefined) {
throw new TypeError("clazz must be a class");
}
const symbol = Operations.symbol(op);
if (symbol === null) {
throw new TypeError(`Unknown operation: ${String(op)}`);
}
this.setOverload(clazz.prototype, symbol, operationFn);
return this;
}
overloads(clazz, def) {
if (clazz.prototype === undefined) {
throw new TypeError("clazz must be a class");
}
for (const key of Reflect.ownKeys(def)) {
const symbol = Operations.symbol(key);
if (symbol === null) {
continue;
}
const operationFn = def[key];
if (typeof operationFn !== "function") {
throw new TypeError(`Expected operation function '${symbol.description}' to be a function, but got ${typeof operationFn}`);
}
this.setOverload(clazz.prototype, symbol, operationFn);
}
return this;
}
getOverloadFromPrototypeChain(prototype, symbol) {
let p = prototype;
while (p !== null) {
const classOverloads = this.overloadings.get(p);
if (classOverloads !== undefined && classOverloads.has(symbol)) {
return classOverloads.get(symbol) ?? null;
}
if (typeof p === "object" && symbol in p && typeof p[symbol] === "function") {
return p[symbol];
}
p = Object.getPrototypeOf(p);
}
return null;
}
getOverloadOnClass(clazz, symbol) {
return this.getOverloadFromPrototypeChain(clazz.prototype, symbol);
}
getOverloadOnInstance(inst, symbol) {
return this.getOverloadFromPrototypeChain(inst, symbol);
}
bindDefaults() {
return this.bind({
true: true,
false: false,
null: null,
undefined: undefined,
Infinity: Infinity,
NaN: NaN,
Object,
Number,
BigInt,
String,
Boolean,
Array,
Date,
Symbol,
JSON,
Math
});
}
bindMaths() {
return this.bind(Object.getOwnPropertyNames(Math).reduce((m, k) => {
Reflect.set(m, k, Reflect.get(Math, k));
return m;
}, {}));
}
overloadDefaults() {
this.overloads(Array, {
"+": (self, other) => [...self, ...other],
"[i]"(index) {
if (typeof index !== "number") {
throw new Error("Index of Array must be a number");
}
return this[normalizeIndex(index, this.length)];
},
"[:]"(slices) {
if (slices.length !== 1) {
throw new Error("Multi slice is not supported");
}
const slice = slices[0];
if (slice.step === 0) {
throw new Error("Slice step cannot be zero");
} else {
slice.step ??= 1;
const result = [];
if (slice.step > 0) {
slice.start = slice.start === undefined ? 0 : normalizeIndex(slice.start, this.length);
slice.end = slice.end === undefined ? this.length : normalizeIndex(slice.end, this.length);
for (let i = slice.start;i < slice.end; i += slice.step) {
result.push(this[i]);
}
} else {
slice.start = slice.start === undefined ? this.length - 1 : normalizeIndex(slice.start, this.length);
slice.end = slice.end === undefined ? -1 : normalizeIndex(slice.end, this.length);
for (let i = slice.start;i > slice.end; i += slice.step) {
result.push(this[i]);
}
}
return result;
}
}
});
this.overloads(String, { "*": (self, n) => self.repeat(n) });
this.overloads(Set, {
"+": (self, b) => new Set([...self, ...b]),
"-": (self, b) => new Set([...self].filter((x) => !b.has(x)))
});
return this;
}
static instance;
static get INST() {
if (this.instance === undefined) {
this.instance = new Rop().bindDefaults().bindMaths().overloadDefaults();
}
return this.instance;
}
static resetDefaultInstance() {
this.instance = undefined;
}
}
// src/index.ts
var o = Rop.INST.o;
export {
o,
Rop
};