exprforge 0.4.0 → 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 +545 -99
- package/ast.js +190 -2
- package/emitters/base.js +36 -4
- package/emitters/cobol.js +21 -2
- package/emitters/registry.js +12 -1
- package/evaluate.js +47 -14
- package/expr.js +61 -6
- package/fn.js +23 -4
- 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/index.js
CHANGED
|
@@ -1,31 +1,126 @@
|
|
|
1
1
|
// exprforge/index.js
|
|
2
|
-
const { num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets } = require("./ast.js");
|
|
2
|
+
const { num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, field, collectLets, checkUnboundVars } = require("./ast.js");
|
|
3
3
|
const { forComponents } = require("./util.js");
|
|
4
4
|
const { expr } = require("./expr.js");
|
|
5
5
|
const { fn } = require("./fn.js");
|
|
6
6
|
const { evaluate } = require("./evaluate.js");
|
|
7
|
+
const { loadMacro, loadExtern, expandMacros, createRegistry } = require("./macros.js");
|
|
8
|
+
const { loadExpr, loadExprSource } = require("./load-expr.js");
|
|
7
9
|
const emitters = require("./emitters/registry.js");
|
|
8
10
|
const { catmullRomAst } = require("./samples/catmull-rom.js");
|
|
9
11
|
const { fibonacciAst } = require("./samples/fibonacci.js");
|
|
10
12
|
const { splineFrameAsts } = require("./samples/spline-frame.js");
|
|
11
13
|
const { kitchenSinkAst } = require("./samples/kitchen-sink.js");
|
|
12
14
|
const { mathDemoAst } = require("./samples/math-demo.js");
|
|
15
|
+
const { macroDemoAst } = require("./samples/macro-demo.js");
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
|
-
* Run
|
|
16
|
-
*
|
|
18
|
+
* Run ONE emitter against one AST function definition. Returns
|
|
19
|
+
* { ext, source }. Throws if `lang` isn't a registered emitter name, or if
|
|
20
|
+
* that emitter itself throws for this fn (an unmapped Math function, a
|
|
21
|
+
* reserved-name collision, ...) -- the caller picked exactly this one
|
|
22
|
+
* target, so there's no "other languages" for a per-language error to be
|
|
23
|
+
* isolated from; let it propagate. Prefer this over emitAll (deprecated,
|
|
24
|
+
* below) when you only need one or a few targets -- it doesn't pay for
|
|
25
|
+
* every registered emitter to get one.
|
|
26
|
+
*
|
|
27
|
+
* `registry` (see macros.js's createRegistry()) defaults to the
|
|
28
|
+
* process-wide default when omitted -- pass a session's own (see
|
|
29
|
+
* createSession() below) to resolve macros/externs registered in that
|
|
30
|
+
* session instead; ordinary callers never need to pass it directly.
|
|
17
31
|
*/
|
|
18
|
-
function
|
|
32
|
+
function emit(fnDef, lang, registry = undefined) {
|
|
33
|
+
const emitter = emitters[lang];
|
|
34
|
+
if (!emitter) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`emit(): no emitter registered for language "${lang}" -- known languages: ${Object.keys(emitters).sort().join(", ")}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return { ext: emitter.ext, source: emitter.emitFunction(fnDef, registry) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run several emitters against one AST function definition, each in
|
|
44
|
+
* isolation from the others: one language's emitter throwing shows up as
|
|
45
|
+
* { source: null, error } for THAT language only, alongside every other
|
|
46
|
+
* requested language's real { source, error: null } result -- a single
|
|
47
|
+
* bad target (e.g. a parameter name COBOL's reserved-word check rejects)
|
|
48
|
+
* no longer takes the whole batch down with it. `langs` defaults to every
|
|
49
|
+
* registered language; pass an explicit array to avoid running (and
|
|
50
|
+
* paying for) emitters you don't need.
|
|
51
|
+
*
|
|
52
|
+
* `registry` -- see emit()'s own doc comment above -- same meaning here,
|
|
53
|
+
* applied to every language in `langs`.
|
|
54
|
+
*/
|
|
55
|
+
function emitMany(fnDef, langs = Object.keys(emitters), registry = undefined) {
|
|
19
56
|
const result = {};
|
|
20
|
-
for (const
|
|
21
|
-
|
|
57
|
+
for (const lang of langs) {
|
|
58
|
+
const emitter = emitters[lang];
|
|
59
|
+
if (!emitter) {
|
|
60
|
+
result[lang] = { ext: null, source: null, error: `no emitter registered for language "${lang}"` };
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
result[lang] = { ext: emitter.ext, source: emitter.emitFunction(fnDef, registry), error: null };
|
|
65
|
+
} catch (e) {
|
|
66
|
+
result[lang] = { ext: emitter.ext, source: null, error: e instanceof Error ? e.message : String(e) };
|
|
67
|
+
}
|
|
22
68
|
}
|
|
23
69
|
return result;
|
|
24
70
|
}
|
|
25
71
|
|
|
72
|
+
/**
|
|
73
|
+
* @deprecated Runs every registered emitter unconditionally, whether you
|
|
74
|
+
* need all of them or not -- prefer emit(fnDef, lang) for one target, or
|
|
75
|
+
* emitMany(fnDef, langs) for an explicit subset (or emitMany(fnDef) with
|
|
76
|
+
* no `langs` for the same "every language" behavior this has always had).
|
|
77
|
+
* Kept working, not scheduled for removal -- existing callers reading
|
|
78
|
+
* result[lang].ext/.source see no change; the only behavior change is the
|
|
79
|
+
* bug fix this shares with emitMany: a single emitter throwing used to
|
|
80
|
+
* abort the whole batch (nothing for ANY language came back), and now
|
|
81
|
+
* surfaces as { source: null, error } for that language alone.
|
|
82
|
+
*/
|
|
83
|
+
function emitAll(fnDef) {
|
|
84
|
+
return emitMany(fnDef);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates an isolated "session": its own private macro/extern registry
|
|
89
|
+
* (see macros.js's createRegistry()), plus every macro/extern/evaluate/
|
|
90
|
+
* emit-shaped function bound to use it instead of the process-wide
|
|
91
|
+
* default registry loadMacro/loadExtern/evaluate/emit/emitMany use when
|
|
92
|
+
* called bare. Purely additive -- loadMacro/loadExtern/etc. above are
|
|
93
|
+
* completely unaffected by a session's existence, and a session's own
|
|
94
|
+
* registrations are invisible to them and to every OTHER session,
|
|
95
|
+
* garbage-collected normally once the session object itself is no
|
|
96
|
+
* longer referenced. No removal API: rebuild a fresh session (via a new
|
|
97
|
+
* createSession() call) instead of trying to unregister one macro/extern
|
|
98
|
+
* out of an existing one -- see the design discussion this came out of
|
|
99
|
+
* (github.com/theraccoonbear/exprforge/issues/21).
|
|
100
|
+
*
|
|
101
|
+
* Useful whenever a program legitimately needs more than one independent
|
|
102
|
+
* "namespace" of macros/externs at once -- e.g. a multi-tenant service
|
|
103
|
+
* evaluating math defined by different untrusted users, where one
|
|
104
|
+
* user's loadMacro("helper", ...) must never resolve inside another
|
|
105
|
+
* user's expression just because they picked the same name.
|
|
106
|
+
*/
|
|
107
|
+
function createSession() {
|
|
108
|
+
const registry = createRegistry();
|
|
109
|
+
return {
|
|
110
|
+
loadMacro: (name, def) => loadMacro(name, def, registry),
|
|
111
|
+
loadExtern: (name, def) => loadExtern(name, def, registry),
|
|
112
|
+
expandMacros: (fnOrNode, extraRegistry = null) => expandMacros(fnOrNode, extraRegistry, registry),
|
|
113
|
+
evaluate: (fn, args) => evaluate(fn, args, registry),
|
|
114
|
+
emit: (fnDef, lang) => emit(fnDef, lang, registry),
|
|
115
|
+
emitMany: (fnDef, langs = Object.keys(emitters)) => emitMany(fnDef, langs, registry),
|
|
116
|
+
loadExpr: (path) => loadExpr(path, registry),
|
|
117
|
+
loadExprSource: (source, label = "loadExprSource()") => loadExprSource(source, label, registry),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
26
121
|
module.exports = {
|
|
27
122
|
// AST builders — use these to define your own formulas.
|
|
28
|
-
num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets,
|
|
123
|
+
num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, field, collectLets, checkUnboundVars,
|
|
29
124
|
// Authoring convenience — not an AST primitive, see util.js.
|
|
30
125
|
forComponents,
|
|
31
126
|
// Infix syntax sugar over the builders above — same Nodes, see expr.js.
|
|
@@ -37,6 +132,28 @@ module.exports = {
|
|
|
37
132
|
// A native interpreter over the AST -- evaluate(fn, args) computes a
|
|
38
133
|
// result directly in JS, no codegen/compile step. See evaluate.js.
|
|
39
134
|
evaluate,
|
|
135
|
+
// Register a macro: a name usable inside fn`...`/expr`...` text
|
|
136
|
+
// beyond the built-in primitives, inline-expanded at build time,
|
|
137
|
+
// never emitted as a real call. See macros.js's own header comment.
|
|
138
|
+
loadMacro,
|
|
139
|
+
// Register an extern: same usable-by-name mechanism, but a real
|
|
140
|
+
// per-target native call instead -- caller-owned risk, ExprForge
|
|
141
|
+
// can't verify it. See macros.js's own header comment.
|
|
142
|
+
loadExtern,
|
|
143
|
+
// Runs the same macro/field-access expansion evaluate() and every
|
|
144
|
+
// emitter already run internally -- exposed for callers who want the
|
|
145
|
+
// expanded tree itself (e.g. to inspect or re-emit it without
|
|
146
|
+
// re-running expansion). Ordinary callers never need this.
|
|
147
|
+
expandMacros,
|
|
148
|
+
// Parses a .expr file (the exprsyntax emitter's own round-trip
|
|
149
|
+
// format) into its function definitions, letting later functions in
|
|
150
|
+
// the file reference earlier ones as inline macros. See load-expr.js.
|
|
151
|
+
loadExpr,
|
|
152
|
+
// Same parser, given source text directly instead of a file path --
|
|
153
|
+
// no filesystem involved, so this is the one usable from a browser
|
|
154
|
+
// (a text editor buffer, an HTTP response, ...). loadExpr(path) is
|
|
155
|
+
// now just this plus a readFileSync.
|
|
156
|
+
loadExprSource,
|
|
40
157
|
// Built-in example formulas — see samples/ for the source.
|
|
41
158
|
catmullRomAst,
|
|
42
159
|
fibonacciAst,
|
|
@@ -47,15 +164,31 @@ module.exports = {
|
|
|
47
164
|
// Also not a worked example -- a conformance-test fixture for
|
|
48
165
|
// require("exprforge/math"). See samples/math-demo.js.
|
|
49
166
|
mathDemoAst,
|
|
167
|
+
// Also not a worked example -- a conformance-test fixture for
|
|
168
|
+
// macros.js's AST-fn-def macro tier specifically (loadMacro(name,
|
|
169
|
+
// fn`...`), not the plain-JS-function tier mathDemoAst already
|
|
170
|
+
// covers). See samples/macro-demo.js.
|
|
171
|
+
macroDemoAst,
|
|
50
172
|
samples: {
|
|
51
173
|
catmullRom: catmullRomAst,
|
|
52
174
|
fibonacci: fibonacciAst,
|
|
53
175
|
splineFrame: splineFrameAsts,
|
|
54
176
|
kitchenSink: kitchenSinkAst,
|
|
55
177
|
mathDemo: mathDemoAst,
|
|
178
|
+
macroDemo: macroDemoAst,
|
|
56
179
|
},
|
|
57
180
|
// Per-language emitter instances, keyed by name (js, qb64, c, java, go, rust).
|
|
58
181
|
emitters,
|
|
59
|
-
//
|
|
182
|
+
// One target, explicit -- see this function's own doc comment above.
|
|
183
|
+
emit,
|
|
184
|
+
// Several targets at once, each isolated from the others' failures.
|
|
185
|
+
emitMany,
|
|
186
|
+
// Deprecated: every target at once, no way to ask for fewer. Prefer
|
|
187
|
+
// emit()/emitMany() above -- kept working, not removed.
|
|
60
188
|
emitAll,
|
|
189
|
+
// Creates an isolated session: its own private macro/extern registry,
|
|
190
|
+
// plus loadMacro/loadExtern/evaluate/emit/emitMany/loadExpr/
|
|
191
|
+
// loadExprSource bound to use it. See this function's own doc
|
|
192
|
+
// comment above.
|
|
193
|
+
createSession,
|
|
61
194
|
};
|
package/load-expr.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// exprforge/load-expr.js
|
|
2
|
+
//
|
|
3
|
+
// Parses the .expr round-trip text format (see emitters/exprsyntax.js and
|
|
4
|
+
// test/conformance.test.js's assertExprSyntaxRoundTrips) as zero or more
|
|
5
|
+
// function definitions, using the exact same grammar/engine fn`...`
|
|
6
|
+
// already uses (see fn.js's parseProgram), just applied repeatedly
|
|
7
|
+
// instead of once. Two entry points: loadExprSource(text) parses text
|
|
8
|
+
// directly (no filesystem involved -- usable anywhere source text comes
|
|
9
|
+
// from, including a browser); loadExpr(path) reads a real file first and
|
|
10
|
+
// delegates to it.
|
|
11
|
+
//
|
|
12
|
+
// Functions defined earlier are available to functions defined LATER (in
|
|
13
|
+
// the same source) as inline macros -- the exact same "inline expansion,
|
|
14
|
+
// not runtime calls" model loadMacro() itself uses (see macros.js's own
|
|
15
|
+
// header comment), and for the same reasons: no call graph, no linking
|
|
16
|
+
// problem, no runtime coupling. And, structurally, no recursion: a
|
|
17
|
+
// definition is only added to this source's own local registry AFTER
|
|
18
|
+
// it's been fully parsed and expanded (see the loop below), so it's
|
|
19
|
+
// never resolvable through its own name while its own body is being
|
|
20
|
+
// expanded, whether directly or transitively through another
|
|
21
|
+
// not-yet-defined function.
|
|
22
|
+
//
|
|
23
|
+
// Each definition MUST have a "name(params):" signature line -- a
|
|
24
|
+
// bare-Node definition with no signature has no name for a later
|
|
25
|
+
// definition (or the caller) to refer to it by, so it can't usefully
|
|
26
|
+
// appear alongside others.
|
|
27
|
+
const fs = require("node:fs");
|
|
28
|
+
const { Parser, tokenizeSegment } = require("./expr.js");
|
|
29
|
+
const { parseProgram } = require("./fn.js");
|
|
30
|
+
const { expandMacros, toMacro } = require("./macros.js");
|
|
31
|
+
|
|
32
|
+
// Tokenizes the WHOLE file as one segment -- unlike fn()/expr() there's
|
|
33
|
+
// no tagged-template interpolation to splice HOLE tokens between (a .expr
|
|
34
|
+
// file is plain text, not JS source with ${...} holes), so this is
|
|
35
|
+
// simpler than fn.js's own tokenizeSegment loop, not a fork of it.
|
|
36
|
+
function tokenizeFile(source, label) {
|
|
37
|
+
const tokens = [];
|
|
38
|
+
tokenizeSegment(source, 0, tokens, { inComment: false }, label);
|
|
39
|
+
tokens.push({ type: "EOF", value: null, pos: source.length });
|
|
40
|
+
return tokens;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parses `source` (plain text, not a file path -- see loadExpr below for
|
|
45
|
+
* the file-reading variant) as zero or more "name(params): let ...;
|
|
46
|
+
* return ...;" definitions back-to-back, in the same grammar fn`...`
|
|
47
|
+
* uses for one. Returns an object keyed by function name, each value the
|
|
48
|
+
* fully-expanded {name, params, body} -- ready to pass straight into
|
|
49
|
+
* evaluate()/emit()/emitMany(), with every reference to an earlier
|
|
50
|
+
* definition in the same source already inlined (see this file's own
|
|
51
|
+
* header comment). `label` identifies the source in error messages (e.g.
|
|
52
|
+
* a file path, or just "playground" for an in-browser text buffer that
|
|
53
|
+
* was never written to disk at all -- this is the one entry point here
|
|
54
|
+
* that has no `fs` dependency, so it's the one usable from a browser).
|
|
55
|
+
*
|
|
56
|
+
* Throws if any definition has no "name(params):" signature line, or if
|
|
57
|
+
* two definitions share a name.
|
|
58
|
+
*
|
|
59
|
+
* `registry` (see macros.js's createRegistry()) defaults to the
|
|
60
|
+
* process-wide default when omitted -- pass a session's own (see
|
|
61
|
+
* index.js's createSession()) to resolve macros/externs defined in that
|
|
62
|
+
* session, alongside whatever's defined earlier in this same source.
|
|
63
|
+
*/
|
|
64
|
+
function loadExprSource(source, label = "loadExprSource()", registry = undefined) {
|
|
65
|
+
const parser = new Parser(tokenizeFile(source, label), source, label);
|
|
66
|
+
|
|
67
|
+
const fileRegistry = new Map(); // name -> {arity, fn, alreadyExpanded} -- see toMacro in macros.js
|
|
68
|
+
const defs = {};
|
|
69
|
+
|
|
70
|
+
while (parser.peek().type !== "EOF") {
|
|
71
|
+
const raw = parseProgram(parser);
|
|
72
|
+
if (!raw || typeof raw.name !== "string") {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`${label}: every definition needs a "name(params):" signature line -- found one with no signature`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (defs[raw.name]) {
|
|
78
|
+
throw new Error(`${label}: duplicate function name "${raw.name}" -- names must be unique in one file`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Expanded against whatever's already in fileRegistry (earlier
|
|
82
|
+
// definitions in this same source) PLUS every macro/extern
|
|
83
|
+
// registered in `registry` (expandMacros merges both -- see
|
|
84
|
+
// macros.js).
|
|
85
|
+
const expanded = expandMacros(raw, fileRegistry, registry);
|
|
86
|
+
defs[raw.name] = expanded;
|
|
87
|
+
|
|
88
|
+
// Available to whatever's defined AFTER this point in the source
|
|
89
|
+
// -- never to itself (expanded above, against fileRegistry
|
|
90
|
+
// BEFORE this line adds it) or to anything defined earlier.
|
|
91
|
+
// `expanded` has nothing left to resolve (macro calls/field
|
|
92
|
+
// access are already gone), so no extraRegistry/registry needs
|
|
93
|
+
// passing here.
|
|
94
|
+
fileRegistry.set(raw.name, toMacro(expanded));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return defs;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reads `path` from disk and parses it via loadExprSource() above -- see
|
|
102
|
+
* that function's own doc comment for the actual grammar/semantics
|
|
103
|
+
* (including `registry`); this is purely the file-reading convenience
|
|
104
|
+
* wrapper around it. Node-only (fs.readFileSync); use loadExprSource(text)
|
|
105
|
+
* directly wherever the source text comes from somewhere else (e.g. a
|
|
106
|
+
* browser text buffer, an HTTP response) instead of a real file on disk.
|
|
107
|
+
*/
|
|
108
|
+
function loadExpr(path, registry = undefined) {
|
|
109
|
+
return loadExprSource(fs.readFileSync(path, "utf8"), `loadExpr(${path})`, registry);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { loadExpr, loadExprSource };
|