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/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
- throw new Error(`collectLets: duplicate let binding name "${name}" in one function`);
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
- module.exports = { num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets };
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
- const template = this.calls[node.name];
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
- return template(args);
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
- emitFunction(fn) {
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
- ` REPOSITORY.\n` +
237
+ `${HDR}REPOSITORY.\n` +
125
238
  Object.values(CMP_HELPERS)
126
- .map(({ suffix }, i, arr) => ` FUNCTION ef-cmp-${suffix}${i === arr.length - 1 ? "." : ""}`)
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 }) => ` IDENTIFICATION DIVISION.
133
- FUNCTION-ID. ef-cmp-${suffix}.
134
- DATA DIVISION.
135
- LINKAGE SECTION.
136
- 01 L USAGE COMP-2.
137
- 01 R USAGE COMP-2.
138
- 01 THEN-VAL USAGE COMP-2.
139
- 01 ELSE-VAL USAGE COMP-2.
140
- 01 RESULT USAGE COMP-2.
141
- PROCEDURE DIVISION USING L R THEN-VAL ELSE-VAL RETURNING RESULT.
142
- IF ${test}
143
- MOVE THEN-VAL TO RESULT
144
- ELSE
145
- MOVE ELSE-VAL TO RESULT
146
- END-IF
147
- GOBACK.
148
- END FUNCTION ef-cmp-${suffix}.
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 = ` ${word}`;
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(` COMPUTE ${name} = ${valueStr}`));
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(fn) {
210
- const { collectLets } = require("../ast.js");
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(` COMPUTE ${name} = ${valueStr}`));
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) => ` 01 ${p} USAGE COMP-2.`).join("\n");
430
- const wsDecls = letDecls.map((n) => ` 01 ${n} USAGE COMP-2.`).join("\n");
431
- return ` >>SOURCE FORMAT FREE\n` +
432
- ` *> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
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
- ` IDENTIFICATION DIVISION.\n` +
435
- ` PROGRAM-ID. ${fn.name}.\n` +
436
- ` ENVIRONMENT DIVISION.\n` +
437
- ` CONFIGURATION SECTION.\n` +
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
- ` DATA DIVISION.\n` +
440
- (wsDecls ? ` WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
441
- ` LINKAGE SECTION.\n` +
576
+ `${HDR}DATA DIVISION.\n` +
577
+ (wsDecls ? `${HDR}WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
578
+ `${HDR}LINKAGE SECTION.\n` +
442
579
  paramDecls + "\n" +
443
- ` PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
580
+ `${HDR}PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
444
581
  [...letLines, ...bodyLines].join("\n") + (letLines.length || bodyLines.length ? "\n" : "") +
445
- wrapLine(` COMPUTE ef-result = ${body}`) + "\n" +
446
- ` GOBACK.\n` +
447
- ` END PROGRAM ${fn.name}.\n`;
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) => ` 01 ${p} USAGE COMP-2.`).join("\n");
455
- const wsDecls = letDecls.map((n) => ` 01 ${n} USAGE COMP-2.`).join("\n");
456
- const assigns = outputNames.map((n) => wrapLine(` COMPUTE ${n} = ${outputStrs[n]}`)).join("\n");
457
- return ` >>SOURCE FORMAT FREE\n` +
458
- ` *> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
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
- ` IDENTIFICATION DIVISION.\n` +
461
- ` PROGRAM-ID. ${fn.name}.\n` +
462
- ` ENVIRONMENT DIVISION.\n` +
463
- ` CONFIGURATION SECTION.\n` +
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
- ` DATA DIVISION.\n` +
466
- (wsDecls ? ` WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
467
- ` LINKAGE SECTION.\n` +
602
+ `${HDR}DATA DIVISION.\n` +
603
+ (wsDecls ? `${HDR}WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
604
+ `${HDR}LINKAGE SECTION.\n` +
468
605
  paramDecls + "\n" +
469
- ` PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
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
- ` GOBACK.\n` +
474
- ` END PROGRAM ${fn.name}.\n`;
610
+ `${STMT}GOBACK.\n` +
611
+ `${HDR}END PROGRAM ${fn.name}.\n`;
475
612
  },
476
613
  });
477
614