exprforge 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +288 -21
- package/emitters/cobol.js +478 -0
- package/emitters/exprsyntax.js +81 -0
- package/emitters/fortran.js +169 -0
- package/emitters/julia.js +67 -0
- package/emitters/perl.js +95 -0
- package/emitters/php.js +79 -0
- package/emitters/registry.js +8 -0
- package/emitters/scheme.js +154 -0
- package/emitters/zig.js +126 -0
- package/evaluate.js +109 -0
- package/expr.js +313 -0
- package/fn.js +117 -0
- package/index.js +14 -2
- package/math/index.js +9 -1
- package/package.json +5 -2
- package/samples/math-demo.js +8 -4
package/emitters/zig.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// exprforge/emitters/zig.js
|
|
2
|
+
const Emitter = require("./base.js");
|
|
3
|
+
|
|
4
|
+
// Zig's reserved words plus the small set of extra identifiers this
|
|
5
|
+
// emitter's own output introduces (Result, the struct-suite convention
|
|
6
|
+
// below) -- same role as QB64_RESERVED in emitters/qb64.js. Zig is
|
|
7
|
+
// case-sensitive, unlike QB64/Fortran, so this checks exact names.
|
|
8
|
+
const ZIG_RESERVED = new Set([
|
|
9
|
+
"addrspace", "align", "allowzero", "and", "anyframe", "anytype", "asm", "async",
|
|
10
|
+
"await", "break", "callconv", "catch", "comptime", "const", "continue", "defer",
|
|
11
|
+
"else", "enum", "errdefer", "error", "export", "extern", "fn", "for", "if",
|
|
12
|
+
"inline", "noalias", "noinline", "nosuspend", "opaque", "or", "orelse", "packed",
|
|
13
|
+
"pub", "resume", "return", "linksection", "struct", "suspend", "switch", "test",
|
|
14
|
+
"threadlocal", "try", "union", "unreachable", "usingnamespace", "var", "volatile",
|
|
15
|
+
"while", "true", "false", "null", "undefined", "void", "type", "anyerror",
|
|
16
|
+
"std", "result",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function checkReservedNames(names) {
|
|
20
|
+
for (const name of names) {
|
|
21
|
+
if (ZIG_RESERVED.has(name)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`emitter for .zig: "${name}" is a reserved Zig keyword/identifier and can't be used as a ` +
|
|
24
|
+
`function/variable/parameter name -- rename it (see ZIG_RESERVED in emitters/zig.js)`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mathFn(name) {
|
|
31
|
+
return (args) => `std.math.${name}(${args.join(", ")})`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function builtin(name) {
|
|
35
|
+
return ([x]) => `@${name}(${x})`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function capitalize(s) {
|
|
39
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const emitter = new Emitter({
|
|
43
|
+
ext: "zig",
|
|
44
|
+
// Every literal is forced through @as(f64, ...), not left bare --
|
|
45
|
+
// confirmed against a real compiler that a bare literal reaching a
|
|
46
|
+
// comptime-eligible call (e.g. std.math.sqrt(2.0) with no runtime f64
|
|
47
|
+
// operand anywhere) gets evaluated at Zig's extended comptime_float
|
|
48
|
+
// precision instead of truncated to an actual IEEE 754 double, giving
|
|
49
|
+
// a DIFFERENT (more precise, not less) result than every other target
|
|
50
|
+
// here. @as(f64, ...) forces real double-precision truncation before
|
|
51
|
+
// any arithmetic happens, regardless of whether the surrounding
|
|
52
|
+
// expression also involves a runtime variable.
|
|
53
|
+
formatNumber: (v) => `@as(f64, ${String(v)})`,
|
|
54
|
+
calls: {
|
|
55
|
+
// Compiler builtins (map to LLVM intrinsics directly).
|
|
56
|
+
abs: builtin("abs"), exp: builtin("exp"), log: builtin("log"),
|
|
57
|
+
log2: builtin("log2"), log10: builtin("log10"),
|
|
58
|
+
floor: builtin("floor"), ceil: builtin("ceil"), round: builtin("round"), trunc: builtin("trunc"),
|
|
59
|
+
min: builtin2("min"), max: builtin2("max"),
|
|
60
|
+
// std.math functions -- not compiler builtins, but ordinary Zig
|
|
61
|
+
// stdlib functions with the expected 1/2-arg float signatures.
|
|
62
|
+
sqrt: mathFn("sqrt"), sin: mathFn("sin"), cos: mathFn("cos"), tan: mathFn("tan"),
|
|
63
|
+
asin: mathFn("asin"), acos: mathFn("acos"), atan: mathFn("atan"), atan2: mathFn("atan2"),
|
|
64
|
+
hypot: mathFn("hypot"),
|
|
65
|
+
// pow needs an explicit type argument (std.math.pow(T, x, y)) --
|
|
66
|
+
// unlike every other std.math function here, it's not purely
|
|
67
|
+
// inferred from its float arguments.
|
|
68
|
+
pow: ([x, y]) => `std.math.pow(f64, ${x}, ${y})`,
|
|
69
|
+
// std.math.sign is genuinely zero-aware (confirmed: sign(0.0) ==
|
|
70
|
+
// 0.0) -- unlike most other targets here, this doesn't need to be
|
|
71
|
+
// hand-built to get the zero case right (see Go's/Rust's sign()
|
|
72
|
+
// history in this project for what happens when a language's
|
|
73
|
+
// native sign function isn't).
|
|
74
|
+
sign: mathFn("sign"),
|
|
75
|
+
},
|
|
76
|
+
// Zig has no ?: ternary, but if/else as an EXPRESSION (not just a
|
|
77
|
+
// statement) works exactly like this project's select() -- including
|
|
78
|
+
// evaluating both branches, since there's no separate "lazy" form.
|
|
79
|
+
emitSelect: function (condNode, thenStr, elseStr) {
|
|
80
|
+
const L = this.emitExpr(condNode.left);
|
|
81
|
+
const R = this.emitExpr(condNode.right);
|
|
82
|
+
return `(if ((${L}) ${condNode.op} (${R})) ${thenStr} else ${elseStr})`;
|
|
83
|
+
},
|
|
84
|
+
formatFunction: (fn, body, letBindings = []) => {
|
|
85
|
+
checkReservedNames([fn.name, ...fn.params, ...letBindings.map((b) => b.name)]);
|
|
86
|
+
const params = fn.params.map((p) => `${p}: f64`).join(", ");
|
|
87
|
+
const lets = letBindings.map(({ name, valueStr }) => ` const ${name}: f64 = ${valueStr};`).join("\n");
|
|
88
|
+
const letsBlock = lets ? lets + "\n" : "";
|
|
89
|
+
return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
90
|
+
`const std = @import("std");\n\n` +
|
|
91
|
+
`pub fn ${fn.name}(${params}) f64 {\n` +
|
|
92
|
+
letsBlock +
|
|
93
|
+
` return ${body};\n` +
|
|
94
|
+
`}\n`;
|
|
95
|
+
},
|
|
96
|
+
// Multiple named outputs from one call: a small struct type (same idea
|
|
97
|
+
// as C's typedef struct, Rust's struct) returned by value -- Zig has
|
|
98
|
+
// no native tuple-with-names or multi-return.
|
|
99
|
+
formatSuite: (fn, outputStrs, letBindings = []) => {
|
|
100
|
+
const outputNames = Object.keys(outputStrs);
|
|
101
|
+
checkReservedNames([fn.name, ...fn.params, ...outputNames, ...letBindings.map((b) => b.name)]);
|
|
102
|
+
const resultName = `${capitalize(fn.name)}Result`;
|
|
103
|
+
const params = fn.params.map((p) => `${p}: f64`).join(", ");
|
|
104
|
+
const lets = letBindings.map(({ name, valueStr }) => ` const ${name}: f64 = ${valueStr};`).join("\n");
|
|
105
|
+
const letsBlock = lets ? lets + "\n" : "";
|
|
106
|
+
const structFields = outputNames.map((n) => ` ${n}: f64,`).join("\n");
|
|
107
|
+
const initFields = outputNames.map((n) => ` .${n} = ${outputStrs[n]},`).join("\n");
|
|
108
|
+
return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
109
|
+
`const std = @import("std");\n\n` +
|
|
110
|
+
`pub const ${resultName} = struct {\n${structFields}\n};\n\n` +
|
|
111
|
+
`pub fn ${fn.name}(${params}) ${resultName} {\n` +
|
|
112
|
+
letsBlock +
|
|
113
|
+
` return ${resultName}{\n${initFields}\n };\n` +
|
|
114
|
+
`}\n`;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// builtin2 is used by the calls table above but defined after it's
|
|
119
|
+
// referenced (function declarations hoist) -- kept next to builtin/mathFn
|
|
120
|
+
// rather than above them so the two "2-arg compiler builtin" and "1-arg
|
|
121
|
+
// compiler builtin" helpers read as a pair.
|
|
122
|
+
function builtin2(name) {
|
|
123
|
+
return ([a, b]) => `@${name}(${a}, ${b})`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = emitter;
|
package/evaluate.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// exprforge/evaluate.js
|
|
2
|
+
//
|
|
3
|
+
// A native tree-walking interpreter over the exact same AST every
|
|
4
|
+
// emitter compiles from -- evaluate(fn, args) computes a result (or a
|
|
5
|
+
// {name: value} object for a multi-output suite) directly in JS, no
|
|
6
|
+
// codegen/compile/subprocess step involved. Reuses collectLets (ast.js)
|
|
7
|
+
// for the same let-lifting every emitter already goes through, so this
|
|
8
|
+
// walks nodes in the identical dependency order every target does, and
|
|
9
|
+
// there's exactly one node-shape contract (ast.js's own header comment)
|
|
10
|
+
// for both this file and every emitters/<lang>.js to agree with.
|
|
11
|
+
//
|
|
12
|
+
// Every intrinsic name maps 1:1 onto emitters/js.js's own `calls` table
|
|
13
|
+
// keys (the simplest existing source of truth for "what the ~22
|
|
14
|
+
// intrinsics are called") straight to the real Math.* function -- this
|
|
15
|
+
// target has no codegen step to route an intermediate string through.
|
|
16
|
+
const { collectLets } = require("./ast.js");
|
|
17
|
+
|
|
18
|
+
const CMP_OPS = {
|
|
19
|
+
">": (a, b) => a > b,
|
|
20
|
+
"<": (a, b) => a < b,
|
|
21
|
+
">=": (a, b) => a >= b,
|
|
22
|
+
"<=": (a, b) => a <= b,
|
|
23
|
+
"==": (a, b) => a === b,
|
|
24
|
+
"!=": (a, b) => a !== b,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const BIN_OPS = {
|
|
28
|
+
"+": (a, b) => a + b,
|
|
29
|
+
"-": (a, b) => a - b,
|
|
30
|
+
"*": (a, b) => a * b,
|
|
31
|
+
"/": (a, b) => a / b,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const CALLS = {
|
|
35
|
+
sqrt: Math.sqrt, abs: Math.abs, sin: Math.sin, cos: Math.cos, tan: Math.tan,
|
|
36
|
+
asin: Math.asin, acos: Math.acos, atan: Math.atan, log: Math.log,
|
|
37
|
+
log2: Math.log2, log10: Math.log10, exp: Math.exp, floor: Math.floor,
|
|
38
|
+
ceil: Math.ceil, round: Math.round, trunc: Math.trunc, sign: Math.sign,
|
|
39
|
+
pow: Math.pow, atan2: Math.atan2, min: Math.min, max: Math.max, hypot: Math.hypot,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Handles every node type EXCEPT "let"/"outputs" -- those are only ever
|
|
43
|
+
// valid pre-collectLets (a function's top-level let-chain/body shape),
|
|
44
|
+
// never nested inside a bin/call/select, same constraint every emitter
|
|
45
|
+
// already relies on (see ast.js's own comments on letIn/outputs).
|
|
46
|
+
function evalNode(node, env) {
|
|
47
|
+
switch (node.type) {
|
|
48
|
+
case "num":
|
|
49
|
+
return node.value;
|
|
50
|
+
case "var":
|
|
51
|
+
if (!(node.name in env)) {
|
|
52
|
+
throw new Error(`evaluate(): unbound variable "${node.name}"`);
|
|
53
|
+
}
|
|
54
|
+
return env[node.name];
|
|
55
|
+
case "bin": {
|
|
56
|
+
const op = BIN_OPS[node.op];
|
|
57
|
+
if (!op) throw new Error(`evaluate(): unknown bin op "${node.op}"`);
|
|
58
|
+
return op(evalNode(node.left, env), evalNode(node.right, env));
|
|
59
|
+
}
|
|
60
|
+
case "call": {
|
|
61
|
+
const impl = CALLS[node.name];
|
|
62
|
+
if (!impl) throw new Error(`evaluate(): no mapping for Math function "${node.name}"`);
|
|
63
|
+
return impl(...node.args.map((a) => evalNode(a, env)));
|
|
64
|
+
}
|
|
65
|
+
case "select": {
|
|
66
|
+
const cmpFn = CMP_OPS[node.cond.op];
|
|
67
|
+
if (!cmpFn) throw new Error(`evaluate(): unknown cmp op "${node.cond.op}"`);
|
|
68
|
+
const cond = cmpFn(evalNode(node.cond.left, env), evalNode(node.cond.right, env));
|
|
69
|
+
return evalNode(cond ? node.then : node.else, env);
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(
|
|
73
|
+
`evaluate(): unexpected node type "${node.type}" -- "let"/"outputs" must already ` +
|
|
74
|
+
`be lifted out by collectLets before evalNode runs`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// evaluate(fn, args) -- fn is a {name, params, body} definition (the
|
|
80
|
+
// same shape emitAll() consumes), args is a plain array positional to
|
|
81
|
+
// fn.params. Returns a number for a plain body, or a {name: value}
|
|
82
|
+
// object for a multi-output (outputs()) body -- matching the shape
|
|
83
|
+
// test/conformance.test.js's own parseSuiteOutput() already expects
|
|
84
|
+
// back from every other target.
|
|
85
|
+
function evaluate(fn, args) {
|
|
86
|
+
if (args.length !== fn.params.length) {
|
|
87
|
+
throw new Error(`evaluate(): ${fn.name} expects ${fn.params.length} argument(s), got ${args.length}`);
|
|
88
|
+
}
|
|
89
|
+
const env = {};
|
|
90
|
+
fn.params.forEach((name, i) => {
|
|
91
|
+
env[name] = args[i];
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const { bindings, body } = collectLets(fn.body);
|
|
95
|
+
for (const { name, node } of bindings) {
|
|
96
|
+
env[name] = evalNode(node, env);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (body.type === "outputs") {
|
|
100
|
+
const result = {};
|
|
101
|
+
for (const [name, node] of Object.entries(body.fields)) {
|
|
102
|
+
result[name] = evalNode(node, env);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
return evalNode(body, env);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { evaluate };
|
package/expr.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// exprforge/expr.js
|
|
2
|
+
//
|
|
3
|
+
// Infix syntax sugar over ast.js's own builders -- NOT a new node type or
|
|
4
|
+
// a new capability. `` expr`a * b + 1` `` builds exactly the same tree
|
|
5
|
+
// `add(mul(v("a"), v("b")), num(1))` would, by calling num/v/add/sub/mul/
|
|
6
|
+
// div/neg/call/cmp/select directly (never constructing a raw {type: ...}
|
|
7
|
+
// object by hand), so every guarantee those builders already have
|
|
8
|
+
// (collectLets round-tripping, emitter compatibility) carries over for
|
|
9
|
+
// free. Unlike util.js's forComponents, this DOES return a Node -- it's
|
|
10
|
+
// closer in spirit to letChain (also in ast.js): a different way to
|
|
11
|
+
// spell the same tree, not a different tree.
|
|
12
|
+
//
|
|
13
|
+
// A tagged template literal, not a plain string function: `expr` is
|
|
14
|
+
// called by JS itself as expr(strings, ...values) -- see the grammar
|
|
15
|
+
// comment below for why that's the whole interpolation mechanism, with
|
|
16
|
+
// no `${` text syntax of its own to parse.
|
|
17
|
+
//
|
|
18
|
+
// Grammar:
|
|
19
|
+
//
|
|
20
|
+
// expression := ternary
|
|
21
|
+
// ternary := additive ( compOp additive "?" expression ":" expression )?
|
|
22
|
+
// compOp := ">" | "<" | ">=" | "<=" | "==" | "!="
|
|
23
|
+
// additive := multiplicative ( ("+"|"-") multiplicative )*
|
|
24
|
+
// multiplicative := unary ( ("*"|"/") unary )*
|
|
25
|
+
// unary := "-" unary | power
|
|
26
|
+
// power := primary ( "^" unary )?
|
|
27
|
+
// primary := NUMBER | IDENT ("(" args ")")? | "(" expression ")" | HOLE
|
|
28
|
+
// args := expression ("," expression)*
|
|
29
|
+
//
|
|
30
|
+
// Deliberately NOT supported (see the plan doc's "Scope decisions"):
|
|
31
|
+
// - No let/outputs blocks -- this is a pure expression grammar, same
|
|
32
|
+
// "expression AST, not a program AST" boundary as ast.js itself.
|
|
33
|
+
// Wrap the result in letIn/letChain/outputs instead.
|
|
34
|
+
// - No &&/|| -- the AST has no boolean-combinator node to lower them
|
|
35
|
+
// to. A comparison is ONLY ever valid as a ternary's condition
|
|
36
|
+
// (matching cmp()'s own documented constraint in ast.js), enforced
|
|
37
|
+
// here at PARSE time with a clear error, not deferred to the
|
|
38
|
+
// "cmp used outside select()" throw every emitter already has.
|
|
39
|
+
// - "^" lowers to call("pow", left, right), never a bin node -- bin.op
|
|
40
|
+
// is only ever "+"|"-"|"*"|"/" (see ast.js), and every emitter's
|
|
41
|
+
// `calls` table keys "pow" by name, even for targets whose own
|
|
42
|
+
// syntax has a native ^/** operator.
|
|
43
|
+
const { num, v, add, sub, mul, div, neg, call, cmp, select } = require("./ast.js");
|
|
44
|
+
|
|
45
|
+
const COMPARE_OPS = [">", "<", ">=", "<=", "==", "!="];
|
|
46
|
+
|
|
47
|
+
// Tokenizes one template-literal string segment, appending {type, value,
|
|
48
|
+
// pos} tokens to `tokens` (pos is an offset into the reconstructed full
|
|
49
|
+
// source string built in expr() below, used only for error messages).
|
|
50
|
+
// `label` is just which tag function's name shows up in error messages
|
|
51
|
+
// -- fn.js passes "fn()" here so a lex error inside `` fn`...` `` isn't
|
|
52
|
+
// misattributed to expr().
|
|
53
|
+
function tokenizeSegment(str, offset, tokens, label = "expr()") {
|
|
54
|
+
let i = 0;
|
|
55
|
+
while (i < str.length) {
|
|
56
|
+
const ch = str[i];
|
|
57
|
+
const start = i;
|
|
58
|
+
if (/\s/.test(ch)) {
|
|
59
|
+
i++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// NUMBER: 123, 123.45, .5, 1e-9, 1.5E+10
|
|
63
|
+
if (/[0-9]/.test(ch) || (ch === "." && /[0-9]/.test(str[i + 1] || ""))) {
|
|
64
|
+
i++;
|
|
65
|
+
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
|
66
|
+
if (str[i] === ".") {
|
|
67
|
+
i++;
|
|
68
|
+
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
|
69
|
+
}
|
|
70
|
+
if (str[i] === "e" || str[i] === "E") {
|
|
71
|
+
let j = i + 1;
|
|
72
|
+
if (str[j] === "+" || str[j] === "-") j++;
|
|
73
|
+
if (/[0-9]/.test(str[j] || "")) {
|
|
74
|
+
i = j;
|
|
75
|
+
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
tokens.push({ type: "NUMBER", value: Number(str.slice(start, i)), pos: offset + start });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// IDENT: variable names and function names, e.g. wy_wire, sqrt.
|
|
82
|
+
if (/[A-Za-z_]/.test(ch)) {
|
|
83
|
+
i++;
|
|
84
|
+
while (i < str.length && /[A-Za-z0-9_]/.test(str[i])) i++;
|
|
85
|
+
tokens.push({ type: "IDENT", value: str.slice(start, i), pos: offset + start });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Two-character comparison operators before their one-character
|
|
89
|
+
// prefixes, so ">=" doesn't get lexed as ">" followed by "=".
|
|
90
|
+
if ((ch === ">" || ch === "<" || ch === "=" || ch === "!") && str[i + 1] === "=") {
|
|
91
|
+
tokens.push({ type: "OP", value: str.slice(i, i + 2), pos: offset + start });
|
|
92
|
+
i += 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// ";", "{", "}", "=" aren't used by expr()'s own grammar -- they're
|
|
96
|
+
// here for fn.js's statement syntax (let name = ...; / return
|
|
97
|
+
// {...};) to reuse this same tokenizer instead of forking it.
|
|
98
|
+
// Inert for expr(): nothing that parses successfully today could
|
|
99
|
+
// contain them anyway ("=" alone was always a lex error before,
|
|
100
|
+
// since only "==" was recognized).
|
|
101
|
+
if ("+-*/^(),?:><;{}=".includes(ch)) {
|
|
102
|
+
tokens.push({ type: "OP", value: ch, pos: offset + start });
|
|
103
|
+
i++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`${label}: unexpected character "${ch}" at position ${offset + start}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// A HOLE's value is resolved to a Node right where it's produced (not
|
|
111
|
+
// deferred into the parser), so a bad interpolation fails immediately
|
|
112
|
+
// with a clear error rather than surfacing as a confusing parse error
|
|
113
|
+
// somewhere else in the tree. `label` -- see tokenizeSegment above.
|
|
114
|
+
function holeToNode(value, label = "expr()") {
|
|
115
|
+
if (typeof value === "number") return num(value);
|
|
116
|
+
if (value && typeof value === "object" && typeof value.type === "string") return value;
|
|
117
|
+
const shown = typeof value === "string" ? `"${value}"` : JSON.stringify(value);
|
|
118
|
+
throw new Error(
|
|
119
|
+
`${label}: interpolated value must be an AST node or a plain number, got ${shown} -- ` +
|
|
120
|
+
`a bare variable name doesn't need interpolation, just write it directly in the template text`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
class Parser {
|
|
125
|
+
// `label` -- see tokenizeSegment above; also threaded through to
|
|
126
|
+
// holeToNode so a bad interpolation inside `` fn`...` `` reports
|
|
127
|
+
// "fn():" too, not just lex/parse errors.
|
|
128
|
+
constructor(tokens, source, label = "expr()") {
|
|
129
|
+
this.tokens = tokens;
|
|
130
|
+
this.source = source;
|
|
131
|
+
this.i = 0;
|
|
132
|
+
this.label = label;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
peek() {
|
|
136
|
+
return this.tokens[this.i];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
next() {
|
|
140
|
+
return this.tokens[this.i++];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
isOp(value) {
|
|
144
|
+
const t = this.peek();
|
|
145
|
+
return t.type === "OP" && t.value === value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
expectOp(value) {
|
|
149
|
+
if (!this.isOp(value)) this.error(`expected "${value}"`);
|
|
150
|
+
return this.next();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
error(message) {
|
|
154
|
+
const t = this.peek();
|
|
155
|
+
const tokDesc = t.type === "EOF" ? "end of input" : `"${t.value}"`;
|
|
156
|
+
throw new Error(`${this.label}: ${message} -- found ${tokDesc} at position ${t.pos} in \`${this.source}\``);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
parseExpression() {
|
|
160
|
+
return this.parseTernary();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Only place a comparison is ever accepted -- matches cmp()'s own
|
|
164
|
+
// documented constraint in ast.js exactly (only valid as select()'s
|
|
165
|
+
// cond). A bare comparison with no trailing "?" is a parse error
|
|
166
|
+
// here, not deferred to the "cmp used outside select()" throw every
|
|
167
|
+
// emitter already has.
|
|
168
|
+
parseTernary() {
|
|
169
|
+
const left = this.parseAdditive();
|
|
170
|
+
const t = this.peek();
|
|
171
|
+
if (t.type === "OP" && COMPARE_OPS.includes(t.value)) {
|
|
172
|
+
const op = this.next().value;
|
|
173
|
+
const right = this.parseAdditive();
|
|
174
|
+
if (!this.isOp("?")) {
|
|
175
|
+
this.error(`comparison ("${op}") must be used as a ternary condition ("cond ${op} ... ? then : else")`);
|
|
176
|
+
}
|
|
177
|
+
this.next(); // consume "?"
|
|
178
|
+
const thenNode = this.parseExpression();
|
|
179
|
+
this.expectOp(":");
|
|
180
|
+
const elseNode = this.parseExpression();
|
|
181
|
+
return select(cmp(left, op, right), thenNode, elseNode);
|
|
182
|
+
}
|
|
183
|
+
if (this.isOp("?")) {
|
|
184
|
+
this.error(`"?" needs an explicit comparison as its condition (e.g. "a > 0 ? x : y") -- a bare value can't be a select() condition`);
|
|
185
|
+
}
|
|
186
|
+
return left;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
parseAdditive() {
|
|
190
|
+
let node = this.parseMultiplicative();
|
|
191
|
+
while (this.isOp("+") || this.isOp("-")) {
|
|
192
|
+
const op = this.next().value;
|
|
193
|
+
const right = this.parseMultiplicative();
|
|
194
|
+
node = op === "+" ? add(node, right) : sub(node, right);
|
|
195
|
+
}
|
|
196
|
+
return node;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
parseMultiplicative() {
|
|
200
|
+
let node = this.parseUnary();
|
|
201
|
+
while (this.isOp("*") || this.isOp("/")) {
|
|
202
|
+
const op = this.next().value;
|
|
203
|
+
const right = this.parseUnary();
|
|
204
|
+
node = op === "*" ? mul(node, right) : div(node, right);
|
|
205
|
+
}
|
|
206
|
+
return node;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Unary minus binds LOOSER than "^" (-2^2 = -4, not 4) -- standard
|
|
210
|
+
// math convention, and the reason `power` sits below `unary` here
|
|
211
|
+
// rather than the other way around.
|
|
212
|
+
parseUnary() {
|
|
213
|
+
if (this.isOp("-")) {
|
|
214
|
+
this.next();
|
|
215
|
+
return neg(this.parseUnary());
|
|
216
|
+
}
|
|
217
|
+
if (this.isOp("+")) {
|
|
218
|
+
this.next(); // unary plus: no-op
|
|
219
|
+
return this.parseUnary();
|
|
220
|
+
}
|
|
221
|
+
return this.parsePower();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Right-associative (2^3^2 = 2^(3^2) = 512): the exponent recurses
|
|
225
|
+
// into `unary`, not `power`, which is also what lets the exponent
|
|
226
|
+
// itself carry a leading unary minus (2^-1 = 0.5).
|
|
227
|
+
parsePower() {
|
|
228
|
+
const base = this.parsePrimary();
|
|
229
|
+
if (this.isOp("^")) {
|
|
230
|
+
this.next();
|
|
231
|
+
const exponent = this.parseUnary();
|
|
232
|
+
return call("pow", base, exponent);
|
|
233
|
+
}
|
|
234
|
+
return base;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
parsePrimary() {
|
|
238
|
+
const t = this.peek();
|
|
239
|
+
if (t.type === "NUMBER") {
|
|
240
|
+
this.next();
|
|
241
|
+
return num(t.value);
|
|
242
|
+
}
|
|
243
|
+
if (t.type === "HOLE") {
|
|
244
|
+
this.next();
|
|
245
|
+
return holeToNode(t.value, this.label);
|
|
246
|
+
}
|
|
247
|
+
if (t.type === "IDENT") {
|
|
248
|
+
this.next();
|
|
249
|
+
if (this.isOp("(")) {
|
|
250
|
+
this.next();
|
|
251
|
+
const args = [];
|
|
252
|
+
if (!this.isOp(")")) {
|
|
253
|
+
args.push(this.parseExpression());
|
|
254
|
+
while (this.isOp(",")) {
|
|
255
|
+
this.next();
|
|
256
|
+
args.push(this.parseExpression());
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
this.expectOp(")");
|
|
260
|
+
// Not validated against the 22 known Math functions here
|
|
261
|
+
// -- deferred to the same "no mapping for Math function"
|
|
262
|
+
// check every hand-built call() node already goes
|
|
263
|
+
// through in emitters/base.js, so there's only one list
|
|
264
|
+
// of known function names to keep in sync, not two.
|
|
265
|
+
return call(t.value, ...args);
|
|
266
|
+
}
|
|
267
|
+
return v(t.value);
|
|
268
|
+
}
|
|
269
|
+
if (this.isOp("(")) {
|
|
270
|
+
this.next();
|
|
271
|
+
const node = this.parseExpression();
|
|
272
|
+
this.expectOp(")");
|
|
273
|
+
return node;
|
|
274
|
+
}
|
|
275
|
+
this.error("expected a number, identifier, function call, or parenthesized expression");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// The tagged-template tag function itself: `` expr`a + b` `` is called by
|
|
280
|
+
// JS as expr(["a + b"], ) -- with interpolations, `` expr`${x} + b` ``
|
|
281
|
+
// is called as expr(["", " + b"], x). strings.length is always
|
|
282
|
+
// values.length + 1. There is no "${" text syntax to lex: JS has already
|
|
283
|
+
// done that splitting before this function ever runs, so a HOLE token is
|
|
284
|
+
// just spliced into the token stream at each boundary, carrying the
|
|
285
|
+
// already-evaluated JS value through untouched.
|
|
286
|
+
function expr(strings, ...values) {
|
|
287
|
+
const tokens = [];
|
|
288
|
+
let source = "";
|
|
289
|
+
for (let i = 0; i < strings.length; i++) {
|
|
290
|
+
tokenizeSegment(strings[i], source.length, tokens);
|
|
291
|
+
source += strings[i];
|
|
292
|
+
if (i < values.length) {
|
|
293
|
+
tokens.push({ type: "HOLE", value: values[i], pos: source.length });
|
|
294
|
+
source += "${...}";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
tokens.push({ type: "EOF", value: null, pos: source.length });
|
|
298
|
+
|
|
299
|
+
const parser = new Parser(tokens, source);
|
|
300
|
+
const node = parser.parseExpression();
|
|
301
|
+
if (parser.peek().type !== "EOF") {
|
|
302
|
+
parser.error("unexpected trailing input");
|
|
303
|
+
}
|
|
304
|
+
return node;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Parser/tokenizeSegment/holeToNode are exported alongside expr itself so
|
|
308
|
+
// fn.js (full function-body syntax: let/return on top of this same
|
|
309
|
+
// expression grammar) can reuse this tokenizer and parsing engine
|
|
310
|
+
// directly instead of forking it -- "fn's contain expr's" literally, not
|
|
311
|
+
// just as a description. Nothing here is part of expr()'s own public
|
|
312
|
+
// contract; treat these as internal to the expr/fn syntax family.
|
|
313
|
+
module.exports = { expr, Parser, tokenizeSegment, holeToNode };
|
package/fn.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// exprforge/fn.js
|
|
2
|
+
//
|
|
3
|
+
// Full function-body syntax on top of expr.js's expression grammar: adds
|
|
4
|
+
// `let` bindings and a `return` statement, so a whole function body
|
|
5
|
+
// (let-chain + a single or multi-output result) can be authored as text
|
|
6
|
+
// instead of nested letChain()/outputs() calls. Every individual
|
|
7
|
+
// expression inside a `fn` template -- each let's value, the returned
|
|
8
|
+
// expression(s) -- is parsed by the *same* Parser class expr.js uses,
|
|
9
|
+
// via its parseExpression() entry point. fn's own grammar is a thin
|
|
10
|
+
// statement-sequence wrapper around that, lowering to the real ast.js
|
|
11
|
+
// builders (letChain, outputs), never a new node shape:
|
|
12
|
+
//
|
|
13
|
+
// program := stmt* returnStmt
|
|
14
|
+
// stmt := "let" IDENT "=" expression ";"
|
|
15
|
+
// returnStmt := "return" expression ";"
|
|
16
|
+
// | "return" "{" IDENT ":" expression ("," IDENT ":" expression)* "}" ";"
|
|
17
|
+
//
|
|
18
|
+
// "let"/"return" are recognized contextually -- an IDENT token whose
|
|
19
|
+
// value happens to be "let"/"return" at statement-start position. They
|
|
20
|
+
// are NOT reserved words in expr.js's own grammar, so nothing about
|
|
21
|
+
// expr()'s behavior changes: `` expr`let * 2` `` still means
|
|
22
|
+
// v("let") * 2 today, same as before this file existed.
|
|
23
|
+
//
|
|
24
|
+
// Duplicate let-names are deliberately NOT checked here -- letChain()
|
|
25
|
+
// doesn't check either (ast.js); collectLets() already does, at
|
|
26
|
+
// emission time. Same "defer semantic validation to emission" precedent
|
|
27
|
+
// expr.js itself follows for function/call names.
|
|
28
|
+
const { letChain, outputs } = require("./ast.js");
|
|
29
|
+
const { Parser, tokenizeSegment } = require("./expr.js");
|
|
30
|
+
|
|
31
|
+
function isKeyword(parser, word) {
|
|
32
|
+
const t = parser.peek();
|
|
33
|
+
return t.type === "IDENT" && t.value === word;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function expectIdent(parser, context) {
|
|
37
|
+
const t = parser.peek();
|
|
38
|
+
if (t.type !== "IDENT") {
|
|
39
|
+
parser.error(`expected an identifier ${context}`);
|
|
40
|
+
}
|
|
41
|
+
parser.next();
|
|
42
|
+
return t.value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseLetStatement(parser) {
|
|
46
|
+
parser.next(); // consume "let", already confirmed present by the caller
|
|
47
|
+
const name = expectIdent(parser, 'after "let"');
|
|
48
|
+
parser.expectOp("=");
|
|
49
|
+
const value = parser.parseExpression();
|
|
50
|
+
parser.expectOp(";");
|
|
51
|
+
return [name, value];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseReturnStatement(parser) {
|
|
55
|
+
parser.next(); // consume "return", already confirmed present by the caller
|
|
56
|
+
if (parser.isOp("{")) {
|
|
57
|
+
parser.next();
|
|
58
|
+
const fields = {};
|
|
59
|
+
const readField = () => {
|
|
60
|
+
const name = expectIdent(parser, 'as an output name inside "return { ... }"');
|
|
61
|
+
parser.expectOp(":");
|
|
62
|
+
fields[name] = parser.parseExpression();
|
|
63
|
+
};
|
|
64
|
+
if (!parser.isOp("}")) {
|
|
65
|
+
readField();
|
|
66
|
+
while (parser.isOp(",")) {
|
|
67
|
+
parser.next();
|
|
68
|
+
readField();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
parser.expectOp("}");
|
|
72
|
+
parser.expectOp(";");
|
|
73
|
+
return outputs(fields);
|
|
74
|
+
}
|
|
75
|
+
const node = parser.parseExpression();
|
|
76
|
+
parser.expectOp(";");
|
|
77
|
+
return node;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseProgram(parser) {
|
|
81
|
+
const bindings = [];
|
|
82
|
+
while (isKeyword(parser, "let")) {
|
|
83
|
+
bindings.push(parseLetStatement(parser));
|
|
84
|
+
}
|
|
85
|
+
if (!isKeyword(parser, "return")) {
|
|
86
|
+
parser.error('expected "return" (a fn`...` body is zero or more "let" statements followed by a "return")');
|
|
87
|
+
}
|
|
88
|
+
const body = parseReturnStatement(parser);
|
|
89
|
+
return bindings.length > 0 ? letChain(bindings, body) : body;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Same token-splicing loop expr() uses in expr.js -- see that file's
|
|
93
|
+
// header comment for why there's no "${" text syntax to lex separately;
|
|
94
|
+
// the only difference here is the entry point (parseProgram instead of
|
|
95
|
+
// parser.parseExpression()).
|
|
96
|
+
function fn(strings, ...values) {
|
|
97
|
+
const tokens = [];
|
|
98
|
+
let source = "";
|
|
99
|
+
for (let i = 0; i < strings.length; i++) {
|
|
100
|
+
tokenizeSegment(strings[i], source.length, tokens, "fn()");
|
|
101
|
+
source += strings[i];
|
|
102
|
+
if (i < values.length) {
|
|
103
|
+
tokens.push({ type: "HOLE", value: values[i], pos: source.length });
|
|
104
|
+
source += "${...}";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
tokens.push({ type: "EOF", value: null, pos: source.length });
|
|
108
|
+
|
|
109
|
+
const parser = new Parser(tokens, source, "fn()");
|
|
110
|
+
const node = parseProgram(parser);
|
|
111
|
+
if (parser.peek().type !== "EOF") {
|
|
112
|
+
parser.error("unexpected trailing input");
|
|
113
|
+
}
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { fn };
|