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.
- package/README.md +199 -16
- package/ast.js +22 -1
- package/emitters/cobol.js +410 -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 +7 -0
- package/emitters/scheme.js +154 -0
- package/emitters/zig.js +126 -0
- package/index.js +7 -2
- package/math/index.js +113 -0
- package/package.json +9 -3
- package/samples/math-demo.js +48 -0
- package/samples/spline-frame.js +60 -42
|
@@ -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;
|