@weborigami/language 0.3.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/main.js CHANGED
@@ -7,13 +7,15 @@ export { default as evaluate } from "./src/runtime/evaluate.js";
7
7
  export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
8
8
  export * as expressionFunction from "./src/runtime/expressionFunction.js";
9
9
  export { default as functionResultsMap } from "./src/runtime/functionResultsMap.js";
10
+ export { default as getHandlers } from "./src/runtime/getHandlers.js";
10
11
  export { default as HandleExtensionsTransform } from "./src/runtime/HandleExtensionsTransform.js";
11
12
  export * from "./src/runtime/handlers.js";
12
13
  export { default as ImportModulesMixin } from "./src/runtime/ImportModulesMixin.js";
13
14
  export { default as InvokeFunctionsTransform } from "./src/runtime/InvokeFunctionsTransform.js";
15
+ export { default as jsGlobals } from "./src/runtime/jsGlobals.js";
14
16
  export * as moduleCache from "./src/runtime/moduleCache.js";
15
17
  export { default as OrigamiFiles } from "./src/runtime/OrigamiFiles.js";
16
18
  export * as symbols from "./src/runtime/symbols.js";
17
- export { default as taggedTemplateIndent } from "./src/runtime/taggedTemplateIndent.js";
19
+ export { default as taggedTemplateIndent } from "./src/runtime/templateIndent.js";
18
20
  export { default as TreeEvent } from "./src/runtime/TreeEvent.js";
19
21
  export { default as WatchFilesMixin } from "./src/runtime/WatchFilesMixin.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.3.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",
15
- "@weborigami/types": "0.3.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
  },
@@ -1,18 +1,28 @@
1
1
  import { createExpressionFunction } from "../runtime/expressionFunction.js";
2
+ import jsGlobals from "../runtime/jsGlobals.js";
2
3
  import optimize from "./optimize.js";
3
4
  import { parse } from "./parse.js";
4
5
 
5
6
  function compile(source, options) {
6
- const { macros, startRule } = options;
7
+ const { front, startRule } = options;
8
+ const mode = options.mode ?? "shell";
9
+ const globals = options.globals ?? jsGlobals;
7
10
  const enableCaching = options.scopeCaching ?? true;
8
11
  if (typeof source === "string") {
9
12
  source = { text: source };
10
13
  }
11
- const code = parse(source.text, {
14
+ let code = parse(source.text, {
15
+ front,
12
16
  grammarSource: source,
17
+ mode,
13
18
  startRule,
14
19
  });
15
- const optimized = optimize(code, enableCaching, macros);
20
+ const cache = enableCaching ? {} : null;
21
+ const optimized = optimize(code, {
22
+ cache,
23
+ globals,
24
+ mode,
25
+ });
16
26
  const fn = createExpressionFunction(optimized);
17
27
  return fn;
18
28
  }
@@ -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,142 +1,309 @@
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
- import { annotate, undetermined } from "./parserHelpers.js";
4
+ import jsGlobals from "../runtime/jsGlobals.js";
5
+ import { annotate, markers } from "./parserHelpers.js";
6
+
7
+ const REFERENCE_LOCAL = 1;
8
+ const REFERENCE_GLOBAL = 2;
9
+ const REFERENCE_EXTERNAL = 3;
4
10
 
5
11
  /**
6
12
  * Optimize an Origami code instruction:
7
13
  *
8
- * - Transform any remaining undetermined references to scope references.
9
- * - Transform those or explicit ops.scope calls to ops.external calls unless
10
- * they refer to local variables (variables defined by object literals or
11
- * lambda parameters).
12
- * - Apply any macros to the code.
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
16
22
  *
17
23
  * @param {AnnotatedCode} code
18
- * @param {boolean} enableCaching
19
- * @param {Record<string, AnnotatedCode>} macros
20
- * @param {Record<string, AnnotatedCode>} cache
21
- * @param {Record<string, boolean>} locals
24
+ * @param {any} options
22
25
  * @returns {AnnotatedCode}
23
26
  */
24
- export default function optimize(
25
- code,
26
- enableCaching = true,
27
- macros = {},
28
- cache = {},
29
- locals = {}
30
- ) {
27
+ export default function optimize(code, options = {}) {
28
+ const globals = options.globals ?? jsGlobals;
29
+ const cache = options.cache ?? {};
30
+
31
+ // The locals is an array, one item for each function or object context that
32
+ // has been entered. The array grows to the right. The array items are
33
+ // subarrays containing the names of local variables defined in that context.
34
+ const locals = options.locals ? options.locals.slice() : [];
35
+
31
36
  // See if we can optimize this level of the code
32
- const [fn, ...args] = code;
33
- let additionalLocalNames;
34
- switch (fn) {
37
+ const [op, ...args] = code;
38
+ switch (op) {
39
+ case markers.global:
40
+ // Replace global op with the globals
41
+ return annotate([globals, args[0]], code.location);
42
+
43
+ case markers.traverse:
44
+ return resolvePath(code, globals, locals, cache);
45
+
35
46
  case ops.lambda:
36
47
  const parameters = args[0];
37
- additionalLocalNames = parameters.map((param) => param[1]);
48
+ if (parameters.length > 0) {
49
+ const names = parameters.map((param) => param[1]);
50
+ locals.push(names);
51
+ }
38
52
  break;
39
53
 
40
54
  case ops.literal:
41
- const value = args[0];
42
- if (!(value instanceof Array)) {
43
- return value;
44
- }
45
- break;
55
+ return inlineLiteral(code);
46
56
 
47
57
  case ops.object:
48
58
  const entries = args;
49
- additionalLocalNames = entries.map(([key]) => trailingSlash.remove(key));
59
+ const keys = entries.map(entryKey);
60
+ locals.push(keys);
50
61
  break;
62
+ }
51
63
 
52
- // Both of these are handled the same way
53
- case undetermined:
54
- case ops.scope:
55
- const key = args[0];
56
- const normalizedKey = trailingSlash.remove(key);
57
- if (macros?.[normalizedKey]) {
58
- // Apply macro
59
- const macro = macros?.[normalizedKey];
60
- return applyMacro(macro, code, enableCaching, macros, cache, locals);
61
- } else if (enableCaching && !locals[normalizedKey]) {
62
- // Upgrade to cached external scope reference
63
- return annotate(
64
- [ops.external, key, annotate([ops.scope, key], code.location), cache],
65
- code.location
66
- );
67
- } else if (fn === undetermined) {
68
- // Transform undetermined reference to regular scope call
69
- return annotate([ops.scope, key], code.location);
64
+ // Optimize children
65
+ const optimized = annotate(
66
+ code.map((child, index) => {
67
+ // Don't optimize lambda parameter names
68
+ if (op === ops.lambda && index === 1) {
69
+ return child;
70
+ } else if (op === ops.object && index > 0) {
71
+ const [key, value] = child;
72
+ const adjustedLocals = avoidLocalRecursion(locals, key);
73
+ return [
74
+ key,
75
+ optimize(/** @type {AnnotatedCode} */ (value), {
76
+ ...options,
77
+ locals: adjustedLocals,
78
+ }),
79
+ ];
80
+ } else if (Array.isArray(child) && "location" in child) {
81
+ // Review: Aside from ops.object (above), what non-instruction arrays
82
+ // does this descend into?
83
+ return optimize(/** @type {AnnotatedCode} */ (child), {
84
+ ...options,
85
+ locals,
86
+ });
70
87
  } else {
71
- // Internal ops.scope call; leave as is
72
- return code;
88
+ return child;
73
89
  }
90
+ }),
91
+ code.location
92
+ );
74
93
 
75
- case ops.traverse:
76
- // Is the first argument a nonscope/undetermined reference?
77
- const isScopeRef =
78
- args[0]?.[0] === ops.scope || args[0]?.[0] === undetermined;
79
- if (enableCaching && isScopeRef) {
80
- // Is the first argument a nonlocal reference?
81
- const normalizedKey = trailingSlash.remove(args[0][1]);
82
- if (!locals[normalizedKey]) {
83
- // Are the remaining arguments all literals?
84
- const allLiterals = args
85
- .slice(1)
86
- .every((arg) => arg[0] === ops.literal);
87
- if (allLiterals) {
88
- // Convert to ops.external
89
- const keys = args.map((arg) => arg[1]);
90
- const path = pathFromKeys(keys);
91
- /** @type {Code} */
92
- const optimized = [ops.external, path, code, cache];
93
- return annotate(optimized, code.location);
94
- }
95
- }
96
- }
97
- break;
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);
98
104
  }
99
105
 
100
- // Add any locals introduced by this code to the list that will be consulted
101
- // when we descend into child nodes.
102
- let updatedLocals;
103
- if (additionalLocalNames) {
104
- updatedLocals = { ...locals };
105
- for (const key of additionalLocalNames) {
106
- updatedLocals[key] = true;
107
- }
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;
108
119
  } else {
109
- updatedLocals = locals;
120
+ return locals;
110
121
  }
122
+ }
111
123
 
112
- // Optimize children
113
- const optimized = code.map((child, index) => {
114
- // Don't optimize lambda parameter names
115
- if (fn === ops.lambda && index === 1) {
116
- return child;
117
- } else if (Array.isArray(child) && "location" in child) {
118
- // Review: This currently descends into arrays that are not instructions,
119
- // such as the entries of an ops.object. This should be harmless, but it'd
120
- // be preferable to only descend into instructions. This would require
121
- // surrounding ops.object entries with ops.array.
122
- return optimize(
123
- /** @type {AnnotatedCode} */ (child),
124
- enableCaching,
125
- macros,
126
- cache,
127
- updatedLocals
128
- );
129
- } else {
130
- return child;
131
- }
132
- });
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
+ }
133
129
 
134
- return annotate(optimized, code.location);
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);
164
+ }
165
+
166
+ // Determine how many contexts up we need to go for a local
167
+ function getLocalReferenceDepth(locals, key) {
168
+ const contextIndex = locals.findLastIndex((names) =>
169
+ names.some(
170
+ (name) => trailingSlash.remove(name) === trailingSlash.remove(key)
171
+ )
172
+ );
173
+ if (contextIndex < 0) {
174
+ return -1; // Not a local reference
175
+ }
176
+ const depth = locals.length - contextIndex - 1;
177
+ return depth;
178
+ }
179
+
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
+ }
135
221
  }
136
222
 
137
- function applyMacro(macro, code, enableCaching, macros, cache, locals) {
138
- const optimized = optimize(macro, enableCaching, macros, cache, locals);
139
- return optimized instanceof Array
140
- ? annotate(optimized, code.location)
141
- : optimized;
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);
307
+ }
308
+ return annotate(code, location);
142
309
  }