exprforge 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +562 -99
- package/ast.js +190 -2
- package/emitters/base.js +36 -4
- package/emitters/cobol.js +191 -54
- package/emitters/exprsyntax.js +39 -8
- package/emitters/registry.js +12 -1
- package/evaluate.js +47 -14
- package/expr.js +107 -9
- package/fn.js +31 -6
- package/index.js +141 -8
- package/load-expr.js +112 -0
- package/macros.js +707 -0
- package/math/index.js +21 -0
- package/package.json +4 -1
- package/primitives.js +24 -0
- package/samples/macro-demo.js +56 -0
package/ast.js
CHANGED
|
@@ -11,24 +11,87 @@
|
|
|
11
11
|
// { type: "cmp", op: ">" | "<" | ">=" | "<=" | "==" | "!=", left: Node, right: Node }
|
|
12
12
|
// { type: "select", cond: CmpNode, then: Node, else: Node }
|
|
13
13
|
// { type: "outputs", fields: { [name: string]: Node } }
|
|
14
|
+
// { type: "field", target: Node, field: string }
|
|
15
|
+
//
|
|
16
|
+
// "field" is postfix "." access (e.g. b.rx) — parser sugar produced only
|
|
17
|
+
// by expr.js/fn.js's grammar, and eliminated by macros.js's
|
|
18
|
+
// expandMacros() before a tree ever reaches checkUnboundVars,
|
|
19
|
+
// evaluate(), or any emitter. It only makes semantic sense when `target`
|
|
20
|
+
// resolves to a name bound to a multi-output *macro* call (see
|
|
21
|
+
// macros.js) — that's checked there, not here, same "defer semantic
|
|
22
|
+
// validation to the consumer" precedent call() already follows for
|
|
23
|
+
// function names. A "field" node reaching evaluate()/an emitter directly
|
|
24
|
+
// means expandMacros() was skipped or didn't run to completion; both
|
|
25
|
+
// throw their own "unknown node type" error in that case.
|
|
14
26
|
//
|
|
15
27
|
// Every "bin" node is emitted with explicit parens in every target, so
|
|
16
28
|
// operation order (and therefore floating-point rounding behavior) is
|
|
17
29
|
// identical everywhere.
|
|
30
|
+
//
|
|
31
|
+
// SECURITY: every builder below validates its own name/op/value
|
|
32
|
+
// argument(s) -- see assertSafeIdentifier/assertSafeOp/assertFiniteNumber
|
|
33
|
+
// just below. This is specifically about these builders being the "raw
|
|
34
|
+
// AST" layer -- the one this project's own README recommends reaching
|
|
35
|
+
// for when you want the least amount of magic between your formula and
|
|
36
|
+
// the code it emits. Without validation, that would have been the LEAST
|
|
37
|
+
// safe layer to build from untrusted input, not the most: fn`...`/
|
|
38
|
+
// expr`...`'s own tokenizer already only ever produces safe identifier
|
|
39
|
+
// characters, so these builders were the one place a malicious/malformed
|
|
40
|
+
// name -- e.g. `v('x); process.exit(1); //')` -- could reach emitted
|
|
41
|
+
// output completely unchecked, verbatim, in all 16 targets at once.
|
|
42
|
+
// Confirmed, not assumed, before this existed.
|
|
43
|
+
|
|
44
|
+
// Matches fn`...`/expr`...`'s own tokenizer IDENT rule exactly (see
|
|
45
|
+
// expr.js's tokenizeSegment) -- anything that couldn't have come out of
|
|
46
|
+
// the real parser isn't allowed in here either.
|
|
47
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
48
|
+
|
|
49
|
+
function assertSafeIdentifier(name, context) {
|
|
50
|
+
if (typeof name !== "string" || !IDENTIFIER.test(name)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`${context}: ${JSON.stringify(name)} isn't a safe identifier -- must start with a letter or "_", ` +
|
|
53
|
+
`followed only by letters/digits/"_" (the same rule fn\`...\`/expr\`...\`'s own tokenizer already ` +
|
|
54
|
+
`enforces on anything parsed from text; this only matters when building a tree directly, bypassing ` +
|
|
55
|
+
`the parser)`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const BIN_OPS = new Set(["+", "-", "*", "/"]);
|
|
61
|
+
const CMP_OPS = new Set([">", "<", ">=", "<=", "==", "!="]);
|
|
62
|
+
|
|
63
|
+
function assertSafeOp(op, allowed, context) {
|
|
64
|
+
if (!allowed.has(op)) {
|
|
65
|
+
throw new Error(`${context}: ${JSON.stringify(op)} isn't one of the allowed operators (${[...allowed].join(" ")})`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assertFiniteNumber(value, context) {
|
|
70
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`${context}: ${JSON.stringify(value)} isn't a finite number -- NaN/Infinity/non-numeric values ` +
|
|
73
|
+
`can't be emitted as a literal in every target`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
18
77
|
|
|
19
78
|
function num(value) {
|
|
79
|
+
assertFiniteNumber(value, "num()");
|
|
20
80
|
return { type: "num", value };
|
|
21
81
|
}
|
|
22
82
|
|
|
23
83
|
function v(name) {
|
|
84
|
+
assertSafeIdentifier(name, "v()");
|
|
24
85
|
return { type: "var", name };
|
|
25
86
|
}
|
|
26
87
|
|
|
27
88
|
function bin(op, left, right) {
|
|
89
|
+
assertSafeOp(op, BIN_OPS, "bin()");
|
|
28
90
|
return { type: "bin", op, left, right };
|
|
29
91
|
}
|
|
30
92
|
|
|
31
93
|
function call(name, ...args) {
|
|
94
|
+
assertSafeIdentifier(name, "call()");
|
|
32
95
|
return { type: "call", name, args };
|
|
33
96
|
}
|
|
34
97
|
|
|
@@ -59,6 +122,7 @@ function neg(x) {
|
|
|
59
122
|
// then divide three components by it). Lifted out by collectLets before
|
|
60
123
|
// emission — see there for how `v(name)` ends up referring to it.
|
|
61
124
|
function letIn(name, value, body) {
|
|
125
|
+
assertSafeIdentifier(name, "letIn()");
|
|
62
126
|
return { type: "let", name, value, body };
|
|
63
127
|
}
|
|
64
128
|
|
|
@@ -86,6 +150,7 @@ function letChain(bindings, body) {
|
|
|
86
150
|
// Comparison predicate — only valid as the `cond` of a select(); not a
|
|
87
151
|
// general boolean expression, and shouldn't appear anywhere else in a tree.
|
|
88
152
|
function cmp(left, op, right) {
|
|
153
|
+
assertSafeOp(op, CMP_OPS, "cmp()");
|
|
89
154
|
return { type: "cmp", op, left, right };
|
|
90
155
|
}
|
|
91
156
|
|
|
@@ -107,9 +172,28 @@ function select(cond, thenNode, elseNode) {
|
|
|
107
172
|
// return, an object literal, output parameters) — see formatSuite in each
|
|
108
173
|
// emitters/<lang>.js.
|
|
109
174
|
function outputs(fields) {
|
|
175
|
+
for (const name of Object.keys(fields)) assertSafeIdentifier(name, "outputs()");
|
|
110
176
|
return { type: "outputs", fields };
|
|
111
177
|
}
|
|
112
178
|
|
|
179
|
+
// Postfix "." field access into a multi-output intrinsic's result — see
|
|
180
|
+
// the "field" node-shape comment at the top of this file for what this
|
|
181
|
+
// actually means and who consumes it.
|
|
182
|
+
function field(target, name) {
|
|
183
|
+
assertSafeIdentifier(name, "field()");
|
|
184
|
+
return { type: "field", target, field: name };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The prefix macros.js's own gensym'd internal let-names always start
|
|
188
|
+
// with (see substituteAndRename's "let" case there) — defined HERE, not
|
|
189
|
+
// there, specifically so collectLets below can recognize a collision
|
|
190
|
+
// against one WITHOUT macros.js needing to require this file back (it
|
|
191
|
+
// already does the other direction: macros.js requires ast.js). Kept as
|
|
192
|
+
// one shared constant rather than a duplicated string literal in both
|
|
193
|
+
// files, so it can't quietly drift out of sync between "the name this
|
|
194
|
+
// generates" and "the name this recognizes".
|
|
195
|
+
const MACRO_GENSYM_PREFIX = "efMacro_";
|
|
196
|
+
|
|
113
197
|
// Lifts every `let` node out of the tree into a flat, ordered list of
|
|
114
198
|
// { name, node } bindings, replacing each with a plain v(name) reference.
|
|
115
199
|
// The list is in dependency order — safe to declare/assign top-to-bottom.
|
|
@@ -152,7 +236,23 @@ function collectLets(node) {
|
|
|
152
236
|
const seen = new Set();
|
|
153
237
|
for (const { name } of bindings) {
|
|
154
238
|
if (seen.has(name)) {
|
|
155
|
-
|
|
239
|
+
// A colliding name that happens to start with macros.js's own
|
|
240
|
+
// gensym prefix is almost certainly NOT something you wrote
|
|
241
|
+
// on purpose -- vanishingly unlikely to be an intentional
|
|
242
|
+
// collision, and confusing to debug as a plain "duplicate
|
|
243
|
+
// name" if you don't already know that prefix means
|
|
244
|
+
// "internally generated" -- named explicitly here rather
|
|
245
|
+
// than left for you to work out. The gensym'd name itself is
|
|
246
|
+
// never the one to rename (it's already unique per macro
|
|
247
|
+
// invocation, see toMacro's own comment in macros.js) -- only
|
|
248
|
+
// your OWN same-named binding actually needs to change.
|
|
249
|
+
const hint = name.startsWith(MACRO_GENSYM_PREFIX)
|
|
250
|
+
? ` -- this looks like an internal name macro expansion generates automatically ` +
|
|
251
|
+
`(see macros.js's own gensym'd "let" renaming), not something you wrote; if you ` +
|
|
252
|
+
`have your own let/param actually named "${name}", rename YOURS to something that ` +
|
|
253
|
+
`doesn't start with "${MACRO_GENSYM_PREFIX}"`
|
|
254
|
+
: "";
|
|
255
|
+
throw new Error(`collectLets: duplicate let binding name "${name}" in one function${hint}`);
|
|
156
256
|
}
|
|
157
257
|
seen.add(name);
|
|
158
258
|
}
|
|
@@ -160,4 +260,92 @@ function collectLets(node) {
|
|
|
160
260
|
return { bindings, body };
|
|
161
261
|
}
|
|
162
262
|
|
|
163
|
-
|
|
263
|
+
// Every v(name) reference anywhere in `node`, regardless of whether
|
|
264
|
+
// anything actually declares it -- a pure structural walk, no binding
|
|
265
|
+
// awareness at all. `refs` accumulates across recursive calls so this
|
|
266
|
+
// can be called repeatedly against several subtrees (e.g. once per
|
|
267
|
+
// let-binding's own value, plus once for the final body) and still
|
|
268
|
+
// build one combined set. Mirrors collectLets's own node-type walk
|
|
269
|
+
// above exactly, since it needs to see the identical tree shape.
|
|
270
|
+
function collectVarRefs(node, refs = new Set()) {
|
|
271
|
+
if (node.type === "var") {
|
|
272
|
+
refs.add(node.name);
|
|
273
|
+
} else if (node.type === "bin") {
|
|
274
|
+
collectVarRefs(node.left, refs);
|
|
275
|
+
collectVarRefs(node.right, refs);
|
|
276
|
+
} else if (node.type === "call") {
|
|
277
|
+
for (const a of node.args) collectVarRefs(a, refs);
|
|
278
|
+
} else if (node.type === "cmp") {
|
|
279
|
+
collectVarRefs(node.left, refs);
|
|
280
|
+
collectVarRefs(node.right, refs);
|
|
281
|
+
} else if (node.type === "select") {
|
|
282
|
+
collectVarRefs(node.cond, refs);
|
|
283
|
+
collectVarRefs(node.then, refs);
|
|
284
|
+
collectVarRefs(node.else, refs);
|
|
285
|
+
} else if (node.type === "let") {
|
|
286
|
+
collectVarRefs(node.value, refs);
|
|
287
|
+
collectVarRefs(node.body, refs);
|
|
288
|
+
} else if (node.type === "outputs") {
|
|
289
|
+
for (const fieldNode of Object.values(node.fields)) collectVarRefs(fieldNode, refs);
|
|
290
|
+
}
|
|
291
|
+
// num: nothing to add.
|
|
292
|
+
return refs;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Confirms every var() reference anywhere in fn.body -- inside a
|
|
296
|
+
// let-binding's own value, or in the final body/outputs -- corresponds
|
|
297
|
+
// to something actually declared: a parameter, or a let binding
|
|
298
|
+
// somewhere else in the same function. "Somewhere else", not
|
|
299
|
+
// "somewhere earlier": collectLets's own doc comment already
|
|
300
|
+
// establishes there's no real lexical scoping here -- every let-binding
|
|
301
|
+
// is one flat, function-wide name -- so "declared anywhere in this
|
|
302
|
+
// function" is the right, and only meaningful, check, not an
|
|
303
|
+
// order-sensitive one.
|
|
304
|
+
//
|
|
305
|
+
// Catches a typo'd or forgotten identifier at the earliest possible
|
|
306
|
+
// point, for every target and for evaluate() uniformly, rather than
|
|
307
|
+
// relying on evaluate() happening to hit it at runtime (which it might
|
|
308
|
+
// never do -- e.g. a reference inside a select() branch that a
|
|
309
|
+
// particular call's arguments never take would never surface that way)
|
|
310
|
+
// or on whichever target language's own compiler/runtime eventually
|
|
311
|
+
// notices, with wildly inconsistent timing and clarity (a real compile
|
|
312
|
+
// error in Java, a silent-until-called ReferenceError in JS). Confirmed
|
|
313
|
+
// the gap first, not assumed: before this existed, emitAll() silently
|
|
314
|
+
// succeeded across all 18 targets for a body referencing a completely
|
|
315
|
+
// undeclared name.
|
|
316
|
+
//
|
|
317
|
+
// Also validates fn.name and every fn.params entry as safe identifiers
|
|
318
|
+
// (see assertSafeIdentifier above) -- the SAME concern as every builder
|
|
319
|
+
// above, just here instead of at a constructor call site, since
|
|
320
|
+
// {name, params, body} is a plain object literal shape with no
|
|
321
|
+
// constructor function of its own to hook into. This is the one place
|
|
322
|
+
// every real consumption path (evaluate(), every emitter's
|
|
323
|
+
// emitFunction(), cobol.js's own override) already runs unconditionally,
|
|
324
|
+
// so it's the natural single checkpoint for this too.
|
|
325
|
+
function checkUnboundVars(fn) {
|
|
326
|
+
assertSafeIdentifier(fn.name, "fn.name");
|
|
327
|
+
for (const p of fn.params) assertSafeIdentifier(p, "fn.params");
|
|
328
|
+
|
|
329
|
+
const { bindings, body } = collectLets(fn.body);
|
|
330
|
+
const declared = new Set([...fn.params, ...bindings.map((b) => b.name)]);
|
|
331
|
+
|
|
332
|
+
const referenced = new Set();
|
|
333
|
+
for (const { node } of bindings) collectVarRefs(node, referenced);
|
|
334
|
+
collectVarRefs(body, referenced);
|
|
335
|
+
|
|
336
|
+
for (const name of referenced) {
|
|
337
|
+
if (!declared.has(name)) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`checkUnboundVars: "${name}" is referenced in "${fn.name}" but never declared -- ` +
|
|
340
|
+
`not a parameter (${fn.params.length ? fn.params.join(", ") : "none"}) and no ` +
|
|
341
|
+
`"let ${name} = ..." binding exists anywhere in this function`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = {
|
|
348
|
+
num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, field, collectLets,
|
|
349
|
+
checkUnboundVars,
|
|
350
|
+
MACRO_GENSYM_PREFIX,
|
|
351
|
+
};
|
package/emitters/base.js
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
// suite gets emitted through this emitter; omitted
|
|
27
27
|
// otherwise.
|
|
28
28
|
|
|
29
|
-
const { collectLets } = require("../ast.js");
|
|
29
|
+
const { collectLets, checkUnboundVars } = require("../ast.js");
|
|
30
|
+
const { expandMacros, resolveExternForEmitter, withContext } = require("../macros.js");
|
|
30
31
|
|
|
31
32
|
class Emitter {
|
|
32
33
|
constructor(config) {
|
|
@@ -64,11 +65,23 @@ class Emitter {
|
|
|
64
65
|
}
|
|
65
66
|
case "call": {
|
|
66
67
|
const args = node.args.map((a) => this.emitExpr(a));
|
|
67
|
-
|
|
68
|
+
// `this.lang` is injected by emitters/registry.js (the
|
|
69
|
+
// registered key, e.g. "js"/"cobol") -- not set for an
|
|
70
|
+
// Emitter built standalone outside the registry, in which
|
|
71
|
+
// case an extern simply never resolves here, same as an
|
|
72
|
+
// unmapped name.
|
|
73
|
+
const builtin = this.calls[node.name];
|
|
74
|
+
const template = builtin || (this.lang && resolveExternForEmitter(node.name, this.lang, this._registry));
|
|
68
75
|
if (!template) {
|
|
69
76
|
throw new Error(`emitter for .${this.ext}: no mapping for Math function "${node.name}"`);
|
|
70
77
|
}
|
|
71
|
-
|
|
78
|
+
// Only an extern's own per-target template is caller-
|
|
79
|
+
// supplied code that could have a bug worth attributing
|
|
80
|
+
// -- a built-in primitive's template is exprforge's own,
|
|
81
|
+
// not worth wrapping.
|
|
82
|
+
return builtin
|
|
83
|
+
? template(args)
|
|
84
|
+
: withContext(`emitter for .${this.ext}: while emitting extern "${node.name}"`, () => template(args));
|
|
72
85
|
}
|
|
73
86
|
case "select": {
|
|
74
87
|
const thenStr = this.emitExpr(node.then);
|
|
@@ -89,7 +102,26 @@ class Emitter {
|
|
|
89
102
|
}
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
|
|
105
|
+
// `registry` (see macros.js's createRegistry()) defaults to the
|
|
106
|
+
// process-wide default when omitted -- pass a session's own (see
|
|
107
|
+
// index.js's createSession()) to resolve macros/externs against that
|
|
108
|
+
// session instead. Stashed on `this` for emitExpr's "call" case to
|
|
109
|
+
// read, same instance-state pattern emitters/cobol.js's own
|
|
110
|
+
// `this._pool` already established.
|
|
111
|
+
emitFunction(fn, registry = undefined) {
|
|
112
|
+
this._registry = registry;
|
|
113
|
+
// Resolves every macro call and field() access into plain
|
|
114
|
+
// arithmetic first -- see macros.js's own header comment. Must
|
|
115
|
+
// run before checkUnboundVars: expansion is what introduces the
|
|
116
|
+
// flattened let-bindings a multi-output macro's fields become,
|
|
117
|
+
// and eliminates "field" nodes, which checkUnboundVars/
|
|
118
|
+
// collectLets don't know how to walk.
|
|
119
|
+
fn = expandMacros(fn, null, registry);
|
|
120
|
+
// Checked once, before any per-target work starts -- see
|
|
121
|
+
// checkUnboundVars's own comment in ast.js for why this matters
|
|
122
|
+
// (a typo'd/forgotten identifier used to silently succeed here,
|
|
123
|
+
// for every target, with no error at all).
|
|
124
|
+
checkUnboundVars(fn);
|
|
93
125
|
const { bindings, body } = collectLets(fn.body);
|
|
94
126
|
const letBindings = bindings.map(({ name, node }) => ({
|
|
95
127
|
name,
|
package/emitters/cobol.js
CHANGED
|
@@ -34,6 +34,24 @@
|
|
|
34
34
|
// machinery as select() itself, not a separate mechanism.
|
|
35
35
|
const Emitter = require("./base.js");
|
|
36
36
|
|
|
37
|
+
// Free-format COBOL (this file always emits >>SOURCE FORMAT FREE, see
|
|
38
|
+
// formatFunction/formatSuite below) doesn't need or use the traditional
|
|
39
|
+
// fixed-format column layout -- cols 1-6 sequence numbers, col 7
|
|
40
|
+
// indicator area, content starting at col 8/"Area A" -- that's purely a
|
|
41
|
+
// punch-card-era holdover, not something GnuCOBOL's free-format parser
|
|
42
|
+
// requires. Confirmed by actually compiling with it removed (not just
|
|
43
|
+
// reasoned about): the original 7/11/15-space margins compiled fine,
|
|
44
|
+
// and so does this smaller scheme, run through the exact same
|
|
45
|
+
// test/conformance.test.js COBOL checks. Kept a small, familiar
|
|
46
|
+
// 4-space-per-nesting-level convention purely for readability's sake:
|
|
47
|
+
// HDR (division/section/paragraph headers, 01-level declarations,
|
|
48
|
+
// REPOSITORY entries) at column 1, STMT (top-level PROCEDURE DIVISION
|
|
49
|
+
// statements) one level in, NESTED (statements inside an IF/ELSE, or a
|
|
50
|
+
// wrapped line's continuation) one level deeper still.
|
|
51
|
+
const HDR = "";
|
|
52
|
+
const STMT = " ";
|
|
53
|
+
const NESTED = " ";
|
|
54
|
+
|
|
37
55
|
// COBOL reserved words plus every intrinsic-function name this emitter's
|
|
38
56
|
// calls table depends on -- same role as QB64_RESERVED in emitters/qb64.js.
|
|
39
57
|
// COBOL is case-insensitive, so names are checked lowercased. Not
|
|
@@ -91,6 +109,101 @@ function checkUsingClauseNames(names) {
|
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
111
|
|
|
112
|
+
// See https://github.com/theraccoonbear/exprforge/issues/18 for the full
|
|
113
|
+
// design rationale. Only PARAMETERS get this treatment, deliberately --
|
|
114
|
+
// every one of this project's 18 targets calls functions positionally,
|
|
115
|
+
// so a parameter's declared name is purely an internal binding, never
|
|
116
|
+
// visible to a caller in ANY target, making a per-target rename here
|
|
117
|
+
// completely invisible from the outside. fn.name and outputs() field
|
|
118
|
+
// names are NOT included: both remain part of the actual calling
|
|
119
|
+
// contract (CALL "name" USING ... for the function name; a suite's
|
|
120
|
+
// field names are genuinely consumer-visible in every other target's
|
|
121
|
+
// return shape -- a JS caller reads `result.c`), so those still throw
|
|
122
|
+
// via checkUsingClauseNames above rather than silently diverging from
|
|
123
|
+
// what the AST author wrote.
|
|
124
|
+
//
|
|
125
|
+
// The "EFLF_" prefix (ExprForge Language Fix) itself was verified
|
|
126
|
+
// against a real compile+link+run before choosing it as-is, not
|
|
127
|
+
// assumed safe just because it's a plausible-looking identifier: this
|
|
128
|
+
// file already has a DIFFERENT confirmed finding that a COBOL
|
|
129
|
+
// `FUNCTION` call breaks on an underscored name (see CMP_HELPERS above
|
|
130
|
+
// -- why ef-cmp-* is hyphenated, never ef_cmp_*), which could easily
|
|
131
|
+
// have meant the same restriction applies here too. It doesn't --
|
|
132
|
+
// confirmed directly that `EFLF_c` compiles AND runs correctly used
|
|
133
|
+
// exactly as a parameter name in a real `PROCEDURE DIVISION USING`
|
|
134
|
+
// clause. That earlier finding was specifically about calling a
|
|
135
|
+
// FUNCTION-ID *by name* (`FUNCTION word(...)`), not about declaring or
|
|
136
|
+
// referencing a plain variable/parameter identifier, which is the only
|
|
137
|
+
// thing this does.
|
|
138
|
+
const RENAME_PREFIX = "EFLF_";
|
|
139
|
+
|
|
140
|
+
// Rewrites every `v(name)` reference inside `node` to `v(renames.get(name))`
|
|
141
|
+
// wherever `name` is a key in `renames`, recursively, leaving everything
|
|
142
|
+
// else (including a nested let's OWN binding name -- only var REFERENCES
|
|
143
|
+
// are ever renamed, never a let's declared name) untouched. Mirrors
|
|
144
|
+
// collectLets's own node-type walk in ast.js exactly, since this needs to
|
|
145
|
+
// see the identical tree shape before collectLets ever flattens it.
|
|
146
|
+
function renameVarRefs(node, renames) {
|
|
147
|
+
const { v, bin, call, cmp, select, letIn, outputs } = require("../ast.js");
|
|
148
|
+
if (node.type === "var") {
|
|
149
|
+
return renames.has(node.name) ? v(renames.get(node.name)) : node;
|
|
150
|
+
}
|
|
151
|
+
if (node.type === "num") return node;
|
|
152
|
+
if (node.type === "bin") return bin(node.op, renameVarRefs(node.left, renames), renameVarRefs(node.right, renames));
|
|
153
|
+
if (node.type === "call") return call(node.name, ...node.args.map((a) => renameVarRefs(a, renames)));
|
|
154
|
+
if (node.type === "cmp") return cmp(renameVarRefs(node.left, renames), node.op, renameVarRefs(node.right, renames));
|
|
155
|
+
if (node.type === "select") {
|
|
156
|
+
return select(renameVarRefs(node.cond, renames), renameVarRefs(node.then, renames), renameVarRefs(node.else, renames));
|
|
157
|
+
}
|
|
158
|
+
if (node.type === "let") return letIn(node.name, renameVarRefs(node.value, renames), renameVarRefs(node.body, renames));
|
|
159
|
+
if (node.type === "outputs") {
|
|
160
|
+
const fields = {};
|
|
161
|
+
for (const [name, fieldNode] of Object.entries(node.fields)) {
|
|
162
|
+
fields[name] = renameVarRefs(fieldNode, renames);
|
|
163
|
+
}
|
|
164
|
+
return outputs(fields);
|
|
165
|
+
}
|
|
166
|
+
throw new Error(`emitter for .cob: renameConflictingParams: unknown node type "${node.type}"`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Returns a NEW {name, params, body} -- fn itself is never mutated -- with
|
|
170
|
+
// any parameter colliding with `conflictSet` renamed to
|
|
171
|
+
// `${RENAME_PREFIX}${originalName}` everywhere it's declared and
|
|
172
|
+
// referenced. A no-op (returns fn unchanged) when nothing collides, so
|
|
173
|
+
// this is always safe to call unconditionally at the top of
|
|
174
|
+
// emitFunction.
|
|
175
|
+
function renameConflictingParams(fn, conflictSet) {
|
|
176
|
+
const renames = new Map();
|
|
177
|
+
for (const p of fn.params) {
|
|
178
|
+
if (conflictSet.has(p.toLowerCase())) {
|
|
179
|
+
renames.set(p, `${RENAME_PREFIX}${p}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (renames.size === 0) return fn;
|
|
183
|
+
|
|
184
|
+
const newParams = fn.params.map((p) => renames.get(p) ?? p);
|
|
185
|
+
// Defensive, not expected to ever actually fire given how narrow
|
|
186
|
+
// COBOL_USING_RESERVED is today -- but if a renamed param ever DID
|
|
187
|
+
// collide with another existing param (e.g. both "c" and "EFLF_c"
|
|
188
|
+
// used as real param names in the same function), that would
|
|
189
|
+
// silently produce a duplicate 01-level declaration and a real
|
|
190
|
+
// compiler error far from this code -- fail loudly here instead,
|
|
191
|
+
// at the actual point the ambiguity is introduced.
|
|
192
|
+
const seen = new Set();
|
|
193
|
+
for (const p of newParams) {
|
|
194
|
+
const lower = p.toLowerCase();
|
|
195
|
+
if (seen.has(lower)) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`emitter for .cob: renaming parameter to avoid a COBOL collision produced a duplicate ` +
|
|
198
|
+
`name ("${p}") -- rename your original "${p}"-colliding parameter directly instead`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
seen.add(lower);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { name: fn.name, params: newParams, body: renameVarRefs(fn.body, renames) };
|
|
205
|
+
}
|
|
206
|
+
|
|
94
207
|
function fn1(name) {
|
|
95
208
|
return ([x]) => `FUNCTION ${name}(${x})`;
|
|
96
209
|
}
|
|
@@ -121,31 +234,31 @@ const CMP_HELPERS = {
|
|
|
121
234
|
// statement per line -- confirmed against a real compiler that a missing
|
|
122
235
|
// trailing period here breaks the DATA DIVISION that follows).
|
|
123
236
|
const CMP_REPOSITORY =
|
|
124
|
-
|
|
237
|
+
`${HDR}REPOSITORY.\n` +
|
|
125
238
|
Object.values(CMP_HELPERS)
|
|
126
|
-
.map(({ suffix }, i, arr) =>
|
|
239
|
+
.map(({ suffix }, i, arr) => `${STMT}FUNCTION ef-cmp-${suffix}${i === arr.length - 1 ? "." : ""}`)
|
|
127
240
|
.join("\n") +
|
|
128
241
|
"\n";
|
|
129
242
|
|
|
130
243
|
const CMP_HELPER_SOURCE = Object.values(CMP_HELPERS)
|
|
131
244
|
.map(
|
|
132
|
-
({ suffix, test }) =>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
245
|
+
({ suffix, test }) => `${HDR}IDENTIFICATION DIVISION.
|
|
246
|
+
${HDR}FUNCTION-ID. ef-cmp-${suffix}.
|
|
247
|
+
${HDR}DATA DIVISION.
|
|
248
|
+
${HDR}LINKAGE SECTION.
|
|
249
|
+
${HDR}01 L USAGE COMP-2.
|
|
250
|
+
${HDR}01 R USAGE COMP-2.
|
|
251
|
+
${HDR}01 THEN-VAL USAGE COMP-2.
|
|
252
|
+
${HDR}01 ELSE-VAL USAGE COMP-2.
|
|
253
|
+
${HDR}01 RESULT USAGE COMP-2.
|
|
254
|
+
${HDR}PROCEDURE DIVISION USING L R THEN-VAL ELSE-VAL RETURNING RESULT.
|
|
255
|
+
${STMT}IF ${test}
|
|
256
|
+
${NESTED}MOVE THEN-VAL TO RESULT
|
|
257
|
+
${STMT}ELSE
|
|
258
|
+
${NESTED}MOVE ELSE-VAL TO RESULT
|
|
259
|
+
${STMT}END-IF
|
|
260
|
+
${STMT}GOBACK.
|
|
261
|
+
${HDR}END FUNCTION ef-cmp-${suffix}.
|
|
149
262
|
`,
|
|
150
263
|
)
|
|
151
264
|
.join("\n");
|
|
@@ -165,7 +278,7 @@ function wrapLine(line, maxWidth = 100) {
|
|
|
165
278
|
for (const word of words) {
|
|
166
279
|
if (current && current.length + 1 + word.length > maxWidth) {
|
|
167
280
|
wrapped.push(current);
|
|
168
|
-
current =
|
|
281
|
+
current = `${NESTED}${word}`;
|
|
169
282
|
} else {
|
|
170
283
|
current = current ? `${current} ${word}` : word;
|
|
171
284
|
}
|
|
@@ -200,14 +313,38 @@ class TempPool {
|
|
|
200
313
|
spill(valueStr) {
|
|
201
314
|
const name = `ef-tmp-${this.counter.next++}`;
|
|
202
315
|
this.decls.push(name);
|
|
203
|
-
this.lines.push(wrapLine(
|
|
316
|
+
this.lines.push(wrapLine(`${STMT}COMPUTE ${name} = ${valueStr}`));
|
|
204
317
|
return name;
|
|
205
318
|
}
|
|
206
319
|
}
|
|
207
320
|
|
|
208
321
|
class CobolEmitter extends Emitter {
|
|
209
|
-
emitFunction
|
|
210
|
-
|
|
322
|
+
// `registry` -- see base.js's own emitFunction comment and
|
|
323
|
+
// macros.js's createRegistry()/index.js's createSession() -- same
|
|
324
|
+
// `this._registry` instance-state convention as base.js, read by the
|
|
325
|
+
// inherited emitExpr's "call" case for extern resolution.
|
|
326
|
+
emitFunction(fn, registry = undefined) {
|
|
327
|
+
this._registry = registry;
|
|
328
|
+
const { collectLets, checkUnboundVars } = require("../ast.js");
|
|
329
|
+
const { expandMacros } = require("../macros.js");
|
|
330
|
+
// This class overrides emitFunction entirely (never calls
|
|
331
|
+
// super.emitFunction), so it needs its own copy of the same two
|
|
332
|
+
// steps base.js's Emitter.emitFunction runs -- see
|
|
333
|
+
// expandMacros's and checkUnboundVars's own comments (in
|
|
334
|
+
// macros.js and ast.js respectively). Both run against the
|
|
335
|
+
// ORIGINAL fn, before renameConflictingParams runs below, so an
|
|
336
|
+
// error message reports whatever name the caller actually wrote,
|
|
337
|
+
// not an internally-renamed one (doesn't change which names are
|
|
338
|
+
// considered declared/resolved either way -- the rename is
|
|
339
|
+
// identity-preserving over the set of bound names, just changes
|
|
340
|
+
// their string).
|
|
341
|
+
fn = expandMacros(fn, null, registry);
|
|
342
|
+
checkUnboundVars(fn);
|
|
343
|
+
// Must run before collectLets, and before anything else in this
|
|
344
|
+
// function -- collectLets flattens the tree's let structure away,
|
|
345
|
+
// and every check/emission step below assumes fn.params is
|
|
346
|
+
// already COBOL-safe. See renameConflictingParams above.
|
|
347
|
+
fn = renameConflictingParams(fn, COBOL_USING_RESERVED);
|
|
211
348
|
const { bindings, body } = collectLets(fn.body);
|
|
212
349
|
const counter = { next: 0 };
|
|
213
350
|
|
|
@@ -219,7 +356,7 @@ class CobolEmitter extends Emitter {
|
|
|
219
356
|
const valueStr = this.emitExpr(node);
|
|
220
357
|
letLines.push(...this._pool.lines);
|
|
221
358
|
letDecls.push(...this._pool.decls, name);
|
|
222
|
-
letLines.push(wrapLine(
|
|
359
|
+
letLines.push(wrapLine(`${STMT}COMPUTE ${name} = ${valueStr}`));
|
|
223
360
|
}
|
|
224
361
|
|
|
225
362
|
if (body.type === "outputs") {
|
|
@@ -426,52 +563,52 @@ const emitter = new CobolEmitter({
|
|
|
426
563
|
checkReservedNames([fn.name, ...fn.params]);
|
|
427
564
|
checkUsingClauseNames([fn.name, ...fn.params]);
|
|
428
565
|
const linkageParams = [...fn.params, "ef-result"];
|
|
429
|
-
const paramDecls = linkageParams.map((p) =>
|
|
430
|
-
const wsDecls = letDecls.map((n) =>
|
|
431
|
-
return
|
|
432
|
-
|
|
566
|
+
const paramDecls = linkageParams.map((p) => `${HDR}01 ${p} USAGE COMP-2.`).join("\n");
|
|
567
|
+
const wsDecls = letDecls.map((n) => `${HDR}01 ${n} USAGE COMP-2.`).join("\n");
|
|
568
|
+
return `${HDR}>>SOURCE FORMAT FREE\n` +
|
|
569
|
+
`${HDR}*> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
433
570
|
CMP_HELPER_SOURCE + "\n" +
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
571
|
+
`${HDR}IDENTIFICATION DIVISION.\n` +
|
|
572
|
+
`${HDR}PROGRAM-ID. ${fn.name}.\n` +
|
|
573
|
+
`${HDR}ENVIRONMENT DIVISION.\n` +
|
|
574
|
+
`${HDR}CONFIGURATION SECTION.\n` +
|
|
438
575
|
CMP_REPOSITORY +
|
|
439
|
-
|
|
440
|
-
(wsDecls ?
|
|
441
|
-
|
|
576
|
+
`${HDR}DATA DIVISION.\n` +
|
|
577
|
+
(wsDecls ? `${HDR}WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
|
|
578
|
+
`${HDR}LINKAGE SECTION.\n` +
|
|
442
579
|
paramDecls + "\n" +
|
|
443
|
-
|
|
580
|
+
`${HDR}PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
|
|
444
581
|
[...letLines, ...bodyLines].join("\n") + (letLines.length || bodyLines.length ? "\n" : "") +
|
|
445
|
-
wrapLine(
|
|
446
|
-
|
|
447
|
-
|
|
582
|
+
wrapLine(`${STMT}COMPUTE ef-result = ${body}`) + "\n" +
|
|
583
|
+
`${STMT}GOBACK.\n` +
|
|
584
|
+
`${HDR}END PROGRAM ${fn.name}.\n`;
|
|
448
585
|
},
|
|
449
586
|
formatSuite: (fn, outputStrs, letLines, letDecls, outputLines) => {
|
|
450
587
|
const outputNames = Object.keys(outputStrs);
|
|
451
588
|
checkReservedNames([fn.name, ...fn.params, ...outputNames]);
|
|
452
589
|
checkUsingClauseNames([fn.name, ...fn.params, ...outputNames]);
|
|
453
590
|
const linkageParams = [...fn.params, ...outputNames];
|
|
454
|
-
const paramDecls = linkageParams.map((p) =>
|
|
455
|
-
const wsDecls = letDecls.map((n) =>
|
|
456
|
-
const assigns = outputNames.map((n) => wrapLine(
|
|
457
|
-
return
|
|
458
|
-
|
|
591
|
+
const paramDecls = linkageParams.map((p) => `${HDR}01 ${p} USAGE COMP-2.`).join("\n");
|
|
592
|
+
const wsDecls = letDecls.map((n) => `${HDR}01 ${n} USAGE COMP-2.`).join("\n");
|
|
593
|
+
const assigns = outputNames.map((n) => wrapLine(`${STMT}COMPUTE ${n} = ${outputStrs[n]}`)).join("\n");
|
|
594
|
+
return `${HDR}>>SOURCE FORMAT FREE\n` +
|
|
595
|
+
`${HDR}*> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
459
596
|
CMP_HELPER_SOURCE + "\n" +
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
597
|
+
`${HDR}IDENTIFICATION DIVISION.\n` +
|
|
598
|
+
`${HDR}PROGRAM-ID. ${fn.name}.\n` +
|
|
599
|
+
`${HDR}ENVIRONMENT DIVISION.\n` +
|
|
600
|
+
`${HDR}CONFIGURATION SECTION.\n` +
|
|
464
601
|
CMP_REPOSITORY +
|
|
465
|
-
|
|
466
|
-
(wsDecls ?
|
|
467
|
-
|
|
602
|
+
`${HDR}DATA DIVISION.\n` +
|
|
603
|
+
(wsDecls ? `${HDR}WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
|
|
604
|
+
`${HDR}LINKAGE SECTION.\n` +
|
|
468
605
|
paramDecls + "\n" +
|
|
469
|
-
|
|
606
|
+
`${HDR}PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
|
|
470
607
|
(letLines.length ? letLines.join("\n") + "\n" : "") +
|
|
471
608
|
(outputLines.length ? outputLines.join("\n") + "\n" : "") +
|
|
472
609
|
assigns + "\n" +
|
|
473
|
-
|
|
474
|
-
|
|
610
|
+
`${STMT}GOBACK.\n` +
|
|
611
|
+
`${HDR}END PROGRAM ${fn.name}.\n`;
|
|
475
612
|
},
|
|
476
613
|
});
|
|
477
614
|
|