exprforge 0.3.1 → 0.4.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 CHANGED
@@ -28,6 +28,11 @@ No required dependencies. You can build the AST directly with plain JS
28
28
  functions, or author it as readable infix text via `expr`/`fn` (see
29
29
  below) — either way, the same tree is walked once per target.
30
30
 
31
+ **[▶ Try it live](https://theraccoonbear.github.io/exprforge/)** — write
32
+ a `` fn`...` `` formula in the browser and watch it emitted across every
33
+ target language at once, no install required. Runs the real, current
34
+ library (see `playground/`), not a frozen demo build.
35
+
31
36
  ## Why
32
37
 
33
38
  Codegen tools like SymPy already turn math expressions into code for
@@ -228,6 +233,17 @@ words — Perl/PHP mostly sidestep this (every variable is `$`-sigiled, so
228
233
  it can't collide with a bareword keyword), but the sigil-free languages
229
234
  above genuinely can't be fully guarded against in advance.
230
235
 
236
+ **COBOL is a partial exception**: a *parameter* colliding with one of its
237
+ narrow, syntax-specific quirks (e.g. a bare `c`, which breaks GnuCOBOL's
238
+ `CALL ... USING` clause specifically) gets silently renamed internally
239
+ (`EFLF_c`) rather than thrown at you — safe because every target here
240
+ calls positionally, so a parameter's declared name is never visible to a
241
+ caller in any of them. The function's own name and any `outputs()` field
242
+ names are **not** covered by this — both remain part of the actual
243
+ calling contract (a suite's field names are genuinely consumer-visible in
244
+ every other target's return shape), so those still throw, same as before.
245
+ See `renameConflictingParams` in `emitters/cobol.js`.
246
+
231
247
  ## Named subexpressions and conditional values
232
248
 
233
249
  Beyond `num`/`v`/`bin`/`call`, two more node types stay inside the
@@ -311,6 +327,7 @@ expr`(-b + sqrt(b^2 - 4*a*c)) / (2*a)`
311
327
  | bare `name` | `v("name")` |
312
328
  | `cond ? then : else` | `select(cmp(left, op, right), then, else)` — the **only** place a comparison (`> < >= <= == !=`) is valid, matching `cmp()`'s own documented constraint that it's never a general boolean expression. A bare `a > b` with no `?` is a parse-time error, not a deferred one. Chains naturally: `a>0 ? 1 : b>0 ? 2 : 3`. |
313
329
  | `${...}` | Splices in an existing AST node as-is, or a plain JS number (auto-wrapped via `num()`). Anything else throws immediately. Plain strings aren't interpolatable — a bare identifier in the template text already means "variable", with no `${}` needed. |
330
+ | `# ...` | An end-of-line comment — runs to the next newline, produces no tokens. Works across `${...}` interpolation boundaries too: a value interpolated inside an open comment is silently dropped, never validated (not even for what would otherwise be an invalid interpolation). |
314
331
 
315
332
  Deliberately **not** in the grammar: `let`/`outputs` blocks (it's a pure
316
333
  expression grammar, same "expression AST, not a program AST" boundary as
@@ -366,8 +383,8 @@ const { fn, emitAll, evaluate } = require("exprforge");
366
383
 
367
384
  const normalize2 = fn`
368
385
  normalize2(x, y):
369
- let mag = sqrt(x^2 + y^2);
370
- return { nx: x / mag, ny: y / mag };
386
+ let mag = sqrt(x^2 + y^2);
387
+ return { nx: x / mag, ny: y / mag };
371
388
  `;
372
389
  // normalize2 is now the full {name, params, body} shape directly --
373
390
  // no wrapping object needed.
@@ -400,7 +417,7 @@ handles, backed by the real `Math.*` functions.
400
417
  const { emitAll, evaluate } = require("exprforge");
401
418
 
402
419
  emitAll(normalize2).expr.source;
403
- // "let mag = sqrt(((x^2) + (y^2)));\nreturn { nx: (x / mag), ny: (y / mag) };\n"
420
+ // "normalize2(x, y):\n let mag = sqrt(((x^2) + (y^2)));\n return { nx: (x / mag), ny: (y / mag) };\n"
404
421
 
405
422
  evaluate(normalize2, [3, 4]);
406
423
  // { nx: 0.6, ny: 0.8 }
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,7 +313,7 @@ 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
  }
@@ -208,6 +321,11 @@ class TempPool {
208
321
  class CobolEmitter extends Emitter {
209
322
  emitFunction(fn) {
210
323
  const { collectLets } = require("../ast.js");
324
+ // Must run before collectLets, and before anything else in this
325
+ // function -- collectLets flattens the tree's let structure away,
326
+ // and every check/emission step below assumes fn.params is
327
+ // already COBOL-safe. See renameConflictingParams above.
328
+ fn = renameConflictingParams(fn, COBOL_USING_RESERVED);
211
329
  const { bindings, body } = collectLets(fn.body);
212
330
  const counter = { next: 0 };
213
331
 
@@ -219,7 +337,7 @@ class CobolEmitter extends Emitter {
219
337
  const valueStr = this.emitExpr(node);
220
338
  letLines.push(...this._pool.lines);
221
339
  letDecls.push(...this._pool.decls, name);
222
- letLines.push(wrapLine(` COMPUTE ${name} = ${valueStr}`));
340
+ letLines.push(wrapLine(`${STMT}COMPUTE ${name} = ${valueStr}`));
223
341
  }
224
342
 
225
343
  if (body.type === "outputs") {
@@ -426,52 +544,52 @@ const emitter = new CobolEmitter({
426
544
  checkReservedNames([fn.name, ...fn.params]);
427
545
  checkUsingClauseNames([fn.name, ...fn.params]);
428
546
  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` +
547
+ const paramDecls = linkageParams.map((p) => `${HDR}01 ${p} USAGE COMP-2.`).join("\n");
548
+ const wsDecls = letDecls.map((n) => `${HDR}01 ${n} USAGE COMP-2.`).join("\n");
549
+ return `${HDR}>>SOURCE FORMAT FREE\n` +
550
+ `${HDR}*> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
433
551
  CMP_HELPER_SOURCE + "\n" +
434
- ` IDENTIFICATION DIVISION.\n` +
435
- ` PROGRAM-ID. ${fn.name}.\n` +
436
- ` ENVIRONMENT DIVISION.\n` +
437
- ` CONFIGURATION SECTION.\n` +
552
+ `${HDR}IDENTIFICATION DIVISION.\n` +
553
+ `${HDR}PROGRAM-ID. ${fn.name}.\n` +
554
+ `${HDR}ENVIRONMENT DIVISION.\n` +
555
+ `${HDR}CONFIGURATION SECTION.\n` +
438
556
  CMP_REPOSITORY +
439
- ` DATA DIVISION.\n` +
440
- (wsDecls ? ` WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
441
- ` LINKAGE SECTION.\n` +
557
+ `${HDR}DATA DIVISION.\n` +
558
+ (wsDecls ? `${HDR}WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
559
+ `${HDR}LINKAGE SECTION.\n` +
442
560
  paramDecls + "\n" +
443
- ` PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
561
+ `${HDR}PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
444
562
  [...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`;
563
+ wrapLine(`${STMT}COMPUTE ef-result = ${body}`) + "\n" +
564
+ `${STMT}GOBACK.\n` +
565
+ `${HDR}END PROGRAM ${fn.name}.\n`;
448
566
  },
449
567
  formatSuite: (fn, outputStrs, letLines, letDecls, outputLines) => {
450
568
  const outputNames = Object.keys(outputStrs);
451
569
  checkReservedNames([fn.name, ...fn.params, ...outputNames]);
452
570
  checkUsingClauseNames([fn.name, ...fn.params, ...outputNames]);
453
571
  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` +
572
+ const paramDecls = linkageParams.map((p) => `${HDR}01 ${p} USAGE COMP-2.`).join("\n");
573
+ const wsDecls = letDecls.map((n) => `${HDR}01 ${n} USAGE COMP-2.`).join("\n");
574
+ const assigns = outputNames.map((n) => wrapLine(`${STMT}COMPUTE ${n} = ${outputStrs[n]}`)).join("\n");
575
+ return `${HDR}>>SOURCE FORMAT FREE\n` +
576
+ `${HDR}*> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
459
577
  CMP_HELPER_SOURCE + "\n" +
460
- ` IDENTIFICATION DIVISION.\n` +
461
- ` PROGRAM-ID. ${fn.name}.\n` +
462
- ` ENVIRONMENT DIVISION.\n` +
463
- ` CONFIGURATION SECTION.\n` +
578
+ `${HDR}IDENTIFICATION DIVISION.\n` +
579
+ `${HDR}PROGRAM-ID. ${fn.name}.\n` +
580
+ `${HDR}ENVIRONMENT DIVISION.\n` +
581
+ `${HDR}CONFIGURATION SECTION.\n` +
464
582
  CMP_REPOSITORY +
465
- ` DATA DIVISION.\n` +
466
- (wsDecls ? ` WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
467
- ` LINKAGE SECTION.\n` +
583
+ `${HDR}DATA DIVISION.\n` +
584
+ (wsDecls ? `${HDR}WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
585
+ `${HDR}LINKAGE SECTION.\n` +
468
586
  paramDecls + "\n" +
469
- ` PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
587
+ `${HDR}PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
470
588
  (letLines.length ? letLines.join("\n") + "\n" : "") +
471
589
  (outputLines.length ? outputLines.join("\n") + "\n" : "") +
472
590
  assigns + "\n" +
473
- ` GOBACK.\n` +
474
- ` END PROGRAM ${fn.name}.\n`;
591
+ `${STMT}GOBACK.\n` +
592
+ `${HDR}END PROGRAM ${fn.name}.\n`;
475
593
  },
476
594
  });
477
595
 
@@ -63,17 +63,48 @@ const emitter = new ExprSyntaxEmitter({
63
63
  // That's both the most direct thing to paste into a real
64
64
  // `` fn`...` `` call, and exactly what the round-trip test reparses
65
65
  // with zero unwrapping first.
66
+ //
67
+ // ALWAYS includes the "name(params):" signature line -- every other
68
+ // emitter's formatFunction includes the full declaration per
69
+ // base.js's own documented contract ("Full source text for one
70
+ // function, including any language-specific signature/type/wrapper
71
+ // syntax"); this was the one target that didn't, dropping fn.name/
72
+ // fn.params on the floor entirely. That wasn't a deliberate
73
+ // minimalism choice, it was a leftover from before fn`...`'s
74
+ // optional signature line (see fn.js) existed at all -- without it,
75
+ // reparsing this emitter's own output via fn() could only ever
76
+ // recover a bare Node, never a runnable {name, params, body}, unlike
77
+ // literally every other target's output being immediately usable.
78
+ // Body lines (every let, the return) are indented 2 spaces deeper
79
+ // than the signature line itself -- a Python-esque pretty-print
80
+ // convention, not something the parser requires (whitespace is
81
+ // insignificant to fn's grammar; see expr.js's tokenizer). Applied
82
+ // here, not just in hand-written docs/examples, so every printed
83
+ // AST comes out reading the same way automatically.
66
84
  formatFunction: (fn, bodyStr, letBindings = []) => {
67
- const lines = letLines(letBindings);
68
- lines.push(`return ${bodyStr};`);
69
- return lines.join("\n") + "\n";
85
+ const body = [...letLines(letBindings), `return ${bodyStr};`].map((line) => ` ${line}`);
86
+ return [`${fn.name}(${fn.params.join(", ")}):`, ...body].join("\n") + "\n";
70
87
  },
88
+ // Each output field gets its own line (4 spaces -- one level deeper
89
+ // than "return {" itself, which sits at the usual 2), rather than
90
+ // cramming every field onto one line -- found the gap by comparing
91
+ // this against a hand-formatted multi-output example and noticing
92
+ // the printer didn't follow its own convention once a suite had
93
+ // more than a couple of fields (a real, wide, 5-output formula made
94
+ // this one very long line instead of something readable).
71
95
  formatSuite: (fn, outputStrs, letBindings = []) => {
72
- const lines = letLines(letBindings);
73
- const fields = Object.entries(outputStrs)
74
- .map(([name, valueStr]) => `${name}: ${valueStr}`)
75
- .join(", ");
76
- lines.push(`return { ${fields} };`);
96
+ const entries = Object.entries(outputStrs);
97
+ const fieldLines = entries.map(([name, valueStr], i) => {
98
+ const comma = i < entries.length - 1 ? "," : "";
99
+ return ` ${name}: ${valueStr}${comma}`;
100
+ });
101
+ const lines = [
102
+ `${fn.name}(${fn.params.join(", ")}):`,
103
+ ...letLines(letBindings).map((line) => ` ${line}`),
104
+ " return {",
105
+ ...fieldLines,
106
+ " };",
107
+ ];
77
108
  return lines.join("\n") + "\n";
78
109
  },
79
110
  });
package/expr.js CHANGED
@@ -50,8 +50,28 @@ const COMPARE_OPS = [">", "<", ">=", "<=", "==", "!="];
50
50
  // `label` is just which tag function's name shows up in error messages
51
51
  // -- fn.js passes "fn()" here so a lex error inside `` fn`...` `` isn't
52
52
  // misattributed to expr().
53
- function tokenizeSegment(str, offset, tokens, label = "expr()") {
53
+ //
54
+ // `state.inComment` carries "# comment" status ACROSS segments -- these
55
+ // are tagged template literals, so a source like
56
+ // `` expr`a + b # comment ${x} more` `` tokenizes segment "a + b #
57
+ // comment " and segment " more" separately, with a HOLE for `x` spliced
58
+ // between them by expr()/fn() below. A comment open at the end of one
59
+ // segment has to stay open into the next, or "more" would wrongly
60
+ // become real tokens again. One `state` object is created once per
61
+ // top-level expr()/fn() call and threaded through every call here --
62
+ // never reset per segment.
63
+ function tokenizeSegment(str, offset, tokens, state = { inComment: false }, label = "expr()") {
54
64
  let i = 0;
65
+ if (state.inComment) {
66
+ const nl = str.indexOf("\n");
67
+ if (nl === -1) {
68
+ // The whole segment is still inside the comment -- nothing
69
+ // to tokenize, and still in-comment for whatever's next.
70
+ return;
71
+ }
72
+ i = nl + 1;
73
+ state.inComment = false;
74
+ }
55
75
  while (i < str.length) {
56
76
  const ch = str[i];
57
77
  const start = i;
@@ -59,6 +79,20 @@ function tokenizeSegment(str, offset, tokens, label = "expr()") {
59
79
  i++;
60
80
  continue;
61
81
  }
82
+ // "#" comments run to the next newline (or off the end of this
83
+ // segment, in which case state.inComment stays set for the next
84
+ // one -- see above). Not part of the OP set below: this
85
+ // produces no token at all, the same category as whitespace,
86
+ // not an operator.
87
+ if (ch === "#") {
88
+ const nl = str.indexOf("\n", i);
89
+ if (nl === -1) {
90
+ state.inComment = true;
91
+ return;
92
+ }
93
+ i = nl + 1;
94
+ continue;
95
+ }
62
96
  // NUMBER: 123, 123.45, .5, 1e-9, 1.5E+10
63
97
  if (/[0-9]/.test(ch) || (ch === "." && /[0-9]/.test(str[i + 1] || ""))) {
64
98
  i++;
@@ -296,13 +330,22 @@ class Parser {
296
330
  // already-evaluated JS value through untouched.
297
331
  function expr(strings, ...values) {
298
332
  const tokens = [];
333
+ const state = { inComment: false };
299
334
  let source = "";
300
335
  for (let i = 0; i < strings.length; i++) {
301
- tokenizeSegment(strings[i], source.length, tokens);
336
+ tokenizeSegment(strings[i], source.length, tokens, state);
302
337
  source += strings[i];
303
338
  if (i < values.length) {
304
- tokens.push({ type: "HOLE", value: values[i], pos: source.length });
305
339
  source += "${...}";
340
+ // A value interpolated inside an open "#" comment is
341
+ // silently dropped -- never reaches holeToNode, so it's
342
+ // never validated, even if it would otherwise be an
343
+ // invalid interpolation (a string, undefined, ...). This is
344
+ // deliberate: the whole point of a comment is that its
345
+ // contents don't matter.
346
+ if (!state.inComment) {
347
+ tokens.push({ type: "HOLE", value: values[i], pos: source.length });
348
+ }
306
349
  }
307
350
  }
308
351
  tokens.push({ type: "EOF", value: null, pos: source.length });
package/fn.js CHANGED
@@ -147,13 +147,19 @@ function parseProgram(parser) {
147
147
  // parser.parseExpression()).
148
148
  function fn(strings, ...values) {
149
149
  const tokens = [];
150
+ const state = { inComment: false };
150
151
  let source = "";
151
152
  for (let i = 0; i < strings.length; i++) {
152
- tokenizeSegment(strings[i], source.length, tokens, "fn()");
153
+ tokenizeSegment(strings[i], source.length, tokens, state, "fn()");
153
154
  source += strings[i];
154
155
  if (i < values.length) {
155
- tokens.push({ type: "HOLE", value: values[i], pos: source.length });
156
156
  source += "${...}";
157
+ // See expr.js's tokenizeSegment/expr() for why this is
158
+ // silently dropped rather than pushed -- same rule, same
159
+ // reasoning, shared state object.
160
+ if (!state.inComment) {
161
+ tokens.push({ type: "HOLE", value: values[i], pos: source.length });
162
+ }
157
163
  }
158
164
  }
159
165
  tokens.push({ type: "EOF", value: null, pos: source.length });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exprforge",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Author a math expression once as an AST (or readable infix text via expr/fn), emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, Rust, Perl, PHP, Julia, Fortran, Zig, Scheme, and COBOL, plus a native evaluator and its own readable syntax printer.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",