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 +20 -3
- package/emitters/cobol.js +170 -52
- package/emitters/exprsyntax.js +39 -8
- package/expr.js +46 -3
- package/fn.js +8 -2
- package/package.json +1 -1
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
|
-
|
|
370
|
-
|
|
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)));\
|
|
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
|
-
|
|
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,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(
|
|
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(
|
|
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) =>
|
|
430
|
-
const wsDecls = letDecls.map((n) =>
|
|
431
|
-
return
|
|
432
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
440
|
-
(wsDecls ?
|
|
441
|
-
|
|
557
|
+
`${HDR}DATA DIVISION.\n` +
|
|
558
|
+
(wsDecls ? `${HDR}WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
|
|
559
|
+
`${HDR}LINKAGE SECTION.\n` +
|
|
442
560
|
paramDecls + "\n" +
|
|
443
|
-
|
|
561
|
+
`${HDR}PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
|
|
444
562
|
[...letLines, ...bodyLines].join("\n") + (letLines.length || bodyLines.length ? "\n" : "") +
|
|
445
|
-
wrapLine(
|
|
446
|
-
|
|
447
|
-
|
|
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) =>
|
|
455
|
-
const wsDecls = letDecls.map((n) =>
|
|
456
|
-
const assigns = outputNames.map((n) => wrapLine(
|
|
457
|
-
return
|
|
458
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
466
|
-
(wsDecls ?
|
|
467
|
-
|
|
583
|
+
`${HDR}DATA DIVISION.\n` +
|
|
584
|
+
(wsDecls ? `${HDR}WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
|
|
585
|
+
`${HDR}LINKAGE SECTION.\n` +
|
|
468
586
|
paramDecls + "\n" +
|
|
469
|
-
|
|
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
|
-
|
|
474
|
-
|
|
591
|
+
`${STMT}GOBACK.\n` +
|
|
592
|
+
`${HDR}END PROGRAM ${fn.name}.\n`;
|
|
475
593
|
},
|
|
476
594
|
});
|
|
477
595
|
|
package/emitters/exprsyntax.js
CHANGED
|
@@ -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
|
|
68
|
-
|
|
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
|
|
73
|
-
const
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
+
"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",
|