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/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 };