@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.3.3-jse.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.3-jse.3",
15
- "@weborigami/types": "0.3.3-jse.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
  },
@@ -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
- const code = parse(source.text, {
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
- enableCaching,
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 Origami expression.
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 file path
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_]*[\(\.\/\{=]/.test(text);
26
+ return /^[ \t\r\n]*[A-Za-z0-9_]*[\(\.\/\{<=]/.test(text);
26
27
  }
@@ -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
- * - Transform any remaining reference references to scope references.
10
- * - Transform those or explicit ops.scope calls to ops.external calls unless
11
- * they refer to local variables (variables defined by object literals or
12
- * lambda parameters).
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 [fn, ...args] = code;
43
- let optimized = code;
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
- optimized = annotate([globals, args[0]], code.location);
50
- break;
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
- const value = args[0];
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(([key]) => propertyName(key));
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
- optimized.map((child, index) => {
65
+ const optimized = annotate(
66
+ code.map((child, index) => {
116
67
  // Don't optimize lambda parameter names
117
- if (fn === ops.lambda && index === 1) {
68
+ if (op === ops.lambda && index === 1) {
118
69
  return child;
119
- } else if (fn === ops.object && index > 0) {
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 normalizedKey = trailingSlash.remove(key);
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
- optimized.location
91
+ code.location
153
92
  );
154
93
 
155
- // Cache external scope or merged globals + scope references
156
- if (enableCaching && externalReference) {
157
- // Get all the keys so we can construct a path as a cache key
158
- const keys = optimized
159
- .slice(1)
160
- .map((arg) =>
161
- typeof arg === "string" || typeof arg === "number"
162
- ? arg
163
- : arg instanceof Array && arg[0] === ops.literal
164
- ? arg[1]
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
- return annotate(optimized, code.location);
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) => names.includes(key));
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 propertyName(key) {
191
- if (key[0] === "(" && key[key.length - 1] === ")") {
192
- // Non-enumerable property, remove parentheses
193
- key = key.slice(1, -1);
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 trailingSlash.remove(key);
308
+ return annotate(code, location);
196
309
  }