import { AsText, ErrorResult, ProcResult, TextPiece } from "../words"; export function Expr({}, argv: TextPiece[]): ProcResult { const name = argv[0]; if ("bare" in name && name.bare == "expr") { // being called as [expr ...], not fallback for math argv.splice(0, 1); } const expression = argv.map(AsText).join(" ").trim(); const parser = new ExpressionParser(expression); const result = parser.parseSubExpression(0); if (parser.pos != expression.length) { return { error: "Couldn't parse full expression" }; } if ("value" in result) { return { text: String(result.value) }; } else { return result; } } // --------------------------- // Pratt parser for evaluating arithmatic expressions const PLUS_TOKEN = /\s*(\+)/y; const MINUS_TOKEN = /\s*(\-)/y; const TIMES_TOKEN = /\s*(\*)/y; const FLOOR_TOKEN = /\s*(\/\/)/y; const DIV_TOKEN = /\s*(\/)/y; const MOD_TOKEN = /\s*(\%)/y; const NUMBER_TOKEN = /\s*(\d+)/y; type Value = { value: number }; type TokenHandler = { leftBindingPower: number; token: RegExp; parse: ( left: Value, matched: string, parser: ExpressionParser ) => Value | ErrorResult; }; function map( value: Value | ErrorResult, op: (value: number) => Value | ErrorResult ): Value | ErrorResult { if ("error" in value) { return value; } else { return op(value.value); } } function makeInfixOp( token: RegExp, leftBindingPower: number, rightBindingPower: number, op: (left: number, right: number) => Value | ErrorResult ): TokenHandler { return { leftBindingPower, token, parse: ({ value: left }, matched, parser) => map(parser.parseSubExpression(rightBindingPower), (right) => op(left, right) ), }; } const Operators: TokenHandler[] = [ { leftBindingPower: -1, token: NUMBER_TOKEN, parse: (left, matched) => ({ value: Number(matched) }), }, { leftBindingPower: -1, token: MINUS_TOKEN, parse: (left, matched, parser) => map(parser.parseSubExpression(99), (right) => ({ value: -right })), }, makeInfixOp(PLUS_TOKEN, 10, 11, (left, right) => ({ value: left + right })), makeInfixOp(MINUS_TOKEN, 10, 11, (left, right) => ({ value: left - right })), makeInfixOp(TIMES_TOKEN, 20, 21, (left, right) => ({ value: left * right })), makeInfixOp(FLOOR_TOKEN, 20, 21, (left, right) => ({ value: Math.floor(left / right), })), makeInfixOp(DIV_TOKEN, 20, 21, (left, right) => ({ value: left / right })), makeInfixOp(MOD_TOKEN, 20, 21, (left, right) => ({ value: ((left % right) + right) % right, })), ]; const ZERO = { value: 0 }; class ExpressionParser { pos: number = 0; constructor(public text: string) {} next(token: RegExp): string | null { token.lastIndex = this.pos; const matches = token.exec(this.text); if (matches) { this.pos = token.lastIndex; return matches[1]; } else { return null; } } /** Tries to match one of the given tokens & returns the value of the handler. If none match, returns null. */ tryAll(left: Value, options: TokenHandler[]): Value | ErrorResult | null { for (const option of options) { const match = this.next(option.token); if (match) { return option.parse(left, match, this); } } return null; } parsePrefixOp(): Value | ErrorResult { return ( this.tryAll( ZERO, Operators.filter((operator) => operator.leftBindingPower == -1) ) ?? { error: "Couldn't parse number" } ); } parseSubExpression(rightBindingPower: number): Value | ErrorResult { let value = this.parsePrefixOp(); if ("error" in value) { return value; } do { const newValue = this.tryAll( value, Operators.filter( (operator) => operator.leftBindingPower > rightBindingPower ) ); if (newValue == null) { break; } value = newValue; } while (!("error" in value)); return value; } } // parse expression: // must parse sub-expression with right binding power 0 // must be at EOF // parse sub-expression with right binding power : // must parse a prefix op // parse as many postfix ops w/ left binding power > right binding power // parse op: // must parse associated token (if success- failing the rest of the op fails *everything*) // depending on op: // maybe parse sub-expressions with op's right-binding power // maybe consume other tokens