exprforge 0.2.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,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;
@@ -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;