exprforge 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,410 @@
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
+ function fn1(name) {
66
+ return ([x]) => `FUNCTION ${name}(${x})`;
67
+ }
68
+
69
+ function fn2(name) {
70
+ return ([a, b]) => `FUNCTION ${name}(${a}, ${b})`;
71
+ }
72
+
73
+ // One helper FUNCTION-ID per comparator, each named ef-cmp-<suffix> --
74
+ // hyphenated, never underscored (confirmed against a real compiler that a
75
+ // user-defined FUNCTION call breaks on an underscored name -- see
76
+ // samples/spline-frame.js's "wy_wire" param for why that's not just a
77
+ // theoretical concern). "ne" uses NOT = rather than the symbolic <>,
78
+ // simply because NOT = is unambiguously standard COBOL and <> wasn't worth
79
+ // separately confirming for a helper this narrow.
80
+ const CMP_HELPERS = {
81
+ ">": { suffix: "gt", test: "L > R" },
82
+ "<": { suffix: "lt", test: "L < R" },
83
+ ">=": { suffix: "ge", test: "L >= R" },
84
+ "<=": { suffix: "le", test: "L <= R" },
85
+ "==": { suffix: "eq", test: "L = R" },
86
+ "!=": { suffix: "ne", test: "L NOT = R" },
87
+ };
88
+
89
+ // The REPOSITORY paragraph every caller of an ef-cmp-* helper needs --
90
+ // shared between formatFunction/formatSuite below, and terminated with a
91
+ // period only after the LAST entry (REPOSITORY is one sentence, not one
92
+ // statement per line -- confirmed against a real compiler that a missing
93
+ // trailing period here breaks the DATA DIVISION that follows).
94
+ const CMP_REPOSITORY =
95
+ ` REPOSITORY.\n` +
96
+ Object.values(CMP_HELPERS)
97
+ .map(({ suffix }, i, arr) => ` FUNCTION ef-cmp-${suffix}${i === arr.length - 1 ? "." : ""}`)
98
+ .join("\n") +
99
+ "\n";
100
+
101
+ const CMP_HELPER_SOURCE = Object.values(CMP_HELPERS)
102
+ .map(
103
+ ({ suffix, test }) => ` IDENTIFICATION DIVISION.
104
+ FUNCTION-ID. ef-cmp-${suffix}.
105
+ DATA DIVISION.
106
+ LINKAGE SECTION.
107
+ 01 L USAGE COMP-2.
108
+ 01 R USAGE COMP-2.
109
+ 01 THEN-VAL USAGE COMP-2.
110
+ 01 ELSE-VAL USAGE COMP-2.
111
+ 01 RESULT USAGE COMP-2.
112
+ PROCEDURE DIVISION USING L R THEN-VAL ELSE-VAL RETURNING RESULT.
113
+ IF ${test}
114
+ MOVE THEN-VAL TO RESULT
115
+ ELSE
116
+ MOVE ELSE-VAL TO RESULT
117
+ END-IF
118
+ GOBACK.
119
+ END FUNCTION ef-cmp-${suffix}.
120
+ `,
121
+ )
122
+ .join("\n");
123
+
124
+ // GnuCOBOL caps physical source line length (confirmed against a real
125
+ // compiler: "source text exceeds 512 bytes, will be truncated", on
126
+ // samples/kitchen-sink.js's single expression summing all 22 Math
127
+ // functions -- the one AST big enough to ever hit this). Free-format
128
+ // COBOL allows a statement to simply continue on the next line with no
129
+ // continuation marker (confirmed against a real compiler), so a long line
130
+ // just gets broken at word boundaries, well under the real limit.
131
+ function wrapLine(line, maxWidth = 100) {
132
+ if (line.length <= maxWidth) return line;
133
+ const words = line.split(" ");
134
+ const wrapped = [];
135
+ let current = "";
136
+ for (const word of words) {
137
+ if (current && current.length + 1 + word.length > maxWidth) {
138
+ wrapped.push(current);
139
+ current = ` ${word}`;
140
+ } else {
141
+ current = current ? `${current} ${word}` : word;
142
+ }
143
+ }
144
+ if (current) wrapped.push(current);
145
+ return wrapped.join("\n");
146
+ }
147
+
148
+ // A small stateful pool that emitSelect below uses to hoist arguments into
149
+ // fresh temp variables before ever calling an ef-cmp-* helper -- see the
150
+ // file header for why that's required, not optional. Reset once per
151
+ // top-level emitExpr call (one per let-binding, one per output field, one
152
+ // for a select-free body) so temp declarations only need to cover what
153
+ // that one statement actually produced -- but the NAME counter itself is
154
+ // shared (passed in, not owned) across every pool created in one
155
+ // emitFunction call. Confirmed the hard way: an earlier version gave each
156
+ // pool its own counter starting at 0, so two different let-bindings could
157
+ // each mint an "ef-tmp-0", and GnuCOBOL correctly rejected the resulting
158
+ // duplicate WORKING-STORAGE declaration as "ambiguous; needs
159
+ // qualification".
160
+ class TempPool {
161
+ constructor(counter) {
162
+ this.counter = counter;
163
+ this.lines = [];
164
+ this.decls = [];
165
+ }
166
+
167
+ // Spills `valueStr` into a freshly named temp, recording both the
168
+ // COMPUTE that sets it and the 01-level declaration it'll need, and
169
+ // returns the bare name -- always safe to pass to a user-defined
170
+ // FUNCTION-ID call, unlike valueStr itself.
171
+ spill(valueStr) {
172
+ const name = `ef-tmp-${this.counter.next++}`;
173
+ this.decls.push(name);
174
+ this.lines.push(wrapLine(` COMPUTE ${name} = ${valueStr}`));
175
+ return name;
176
+ }
177
+ }
178
+
179
+ class CobolEmitter extends Emitter {
180
+ emitFunction(fn) {
181
+ const { collectLets } = require("../ast.js");
182
+ const { bindings, body } = collectLets(fn.body);
183
+ const counter = { next: 0 };
184
+
185
+ const letLines = [];
186
+ const letDecls = [];
187
+ for (const { name, node } of bindings) {
188
+ checkReservedNames([name]);
189
+ this._pool = new TempPool(counter);
190
+ const valueStr = this.emitExpr(node);
191
+ letLines.push(...this._pool.lines);
192
+ letDecls.push(...this._pool.decls, name);
193
+ letLines.push(wrapLine(` COMPUTE ${name} = ${valueStr}`));
194
+ }
195
+
196
+ if (body.type === "outputs") {
197
+ if (!this.formatSuiteImpl) {
198
+ throw new Error(`emitter for .${this.ext}: no formatSuite configured -- multi-output suites aren't supported for this target yet`);
199
+ }
200
+ const outputStrs = {};
201
+ const outputLines = [];
202
+ for (const [name, node] of Object.entries(body.fields)) {
203
+ this._pool = new TempPool(counter);
204
+ outputStrs[name] = this.emitExpr(node);
205
+ outputLines.push(...this._pool.lines);
206
+ letDecls.push(...this._pool.decls);
207
+ }
208
+ return this.formatSuiteImpl(fn, outputStrs, letLines, letDecls, outputLines);
209
+ }
210
+
211
+ this._pool = new TempPool(counter);
212
+ const bodyStr = this.emitExpr(body);
213
+ const bodyLines = this._pool.lines;
214
+ letDecls.push(...this._pool.decls);
215
+ return this.formatFunctionImpl(fn, bodyStr, letLines, letDecls, bodyLines);
216
+ }
217
+ }
218
+
219
+ // JS renders very small/large magnitudes in exponential notation
220
+ // (String(1e-9) === "1e-9"), and COBOL numeric literals don't accept that
221
+ // syntax at all -- confirmed against a real compiler ("'1e-9' is not
222
+ // defined"). Expanded to plain decimal digit-by-digit instead of via
223
+ // toFixed(): toFixed(20) reveals a binary float's true (imprecise) decimal
224
+ // expansion for values like 4.2 ("4.20000000000000017764"), where this
225
+ // instead shifts the SAME shortest-round-trip digits String(v) already
226
+ // picked, so e.g. 1e-9 becomes exactly "0.000000001", nothing more.
227
+ function expandExponential(s) {
228
+ const m = s.match(/^(-?)(\d+)(?:\.(\d+))?e([+-]?\d+)$/i);
229
+ if (!m) return s;
230
+ const [, sign, intPart, fracPart = "", expStr] = m;
231
+ const digits = intPart + fracPart;
232
+ const pointPos = intPart.length + Number(expStr);
233
+ let result;
234
+ if (pointPos <= 0) {
235
+ result = `0.${"0".repeat(-pointPos)}${digits}`;
236
+ } else if (pointPos >= digits.length) {
237
+ result = `${digits}${"0".repeat(pointPos - digits.length)}`;
238
+ } else {
239
+ result = `${digits.slice(0, pointPos)}.${digits.slice(pointPos)}`;
240
+ }
241
+ return sign + result;
242
+ }
243
+
244
+ const emitter = new CobolEmitter({
245
+ ext: "cob",
246
+ formatNumber: (v) => {
247
+ const s = String(v);
248
+ return /e/i.test(s) ? expandExponential(s) : s;
249
+ },
250
+ calls: {
251
+ sqrt: fn1("SQRT"), abs: fn1("ABS"), sin: fn1("SIN"), cos: fn1("COS"), tan: fn1("TAN"),
252
+ asin: fn1("ASIN"), acos: fn1("ACOS"), atan: fn1("ATAN"),
253
+ exp: fn1("EXP"), log: fn1("LOG"), log10: fn1("LOG10"),
254
+ min: fn2("MIN"), max: fn2("MAX"),
255
+ pow: ([x, y]) => `(${x} ** ${y})`,
256
+ // No LOG2 intrinsic -- derive it (nesting two intrinsics is fine;
257
+ // only nesting a call inside a USER-DEFINED function's argument
258
+ // was the confirmed problem -- see the file header).
259
+ log2: ([x]) => `(FUNCTION LOG(${x}) / FUNCTION LOG(2.0))`,
260
+ // No HYPOT intrinsic -- derive it the same way.
261
+ hypot: ([a, b]) => `FUNCTION SQRT((${a}) ** 2 + (${b}) ** 2)`,
262
+ // FUNCTION INTEGER is floor (greatest integer <= x, confirmed
263
+ // against a real compiler, including for negatives). COMPUTE's
264
+ // automatic numeric conversion hands it back as COMP-2 with no
265
+ // explicit cast needed, unlike Fortran's REAL(..., 8) wrap.
266
+ floor: fn1("INTEGER"),
267
+ // No CEILING intrinsic -- negate, floor, negate back. Confirmed:
268
+ // ceil(2.2)=3, ceil(-2.2)=-2.
269
+ ceil: ([x]) => `(0 - FUNCTION INTEGER(0 - (${x})))`,
270
+ // FUNCTION INTEGER-PART truncates toward zero directly -- confirmed
271
+ // against a real compiler (2.7->2, -2.7->-2), no derivation needed.
272
+ trunc: fn1("INTEGER-PART"),
273
+ // No ROUND-as-an-expression intrinsic (COBOL's ROUNDED is a
274
+ // COMPUTE/ADD statement modifier, not composable inline). Built
275
+ // from FUNCTION SIGN and FUNCTION INTEGER instead -- both purely
276
+ // intrinsic, so unlike select() this doesn't need the hoisting
277
+ // machinery at all. GnuCOBOL's FUNCTION SIGN is 1-argument
278
+ // (SIGN(x) -> -1/0/1), NOT Fortran's 2-argument SIGN(A,B)
279
+ // "magnitude of A, sign of B" -- confirmed the hard way (a first
280
+ // version of this formula copied Fortran's 2-arg convention here
281
+ // by mistake and got "FUNCTION 'SIGN' has wrong number of
282
+ // arguments" from a real compiler). Also unlike Fortran's SIGN,
283
+ // confirmed zero-safe (SIGN(0.0) == 0.0 for real, not just by
284
+ // accident of a multiplied-away wrong case), so this needs no
285
+ // separate correction the way Fortran's round() does. Rounds ties
286
+ // away from zero, matching every other target here.
287
+ round: ([x]) => `(FUNCTION SIGN(${x}) * FUNCTION INTEGER(FUNCTION ABS(${x}) + 0.5))`,
288
+ // Confirmed zero-safe against a real compiler (SIGN(0.0) == 0.0,
289
+ // unlike Fortran's identically-named but 2-argument intrinsic --
290
+ // see round() above) -- no hoisting/spilling machinery needed,
291
+ // unlike every other emitter here that has to hand-build this.
292
+ sign: fn1("SIGN"),
293
+ // No FUNCTION ATAN2 -- the standard quadrant-corrected formula,
294
+ // built from ef-cmp-* the same way select() itself composes them
295
+ // (see emitSelect below). EVERY argument to EVERY ef-cmp-* call
296
+ // must be a bare, already-spilled name -- including ones built
297
+ // from ANOTHER ef-cmp-* call's result -- since a nested
298
+ // `FUNCTION ef-cmp-x(...)` used directly as an argument to another
299
+ // `FUNCTION ef-cmp-y(...)` hits the exact same confirmed bug as a
300
+ // nested intrinsic call would (see the file header); the first
301
+ // version of this formula got that wrong (nested an ef-cmp-lt
302
+ // call straight into ef-cmp-gt's argument list) and silently
303
+ // computed garbage, caught only by actually compiling and running
304
+ // it. So: absolutely nothing here is inlined -- every intermediate
305
+ // result, including literals, gets its own spill() first.
306
+ //
307
+ // Arrow function, not a plain one: base.js's emitExpr calls
308
+ // `this.calls[node.name](args)` unbound (unlike emitSelectImpl,
309
+ // which the Emitter constructor explicitly .bind(this)s) -- an
310
+ // arrow here closes over the `emitter` const below by reference
311
+ // instead, which is fully assigned by the time this ever actually
312
+ // runs (during some later emitFunction call), even though it's
313
+ // referenced before that `const` is declared in this same object
314
+ // literal.
315
+ atan2: (args) => {
316
+ const [yRaw, xRaw] = args;
317
+ const pool = emitter._pool;
318
+ const y = pool.spill(yRaw);
319
+ const x = pool.spill(xRaw);
320
+ const atanYX = pool.spill(`FUNCTION ATAN(${y} / ${x})`);
321
+ const zero = pool.spill("0.0");
322
+ const piOver2 = pool.spill("1.5707963267948966");
323
+ const negPiOver2 = pool.spill("-1.5707963267948966");
324
+ const atanPlusPi = pool.spill(`(${atanYX} + 3.141592653589793)`);
325
+ const atanMinusPi = pool.spill(`(${atanYX} - 3.141592653589793)`);
326
+ // x == 0 case: sign of y (0 conventionally maps to 0.0, same
327
+ // convention JS's/Python's atan2(0,0) use).
328
+ const yNegSubcase = pool.spill(`FUNCTION ef-cmp-lt(${y}, ${zero}, ${negPiOver2}, ${zero})`);
329
+ const xZeroCase = pool.spill(`FUNCTION ef-cmp-gt(${y}, ${zero}, ${piOver2}, ${yNegSubcase})`);
330
+ // x < 0 case: quadrant-corrected by the sign of y.
331
+ const xNegCase = pool.spill(`FUNCTION ef-cmp-ge(${y}, ${zero}, ${atanPlusPi}, ${atanMinusPi})`);
332
+ const xNegOrZeroCase = pool.spill(`FUNCTION ef-cmp-lt(${x}, ${zero}, ${xNegCase}, ${xZeroCase})`);
333
+ return `FUNCTION ef-cmp-gt(${x}, ${zero}, ${atanYX}, ${xNegOrZeroCase})`;
334
+ },
335
+ },
336
+ // See the file header and TempPool above for why this spills into
337
+ // temps instead of nesting inline: a user-defined FUNCTION-ID call
338
+ // (ef-cmp-*) confirmed miscomputes when given a complex argument, so
339
+ // every one of L/R/then/else gets its own COMPUTE into a fresh temp
340
+ // first, and the picker call itself only ever receives bare names.
341
+ emitSelect: function (condNode, thenStr, elseStr) {
342
+ const { suffix } = CMP_HELPERS[condNode.op];
343
+ const L = this._pool.spill(this.emitExpr(condNode.left));
344
+ const R = this._pool.spill(this.emitExpr(condNode.right));
345
+ const thenTmp = this._pool.spill(thenStr);
346
+ const elseTmp = this._pool.spill(elseStr);
347
+ return `FUNCTION ef-cmp-${suffix}(${L}, ${R}, ${thenTmp}, ${elseTmp})`;
348
+ },
349
+ // Both the scalar and suite cases use the SAME convention: a callable
350
+ // PROGRAM-ID with every output (the single return value, or every
351
+ // outputs() field) as a trailing BY REFERENCE parameter -- COBOL's
352
+ // default parameter-passing mode, confirmed reliable (BY VALUE is
353
+ // explicitly flagged "unfinished" by a real GnuCOBOL compile). This
354
+ // also sidesteps a second confirmed rough edge: calling a FUNCTION-ID
355
+ // module by name (`FUNCTION word(...)`) breaks if that name contains
356
+ // an underscore (confirmed against a real compiler -- see e.g.
357
+ // samples/spline-frame.js's "wy_wire" param), while CALL "name" takes
358
+ // the program name as a plain string literal, immune to that.
359
+ formatFunction: (fn, body, letLines, letDecls, bodyLines) => {
360
+ checkReservedNames([fn.name, ...fn.params]);
361
+ const linkageParams = [...fn.params, "ef-result"];
362
+ const paramDecls = linkageParams.map((p) => ` 01 ${p} USAGE COMP-2.`).join("\n");
363
+ const wsDecls = letDecls.map((n) => ` 01 ${n} USAGE COMP-2.`).join("\n");
364
+ return ` >>SOURCE FORMAT FREE\n` +
365
+ ` *> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
366
+ CMP_HELPER_SOURCE + "\n" +
367
+ ` IDENTIFICATION DIVISION.\n` +
368
+ ` PROGRAM-ID. ${fn.name}.\n` +
369
+ ` ENVIRONMENT DIVISION.\n` +
370
+ ` CONFIGURATION SECTION.\n` +
371
+ CMP_REPOSITORY +
372
+ ` DATA DIVISION.\n` +
373
+ (wsDecls ? ` WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
374
+ ` LINKAGE SECTION.\n` +
375
+ paramDecls + "\n" +
376
+ ` PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
377
+ [...letLines, ...bodyLines].join("\n") + (letLines.length || bodyLines.length ? "\n" : "") +
378
+ wrapLine(` COMPUTE ef-result = ${body}`) + "\n" +
379
+ ` GOBACK.\n` +
380
+ ` END PROGRAM ${fn.name}.\n`;
381
+ },
382
+ formatSuite: (fn, outputStrs, letLines, letDecls, outputLines) => {
383
+ const outputNames = Object.keys(outputStrs);
384
+ checkReservedNames([fn.name, ...fn.params, ...outputNames]);
385
+ const linkageParams = [...fn.params, ...outputNames];
386
+ const paramDecls = linkageParams.map((p) => ` 01 ${p} USAGE COMP-2.`).join("\n");
387
+ const wsDecls = letDecls.map((n) => ` 01 ${n} USAGE COMP-2.`).join("\n");
388
+ const assigns = outputNames.map((n) => wrapLine(` COMPUTE ${n} = ${outputStrs[n]}`)).join("\n");
389
+ return ` >>SOURCE FORMAT FREE\n` +
390
+ ` *> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
391
+ CMP_HELPER_SOURCE + "\n" +
392
+ ` IDENTIFICATION DIVISION.\n` +
393
+ ` PROGRAM-ID. ${fn.name}.\n` +
394
+ ` ENVIRONMENT DIVISION.\n` +
395
+ ` CONFIGURATION SECTION.\n` +
396
+ CMP_REPOSITORY +
397
+ ` DATA DIVISION.\n` +
398
+ (wsDecls ? ` WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
399
+ ` LINKAGE SECTION.\n` +
400
+ paramDecls + "\n" +
401
+ ` PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
402
+ (letLines.length ? letLines.join("\n") + "\n" : "") +
403
+ (outputLines.length ? outputLines.join("\n") + "\n" : "") +
404
+ assigns + "\n" +
405
+ ` GOBACK.\n` +
406
+ ` END PROGRAM ${fn.name}.\n`;
407
+ },
408
+ });
409
+
410
+ module.exports = emitter;
@@ -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;