@weborigami/language 0.3.3-jse.3 → 0.3.4-jse.4
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/package.json +3 -3
- package/src/compiler/compile.js +5 -3
- package/src/compiler/isOrigamiFrontMatter.js +4 -3
- package/src/compiler/optimize.js +225 -112
- package/src/compiler/origami.pegjs +214 -201
- package/src/compiler/parse.js +1403 -1473
- package/src/compiler/parserHelpers.js +65 -157
- package/src/runtime/expressionObject.js +13 -7
- package/src/runtime/handlers.js +1 -1
- package/src/runtime/jsGlobals.js +7 -0
- package/src/runtime/ops.js +4 -18
- package/test/compiler/compile.test.js +14 -9
- package/test/compiler/optimize.test.js +234 -64
- package/test/compiler/parse.test.js +618 -476
- package/test/runtime/expressionObject.test.js +2 -2
- package/test/runtime/handlers.test.js +2 -2
- package/src/runtime/templateStandard.js +0 -13
- package/test/runtime/templateText.test.js +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/language",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4-jse.4",
|
|
4
4
|
"description": "Web Origami expression language compiler and runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"typescript": "5.8.2"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/async-tree": "0.3.
|
|
15
|
-
"@weborigami/types": "0.3.
|
|
14
|
+
"@weborigami/async-tree": "0.3.4-jse.4",
|
|
15
|
+
"@weborigami/types": "0.3.4-jse.4",
|
|
16
16
|
"watcher": "2.3.1",
|
|
17
17
|
"yaml": "2.7.0"
|
|
18
18
|
},
|
package/src/compiler/compile.js
CHANGED
|
@@ -4,20 +4,22 @@ import optimize from "./optimize.js";
|
|
|
4
4
|
import { parse } from "./parse.js";
|
|
5
5
|
|
|
6
6
|
function compile(source, options) {
|
|
7
|
-
const { startRule } = options;
|
|
7
|
+
const { front, startRule } = options;
|
|
8
8
|
const mode = options.mode ?? "shell";
|
|
9
9
|
const globals = options.globals ?? jsGlobals;
|
|
10
10
|
const enableCaching = options.scopeCaching ?? true;
|
|
11
11
|
if (typeof source === "string") {
|
|
12
12
|
source = { text: source };
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
let code = parse(source.text, {
|
|
15
|
+
front,
|
|
15
16
|
grammarSource: source,
|
|
16
17
|
mode,
|
|
17
18
|
startRule,
|
|
18
19
|
});
|
|
20
|
+
const cache = enableCaching ? {} : null;
|
|
19
21
|
const optimized = optimize(code, {
|
|
20
|
-
|
|
22
|
+
cache,
|
|
21
23
|
globals,
|
|
22
24
|
mode,
|
|
23
25
|
});
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Our heurstic is to see skip any initial alphanumeric or underscore
|
|
5
5
|
* characters, then see if the next character is a parenthesis, dot, slash,
|
|
6
|
-
* curly brace, or equals sign. If so, we assume this is an
|
|
6
|
+
* curly brace, left angle bracket, or equals sign. If so, we assume this is an
|
|
7
|
+
* Origami expression.
|
|
7
8
|
*
|
|
8
9
|
* The goal is to identify Origami front matter like:
|
|
9
10
|
*
|
|
10
11
|
* ```
|
|
11
12
|
* fn(x) function call
|
|
12
13
|
* index.ori() file extension
|
|
13
|
-
* src/data.json
|
|
14
|
+
* <src/data.json> file path
|
|
14
15
|
* // Hello comment
|
|
15
16
|
* { a: 1 } object literal
|
|
16
17
|
* ```
|
|
@@ -22,5 +23,5 @@
|
|
|
22
23
|
* @param {string} text
|
|
23
24
|
*/
|
|
24
25
|
export default function isOrigamiFrontMatter(text) {
|
|
25
|
-
return /^[ \t\r\n]*[A-Za-z0-9_]*[\(\.\/\{
|
|
26
|
+
return /^[ \t\r\n]*[A-Za-z0-9_]*[\(\.\/\{<=]/.test(text);
|
|
26
27
|
}
|
package/src/compiler/optimize.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { pathFromKeys, trailingSlash } from "@weborigami/async-tree";
|
|
2
|
+
import { entryKey } from "../runtime/expressionObject.js";
|
|
2
3
|
import { ops } from "../runtime/internal.js";
|
|
3
4
|
import jsGlobals from "../runtime/jsGlobals.js";
|
|
4
5
|
import { annotate, markers } from "./parserHelpers.js";
|
|
5
6
|
|
|
7
|
+
const REFERENCE_LOCAL = 1;
|
|
8
|
+
const REFERENCE_GLOBAL = 2;
|
|
9
|
+
const REFERENCE_EXTERNAL = 3;
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* Optimize an Origami code instruction:
|
|
8
13
|
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* - Resolve local references to the appropriate context
|
|
15
|
+
* - Resolve global references to the globals object
|
|
16
|
+
* - Resolve other references to external values
|
|
17
|
+
* - Determine whether x.y is a file name or property access
|
|
18
|
+
* - Determine whether x/y is a file path or division operation
|
|
13
19
|
*
|
|
14
20
|
* @typedef {import("./parserHelpers.js").AnnotatedCode} AnnotatedCode
|
|
15
21
|
* @typedef {import("./parserHelpers.js").Code} Code
|
|
@@ -19,9 +25,7 @@ import { annotate, markers } from "./parserHelpers.js";
|
|
|
19
25
|
* @returns {AnnotatedCode}
|
|
20
26
|
*/
|
|
21
27
|
export default function optimize(code, options = {}) {
|
|
22
|
-
const enableCaching = options.enableCaching ?? true;
|
|
23
28
|
const globals = options.globals ?? jsGlobals;
|
|
24
|
-
const mode = options.mode ?? "shell";
|
|
25
29
|
const cache = options.cache ?? {};
|
|
26
30
|
|
|
27
31
|
// The locals is an array, one item for each function or object context that
|
|
@@ -29,25 +33,15 @@ export default function optimize(code, options = {}) {
|
|
|
29
33
|
// subarrays containing the names of local variables defined in that context.
|
|
30
34
|
const locals = options.locals ? options.locals.slice() : [];
|
|
31
35
|
|
|
32
|
-
const externalScope =
|
|
33
|
-
mode === "shell"
|
|
34
|
-
? // External scope is parent scope + globals
|
|
35
|
-
annotate(
|
|
36
|
-
[ops.merge, globals, annotate([ops.scope], code.location)],
|
|
37
|
-
code.location
|
|
38
|
-
)
|
|
39
|
-
: annotate([ops.scope], code.location);
|
|
40
|
-
|
|
41
36
|
// See if we can optimize this level of the code
|
|
42
|
-
const [
|
|
43
|
-
|
|
44
|
-
let externalReference = fn instanceof Array && fn[0] === ops.scope;
|
|
45
|
-
let depth;
|
|
46
|
-
switch (fn) {
|
|
37
|
+
const [op, ...args] = code;
|
|
38
|
+
switch (op) {
|
|
47
39
|
case markers.global:
|
|
48
40
|
// Replace global op with the globals
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
return annotate([globals, args[0]], code.location);
|
|
42
|
+
|
|
43
|
+
case markers.traverse:
|
|
44
|
+
return resolvePath(code, globals, locals, cache);
|
|
51
45
|
|
|
52
46
|
case ops.lambda:
|
|
53
47
|
const parameters = args[0];
|
|
@@ -58,79 +52,24 @@ export default function optimize(code, options = {}) {
|
|
|
58
52
|
break;
|
|
59
53
|
|
|
60
54
|
case ops.literal:
|
|
61
|
-
|
|
62
|
-
if (!(value instanceof Array)) {
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
break;
|
|
55
|
+
return inlineLiteral(code);
|
|
66
56
|
|
|
67
57
|
case ops.object:
|
|
68
58
|
const entries = args;
|
|
69
|
-
const keys = entries.map(
|
|
59
|
+
const keys = entries.map(entryKey);
|
|
70
60
|
locals.push(keys);
|
|
71
61
|
break;
|
|
72
|
-
|
|
73
|
-
case markers.reference:
|
|
74
|
-
// Determine whether reference is local and, if so, transform to
|
|
75
|
-
// ops.local call. Otherwise transform to ops.scope call.
|
|
76
|
-
let key = args[0];
|
|
77
|
-
if (key instanceof Array && key[0] === ops.literal) {
|
|
78
|
-
key = key[1];
|
|
79
|
-
}
|
|
80
|
-
const normalizedKey = trailingSlash.remove(key);
|
|
81
|
-
let target;
|
|
82
|
-
depth = getLocalReferenceDepth(locals, normalizedKey);
|
|
83
|
-
if (depth >= 0) {
|
|
84
|
-
// Transform local reference
|
|
85
|
-
const contextCode = [ops.context];
|
|
86
|
-
if (depth > 0) {
|
|
87
|
-
contextCode.push(depth);
|
|
88
|
-
}
|
|
89
|
-
target = annotate(contextCode, code.location);
|
|
90
|
-
} else if (mode === "shell") {
|
|
91
|
-
// Transform non-local reference
|
|
92
|
-
target = externalScope;
|
|
93
|
-
externalReference = true;
|
|
94
|
-
} else if (mode === "jse") {
|
|
95
|
-
target = globals;
|
|
96
|
-
}
|
|
97
|
-
optimized = annotate([target, ...args], code.location);
|
|
98
|
-
break;
|
|
99
|
-
|
|
100
|
-
case ops.scope:
|
|
101
|
-
depth = locals.length;
|
|
102
|
-
if (depth === 0) {
|
|
103
|
-
// Use scope call as is
|
|
104
|
-
optimized = code;
|
|
105
|
-
} else {
|
|
106
|
-
// Add context for appropriate depth to scope call
|
|
107
|
-
const contextCode = annotate([ops.context, depth], code.location);
|
|
108
|
-
optimized = annotate([ops.scope, contextCode], code.location);
|
|
109
|
-
}
|
|
110
|
-
break;
|
|
111
62
|
}
|
|
112
63
|
|
|
113
64
|
// Optimize children
|
|
114
|
-
optimized = annotate(
|
|
115
|
-
|
|
65
|
+
const optimized = annotate(
|
|
66
|
+
code.map((child, index) => {
|
|
116
67
|
// Don't optimize lambda parameter names
|
|
117
|
-
if (
|
|
68
|
+
if (op === ops.lambda && index === 1) {
|
|
118
69
|
return child;
|
|
119
|
-
} else if (
|
|
120
|
-
// Code that defines a property `x` that contains references to `x`
|
|
121
|
-
// shouldn't find this context but look further up.
|
|
70
|
+
} else if (op === ops.object && index > 0) {
|
|
122
71
|
const [key, value] = child;
|
|
123
|
-
const
|
|
124
|
-
let adjustedLocals;
|
|
125
|
-
if (locals.at(-1)?.includes(normalizedKey)) {
|
|
126
|
-
adjustedLocals = locals.slice();
|
|
127
|
-
// Remove the key from the current context's locals
|
|
128
|
-
adjustedLocals[adjustedLocals.length - 1] = locals
|
|
129
|
-
.at(-1)
|
|
130
|
-
.filter((name) => name !== normalizedKey);
|
|
131
|
-
} else {
|
|
132
|
-
adjustedLocals = locals;
|
|
133
|
-
}
|
|
72
|
+
const adjustedLocals = avoidLocalRecursion(locals, key);
|
|
134
73
|
return [
|
|
135
74
|
key,
|
|
136
75
|
optimize(/** @type {AnnotatedCode} */ (value), {
|
|
@@ -149,37 +88,88 @@ export default function optimize(code, options = {}) {
|
|
|
149
88
|
return child;
|
|
150
89
|
}
|
|
151
90
|
}),
|
|
152
|
-
|
|
91
|
+
code.location
|
|
153
92
|
);
|
|
154
93
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
: null
|
|
166
|
-
);
|
|
167
|
-
if (keys.some((key) => key === null)) {
|
|
168
|
-
throw new Error("Internal error: scope reference with non-literal key");
|
|
169
|
-
}
|
|
170
|
-
const path = pathFromKeys(keys);
|
|
171
|
-
optimized = annotate(
|
|
172
|
-
[ops.cache, cache, path, optimized],
|
|
173
|
-
optimized.location
|
|
174
|
-
);
|
|
94
|
+
return annotate(optimized, code.location);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// When defining a property named `key` (or `key/` or `(key)`), we need to
|
|
98
|
+
// remove any local variable with that name from the stack of locals to avoid a
|
|
99
|
+
// recursive reference.
|
|
100
|
+
function avoidLocalRecursion(locals, key) {
|
|
101
|
+
if (key[0] === "(" && key[key.length - 1] === ")") {
|
|
102
|
+
// Non-enumerable property, remove parentheses
|
|
103
|
+
key = key.slice(1, -1);
|
|
175
104
|
}
|
|
176
105
|
|
|
177
|
-
|
|
106
|
+
const currentFrame = locals.length - 1;
|
|
107
|
+
const matchingKeyIndex = locals[currentFrame].findIndex(
|
|
108
|
+
(localKey) =>
|
|
109
|
+
// Ignore trailing slashes when comparing keys
|
|
110
|
+
trailingSlash.remove(localKey) === trailingSlash.remove(key)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (matchingKeyIndex >= 0) {
|
|
114
|
+
// Remove the key from the current context's locals
|
|
115
|
+
const adjustedLocals = locals.slice();
|
|
116
|
+
adjustedLocals[currentFrame] = adjustedLocals[currentFrame].slice();
|
|
117
|
+
adjustedLocals[currentFrame].splice(matchingKeyIndex, 1);
|
|
118
|
+
return adjustedLocals;
|
|
119
|
+
} else {
|
|
120
|
+
return locals;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cachePath(code, cache) {
|
|
125
|
+
const keys = code.map(keyFromCode).filter((key) => key !== null);
|
|
126
|
+
const path = pathFromKeys(keys);
|
|
127
|
+
return annotate([ops.cache, cache, path, code], code.location);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// A reference with periods like x.y.z
|
|
131
|
+
function compoundReference(key, globals, locals, location) {
|
|
132
|
+
const parts = key.split(".");
|
|
133
|
+
if (parts.length === 1) {
|
|
134
|
+
// Not a compound reference
|
|
135
|
+
return { type: REFERENCE_EXTERNAL, result: null };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check first part to see if it's a global or local reference
|
|
139
|
+
const [head, ...tail] = parts;
|
|
140
|
+
const type = referenceType(head, globals, locals);
|
|
141
|
+
let result;
|
|
142
|
+
if (type === REFERENCE_GLOBAL) {
|
|
143
|
+
result = globalReference(head, globals, location);
|
|
144
|
+
} else if (type === REFERENCE_LOCAL) {
|
|
145
|
+
result = localReference(head, locals, location);
|
|
146
|
+
} else {
|
|
147
|
+
// Not a compound reference
|
|
148
|
+
return { type: REFERENCE_EXTERNAL, result: null };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Process the remaining parts as property accesses
|
|
152
|
+
while (tail.length > 0) {
|
|
153
|
+
const part = tail.shift();
|
|
154
|
+
result = annotate([result, part], location);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { type, result };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function externalReference(key, locals, location) {
|
|
161
|
+
const scope = scopeCall(locals, location);
|
|
162
|
+
const literal = annotate([ops.literal, key], location);
|
|
163
|
+
return annotate([scope, literal], location);
|
|
178
164
|
}
|
|
179
165
|
|
|
180
166
|
// Determine how many contexts up we need to go for a local
|
|
181
167
|
function getLocalReferenceDepth(locals, key) {
|
|
182
|
-
const contextIndex = locals.findLastIndex((names) =>
|
|
168
|
+
const contextIndex = locals.findLastIndex((names) =>
|
|
169
|
+
names.some(
|
|
170
|
+
(name) => trailingSlash.remove(name) === trailingSlash.remove(key)
|
|
171
|
+
)
|
|
172
|
+
);
|
|
183
173
|
if (contextIndex < 0) {
|
|
184
174
|
return -1; // Not a local reference
|
|
185
175
|
}
|
|
@@ -187,10 +177,133 @@ function getLocalReferenceDepth(locals, key) {
|
|
|
187
177
|
return depth;
|
|
188
178
|
}
|
|
189
179
|
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
180
|
+
function globalReference(key, globals, location) {
|
|
181
|
+
const normalized = trailingSlash.remove(key);
|
|
182
|
+
return annotate([globals, normalized], location);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function inlineLiteral(code) {
|
|
186
|
+
// If the literal value is an array, it's likely the strings array
|
|
187
|
+
// of a template literal, so return it as is.
|
|
188
|
+
return code[0] === ops.literal && !Array.isArray(code[1]) ? code[1] : code;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function localReference(key, locals, location) {
|
|
192
|
+
const normalized = trailingSlash.remove(key);
|
|
193
|
+
const depth = getLocalReferenceDepth(locals, normalized);
|
|
194
|
+
const context = [ops.context];
|
|
195
|
+
if (depth > 0) {
|
|
196
|
+
context.push(depth);
|
|
197
|
+
}
|
|
198
|
+
const contextCall = annotate(context, location);
|
|
199
|
+
const literal = annotate([ops.literal, key], location);
|
|
200
|
+
return annotate([contextCall, literal], location);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function keyFromCode(code) {
|
|
204
|
+
const op = code instanceof Array ? code[0] : code;
|
|
205
|
+
switch (op) {
|
|
206
|
+
case ops.homeDirectory:
|
|
207
|
+
return "~";
|
|
208
|
+
|
|
209
|
+
case markers.external:
|
|
210
|
+
case markers.global:
|
|
211
|
+
case markers.reference:
|
|
212
|
+
case ops.literal:
|
|
213
|
+
return code[1];
|
|
214
|
+
|
|
215
|
+
case ops.rootDirectory:
|
|
216
|
+
return "/";
|
|
217
|
+
|
|
218
|
+
default:
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function reference(code, globals, locals) {
|
|
224
|
+
const key = keyFromCode(code);
|
|
225
|
+
const normalized = trailingSlash.remove(key);
|
|
226
|
+
const location = code.location;
|
|
227
|
+
|
|
228
|
+
if (normalized === "~") {
|
|
229
|
+
// Special case for home directory
|
|
230
|
+
return {
|
|
231
|
+
type: REFERENCE_EXTERNAL,
|
|
232
|
+
result: annotate([ops.homeDirectory], location),
|
|
233
|
+
};
|
|
234
|
+
} else if (normalized === "") {
|
|
235
|
+
// Special case for root directory
|
|
236
|
+
return {
|
|
237
|
+
type: REFERENCE_EXTERNAL,
|
|
238
|
+
result: annotate([ops.rootDirectory], location),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (code[0] === markers.external) {
|
|
243
|
+
// Explicit external reference
|
|
244
|
+
return {
|
|
245
|
+
type: REFERENCE_EXTERNAL,
|
|
246
|
+
result: externalReference(key, locals, location),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// See if the whole key is a global or local variable
|
|
251
|
+
let type = referenceType(key, globals, locals);
|
|
252
|
+
let result;
|
|
253
|
+
if (type === REFERENCE_GLOBAL) {
|
|
254
|
+
result = globalReference(key, globals, location);
|
|
255
|
+
} else if (type === REFERENCE_LOCAL) {
|
|
256
|
+
result = localReference(key, locals, location);
|
|
257
|
+
} else {
|
|
258
|
+
// Try key as a compound reference x.y.z
|
|
259
|
+
const compound = compoundReference(key, globals, locals, location);
|
|
260
|
+
result = compound?.result;
|
|
261
|
+
type = compound?.type;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!result) {
|
|
265
|
+
// If none of the above worked, it must be an external reference
|
|
266
|
+
result = externalReference(key, locals, location);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { type, result };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function referenceType(key, globals, locals) {
|
|
273
|
+
// Check if the key is a global variable
|
|
274
|
+
const normalized = trailingSlash.remove(key);
|
|
275
|
+
if (getLocalReferenceDepth(locals, normalized) >= 0) {
|
|
276
|
+
return REFERENCE_LOCAL;
|
|
277
|
+
} else if (normalized in globals) {
|
|
278
|
+
return REFERENCE_GLOBAL;
|
|
279
|
+
} else {
|
|
280
|
+
return REFERENCE_EXTERNAL;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function resolvePath(code, globals, locals, cache) {
|
|
285
|
+
const args = code.slice(1);
|
|
286
|
+
const [head, ...tail] = args;
|
|
287
|
+
|
|
288
|
+
let { type, result } = reference(head, globals, locals);
|
|
289
|
+
|
|
290
|
+
result.push(...tail);
|
|
291
|
+
|
|
292
|
+
if (type === REFERENCE_EXTERNAL && cache !== null) {
|
|
293
|
+
// Cache external path
|
|
294
|
+
return cachePath(result, cache);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function scopeCall(locals, location) {
|
|
301
|
+
const depth = locals.length;
|
|
302
|
+
const code = [ops.scope];
|
|
303
|
+
if (depth > 0) {
|
|
304
|
+
// Add context for appropriate depth to scope call
|
|
305
|
+
const contextCode = annotate([ops.context, depth], location);
|
|
306
|
+
code.push(contextCode);
|
|
194
307
|
}
|
|
195
|
-
return
|
|
308
|
+
return annotate(code, location);
|
|
196
309
|
}
|