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.
@@ -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;