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/README.md +562 -99
- package/ast.js +190 -2
- package/emitters/base.js +36 -4
- package/emitters/cobol.js +191 -54
- package/emitters/exprsyntax.js +39 -8
- package/emitters/registry.js +12 -1
- package/evaluate.js +47 -14
- package/expr.js +107 -9
- package/fn.js +31 -6
- 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/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 };
|