'use strict'; /* ════════════════════════════════════════════════════════════════════════ SimExpr — безопасный движок выражений для конструктора симуляций (Фаза 0). Спека симуляции — это ДАННЫЕ, которые шарятся между людьми, поэтому код выражений НИКОГДА не исполняется через eval/new Function. Здесь — собственный конвейер: токенайзер → AST → evaluate(ast, env). Логика расширяет парсер y=f(x) из graph.js (тот же подход к токенам и неявному умножению), но: - окружение многопеременное: любой идентификатор берётся из env (params, t, значения объектов), а не только x; - добавлены сравнения (< <= > >= == !=), логика (&& ||), тернарник ?:, функции min/max/mod/log(base,x) и константы pi/e; - результат компиляции — AST + замыкание fn(env), считается детерминированно. Ошибки времени выполнения (деление на 0, NaN, неизвестный идентификатор) НЕ кидаются из fn(env): они дают 0 (или флаг через evalSafe), чтобы один кривой кадр не ронял весь рантайм. Ошибки КОМПИЛЯЦИИ (синтаксис) возвращаются строкой в compile(src).error и не бросаются. API: SimExpr.compile(src) -> { ast, fn, error } fn(env) -> number (никогда не бросает; при сбое -> 0) SimExpr.evaluate(ast, env) -> number (никогда не бросает; при сбое -> 0) SimExpr.evalSafe(ast, env) -> { value, error } (для отладки/билдера) SimExpr.FUNCTIONS -> Set имён whitelisted-функций (для подсветки/билдера) SimExpr.CONSTANTS -> Set имён констант (pi, e) env — простой объект { имя: number }. Имена объектов спеки удобно передавать как "obj.x"/"obj.y" — для этого допускается точка в идентификаторе. ════════════════════════════════════════════════════════════════════════ */ (function (global) { /* ── whitelist функций (имя -> арность: 1, 2 или -1 для переменной) ── */ // -1 => принимает >=1 аргумент (min/max). log: 1 арг = ln по основанию e? нет — // log(x) трактуем как десятичный (как в graph.js log===log10), log(b,x)=log_b(x). var FN_ARITY = { sin: 1, cos: 1, tan: 1, tg: 1, ctg: 1, cot: 1, asin: 1, acos: 1, atan: 1, arcsin: 1, arccos: 1, arctan: 1, arctg: 1, sqrt: 1, abs: 1, exp: 1, ln: 1, log: -2, log2: 1, log10: 1, floor: 1, ceil: 1, round: 1, sign: 1, min: -1, max: -1, mod: 2, atan2: 2, pow: 2, hypot: -1 }; // Реализации. Все защищены от исключений на уровне evaluate (домены проверяются // там, где можно дать NaN -> наружу станет 0). var FN_IMPL = { sin: Math.sin, cos: Math.cos, tan: Math.tan, tg: Math.tan, ctg: function (x) { return 1 / Math.tan(x); }, cot: function (x) { return 1 / Math.tan(x); }, asin: Math.asin, acos: Math.acos, atan: Math.atan, arcsin: Math.asin, arccos: Math.acos, arctan: Math.atan, arctg: Math.atan, sqrt: Math.sqrt, abs: Math.abs, exp: Math.exp, ln: Math.log, log: function (a, b) { // log(x) -> log10(x); log(base, x) -> log_base(x) if (b === undefined) return Math.log(a) / Math.LN10; return Math.log(b) / Math.log(a); }, log2: Math.log2, log10: Math.log10, floor: Math.floor, ceil: Math.ceil, round: Math.round, sign: Math.sign, min: Math.min, max: Math.max, mod: function (a, b) { return b === 0 ? 0 : a % b; }, atan2: Math.atan2, pow: Math.pow, hypot: Math.hypot }; var CONSTANTS = { pi: Math.PI, PI: Math.PI, e: Math.E, E: Math.E, tau: Math.PI * 2 }; /* ════════════════════ TOKENIZER ════════════════════ */ function tokenize(src) { var out = []; var i = 0, n = src.length; while (i < n) { var ch = src[i]; if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { i++; continue; } /* число */ if ((ch >= '0' && ch <= '9') || (ch === '.' && src[i + 1] >= '0' && src[i + 1] <= '9')) { var j = i; while (j < n && src[j] >= '0' && src[j] <= '9') j++; if (j < n && src[j] === '.') { j++; while (j < n && src[j] >= '0' && src[j] <= '9') j++; } if (j < n && (src[j] === 'e' || src[j] === 'E')) { var k = j + 1; if (k < n && (src[k] === '+' || src[k] === '-')) k++; if (k < n && src[k] >= '0' && src[k] <= '9') { j = k; while (j < n && src[j] >= '0' && src[j] <= '9') j++; } } out.push({ t: 'num', v: parseFloat(src.slice(i, j)) }); i = j; continue; } /* идентификатор (буквы/цифры/_/.) — точка допускает obj.x */ if (isIdentStart(ch)) { var p = i + 1; while (p < n && isIdentPart(src[p])) p++; out.push({ t: 'id', v: src.slice(i, p) }); i = p; continue; } /* двухсимвольные операторы */ var two = src.substr(i, 2); if (two === '<=' || two === '>=' || two === '==' || two === '!=' || two === '&&' || two === '||') { out.push({ t: 'op', v: two }); i += 2; continue; } /* односимвольные операторы / скобки / запятая / ?: */ if ('+-*/^%()<>?:,'.indexOf(ch) !== -1) { out.push({ t: 'op', v: ch }); i++; continue; } // одиночный '=' трактуем как сравнение (часто пишут a=b по привычке) if (ch === '=') { out.push({ t: 'op', v: '==' }); i++; continue; } // одиночный '!' — логическое НЕ if (ch === '!') { out.push({ t: 'op', v: '!' }); i++; continue; } throw new Error('Неизвестный символ: «' + ch + '»'); } return out; } function isIdentStart(ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_'; } function isIdentPart(ch) { return isIdentStart(ch) || (ch >= '0' && ch <= '9') || ch === '.'; } /* Вставка неявного умножения: 2x -> 2*x, 2(.. -> 2*(.., )( -> )*(, )x -> )*x. Функция перед '(' умножение НЕ получает. */ function insertImplicitMul(tokens) { var out = []; for (var i = 0; i < tokens.length; i++) { out.push(tokens[i]); var cur = tokens[i], nxt = tokens[i + 1]; if (!nxt) continue; var curIsFn = cur.t === 'id' && Object.prototype.hasOwnProperty.call(FN_ARITY, cur.v); var curEnds = cur.t === 'num' || (cur.t === 'id' && !curIsFn) || (cur.t === 'op' && cur.v === ')'); var nxtStarts = nxt.t === 'num' || nxt.t === 'id' || (nxt.t === 'op' && nxt.v === '('); // не вставляем '*' если cur — функция (тогда дальше идёт её '(') if (curEnds && nxtStarts) out.push({ t: 'op', v: '*' }); } return out; } /* ════════════════════ PARSER (рекурсивный спуск -> AST) ════════════════════ Грамматика (по убыванию приоритета связывания): ternary := logicOr ('?' ternary ':' ternary)? logicOr := logicAnd ('||' logicAnd)* logicAnd := compare ('&&' compare)* compare := addSub (('<'|'<='|'>'|'>='|'=='|'!=') addSub)? addSub := mulDiv (('+'|'-') mulDiv)* mulDiv := power (('*'|'/'|'%') power)* power := unary ('^' power)? // правоассоциативно unary := ('-'|'+'|'!') unary | primary primary := num | ident | const | fn(args...) | '(' ternary ')' Узлы AST: { k:'num', v } { k:'var', name } { k:'const', v } { k:'bin', op, a, b } { k:'un', op, a } { k:'cmp', op, a, b } { k:'logic', op, a, b } { k:'not', a } { k:'cond', c, a, b } { k:'call', name, args:[...] } */ function parse(tokens) { var pos = 0; function peek() { return tokens[pos]; } function next() { return tokens[pos++]; } function expect(v) { var t = peek(); if (!t || t.v !== v) throw new Error('Ожидалось «' + v + '»'); pos++; } function isOp(v) { var t = peek(); return t && t.t === 'op' && t.v === v; } function ternary() { var c = logicOr(); if (isOp('?')) { next(); var a = ternary(); expect(':'); var b = ternary(); return { k: 'cond', c: c, a: a, b: b }; } return c; } function logicOr() { var l = logicAnd(); while (isOp('||')) { next(); l = { k: 'logic', op: '||', a: l, b: logicAnd() }; } return l; } function logicAnd() { var l = compare(); while (isOp('&&')) { next(); l = { k: 'logic', op: '&&', a: l, b: compare() }; } return l; } function compare() { var l = addSub(); var t = peek(); if (t && t.t === 'op' && (t.v === '<' || t.v === '<=' || t.v === '>' || t.v === '>=' || t.v === '==' || t.v === '!=')) { var op = next().v; return { k: 'cmp', op: op, a: l, b: addSub() }; } return l; } function addSub() { var l = mulDiv(); while (isOp('+') || isOp('-')) { var op = next().v; l = { k: 'bin', op: op, a: l, b: mulDiv() }; } return l; } function mulDiv() { var l = power(); while (isOp('*') || isOp('/') || isOp('%')) { var op = next().v; l = { k: 'bin', op: op, a: l, b: power() }; } return l; } function power() { var base = unary(); if (isOp('^')) { next(); return { k: 'bin', op: '^', a: base, b: power() }; } return base; } function unary() { if (isOp('-')) { next(); return { k: 'un', op: '-', a: unary() }; } if (isOp('+')) { next(); return unary(); } if (isOp('!')) { next(); return { k: 'not', a: unary() }; } return primary(); } function primary() { var t = peek(); if (!t) throw new Error('Неожиданный конец выражения'); if (t.t === 'num') { next(); return { k: 'num', v: t.v }; } if (t.t === 'id') { next(); var name = t.v; if (Object.prototype.hasOwnProperty.call(FN_ARITY, name)) { expect('('); var args = []; if (!isOp(')')) { args.push(ternary()); while (isOp(',')) { next(); args.push(ternary()); } } expect(')'); checkArity(name, args.length); return { k: 'call', name: name, args: args }; } if (Object.prototype.hasOwnProperty.call(CONSTANTS, name)) { return { k: 'const', v: CONSTANTS[name] }; } // переменная окружения return { k: 'var', name: name }; } if (t.t === 'op' && t.v === '(') { next(); var e = ternary(); expect(')'); return e; } throw new Error('Неожиданный токен: «' + t.v + '»'); } var ast = ternary(); if (pos !== tokens.length) throw new Error('Лишние токены после выражения'); return ast; } function checkArity(name, got) { var ar = FN_ARITY[name]; if (ar === -1) { // >=1 if (got < 1) throw new Error('Функции «' + name + '» нужен хотя бы 1 аргумент'); return; } if (ar === -2) { // 1..2 (log) if (got < 1 || got > 2) throw new Error('Функция «' + name + '» принимает 1 или 2 аргумента'); return; } if (got !== ar) throw new Error('Функция «' + name + '» принимает ' + ar + ' арг., дано ' + got); } /* ════════════════════ EVALUATE ════════════════════ Чистый интерпретатор AST по окружению env. НЕ бросает наружу (см. evaluate). Внутренний _ev может вернуть NaN/Infinity — нормализуется в evaluate. */ function _ev(node, env) { switch (node.k) { case 'num': return node.v; case 'const': return node.v; case 'var': { var val = env ? env[node.name] : undefined; return typeof val === 'number' ? val : 0; // неизвестная переменная -> 0 } case 'un': // только '-' return -_ev(node.a, env); case 'not': return _ev(node.a, env) ? 0 : 1; case 'bin': { var a = _ev(node.a, env), b = _ev(node.b, env); switch (node.op) { case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': return b === 0 ? 0 : a / b; // деление на 0 -> 0 case '%': return b === 0 ? 0 : a % b; case '^': return Math.pow(a, b); } return 0; } case 'cmp': { var x = _ev(node.a, env), y = _ev(node.b, env); switch (node.op) { case '<': return x < y ? 1 : 0; case '<=': return x <= y ? 1 : 0; case '>': return x > y ? 1 : 0; case '>=': return x >= y ? 1 : 0; case '==': return x === y ? 1 : 0; case '!=': return x !== y ? 1 : 0; } return 0; } case 'logic': { if (node.op === '&&') return (_ev(node.a, env) && _ev(node.b, env)) ? 1 : 0; return (_ev(node.a, env) || _ev(node.b, env)) ? 1 : 0; } case 'cond': return _ev(node.c, env) ? _ev(node.a, env) : _ev(node.b, env); case 'call': { var fn = FN_IMPL[node.name]; var args = node.args; if (args.length === 1) return fn(_ev(args[0], env)); if (args.length === 2) return fn(_ev(args[0], env), _ev(args[1], env)); // переменное число (min/max/hypot) var vals = new Array(args.length); for (var i = 0; i < args.length; i++) vals[i] = _ev(args[i], env); return fn.apply(null, vals); } } return 0; } // Никогда не бросает; NaN/Infinity -> 0. function evaluate(ast, env) { if (!ast) return 0; var v; try { v = _ev(ast, env); } catch (e) { return 0; } return (typeof v === 'number' && isFinite(v)) ? v : 0; } // Для отладки/билдера: возвращает значение + флаг (NaN/Infinity -> error). function evalSafe(ast, env) { if (!ast) return { value: 0, error: 'нет выражения' }; var v; try { v = _ev(ast, env); } catch (e) { return { value: 0, error: String(e.message || e) }; } if (typeof v !== 'number' || !isFinite(v)) return { value: 0, error: 'не число (NaN/∞)' }; return { value: v, error: null }; } /* ════════════════════ COMPILE ════════════════════ Возвращает { ast, fn, error }. Синтаксическая ошибка -> error:строка, fn:()=>0. */ function compile(src) { if (src == null) return { ast: null, fn: function () { return 0; }, error: 'пустое выражение' }; var raw = String(src).trim(); // допускаем ведущее "y=" / "name=" (привычка), срезаем как в graph.js raw = raw.replace(/^\s*[a-zA-Z_][a-zA-Z_0-9.]*\s*=(?![=])\s*/, ''); if (!raw) return { ast: null, fn: function () { return 0; }, error: 'пустое выражение' }; var ast; try { var toks = insertImplicitMul(tokenize(raw)); ast = parse(toks); } catch (e) { return { ast: null, fn: function () { return 0; }, error: String(e.message || e) }; } var fn = function (env) { return evaluate(ast, env); }; return { ast: ast, fn: fn, error: null }; } /* Хелпер: значение — число вернуть как есть; строка — скомпилировать и вернуть { fn(env), error, constant }. Используется движком для свойств-привязок. */ function compileValue(value) { if (typeof value === 'number') { var c = value; return { fn: function () { return c; }, error: null, constant: true, ast: { k: 'num', v: c } }; } if (typeof value === 'string') { var r = compile(value); return { fn: r.fn, error: r.error, constant: false, ast: r.ast }; } // прочие типы -> 0 return { fn: function () { return 0; }, error: null, constant: true, ast: { k: 'num', v: 0 } }; } var FUNCTIONS = {}; // имя -> true (как «множество» без ES6 Set ради совместимости) Object.keys(FN_ARITY).forEach(function (k) { FUNCTIONS[k] = true; }); var CONSTSET = {}; Object.keys(CONSTANTS).forEach(function (k) { CONSTSET[k] = true; }); global.SimExpr = { compile: compile, compileValue: compileValue, evaluate: evaluate, evalSafe: evalSafe, tokenize: tokenize, // экспортируем для тестов/билдера parse: function (src) { // удобный helper: строка -> AST (бросает при ошибке) return parse(insertImplicitMul(tokenize(String(src).trim()))); }, FUNCTIONS: FUNCTIONS, CONSTANTS: CONSTSET }; })(typeof window !== 'undefined' ? window : globalThis);