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,478 @@
|
|
|
1
|
+
// exprforge/emitters/cobol.js
|
|
2
|
+
//
|
|
3
|
+
// Targets GnuCOBOL (free-format source, USAGE COMP-2 for double precision)
|
|
4
|
+
// -- see test/conformance.test.js for the exact `cobc` invocation this is
|
|
5
|
+
// verified against.
|
|
6
|
+
//
|
|
7
|
+
// select()/cmp() needed a genuinely different emission strategy than every
|
|
8
|
+
// other target here, because of one real, confirmed-by-compiling
|
|
9
|
+
// structural limit: GnuCOBOL has no expression-level conditional at all --
|
|
10
|
+
// no ternary, no Fortran-style MERGE(). A user-defined `FUNCTION-ID`
|
|
11
|
+
// module CAN be called from inside an expression (`FUNCTION name(...)`),
|
|
12
|
+
// which looks like a way to build one (a tiny "pick a branch" helper
|
|
13
|
+
// function) -- but confirmed against a real compile+run that GnuCOBOL 3.2
|
|
14
|
+
// silently miscomputes (no error, just a wrong/garbage numeric result)
|
|
15
|
+
// when such a call receives a COMPLEX argument, i.e. one containing its
|
|
16
|
+
// own nested FUNCTION call, rather than a bare variable or literal. Every
|
|
17
|
+
// real select() usage has exactly that shape (see
|
|
18
|
+
// samples/spline-frame.js, math/index.js's safeDiv/clamp/normalize3).
|
|
19
|
+
//
|
|
20
|
+
// The fix: six small helper FUNCTION-ID modules (one per comparator, EF_CMP
|
|
21
|
+
// below), always prepended to the emitted source, PLUS forcing every
|
|
22
|
+
// argument passed to them through its own COMPUTE into a fresh temp
|
|
23
|
+
// variable first -- confirmed safe, since a bare-variable-argument call to
|
|
24
|
+
// a user-defined FUNCTION-ID was the one case that worked correctly in
|
|
25
|
+
// testing. That turns emitSelect from a pure "node -> expression string"
|
|
26
|
+
// function (every other emitter's shape, including this file's own calls
|
|
27
|
+
// table) into a STATEFUL one that also spills COMPUTE statements -- see
|
|
28
|
+
// the CobolEmitter class below, which is the one thing in this file that
|
|
29
|
+
// isn't just config passed to `new Emitter(...)` like every other target.
|
|
30
|
+
//
|
|
31
|
+
// No FUNCTION ATAN2 exists in GnuCOBOL either (FUNCTION ATAN is
|
|
32
|
+
// 1-argument only), and the standard quadrant-corrected formula needs
|
|
33
|
+
// exactly this same branching -- built from the same select-hoisting
|
|
34
|
+
// machinery as select() itself, not a separate mechanism.
|
|
35
|
+
const Emitter = require("./base.js");
|
|
36
|
+
|
|
37
|
+
// COBOL reserved words plus every intrinsic-function name this emitter's
|
|
38
|
+
// calls table depends on -- same role as QB64_RESERVED in emitters/qb64.js.
|
|
39
|
+
// COBOL is case-insensitive, so names are checked lowercased. Not
|
|
40
|
+
// exhaustive (COBOL's real reserved-word list runs into the hundreds), but
|
|
41
|
+
// covers the words a generated variable/parameter/function name could
|
|
42
|
+
// plausibly collide with in practice.
|
|
43
|
+
const COBOL_RESERVED = new Set([
|
|
44
|
+
"identification", "division", "program-id", "function-id", "environment",
|
|
45
|
+
"configuration", "repository", "data", "working-storage", "linkage", "section",
|
|
46
|
+
"procedure", "using", "returning", "intent", "usage", "comp-2", "value", "by",
|
|
47
|
+
"reference", "content", "if", "else", "end-if", "compute", "move", "to", "call",
|
|
48
|
+
"goback", "stop", "run", "display", "perform", "until", "end-perform", "and",
|
|
49
|
+
"or", "not", "true", "false", "zero", "zeros", "zeroes", "spaces", "high-value",
|
|
50
|
+
"low-value", "function", "sqrt", "abs", "sin", "cos", "tan", "asin", "acos",
|
|
51
|
+
"atan", "exp", "log", "log10", "integer", "integer-part", "sign", "min", "max", "mod",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
function checkReservedNames(names) {
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
if (COBOL_RESERVED.has(name.toLowerCase())) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`emitter for .cob: "${name}" is a reserved COBOL word/intrinsic and can't be used as a ` +
|
|
59
|
+
`function/variable/parameter name -- rename it (see COBOL_RESERVED in emitters/cobol.js)`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Separate from COBOL_RESERVED above (general keywords/intrinsics, safe
|
|
66
|
+
// to forbid everywhere including internal let-bindings) and checked only
|
|
67
|
+
// against names that end up in a CALL/PROCEDURE DIVISION USING clause
|
|
68
|
+
// (fn.name, params, output fields) -- NOT let-bindings. A bare "c"
|
|
69
|
+
// (case-insensitive) breaks specifically in a USING identifier list --
|
|
70
|
+
// confirmed against a real compiler ("syntax error, unexpected C"),
|
|
71
|
+
// reproducible in isolation, and not shared by neighboring single letters
|
|
72
|
+
// (b/d/e/... all compile fine in the identical position; likely GnuCOBOL
|
|
73
|
+
// misparsing it as an attempted abbreviation of BY CONTENT). Deliberately
|
|
74
|
+
// NOT added to COBOL_RESERVED: a plain WORKING-STORAGE item named "c"
|
|
75
|
+
// referenced only in COMPUTE statements compiles fine (confirmed too),
|
|
76
|
+
// and samples/spline-frame.js already has a working "c" let-binding (for
|
|
77
|
+
// cos(rad)) that would break for no real reason if this were checked
|
|
78
|
+
// there as well.
|
|
79
|
+
const COBOL_USING_RESERVED = new Set(["c"]);
|
|
80
|
+
|
|
81
|
+
function checkUsingClauseNames(names) {
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
if (COBOL_USING_RESERVED.has(name.toLowerCase())) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`emitter for .cob: "${name}" can't be a function/parameter/output name -- it breaks ` +
|
|
86
|
+
`GnuCOBOL's CALL ... USING clause specifically (confirmed against a real compiler), ` +
|
|
87
|
+
`even though it's fine as an internal let-binding name (see COBOL_USING_RESERVED in ` +
|
|
88
|
+
`emitters/cobol.js) -- rename it`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fn1(name) {
|
|
95
|
+
return ([x]) => `FUNCTION ${name}(${x})`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function fn2(name) {
|
|
99
|
+
return ([a, b]) => `FUNCTION ${name}(${a}, ${b})`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// One helper FUNCTION-ID per comparator, each named ef-cmp-<suffix> --
|
|
103
|
+
// hyphenated, never underscored (confirmed against a real compiler that a
|
|
104
|
+
// user-defined FUNCTION call breaks on an underscored name -- see
|
|
105
|
+
// samples/spline-frame.js's "wy_wire" param for why that's not just a
|
|
106
|
+
// theoretical concern). "ne" uses NOT = rather than the symbolic <>,
|
|
107
|
+
// simply because NOT = is unambiguously standard COBOL and <> wasn't worth
|
|
108
|
+
// separately confirming for a helper this narrow.
|
|
109
|
+
const CMP_HELPERS = {
|
|
110
|
+
">": { suffix: "gt", test: "L > R" },
|
|
111
|
+
"<": { suffix: "lt", test: "L < R" },
|
|
112
|
+
">=": { suffix: "ge", test: "L >= R" },
|
|
113
|
+
"<=": { suffix: "le", test: "L <= R" },
|
|
114
|
+
"==": { suffix: "eq", test: "L = R" },
|
|
115
|
+
"!=": { suffix: "ne", test: "L NOT = R" },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// The REPOSITORY paragraph every caller of an ef-cmp-* helper needs --
|
|
119
|
+
// shared between formatFunction/formatSuite below, and terminated with a
|
|
120
|
+
// period only after the LAST entry (REPOSITORY is one sentence, not one
|
|
121
|
+
// statement per line -- confirmed against a real compiler that a missing
|
|
122
|
+
// trailing period here breaks the DATA DIVISION that follows).
|
|
123
|
+
const CMP_REPOSITORY =
|
|
124
|
+
` REPOSITORY.\n` +
|
|
125
|
+
Object.values(CMP_HELPERS)
|
|
126
|
+
.map(({ suffix }, i, arr) => ` FUNCTION ef-cmp-${suffix}${i === arr.length - 1 ? "." : ""}`)
|
|
127
|
+
.join("\n") +
|
|
128
|
+
"\n";
|
|
129
|
+
|
|
130
|
+
const CMP_HELPER_SOURCE = Object.values(CMP_HELPERS)
|
|
131
|
+
.map(
|
|
132
|
+
({ suffix, test }) => ` IDENTIFICATION DIVISION.
|
|
133
|
+
FUNCTION-ID. ef-cmp-${suffix}.
|
|
134
|
+
DATA DIVISION.
|
|
135
|
+
LINKAGE SECTION.
|
|
136
|
+
01 L USAGE COMP-2.
|
|
137
|
+
01 R USAGE COMP-2.
|
|
138
|
+
01 THEN-VAL USAGE COMP-2.
|
|
139
|
+
01 ELSE-VAL USAGE COMP-2.
|
|
140
|
+
01 RESULT USAGE COMP-2.
|
|
141
|
+
PROCEDURE DIVISION USING L R THEN-VAL ELSE-VAL RETURNING RESULT.
|
|
142
|
+
IF ${test}
|
|
143
|
+
MOVE THEN-VAL TO RESULT
|
|
144
|
+
ELSE
|
|
145
|
+
MOVE ELSE-VAL TO RESULT
|
|
146
|
+
END-IF
|
|
147
|
+
GOBACK.
|
|
148
|
+
END FUNCTION ef-cmp-${suffix}.
|
|
149
|
+
`,
|
|
150
|
+
)
|
|
151
|
+
.join("\n");
|
|
152
|
+
|
|
153
|
+
// GnuCOBOL caps physical source line length (confirmed against a real
|
|
154
|
+
// compiler: "source text exceeds 512 bytes, will be truncated", on
|
|
155
|
+
// samples/kitchen-sink.js's single expression summing all 22 Math
|
|
156
|
+
// functions -- the one AST big enough to ever hit this). Free-format
|
|
157
|
+
// COBOL allows a statement to simply continue on the next line with no
|
|
158
|
+
// continuation marker (confirmed against a real compiler), so a long line
|
|
159
|
+
// just gets broken at word boundaries, well under the real limit.
|
|
160
|
+
function wrapLine(line, maxWidth = 100) {
|
|
161
|
+
if (line.length <= maxWidth) return line;
|
|
162
|
+
const words = line.split(" ");
|
|
163
|
+
const wrapped = [];
|
|
164
|
+
let current = "";
|
|
165
|
+
for (const word of words) {
|
|
166
|
+
if (current && current.length + 1 + word.length > maxWidth) {
|
|
167
|
+
wrapped.push(current);
|
|
168
|
+
current = ` ${word}`;
|
|
169
|
+
} else {
|
|
170
|
+
current = current ? `${current} ${word}` : word;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (current) wrapped.push(current);
|
|
174
|
+
return wrapped.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// A small stateful pool that emitSelect below uses to hoist arguments into
|
|
178
|
+
// fresh temp variables before ever calling an ef-cmp-* helper -- see the
|
|
179
|
+
// file header for why that's required, not optional. Reset once per
|
|
180
|
+
// top-level emitExpr call (one per let-binding, one per output field, one
|
|
181
|
+
// for a select-free body) so temp declarations only need to cover what
|
|
182
|
+
// that one statement actually produced -- but the NAME counter itself is
|
|
183
|
+
// shared (passed in, not owned) across every pool created in one
|
|
184
|
+
// emitFunction call. Confirmed the hard way: an earlier version gave each
|
|
185
|
+
// pool its own counter starting at 0, so two different let-bindings could
|
|
186
|
+
// each mint an "ef-tmp-0", and GnuCOBOL correctly rejected the resulting
|
|
187
|
+
// duplicate WORKING-STORAGE declaration as "ambiguous; needs
|
|
188
|
+
// qualification".
|
|
189
|
+
class TempPool {
|
|
190
|
+
constructor(counter) {
|
|
191
|
+
this.counter = counter;
|
|
192
|
+
this.lines = [];
|
|
193
|
+
this.decls = [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Spills `valueStr` into a freshly named temp, recording both the
|
|
197
|
+
// COMPUTE that sets it and the 01-level declaration it'll need, and
|
|
198
|
+
// returns the bare name -- always safe to pass to a user-defined
|
|
199
|
+
// FUNCTION-ID call, unlike valueStr itself.
|
|
200
|
+
spill(valueStr) {
|
|
201
|
+
const name = `ef-tmp-${this.counter.next++}`;
|
|
202
|
+
this.decls.push(name);
|
|
203
|
+
this.lines.push(wrapLine(` COMPUTE ${name} = ${valueStr}`));
|
|
204
|
+
return name;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
class CobolEmitter extends Emitter {
|
|
209
|
+
emitFunction(fn) {
|
|
210
|
+
const { collectLets } = require("../ast.js");
|
|
211
|
+
const { bindings, body } = collectLets(fn.body);
|
|
212
|
+
const counter = { next: 0 };
|
|
213
|
+
|
|
214
|
+
const letLines = [];
|
|
215
|
+
const letDecls = [];
|
|
216
|
+
for (const { name, node } of bindings) {
|
|
217
|
+
checkReservedNames([name]);
|
|
218
|
+
this._pool = new TempPool(counter);
|
|
219
|
+
const valueStr = this.emitExpr(node);
|
|
220
|
+
letLines.push(...this._pool.lines);
|
|
221
|
+
letDecls.push(...this._pool.decls, name);
|
|
222
|
+
letLines.push(wrapLine(` COMPUTE ${name} = ${valueStr}`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (body.type === "outputs") {
|
|
226
|
+
if (!this.formatSuiteImpl) {
|
|
227
|
+
throw new Error(`emitter for .${this.ext}: no formatSuite configured -- multi-output suites aren't supported for this target yet`);
|
|
228
|
+
}
|
|
229
|
+
const outputStrs = {};
|
|
230
|
+
const outputLines = [];
|
|
231
|
+
for (const [name, node] of Object.entries(body.fields)) {
|
|
232
|
+
this._pool = new TempPool(counter);
|
|
233
|
+
outputStrs[name] = this.emitExpr(node);
|
|
234
|
+
outputLines.push(...this._pool.lines);
|
|
235
|
+
letDecls.push(...this._pool.decls);
|
|
236
|
+
}
|
|
237
|
+
return this.formatSuiteImpl(fn, outputStrs, letLines, letDecls, outputLines);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this._pool = new TempPool(counter);
|
|
241
|
+
const bodyStr = this.emitExpr(body);
|
|
242
|
+
const bodyLines = this._pool.lines;
|
|
243
|
+
letDecls.push(...this._pool.decls);
|
|
244
|
+
return this.formatFunctionImpl(fn, bodyStr, letLines, letDecls, bodyLines);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// JS renders very small/large magnitudes in exponential notation
|
|
249
|
+
// (String(1e-9) === "1e-9"), and COBOL numeric literals don't accept that
|
|
250
|
+
// syntax at all -- confirmed against a real compiler ("'1e-9' is not
|
|
251
|
+
// defined"). Expanded to plain decimal digit-by-digit instead of via
|
|
252
|
+
// toFixed(): toFixed(20) reveals a binary float's true (imprecise) decimal
|
|
253
|
+
// expansion for values like 4.2 ("4.20000000000000017764"), where this
|
|
254
|
+
// instead shifts the SAME shortest-round-trip digits String(v) already
|
|
255
|
+
// picked, so e.g. 1e-9 becomes exactly "0.000000001", nothing more.
|
|
256
|
+
function expandExponential(s) {
|
|
257
|
+
const m = s.match(/^(-?)(\d+)(?:\.(\d+))?e([+-]?\d+)$/i);
|
|
258
|
+
if (!m) return s;
|
|
259
|
+
const [, sign, intPart, fracPart = "", expStr] = m;
|
|
260
|
+
const digits = intPart + fracPart;
|
|
261
|
+
const pointPos = intPart.length + Number(expStr);
|
|
262
|
+
let result;
|
|
263
|
+
if (pointPos <= 0) {
|
|
264
|
+
result = `0.${"0".repeat(-pointPos)}${digits}`;
|
|
265
|
+
} else if (pointPos >= digits.length) {
|
|
266
|
+
result = `${digits}${"0".repeat(pointPos - digits.length)}`;
|
|
267
|
+
} else {
|
|
268
|
+
result = `${digits.slice(0, pointPos)}.${digits.slice(pointPos)}`;
|
|
269
|
+
}
|
|
270
|
+
return sign + result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// "**" must never end up nested inside ANOTHER call's argument -- that's
|
|
274
|
+
// the actual, general form of the confirmed bug (see the file header):
|
|
275
|
+
// some GnuCOBOL 4.x builds route a "**" expression through a broken
|
|
276
|
+
// internal decimal codegen path (missing header for cob_decimal) when
|
|
277
|
+
// it's nested inside a FUNCTION(...) argument, e.g. sqrt(a^2 + b^2) --
|
|
278
|
+
// confirmed against real CI, not reproducible against this project's own
|
|
279
|
+
// (older) local GnuCOBOL. A plain top-level "**" (kitchen-sink's
|
|
280
|
+
// pow(x, y), summed directly, never nested as another call's argument)
|
|
281
|
+
// is fine.
|
|
282
|
+
//
|
|
283
|
+
// Rather than rely on every caller remembering to keep a pow() result out
|
|
284
|
+
// of anywhere risky, pow ALWAYS spills its own result here: call("pow", a,
|
|
285
|
+
// b) unconditionally evaluates to a bare temp name, so "**" can only ever
|
|
286
|
+
// appear in its own dedicated COMPUTE statement, never nested inside
|
|
287
|
+
// anything else regardless of what the surrounding expression does with
|
|
288
|
+
// the result. hypot's internal sum-of-squares reuses this directly, since
|
|
289
|
+
// it builds "**" itself rather than going through call("pow", ...).
|
|
290
|
+
//
|
|
291
|
+
// Arrow function callers below, not plain ones: base.js's emitExpr calls
|
|
292
|
+
// `this.calls[node.name](args)` unbound -- this closes over the `emitter`
|
|
293
|
+
// const the same way atan2 already does (see its own comment further
|
|
294
|
+
// down), fully assigned by the time this ever actually runs.
|
|
295
|
+
function spillPow(baseRaw, exponentRaw) {
|
|
296
|
+
const pool = emitter._pool;
|
|
297
|
+
const base = pool.spill(baseRaw);
|
|
298
|
+
const exponent = pool.spill(exponentRaw);
|
|
299
|
+
return pool.spill(`(${base} ** ${exponent})`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const emitter = new CobolEmitter({
|
|
303
|
+
ext: "cob",
|
|
304
|
+
formatNumber: (v) => {
|
|
305
|
+
const s = String(v);
|
|
306
|
+
return /e/i.test(s) ? expandExponential(s) : s;
|
|
307
|
+
},
|
|
308
|
+
calls: {
|
|
309
|
+
sqrt: fn1("SQRT"), abs: fn1("ABS"), sin: fn1("SIN"), cos: fn1("COS"), tan: fn1("TAN"),
|
|
310
|
+
asin: fn1("ASIN"), acos: fn1("ACOS"), atan: fn1("ATAN"),
|
|
311
|
+
exp: fn1("EXP"), log: fn1("LOG"), log10: fn1("LOG10"),
|
|
312
|
+
min: fn2("MIN"), max: fn2("MAX"),
|
|
313
|
+
pow: ([x, y]) => spillPow(x, y),
|
|
314
|
+
// No LOG2 intrinsic -- derive it (nesting two intrinsics is fine;
|
|
315
|
+
// only nesting a call inside a USER-DEFINED function's argument
|
|
316
|
+
// was the confirmed problem -- see the file header).
|
|
317
|
+
log2: ([x]) => `(FUNCTION LOG(${x}) / FUNCTION LOG(2.0))`,
|
|
318
|
+
// No HYPOT intrinsic -- derive it via spillPow (see its own
|
|
319
|
+
// comment): the sum-of-squares itself also gets spilled to its
|
|
320
|
+
// own temp before FUNCTION SQRT ever sees it, so SQRT's argument
|
|
321
|
+
// is always a bare name, never a "**"-containing expression.
|
|
322
|
+
hypot: ([aRaw, bRaw]) => {
|
|
323
|
+
const aSq = spillPow(aRaw, "2.0");
|
|
324
|
+
const bSq = spillPow(bRaw, "2.0");
|
|
325
|
+
const sumSq = emitter._pool.spill(`${aSq} + ${bSq}`);
|
|
326
|
+
return `FUNCTION SQRT(${sumSq})`;
|
|
327
|
+
},
|
|
328
|
+
// FUNCTION INTEGER is floor (greatest integer <= x, confirmed
|
|
329
|
+
// against a real compiler, including for negatives). COMPUTE's
|
|
330
|
+
// automatic numeric conversion hands it back as COMP-2 with no
|
|
331
|
+
// explicit cast needed, unlike Fortran's REAL(..., 8) wrap.
|
|
332
|
+
floor: fn1("INTEGER"),
|
|
333
|
+
// No CEILING intrinsic -- negate, floor, negate back. Confirmed:
|
|
334
|
+
// ceil(2.2)=3, ceil(-2.2)=-2.
|
|
335
|
+
ceil: ([x]) => `(0 - FUNCTION INTEGER(0 - (${x})))`,
|
|
336
|
+
// FUNCTION INTEGER-PART truncates toward zero directly -- confirmed
|
|
337
|
+
// against a real compiler (2.7->2, -2.7->-2), no derivation needed.
|
|
338
|
+
trunc: fn1("INTEGER-PART"),
|
|
339
|
+
// No ROUND-as-an-expression intrinsic (COBOL's ROUNDED is a
|
|
340
|
+
// COMPUTE/ADD statement modifier, not composable inline). Built
|
|
341
|
+
// from FUNCTION SIGN and FUNCTION INTEGER instead -- both purely
|
|
342
|
+
// intrinsic, so unlike select() this doesn't need the hoisting
|
|
343
|
+
// machinery at all. GnuCOBOL's FUNCTION SIGN is 1-argument
|
|
344
|
+
// (SIGN(x) -> -1/0/1), NOT Fortran's 2-argument SIGN(A,B)
|
|
345
|
+
// "magnitude of A, sign of B" -- confirmed the hard way (a first
|
|
346
|
+
// version of this formula copied Fortran's 2-arg convention here
|
|
347
|
+
// by mistake and got "FUNCTION 'SIGN' has wrong number of
|
|
348
|
+
// arguments" from a real compiler). Also unlike Fortran's SIGN,
|
|
349
|
+
// confirmed zero-safe (SIGN(0.0) == 0.0 for real, not just by
|
|
350
|
+
// accident of a multiplied-away wrong case), so this needs no
|
|
351
|
+
// separate correction the way Fortran's round() does. Rounds ties
|
|
352
|
+
// away from zero, matching every other target here.
|
|
353
|
+
round: ([x]) => `(FUNCTION SIGN(${x}) * FUNCTION INTEGER(FUNCTION ABS(${x}) + 0.5))`,
|
|
354
|
+
// Confirmed zero-safe against a real compiler (SIGN(0.0) == 0.0,
|
|
355
|
+
// unlike Fortran's identically-named but 2-argument intrinsic --
|
|
356
|
+
// see round() above) -- no hoisting/spilling machinery needed,
|
|
357
|
+
// unlike every other emitter here that has to hand-build this.
|
|
358
|
+
sign: fn1("SIGN"),
|
|
359
|
+
// No FUNCTION ATAN2 -- the standard quadrant-corrected formula,
|
|
360
|
+
// built from ef-cmp-* the same way select() itself composes them
|
|
361
|
+
// (see emitSelect below). EVERY argument to EVERY ef-cmp-* call
|
|
362
|
+
// must be a bare, already-spilled name -- including ones built
|
|
363
|
+
// from ANOTHER ef-cmp-* call's result -- since a nested
|
|
364
|
+
// `FUNCTION ef-cmp-x(...)` used directly as an argument to another
|
|
365
|
+
// `FUNCTION ef-cmp-y(...)` hits the exact same confirmed bug as a
|
|
366
|
+
// nested intrinsic call would (see the file header); the first
|
|
367
|
+
// version of this formula got that wrong (nested an ef-cmp-lt
|
|
368
|
+
// call straight into ef-cmp-gt's argument list) and silently
|
|
369
|
+
// computed garbage, caught only by actually compiling and running
|
|
370
|
+
// it. So: absolutely nothing here is inlined -- every intermediate
|
|
371
|
+
// result, including literals, gets its own spill() first.
|
|
372
|
+
//
|
|
373
|
+
// Arrow function, not a plain one: base.js's emitExpr calls
|
|
374
|
+
// `this.calls[node.name](args)` unbound (unlike emitSelectImpl,
|
|
375
|
+
// which the Emitter constructor explicitly .bind(this)s) -- an
|
|
376
|
+
// arrow here closes over the `emitter` const below by reference
|
|
377
|
+
// instead, which is fully assigned by the time this ever actually
|
|
378
|
+
// runs (during some later emitFunction call), even though it's
|
|
379
|
+
// referenced before that `const` is declared in this same object
|
|
380
|
+
// literal.
|
|
381
|
+
atan2: (args) => {
|
|
382
|
+
const [yRaw, xRaw] = args;
|
|
383
|
+
const pool = emitter._pool;
|
|
384
|
+
const y = pool.spill(yRaw);
|
|
385
|
+
const x = pool.spill(xRaw);
|
|
386
|
+
const atanYX = pool.spill(`FUNCTION ATAN(${y} / ${x})`);
|
|
387
|
+
const zero = pool.spill("0.0");
|
|
388
|
+
const piOver2 = pool.spill("1.5707963267948966");
|
|
389
|
+
const negPiOver2 = pool.spill("-1.5707963267948966");
|
|
390
|
+
const atanPlusPi = pool.spill(`(${atanYX} + 3.141592653589793)`);
|
|
391
|
+
const atanMinusPi = pool.spill(`(${atanYX} - 3.141592653589793)`);
|
|
392
|
+
// x == 0 case: sign of y (0 conventionally maps to 0.0, same
|
|
393
|
+
// convention JS's/Python's atan2(0,0) use).
|
|
394
|
+
const yNegSubcase = pool.spill(`FUNCTION ef-cmp-lt(${y}, ${zero}, ${negPiOver2}, ${zero})`);
|
|
395
|
+
const xZeroCase = pool.spill(`FUNCTION ef-cmp-gt(${y}, ${zero}, ${piOver2}, ${yNegSubcase})`);
|
|
396
|
+
// x < 0 case: quadrant-corrected by the sign of y.
|
|
397
|
+
const xNegCase = pool.spill(`FUNCTION ef-cmp-ge(${y}, ${zero}, ${atanPlusPi}, ${atanMinusPi})`);
|
|
398
|
+
const xNegOrZeroCase = pool.spill(`FUNCTION ef-cmp-lt(${x}, ${zero}, ${xNegCase}, ${xZeroCase})`);
|
|
399
|
+
return `FUNCTION ef-cmp-gt(${x}, ${zero}, ${atanYX}, ${xNegOrZeroCase})`;
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
// See the file header and TempPool above for why this spills into
|
|
403
|
+
// temps instead of nesting inline: a user-defined FUNCTION-ID call
|
|
404
|
+
// (ef-cmp-*) confirmed miscomputes when given a complex argument, so
|
|
405
|
+
// every one of L/R/then/else gets its own COMPUTE into a fresh temp
|
|
406
|
+
// first, and the picker call itself only ever receives bare names.
|
|
407
|
+
emitSelect: function (condNode, thenStr, elseStr) {
|
|
408
|
+
const { suffix } = CMP_HELPERS[condNode.op];
|
|
409
|
+
const L = this._pool.spill(this.emitExpr(condNode.left));
|
|
410
|
+
const R = this._pool.spill(this.emitExpr(condNode.right));
|
|
411
|
+
const thenTmp = this._pool.spill(thenStr);
|
|
412
|
+
const elseTmp = this._pool.spill(elseStr);
|
|
413
|
+
return `FUNCTION ef-cmp-${suffix}(${L}, ${R}, ${thenTmp}, ${elseTmp})`;
|
|
414
|
+
},
|
|
415
|
+
// Both the scalar and suite cases use the SAME convention: a callable
|
|
416
|
+
// PROGRAM-ID with every output (the single return value, or every
|
|
417
|
+
// outputs() field) as a trailing BY REFERENCE parameter -- COBOL's
|
|
418
|
+
// default parameter-passing mode, confirmed reliable (BY VALUE is
|
|
419
|
+
// explicitly flagged "unfinished" by a real GnuCOBOL compile). This
|
|
420
|
+
// also sidesteps a second confirmed rough edge: calling a FUNCTION-ID
|
|
421
|
+
// module by name (`FUNCTION word(...)`) breaks if that name contains
|
|
422
|
+
// an underscore (confirmed against a real compiler -- see e.g.
|
|
423
|
+
// samples/spline-frame.js's "wy_wire" param), while CALL "name" takes
|
|
424
|
+
// the program name as a plain string literal, immune to that.
|
|
425
|
+
formatFunction: (fn, body, letLines, letDecls, bodyLines) => {
|
|
426
|
+
checkReservedNames([fn.name, ...fn.params]);
|
|
427
|
+
checkUsingClauseNames([fn.name, ...fn.params]);
|
|
428
|
+
const linkageParams = [...fn.params, "ef-result"];
|
|
429
|
+
const paramDecls = linkageParams.map((p) => ` 01 ${p} USAGE COMP-2.`).join("\n");
|
|
430
|
+
const wsDecls = letDecls.map((n) => ` 01 ${n} USAGE COMP-2.`).join("\n");
|
|
431
|
+
return ` >>SOURCE FORMAT FREE\n` +
|
|
432
|
+
` *> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
433
|
+
CMP_HELPER_SOURCE + "\n" +
|
|
434
|
+
` IDENTIFICATION DIVISION.\n` +
|
|
435
|
+
` PROGRAM-ID. ${fn.name}.\n` +
|
|
436
|
+
` ENVIRONMENT DIVISION.\n` +
|
|
437
|
+
` CONFIGURATION SECTION.\n` +
|
|
438
|
+
CMP_REPOSITORY +
|
|
439
|
+
` DATA DIVISION.\n` +
|
|
440
|
+
(wsDecls ? ` WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
|
|
441
|
+
` LINKAGE SECTION.\n` +
|
|
442
|
+
paramDecls + "\n" +
|
|
443
|
+
` PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
|
|
444
|
+
[...letLines, ...bodyLines].join("\n") + (letLines.length || bodyLines.length ? "\n" : "") +
|
|
445
|
+
wrapLine(` COMPUTE ef-result = ${body}`) + "\n" +
|
|
446
|
+
` GOBACK.\n` +
|
|
447
|
+
` END PROGRAM ${fn.name}.\n`;
|
|
448
|
+
},
|
|
449
|
+
formatSuite: (fn, outputStrs, letLines, letDecls, outputLines) => {
|
|
450
|
+
const outputNames = Object.keys(outputStrs);
|
|
451
|
+
checkReservedNames([fn.name, ...fn.params, ...outputNames]);
|
|
452
|
+
checkUsingClauseNames([fn.name, ...fn.params, ...outputNames]);
|
|
453
|
+
const linkageParams = [...fn.params, ...outputNames];
|
|
454
|
+
const paramDecls = linkageParams.map((p) => ` 01 ${p} USAGE COMP-2.`).join("\n");
|
|
455
|
+
const wsDecls = letDecls.map((n) => ` 01 ${n} USAGE COMP-2.`).join("\n");
|
|
456
|
+
const assigns = outputNames.map((n) => wrapLine(` COMPUTE ${n} = ${outputStrs[n]}`)).join("\n");
|
|
457
|
+
return ` >>SOURCE FORMAT FREE\n` +
|
|
458
|
+
` *> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
459
|
+
CMP_HELPER_SOURCE + "\n" +
|
|
460
|
+
` IDENTIFICATION DIVISION.\n` +
|
|
461
|
+
` PROGRAM-ID. ${fn.name}.\n` +
|
|
462
|
+
` ENVIRONMENT DIVISION.\n` +
|
|
463
|
+
` CONFIGURATION SECTION.\n` +
|
|
464
|
+
CMP_REPOSITORY +
|
|
465
|
+
` DATA DIVISION.\n` +
|
|
466
|
+
(wsDecls ? ` WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
|
|
467
|
+
` LINKAGE SECTION.\n` +
|
|
468
|
+
paramDecls + "\n" +
|
|
469
|
+
` PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
|
|
470
|
+
(letLines.length ? letLines.join("\n") + "\n" : "") +
|
|
471
|
+
(outputLines.length ? outputLines.join("\n") + "\n" : "") +
|
|
472
|
+
assigns + "\n" +
|
|
473
|
+
` GOBACK.\n` +
|
|
474
|
+
` END PROGRAM ${fn.name}.\n`;
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
module.exports = emitter;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// exprforge/emitters/exprsyntax.js
|
|
2
|
+
//
|
|
3
|
+
// Prints an AST back out as fn`...`/expr`...`-compatible source text --
|
|
4
|
+
// the reverse direction of expr.js/fn.js. Not a third-party language:
|
|
5
|
+
// there's no external compiler/interpreter to run this output through,
|
|
6
|
+
// so it's verified by round-trip instead (emit -> reparse via fn() ->
|
|
7
|
+
// deepStrictEqual the original, see test/exprsyntax-emitter.test.js).
|
|
8
|
+
// Registered as a real target anyway (emitters.expr) since fn/expr
|
|
9
|
+
// syntax is a real, standalone, human-pasteable output format in its
|
|
10
|
+
// own right, not just a test fixture.
|
|
11
|
+
//
|
|
12
|
+
// "call" is overridden wholesale rather than populated via a `calls`
|
|
13
|
+
// table: every intrinsic passes through uniformly as `name(args...)`
|
|
14
|
+
// except pow, which recovers fn/expr's own `^` sugar -- both forms
|
|
15
|
+
// lower back to the identical call("pow", ...) node (see expr.js's
|
|
16
|
+
// grammar comment on rule 2), so this is a readability choice, not a
|
|
17
|
+
// correctness one. A side effect: this is the one emitter that never
|
|
18
|
+
// needs a table update when a new intrinsic is added anywhere else in
|
|
19
|
+
// the project.
|
|
20
|
+
const Emitter = require("./base.js");
|
|
21
|
+
|
|
22
|
+
class ExprSyntaxEmitter extends Emitter {
|
|
23
|
+
emitExpr(node) {
|
|
24
|
+
if (node.type === "call" && node.name === "pow" && node.args.length === 2) {
|
|
25
|
+
const [base, exponent] = node.args.map((a) => this.emitExpr(a));
|
|
26
|
+
return `(${base}^${exponent})`;
|
|
27
|
+
}
|
|
28
|
+
if (node.type === "call") {
|
|
29
|
+
const args = node.args.map((a) => this.emitExpr(a));
|
|
30
|
+
return `${node.name}(${args.join(", ")})`;
|
|
31
|
+
}
|
|
32
|
+
return super.emitExpr(node);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function letLines(letBindings) {
|
|
37
|
+
return letBindings.map(({ name, valueStr }) => `let ${name} = ${valueStr};`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const emitter = new ExprSyntaxEmitter({
|
|
41
|
+
ext: "fn",
|
|
42
|
+
// fn/expr accepts JS-style numeric literal syntax directly -- it's
|
|
43
|
+
// literally the same NUMBER lexing rule (see expr.js's tokenizer).
|
|
44
|
+
formatNumber: (v) => String(v),
|
|
45
|
+
// base.js's default _defaultSelect wraps just the comparison in its
|
|
46
|
+
// own parens ("((L op R) ? then : else)") -- fn/expr's ternary
|
|
47
|
+
// grammar has no parenthesized-COMPARISON form (see expr.js's
|
|
48
|
+
// grammar comment: `ternary := additive compOp additive "?" ...`),
|
|
49
|
+
// so "(L op R)" alone would try to parse as a complete, paren-
|
|
50
|
+
// wrapped ternary condition with no "?" in sight and hit the parser's
|
|
51
|
+
// own "comparison must be used as a ternary condition" error. This
|
|
52
|
+
// wraps the WHOLE ternary in one outer paren pair instead -- still a
|
|
53
|
+
// valid `primary := ... | "(" expression ")" | ...`, and unlike the
|
|
54
|
+
// unwrapped form, safe to embed as a sub-expression anywhere (e.g.
|
|
55
|
+
// `crossX / (rLen > eps ? rLen : 1)`) without the surrounding
|
|
56
|
+
// operator's precedence reaching into the ternary's own condition.
|
|
57
|
+
emitSelect: function (condNode, thenStr, elseStr) {
|
|
58
|
+
const L = this.emitExpr(condNode.left);
|
|
59
|
+
const R = this.emitExpr(condNode.right);
|
|
60
|
+
return `(${L} ${condNode.op} ${R} ? ${thenStr} : ${elseStr})`;
|
|
61
|
+
},
|
|
62
|
+
// Output is bare fn/expr source text -- no JS wrapper/boilerplate.
|
|
63
|
+
// That's both the most direct thing to paste into a real
|
|
64
|
+
// `` fn`...` `` call, and exactly what the round-trip test reparses
|
|
65
|
+
// with zero unwrapping first.
|
|
66
|
+
formatFunction: (fn, bodyStr, letBindings = []) => {
|
|
67
|
+
const lines = letLines(letBindings);
|
|
68
|
+
lines.push(`return ${bodyStr};`);
|
|
69
|
+
return lines.join("\n") + "\n";
|
|
70
|
+
},
|
|
71
|
+
formatSuite: (fn, outputStrs, letBindings = []) => {
|
|
72
|
+
const lines = letLines(letBindings);
|
|
73
|
+
const fields = Object.entries(outputStrs)
|
|
74
|
+
.map(([name, valueStr]) => `${name}: ${valueStr}`)
|
|
75
|
+
.join(", ");
|
|
76
|
+
lines.push(`return { ${fields} };`);
|
|
77
|
+
return lines.join("\n") + "\n";
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
module.exports = emitter;
|