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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// exprforge/emitters/fortran.js
|
|
2
|
+
const Emitter = require("./base.js");
|
|
3
|
+
|
|
4
|
+
// Fortran keywords/statement words plus every intrinsic this emitter's own
|
|
5
|
+
// calls table uses -- same role as QB64_RESERVED in emitters/qb64.js.
|
|
6
|
+
// Fortran is case-insensitive, so names are checked lowercased. Not
|
|
7
|
+
// exhaustive (Fortran has no fixed reserved-word list at all -- context
|
|
8
|
+
// determines meaning), but covers the words a generated variable/parameter/
|
|
9
|
+
// function name could plausibly collide with in practice.
|
|
10
|
+
const FORTRAN_RESERVED = new Set([
|
|
11
|
+
"program", "subroutine", "function", "end", "implicit", "none",
|
|
12
|
+
"real", "integer", "double", "precision", "complex", "logical", "character",
|
|
13
|
+
"dimension", "intent", "in", "out", "inout", "result", "kind",
|
|
14
|
+
"if", "then", "else", "elseif", "endif", "do", "while", "continue", "exit", "cycle",
|
|
15
|
+
"select", "case", "where", "forall", "goto", "stop", "return", "call",
|
|
16
|
+
"contains", "module", "use", "interface", "type", "class",
|
|
17
|
+
"print", "write", "read", "format", "data", "parameter", "common", "equivalence",
|
|
18
|
+
"allocate", "deallocate", "pointer", "target", "public", "private",
|
|
19
|
+
"elemental", "pure", "recursive", "merge",
|
|
20
|
+
"sqrt", "abs", "sin", "cos", "tan", "asin", "acos", "atan", "atan2",
|
|
21
|
+
"log", "log10", "exp", "floor", "ceiling", "anint", "aint", "nint",
|
|
22
|
+
"min", "max", "hypot", "sign", "mod", "len", "len_trim", "trim", "index",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function checkReservedNames(names) {
|
|
26
|
+
for (const name of names) {
|
|
27
|
+
if (FORTRAN_RESERVED.has(name.toLowerCase())) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`emitter for .f90: "${name}" is a reserved Fortran keyword/intrinsic and can't be used as a ` +
|
|
30
|
+
`function/variable/parameter name -- rename it (see FORTRAN_RESERVED in emitters/fortran.js)`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fn1(name) {
|
|
37
|
+
return ([x]) => `${name}(${x})`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fn2(name) {
|
|
41
|
+
return ([a, b]) => `${name}(${a}, ${b})`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fortran free-form source has a real, standards-mandated 132-character
|
|
45
|
+
// line limit -- confirmed the hard way (a real compiler, "Line truncated
|
|
46
|
+
// ... [-Werror=line-truncation]") on samples/catmull-rom.js's one-line
|
|
47
|
+
// polynomial, which a different gfortran build/version apparently let
|
|
48
|
+
// through as a non-fatal warning during development, masking this until a
|
|
49
|
+
// stricter compiler caught it for real. A trailing `&` continues a
|
|
50
|
+
// statement onto the next line (confirmed against a real compiler) -- long
|
|
51
|
+
// lines get broken at word boundaries well under the actual limit.
|
|
52
|
+
function wrapLine(line, maxWidth = 100) {
|
|
53
|
+
if (line.length <= maxWidth) return line;
|
|
54
|
+
const words = line.split(" ");
|
|
55
|
+
const wrapped = [];
|
|
56
|
+
let current = "";
|
|
57
|
+
for (const word of words) {
|
|
58
|
+
if (current && current.length + 1 + word.length > maxWidth) {
|
|
59
|
+
wrapped.push(`${current} &`);
|
|
60
|
+
current = ` ${word}`;
|
|
61
|
+
} else {
|
|
62
|
+
current = current ? `${current} ${word}` : word;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (current) wrapped.push(current);
|
|
66
|
+
return wrapped.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const emitter = new Emitter({
|
|
70
|
+
ext: "f90",
|
|
71
|
+
// Fortran's D exponent marker (not E) forces a literal to be
|
|
72
|
+
// double-precision regardless of context -- same reasoning as QB64's #
|
|
73
|
+
// suffix/D marker (see qb64.js). Without it, a plain "3.14159" literal
|
|
74
|
+
// is parsed as single precision FIRST, then widened -- silently losing
|
|
75
|
+
// precision before it ever reaches a real(8) variable. Every literal
|
|
76
|
+
// gets this, not just ones already in scientific notation.
|
|
77
|
+
formatNumber: (v) => {
|
|
78
|
+
const s = String(v);
|
|
79
|
+
if (/e/i.test(s)) return s.replace(/e/i, "D");
|
|
80
|
+
return s.includes(".") ? `${s}D0` : `${s}.0D0`;
|
|
81
|
+
},
|
|
82
|
+
calls: {
|
|
83
|
+
sqrt: fn1("SQRT"), abs: fn1("ABS"), sin: fn1("SIN"), cos: fn1("COS"), tan: fn1("TAN"),
|
|
84
|
+
asin: fn1("ASIN"), acos: fn1("ACOS"), atan: fn1("ATAN"), atan2: fn2("ATAN2"),
|
|
85
|
+
log: fn1("LOG"), log10: fn1("LOG10"), exp: fn1("EXP"),
|
|
86
|
+
pow: ([x, y]) => `(${x} ** ${y})`,
|
|
87
|
+
min: fn2("MIN"), max: fn2("MAX"),
|
|
88
|
+
// HYPOT is an F2008 intrinsic -- no need to derive it by hand.
|
|
89
|
+
hypot: fn2("HYPOT"),
|
|
90
|
+
// ANINT/AINT already return a REAL of the same kind as their
|
|
91
|
+
// argument (confirmed: real(8) in, real(8) out) -- unlike
|
|
92
|
+
// FLOOR/CEILING below, no conversion needed. ANINT rounds ties
|
|
93
|
+
// away from zero, matching every other target here.
|
|
94
|
+
round: fn1("ANINT"),
|
|
95
|
+
trunc: fn1("AINT"),
|
|
96
|
+
// FLOOR/CEILING return the default INTEGER kind, not REAL --
|
|
97
|
+
// REAL(..., 8) converts back to double, matching this project's
|
|
98
|
+
// float64-only model everywhere else (same reasoning as Python's
|
|
99
|
+
// float(math.floor(...))).
|
|
100
|
+
floor: ([x]) => `REAL(FLOOR(${x}), 8)`,
|
|
101
|
+
ceil: ([x]) => `REAL(CEILING(${x}), 8)`,
|
|
102
|
+
// No LOG2 intrinsic -- derive it.
|
|
103
|
+
log2: ([x]) => `(LOG(${x}) / LOG(2.0D0))`,
|
|
104
|
+
// The native SIGN(A, B) intrinsic ("magnitude of A, sign of B") is
|
|
105
|
+
// NOT this project's sign(x) -- confirmed against a real compiler
|
|
106
|
+
// that SIGN(1.0D0, 0.0D0) returns 1.0D0, not 0.0D0 (IEEE 754
|
|
107
|
+
// treats +0.0 as positive-signed). Built from MERGE instead, same
|
|
108
|
+
// zero-aware construction as every other emitter here that can't
|
|
109
|
+
// trust its language's native sign function at exactly zero (see
|
|
110
|
+
// Go's/Rust's sign() history in this project).
|
|
111
|
+
sign: ([x]) => `MERGE(1.0D0, MERGE(-1.0D0, 0.0D0, (${x}) < 0.0D0), (${x}) > 0.0D0)`,
|
|
112
|
+
},
|
|
113
|
+
// Fortran has no ternary operator, but MERGE(TSOURCE, FSOURCE, MASK) is
|
|
114
|
+
// exactly an expression-level conditional value-select -- confirmed
|
|
115
|
+
// against a real compiler to behave like this project's select(), down
|
|
116
|
+
// to evaluating both TSOURCE and FSOURCE regardless of MASK (elemental
|
|
117
|
+
// intrinsics don't short-circuit), which matches select()'s own
|
|
118
|
+
// "both branches always evaluated" contract (see ast.js) instead of
|
|
119
|
+
// fighting it.
|
|
120
|
+
emitSelect: function (condNode, thenStr, elseStr) {
|
|
121
|
+
const L = this.emitExpr(condNode.left);
|
|
122
|
+
const R = this.emitExpr(condNode.right);
|
|
123
|
+
return `MERGE(${thenStr}, ${elseStr}, (${L}) ${condNode.op} (${R}))`;
|
|
124
|
+
},
|
|
125
|
+
formatFunction: (fn, body, letBindings = []) => {
|
|
126
|
+
checkReservedNames([fn.name, ...fn.params, ...letBindings.map((b) => b.name)]);
|
|
127
|
+
const params = fn.params.join(", ");
|
|
128
|
+
const paramDecl = fn.params.length ? wrapLine(` real(8), intent(in) :: ${fn.params.join(", ")}`) + "\n" : "";
|
|
129
|
+
const letDecl = letBindings.length
|
|
130
|
+
? wrapLine(` real(8) :: ${letBindings.map((b) => b.name).join(", ")}`) + "\n"
|
|
131
|
+
: "";
|
|
132
|
+
const letsBlock = letBindings.map(({ name, valueStr }) => wrapLine(` ${name} = ${valueStr}`)).join("\n");
|
|
133
|
+
return `! AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
134
|
+
wrapLine(`real(8) function ${fn.name}(${params})`) + "\n" +
|
|
135
|
+
` implicit none\n` +
|
|
136
|
+
paramDecl +
|
|
137
|
+
letDecl +
|
|
138
|
+
(letsBlock ? letsBlock + "\n" : "") +
|
|
139
|
+
wrapLine(` ${fn.name} = ${body}`) + "\n" +
|
|
140
|
+
`end function ${fn.name}\n`;
|
|
141
|
+
},
|
|
142
|
+
// Multiple named outputs from one call: a subroutine with the outputs
|
|
143
|
+
// as trailing intent(out) parameters -- the same by-reference idiom
|
|
144
|
+
// QB64's SUB uses (see qb64.js), Fortran's closest equivalent since it
|
|
145
|
+
// has no native struct/tuple return either.
|
|
146
|
+
formatSuite: (fn, outputStrs, letBindings = []) => {
|
|
147
|
+
const outputNames = Object.keys(outputStrs);
|
|
148
|
+
checkReservedNames([fn.name, ...fn.params, ...outputNames, ...letBindings.map((b) => b.name)]);
|
|
149
|
+
const allParams = [...fn.params, ...outputNames].join(", ");
|
|
150
|
+
const paramDecl = fn.params.length ? wrapLine(` real(8), intent(in) :: ${fn.params.join(", ")}`) + "\n" : "";
|
|
151
|
+
const outDecl = wrapLine(` real(8), intent(out) :: ${outputNames.join(", ")}`) + "\n";
|
|
152
|
+
const letDecl = letBindings.length
|
|
153
|
+
? wrapLine(` real(8) :: ${letBindings.map((b) => b.name).join(", ")}`) + "\n"
|
|
154
|
+
: "";
|
|
155
|
+
const letsBlock = letBindings.map(({ name, valueStr }) => wrapLine(` ${name} = ${valueStr}`)).join("\n");
|
|
156
|
+
const assigns = outputNames.map((n) => wrapLine(` ${n} = ${outputStrs[n]}`)).join("\n");
|
|
157
|
+
return `! AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
158
|
+
wrapLine(`subroutine ${fn.name}(${allParams})`) + "\n" +
|
|
159
|
+
` implicit none\n` +
|
|
160
|
+
paramDecl +
|
|
161
|
+
outDecl +
|
|
162
|
+
letDecl +
|
|
163
|
+
(letsBlock ? letsBlock + "\n" : "") +
|
|
164
|
+
`${assigns}\n` +
|
|
165
|
+
`end subroutine ${fn.name}\n`;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
module.exports = emitter;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// exprforge/emitters/julia.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
|
+
const emitter = new Emitter({
|
|
13
|
+
ext: "jl",
|
|
14
|
+
// Julia accepts JS-style numeric literal syntax directly, including
|
|
15
|
+
// exponential notation ("1e-9") -- no suffix or conversion needed.
|
|
16
|
+
formatNumber: (v) => String(v),
|
|
17
|
+
calls: {
|
|
18
|
+
// All 22 are Julia Base functions -- no import, no derivation, no
|
|
19
|
+
// wrapping needed for any of them, unlike every other target here.
|
|
20
|
+
sqrt: fn1("sqrt"), abs: fn1("abs"), sin: fn1("sin"), cos: fn1("cos"), tan: fn1("tan"),
|
|
21
|
+
asin: fn1("asin"), acos: fn1("acos"), atan: fn1("atan"), atan2: fn2("atan"),
|
|
22
|
+
log: fn1("log"), log2: fn1("log2"), log10: fn1("log10"), exp: fn1("exp"),
|
|
23
|
+
pow: ([x, y]) => `(${x} ^ ${y})`,
|
|
24
|
+
floor: fn1("floor"), ceil: fn1("ceil"), trunc: fn1("trunc"),
|
|
25
|
+
min: fn2("min"), max: fn2("max"), hypot: fn2("hypot"),
|
|
26
|
+
// Julia's round() defaults to round-half-to-even (banker's
|
|
27
|
+
// rounding), not the round-half-away-from-zero every other target
|
|
28
|
+
// here uses -- RoundNearestTiesAway asks for that explicitly.
|
|
29
|
+
// Doesn't affect the conformance suite either way (it deliberately
|
|
30
|
+
// avoids exact .5 boundaries, see test/conformance.test.js), but
|
|
31
|
+
// this is the genuinely-matching behavior, not just the
|
|
32
|
+
// untested-so-it-doesn't-matter one.
|
|
33
|
+
round: ([x]) => `round(${x}, RoundNearestTiesAway)`,
|
|
34
|
+
// Julia does have sign(), and sign(0.0) == 0.0 -- matches every
|
|
35
|
+
// other target's zero-aware convention already, so no need to
|
|
36
|
+
// build this one by hand (unlike most other emitters here).
|
|
37
|
+
sign: fn1("sign"),
|
|
38
|
+
},
|
|
39
|
+
// Julia's ?: is exactly base.js's default ternary -- no override needed.
|
|
40
|
+
formatFunction: (fn, body, letBindings = []) => {
|
|
41
|
+
const params = fn.params.join(", ");
|
|
42
|
+
const lets = letBindings.map(({ name, valueStr }) => ` ${name} = ${valueStr}`).join("\n");
|
|
43
|
+
const letsBlock = lets ? lets + "\n" : "";
|
|
44
|
+
return `# AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
45
|
+
`function ${fn.name}(${params})\n` +
|
|
46
|
+
letsBlock +
|
|
47
|
+
` return ${body}\n` +
|
|
48
|
+
`end\n`;
|
|
49
|
+
},
|
|
50
|
+
// Multiple named outputs from one call: Julia's native named tuple
|
|
51
|
+
// (`(rx=..., ry=...)`, dot access at the call site) -- the same idiom
|
|
52
|
+
// C#'s emitter uses, and needs no wrapper type declared up front.
|
|
53
|
+
formatSuite: (fn, outputStrs, letBindings = []) => {
|
|
54
|
+
const params = fn.params.join(", ");
|
|
55
|
+
const lets = letBindings.map(({ name, valueStr }) => ` ${name} = ${valueStr}`).join("\n");
|
|
56
|
+
const letsBlock = lets ? lets + "\n" : "";
|
|
57
|
+
const outputNames = Object.keys(outputStrs);
|
|
58
|
+
const returnExpr = outputNames.map((n) => `${n}=${outputStrs[n]}`).join(", ");
|
|
59
|
+
return `# AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
60
|
+
`function ${fn.name}(${params})\n` +
|
|
61
|
+
letsBlock +
|
|
62
|
+
` return (${returnExpr})\n` +
|
|
63
|
+
`end\n`;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
module.exports = emitter;
|
package/emitters/perl.js
ADDED
|
@@ -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;
|
package/emitters/php.js
ADDED
|
@@ -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;
|
package/emitters/registry.js
CHANGED
|
@@ -12,4 +12,12 @@ 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"),
|
|
22
|
+
expr: require("./exprsyntax.js"),
|
|
15
23
|
};
|
|
@@ -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;
|