exprforge 0.1.0 → 0.2.1

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.
@@ -0,0 +1,95 @@
1
+ // exprforge/emitters/perl.js
2
+ const Emitter = require("./base.js");
3
+
4
+ function fn1(name) {
5
+ return ([x]) => `${name}(${x})`;
6
+ }
7
+
8
+ function posix1(name) {
9
+ return ([x]) => `POSIX::${name}(${x})`;
10
+ }
11
+
12
+ // Every scalar variable reference in Perl needs a leading `$` -- unlike
13
+ // every other emitter here, base.js's default "var" case (bare
14
+ // `node.name`) is wrong for every single reference, not just declarations.
15
+ // Emitter is a real class (see base.js), so this overrides just the one
16
+ // case and defers to the base class for everything else, instead of
17
+ // needing a new config hook shared by every other emitter.
18
+ class PerlEmitter extends Emitter {
19
+ emitExpr(node) {
20
+ if (node.type === "var") return `$${node.name}`;
21
+ return super.emitExpr(node);
22
+ }
23
+ }
24
+
25
+ const emitter = new PerlEmitter({
26
+ ext: "pl",
27
+ // Perl accepts JS-style numeric literal syntax directly, including
28
+ // exponential notation ("1e-9") -- no suffix or conversion needed.
29
+ formatNumber: (v) => String(v),
30
+ calls: {
31
+ // Core builtins -- no module needed.
32
+ sqrt: fn1("sqrt"), abs: fn1("abs"), sin: fn1("sin"), cos: fn1("cos"),
33
+ exp: fn1("exp"), log: fn1("log"), atan2: ([a, b]) => `atan2(${a}, ${b})`,
34
+ pow: ([x, y]) => `(${x} ** ${y})`,
35
+ // Everything else Perl core doesn't have is in the POSIX module --
36
+ // called fully-qualified (POSIX::name) rather than imported, so
37
+ // there's no import list to keep in sync with this table and no
38
+ // risk of a POSIX symbol shadowing a core builtin of the same name.
39
+ tan: posix1("tan"), asin: posix1("asin"), acos: posix1("acos"), atan: posix1("atan"),
40
+ log10: posix1("log10"), floor: posix1("floor"), ceil: posix1("ceil"),
41
+ round: posix1("round"), trunc: posix1("trunc"),
42
+ hypot: ([a, b]) => `POSIX::hypot(${a}, ${b})`,
43
+ // No log2 anywhere in core or POSIX -- derive it.
44
+ log2: ([x]) => `(log(${x}) / log(2))`,
45
+ // List::Util, same fully-qualified convention as POSIX above.
46
+ min: ([a, b]) => `List::Util::min(${a}, ${b})`,
47
+ max: ([a, b]) => `List::Util::max(${a}, ${b})`,
48
+ // No sign() anywhere in core, POSIX, or List::Util -- build it
49
+ // directly. Zero-aware by construction (see Go's/Rust's sign()
50
+ // history in this project for what happens when it isn't).
51
+ sign: ([x]) => `(${x} > 0 ? 1.0 : (${x} < 0 ? -1.0 : 0.0))`,
52
+ },
53
+ // Perl's ?: is exactly base.js's default ternary -- no override needed.
54
+ formatFunction: (fn, body, letBindings = []) => {
55
+ const params = fn.params.length ? ` my (${fn.params.map((p) => `$${p}`).join(", ")}) = @_;\n` : "";
56
+ const lets = letBindings.map(({ name, valueStr }) => ` my $${name} = ${valueStr};`).join("\n");
57
+ const letsBlock = lets ? lets + "\n" : "";
58
+ return `# AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
59
+ `use strict;\n` +
60
+ `use warnings;\n` +
61
+ `use POSIX ();\n` +
62
+ `use List::Util ();\n\n` +
63
+ `sub ${fn.name} {\n` +
64
+ params +
65
+ letsBlock +
66
+ ` return ${body};\n` +
67
+ `}\n\n` +
68
+ `1;\n`;
69
+ },
70
+ // Multiple named outputs from one call: a plain hash ref (`{ rx => ...,
71
+ // ry => ... }`), Perl's lightest-weight named-record idiom -- no
72
+ // package/class needed just to carry a few doubles back to the caller.
73
+ formatSuite: (fn, outputStrs, letBindings = []) => {
74
+ const params = fn.params.length ? ` my (${fn.params.map((p) => `$${p}`).join(", ")}) = @_;\n` : "";
75
+ const lets = letBindings.map(({ name, valueStr }) => ` my $${name} = ${valueStr};`).join("\n");
76
+ const letsBlock = lets ? lets + "\n" : "";
77
+ const outputNames = Object.keys(outputStrs);
78
+ const fields = outputNames.map((n) => ` ${n} => ${outputStrs[n]},`).join("\n");
79
+ return `# AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
80
+ `use strict;\n` +
81
+ `use warnings;\n` +
82
+ `use POSIX ();\n` +
83
+ `use List::Util ();\n\n` +
84
+ `sub ${fn.name} {\n` +
85
+ params +
86
+ letsBlock +
87
+ ` return {\n` +
88
+ fields + "\n" +
89
+ ` };\n` +
90
+ `}\n\n` +
91
+ `1;\n`;
92
+ },
93
+ });
94
+
95
+ module.exports = emitter;
@@ -0,0 +1,79 @@
1
+ // exprforge/emitters/php.js
2
+ const Emitter = require("./base.js");
3
+
4
+ function fn1(name) {
5
+ return ([x]) => `${name}(${x})`;
6
+ }
7
+
8
+ function fn2(name) {
9
+ return ([a, b]) => `${name}(${a}, ${b})`;
10
+ }
11
+
12
+ // Every variable reference in PHP needs a leading `$` -- unlike every other
13
+ // emitter here, base.js's default "var" case (bare `node.name`) is wrong
14
+ // for every single reference, not just declarations. Same fix as Perl's
15
+ // emitter: override just the one case, defer to the base class otherwise.
16
+ class PhpEmitter extends Emitter {
17
+ emitExpr(node) {
18
+ if (node.type === "var") return `$${node.name}`;
19
+ return super.emitExpr(node);
20
+ }
21
+ }
22
+
23
+ const emitter = new PhpEmitter({
24
+ ext: "php",
25
+ // PHP accepts JS-style numeric literal syntax directly, including
26
+ // exponential notation ("1e-9") -- no suffix or conversion needed.
27
+ formatNumber: (v) => String(v),
28
+ calls: {
29
+ sqrt: fn1("sqrt"), abs: fn1("abs"), sin: fn1("sin"), cos: fn1("cos"), tan: fn1("tan"),
30
+ asin: fn1("asin"), acos: fn1("acos"), atan: fn1("atan"), atan2: fn2("atan2"),
31
+ log: fn1("log"), log10: fn1("log10"), exp: fn1("exp"), pow: fn2("pow"),
32
+ floor: fn1("floor"), ceil: fn1("ceil"), round: fn1("round"),
33
+ min: fn2("min"), max: fn2("max"), hypot: fn2("hypot"),
34
+ // log() takes an optional base argument -- covers log2 without a
35
+ // separate function (PHP has no log2() of its own).
36
+ log2: ([x]) => `log(${x}, 2)`,
37
+ // No trunc() anywhere in PHP core. floor for non-negatives, ceil
38
+ // for negatives -- avoids an (int) cast, which would silently
39
+ // misbehave outside PHP's platform integer range.
40
+ trunc: ([x]) => `(${x} >= 0 ? floor(${x}) : ceil(${x}))`,
41
+ // No sign() either -- build it directly. Zero-aware by construction
42
+ // (see Go's/Rust's sign() history in this project for what happens
43
+ // when it isn't).
44
+ sign: ([x]) => `(${x} > 0 ? 1.0 : (${x} < 0 ? -1.0 : 0.0))`,
45
+ },
46
+ // PHP's ?: is exactly base.js's default ternary -- no override needed.
47
+ formatFunction: (fn, body, letBindings = []) => {
48
+ const params = fn.params.map((p) => `$${p}`).join(", ");
49
+ const lets = letBindings.map(({ name, valueStr }) => ` $${name} = ${valueStr};`).join("\n");
50
+ const letsBlock = lets ? lets + "\n" : "";
51
+ return `<?php\n` +
52
+ `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n\n` +
53
+ `function ${fn.name}(${params}) {\n` +
54
+ letsBlock +
55
+ ` return ${body};\n` +
56
+ `}\n`;
57
+ },
58
+ // Multiple named outputs from one call: a plain associative array
59
+ // (`['rx' => ..., 'ry' => ...]`), PHP's lightest-weight named-record
60
+ // idiom -- no class needed just to carry a few doubles back to the
61
+ // caller.
62
+ formatSuite: (fn, outputStrs, letBindings = []) => {
63
+ const params = fn.params.map((p) => `$${p}`).join(", ");
64
+ const lets = letBindings.map(({ name, valueStr }) => ` $${name} = ${valueStr};`).join("\n");
65
+ const letsBlock = lets ? lets + "\n" : "";
66
+ const outputNames = Object.keys(outputStrs);
67
+ const fields = outputNames.map((n) => ` '${n}' => ${outputStrs[n]},`).join("\n");
68
+ return `<?php\n` +
69
+ `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n\n` +
70
+ `function ${fn.name}(${params}) {\n` +
71
+ letsBlock +
72
+ ` return [\n` +
73
+ fields + "\n" +
74
+ ` ];\n` +
75
+ `}\n`;
76
+ },
77
+ });
78
+
79
+ module.exports = emitter;
@@ -12,4 +12,11 @@ module.exports = {
12
12
  csharp: require("./csharp.js"),
13
13
  python: require("./python.js"),
14
14
  lua: require("./lua.js"),
15
+ perl: require("./perl.js"),
16
+ php: require("./php.js"),
17
+ julia: require("./julia.js"),
18
+ fortran: require("./fortran.js"),
19
+ zig: require("./zig.js"),
20
+ scheme: require("./scheme.js"),
21
+ cobol: require("./cobol.js"),
15
22
  };
@@ -0,0 +1,154 @@
1
+ // exprforge/emitters/scheme.js
2
+ //
3
+ // Targets Guile (R7RS-ish Scheme) specifically -- see
4
+ // test/conformance.test.js for the exact `guile3.0` invocation this is
5
+ // verified against. Prefix notation throughout: this project's "bin" node
6
+ // is the one thing every OTHER emitter can handle via base.js's default
7
+ // infix `(${L} ${op} ${R})` -- Scheme is the one target here where even
8
+ // `+`/`-`/`*`/`/` themselves need to move into the operator position, so
9
+ // this overrides emitExpr's "bin" case specifically (Emitter is a real
10
+ // class -- see base.js -- so this is a small subclass, not a new shared
11
+ // hook every other emitter would have to ignore).
12
+ const Emitter = require("./base.js");
13
+
14
+ // Guile special forms plus the procedure names this emitter's own calls
15
+ // table depends on -- same role as QB64_RESERVED in emitters/qb64.js.
16
+ // Scheme technically allows shadowing a procedure name like `sqrt` with a
17
+ // local binding, but doing so would break every OTHER call in the same
18
+ // scope that still expects it to mean the real one, so it's guarded here
19
+ // same as a true syntactic keyword.
20
+ const SCHEME_RESERVED = new Set([
21
+ "define", "lambda", "let", "let*", "letrec", "letrec*", "if", "cond", "case",
22
+ "and", "or", "not", "begin", "set!", "quote", "quasiquote", "unquote", "do",
23
+ "delay", "values", "call-with-values", "else", "define-record-type",
24
+ "sqrt", "abs", "sin", "cos", "tan", "asin", "acos", "atan", "exp", "log",
25
+ "expt", "floor", "ceiling", "round", "truncate", "min", "max",
26
+ ]);
27
+
28
+ function checkReservedNames(names) {
29
+ for (const name of names) {
30
+ if (SCHEME_RESERVED.has(name)) {
31
+ throw new Error(
32
+ `emitter for .scm: "${name}" is a reserved Scheme special form/procedure name and can't be used ` +
33
+ `as a function/variable/parameter name -- rename it (see SCHEME_RESERVED in emitters/scheme.js)`,
34
+ );
35
+ }
36
+ }
37
+ }
38
+
39
+ class SchemeEmitter extends Emitter {
40
+ emitExpr(node) {
41
+ if (node.type === "bin") {
42
+ // ast.js's op set (+ - * /) is already valid Scheme procedure-
43
+ // position syntax verbatim -- no translation table needed,
44
+ // just moving it from infix to prefix position.
45
+ return `(${node.op} ${this.emitExpr(node.left)} ${this.emitExpr(node.right)})`;
46
+ }
47
+ return super.emitExpr(node);
48
+ }
49
+ }
50
+
51
+ function fn1(name) {
52
+ return ([x]) => `(${name} ${x})`;
53
+ }
54
+
55
+ function fn2(name) {
56
+ return ([a, b]) => `(${name} ${a} ${b})`;
57
+ }
58
+
59
+ const emitter = new SchemeEmitter({
60
+ ext: "scm",
61
+ // Guile accepts JS-style numeric literal syntax directly, including
62
+ // exponential notation ("1e-9") -- no conversion needed for the digits
63
+ // themselves. But a bare integer literal like "2" is EXACT in Scheme's
64
+ // reader syntax, and exact arithmetic that never happens to touch an
65
+ // inexact (float) operand stays exact -- confirmed a real compiler
66
+ // prints (/ 1 3) as the fraction "1/3", not "0.333...". Every literal
67
+ // this project emits is meant to behave as an IEEE double like every
68
+ // other target, so any literal with neither a decimal point nor an
69
+ // exponent marker gets ".0" appended, forcing inexactness by literal
70
+ // syntax alone -- not relying on some other operand in the same
71
+ // expression happening to already be inexact.
72
+ formatNumber: (v) => {
73
+ const s = String(v);
74
+ return /[.e]/i.test(s) ? s : `${s}.0`;
75
+ },
76
+ calls: {
77
+ sqrt: fn1("sqrt"), abs: fn1("abs"), sin: fn1("sin"), cos: fn1("cos"), tan: fn1("tan"),
78
+ asin: fn1("asin"), acos: fn1("acos"), atan: fn1("atan"), exp: fn1("exp"), log: fn1("log"),
79
+ // expt is Scheme's exponentiation procedure -- there's no infix **.
80
+ pow: fn2("expt"),
81
+ // R7RS's 2-argument atan IS atan2 -- no separate name for it.
82
+ atan2: fn2("atan"),
83
+ // No log2/log10 procedure in R7RS or Guile's core -- derive both.
84
+ log2: ([x]) => `(/ (log ${x}) (log 2.0))`,
85
+ log10: ([x]) => `(/ (log ${x}) (log 10.0))`,
86
+ floor: fn1("floor"), ceil: fn1("ceiling"),
87
+ // Guile's round is round-half-to-even (banker's rounding), not the
88
+ // round-half-away-from-zero most other targets here use -- same
89
+ // already-documented, already-avoided-in-tests divergence as
90
+ // Lua's/Julia's round(), not a new one (see
91
+ // test/conformance.test.js's kitchen-sink comment).
92
+ round: fn1("round"),
93
+ trunc: fn1("truncate"),
94
+ min: fn2("min"), max: fn2("max"),
95
+ // No hypot procedure -- derive it directly.
96
+ hypot: ([a, b]) => `(sqrt (+ (* ${a} ${a}) (* ${b} ${b})))`,
97
+ // No sign procedure either -- build it directly. Zero-aware by
98
+ // construction (see Go's/Rust's sign() history in this project for
99
+ // what happens when it isn't).
100
+ sign: ([x]) => `(if (> ${x} 0.0) 1.0 (if (< ${x} 0.0) -1.0 0.0))`,
101
+ },
102
+ // Scheme's `if` already IS an expression (no separate statement form),
103
+ // so this is the most direct emitSelect override of any target here --
104
+ // just prefix notation for the comparison, same as every "bin" node.
105
+ // "!=" needs `(not (= ...))`: R7RS has no single-procedure not-equal.
106
+ emitSelect: function (condNode, thenStr, elseStr) {
107
+ const L = this.emitExpr(condNode.left);
108
+ const R = this.emitExpr(condNode.right);
109
+ const condExpr =
110
+ condNode.op === "!=" ? `(not (= ${L} ${R}))` : `(${condNode.op === "==" ? "=" : condNode.op} ${L} ${R})`;
111
+ return `(if ${condExpr} ${thenStr} ${elseStr})`;
112
+ },
113
+ formatFunction: (fn, body, letBindings = []) => {
114
+ checkReservedNames([fn.name, ...fn.params, ...letBindings.map((b) => b.name)]);
115
+ const params = fn.params.join(" ");
116
+ // let* (not let): each binding can see every earlier one, matching
117
+ // the dependency order collectLets already produced -- exactly
118
+ // what our flat, ordered bindings list needs, no extra nesting.
119
+ if (letBindings.length === 0) {
120
+ return `;; AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
121
+ `(define (${fn.name} ${params})\n` +
122
+ ` ${body})\n`;
123
+ }
124
+ const lets = letBindings.map(({ name, valueStr }) => ` (${name} ${valueStr})`).join("\n");
125
+ return `;; AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
126
+ `(define (${fn.name} ${params})\n` +
127
+ ` (let* (${lets.trimStart()})\n` +
128
+ ` ${body}))\n`;
129
+ },
130
+ // Multiple named outputs from one call: Scheme's native (values ...)
131
+ // -- same idea as Lua's native multiple return, positional rather than
132
+ // named, so a leading comment documents field order (matching Lua's
133
+ // convention here) since Scheme's values have no names at the call
134
+ // site.
135
+ formatSuite: (fn, outputStrs, letBindings = []) => {
136
+ const outputNames = Object.keys(outputStrs);
137
+ checkReservedNames([fn.name, ...fn.params, ...outputNames, ...letBindings.map((b) => b.name)]);
138
+ const params = fn.params.join(" ");
139
+ const returnExpr = `(values ${outputNames.map((n) => outputStrs[n]).join(" ")})`;
140
+ const header =
141
+ `;; AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
142
+ `;; Returns (values ${outputNames.join(" ")}).\n`;
143
+ if (letBindings.length === 0) {
144
+ return header + `(define (${fn.name} ${params})\n` + ` ${returnExpr})\n`;
145
+ }
146
+ const lets = letBindings.map(({ name, valueStr }) => ` (${name} ${valueStr})`).join("\n");
147
+ return header +
148
+ `(define (${fn.name} ${params})\n` +
149
+ ` (let* (${lets.trimStart()})\n` +
150
+ ` ${returnExpr}))\n`;
151
+ },
152
+ });
153
+
154
+ module.exports = emitter;
@@ -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/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  // exprforge/index.js
2
- const { num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets } = require("./ast.js");
2
+ const { num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets } = require("./ast.js");
3
3
  const { forComponents } = require("./util.js");
4
4
  const emitters = require("./emitters/registry.js");
5
5
  const { catmullRomAst } = require("./samples/catmull-rom.js");
6
6
  const { fibonacciAst } = require("./samples/fibonacci.js");
7
7
  const { splineFrameAsts } = require("./samples/spline-frame.js");
8
8
  const { kitchenSinkAst } = require("./samples/kitchen-sink.js");
9
+ const { mathDemoAst } = require("./samples/math-demo.js");
9
10
 
10
11
  /**
11
12
  * Run every registered emitter against one AST function definition.
@@ -21,7 +22,7 @@ function emitAll(fn) {
21
22
 
22
23
  module.exports = {
23
24
  // AST builders — use these to define your own formulas.
24
- num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets,
25
+ num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets,
25
26
  // Authoring convenience — not an AST primitive, see util.js.
26
27
  forComponents,
27
28
  // Built-in example formulas — see samples/ for the source.
@@ -31,11 +32,15 @@ module.exports = {
31
32
  // Not a worked example -- a conformance-test fixture that calls every
32
33
  // supported Math function once. See samples/kitchen-sink.js.
33
34
  kitchenSinkAst,
35
+ // Also not a worked example -- a conformance-test fixture for
36
+ // require("exprforge/math"). See samples/math-demo.js.
37
+ mathDemoAst,
34
38
  samples: {
35
39
  catmullRom: catmullRomAst,
36
40
  fibonacci: fibonacciAst,
37
41
  splineFrame: splineFrameAsts,
38
42
  kitchenSink: kitchenSinkAst,
43
+ mathDemo: mathDemoAst,
39
44
  },
40
45
  // Per-language emitter instances, keyed by name (js, qb64, c, java, go, rust).
41
46
  emitters,
package/math/index.js ADDED
@@ -0,0 +1,113 @@
1
+ // exprforge/math/index.js
2
+ // Standard math utilities — see docs/v0.2.0-math-utilities.md for the
3
+ // design doc this implements. This module is level 1: pure compositions of
4
+ // the level-0 AST primitives (ast.js), built to save every consumer from
5
+ // re-deriving the same safe-math/vector patterns samples/spline-frame.js
6
+ // used to hand-roll locally (dot3/len3/safeDiv/EPS there predate this file
7
+ // and motivated it). No new emitter logic — emitAll handles these
8
+ // transparently, same as any other AST the caller builds by hand.
9
+ //
10
+ // require("exprforge/math") is a separate export path from require("exprforge")
11
+ // itself (see package.json's "exports" map) — additive, not merged into the
12
+ // core barrel.
13
+ const { num, v, call, add, mul, sub, div, letIn, cmp, select } = require("../ast.js");
14
+
15
+ // Shared epsilon for all near-zero guards below. Exposed so callers can
16
+ // reuse it in their own cmp() calls for consistency with safeDiv/normalize3,
17
+ // same convention as samples/spline-frame.js's local EPS.
18
+ const EPS = num(0.000001);
19
+
20
+ // Guard against division by zero: numerator/denominatorExpr when
21
+ // |denominatorExpr| > EPS, else fallback. Per select()'s doc comment in
22
+ // ast.js, both branches of a select are always evaluated on every target —
23
+ // so this does NOT guard the division directly (div(numerator,
24
+ // denominatorExpr) would still be reached with a near-zero denominator on
25
+ // any target that can't short-circuit, e.g. QB64). Instead the denominator
26
+ // is clamped to a safe, always-nonzero value by its own select first,
27
+ // mirroring the local safeDiv in samples/spline-frame.js.
28
+ //
29
+ // denominatorExpr is referenced twice in the resulting tree (once for the
30
+ // |.| > EPS check, once in the clamped-denominator select) — cheap if it's
31
+ // a var reference or a simple expression, but if it's an expensive
32
+ // subexpression (e.g. a len3() call), pass an already-letIn-bound v(name)
33
+ // instead of the raw expression to avoid computing it twice per emitted
34
+ // target. normalize3() below does exactly that internally.
35
+ function safeDiv(numerator, denominatorExpr, fallback) {
36
+ const isSafe = cmp(call("abs", denominatorExpr), ">", EPS);
37
+ const safeDenom = select(isSafe, denominatorExpr, num(1));
38
+ return select(isSafe, div(numerator, safeDenom), fallback);
39
+ }
40
+
41
+ // 3-D dot product: ax*bx + ay*by + az*bz.
42
+ function dot3(ax, ay, az, bx, by, bz) {
43
+ return add(mul(ax, bx), mul(ay, by), mul(az, bz));
44
+ }
45
+
46
+ // 3-D Euclidean length: sqrt(x² + y² + z²). Emits a sqrt intrinsic (see
47
+ // README's "Supported Math functions").
48
+ function len3(x, y, z) {
49
+ return call("sqrt", dot3(x, y, z, x, y, z));
50
+ }
51
+
52
+ // 3-D cross product. Returns a plain JS object { x, y, z } of AST nodes,
53
+ // NOT an AST node itself — a deliberate ergonomic choice so callers can
54
+ // destructure and name each component in their own letIn chain, rather
55
+ // than exprforge picking the names for them.
56
+ function cross3(ax, ay, az, bx, by, bz) {
57
+ return {
58
+ x: sub(mul(ay, bz), mul(az, by)),
59
+ y: sub(mul(az, bx), mul(ax, bz)),
60
+ z: sub(mul(ax, by), mul(ay, bx)),
61
+ };
62
+ }
63
+
64
+ // Monotonic counter behind normalize3's internal let-binding names — see
65
+ // the comment inside normalize3 for why it needs one at all. Global (not
66
+ // per-call-site) is deliberately overkill: it only has to avoid colliding
67
+ // with another normalize3() binding inside the same function body, and a
68
+ // process-wide counter trivially guarantees that regardless of how many
69
+ // times normalize3 is called across however many functions.
70
+ //
71
+ // The name itself starts with a letter, not "__" -- confirmed against a
72
+ // real Fortran compiler ("Invalid character in name") that a leading
73
+ // underscore isn't a valid identifier start there, unlike JS/Python/etc.
74
+ // This is an internal implementation detail (never part of any documented
75
+ // return value or public name), so there's nothing for a leading-"__"
76
+ // convention to usefully signal here that a portable identifier can't
77
+ // signal just as well.
78
+ let normalizeGensymCounter = 0;
79
+
80
+ // Safe-normalize a 3-D vector. Returns { x, y, z } (same shape as cross3).
81
+ // Falls back to (fx, fy, fz) — default (0, 1, 0) — when the vector's length
82
+ // is at or below EPS.
83
+ //
84
+ // Per the spec's recommendation, this computes len3(x, y, z) ONCE and
85
+ // shares it across all three divisions (one EPS check, one sqrt), instead
86
+ // of calling safeDiv three times against three independent len3() calls.
87
+ // The mechanism: the length is let-bound inside the `x` field's own tree,
88
+ // and `y`/`z` just reference that bound name bare. collectLets (ast.js)
89
+ // hoists a let found anywhere in a function body to one flat, ordered list
90
+ // regardless of which sibling subtree it was found in — see
91
+ // test/ast.test.js's "collectLets hoists a let nested inside one output
92
+ // field's own value" for the exact behavior this relies on. The gensym'd
93
+ // name avoids a "duplicate let binding name" throw if normalize3 is called
94
+ // more than once inside one function (e.g. normalizing two vectors).
95
+ function normalize3(x, y, z, fx = num(0), fy = num(1), fz = num(0)) {
96
+ const lenName = `efMathNrmLen${normalizeGensymCounter++}`;
97
+ return {
98
+ x: letIn(lenName, len3(x, y, z), safeDiv(x, v(lenName), fx)),
99
+ y: safeDiv(y, v(lenName), fy),
100
+ z: safeDiv(z, v(lenName), fz),
101
+ };
102
+ }
103
+
104
+ // Clamps val to [lo, hi]. Expressed as nested select/cmp — no runtime
105
+ // intrinsic required, matching cmp/select's existing usage elsewhere (see
106
+ // samples/spline-frame.js). val is referenced three times in the resulting
107
+ // tree; pass a var reference (or an already-let-bound one) if it's not
108
+ // already cheap to re-evaluate.
109
+ function clamp(val, lo, hi) {
110
+ return select(cmp(val, "<", lo), lo, select(cmp(val, ">", hi), hi, val));
111
+ }
112
+
113
+ module.exports = { EPS, safeDiv, dot3, len3, cross3, normalize3, clamp };
package/package.json CHANGED
@@ -1,16 +1,22 @@
1
1
  {
2
2
  "name": "exprforge",
3
- "version": "0.1.0",
4
- "description": "Author a math expression once as an AST, emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, and Rust.",
3
+ "version": "0.2.1",
4
+ "description": "Author a math expression once as an AST, emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, Rust, Perl, PHP, Julia, Fortran, Zig, Scheme, and COBOL.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./math": "./math/index.js",
10
+ "./package.json": "./package.json"
11
+ },
7
12
  "files": [
8
13
  "index.js",
9
14
  "ast.js",
10
15
  "util.js",
11
16
  "build.js",
12
17
  "emitters/",
13
- "samples/"
18
+ "samples/",
19
+ "math/"
14
20
  ],
15
21
  "scripts": {
16
22
  "build": "node build.js",