@weborigami/language 0.3.3-jse.3 → 0.3.3

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,15 +7,13 @@ 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";
11
10
  export { default as HandleExtensionsTransform } from "./src/runtime/HandleExtensionsTransform.js";
12
11
  export * from "./src/runtime/handlers.js";
13
12
  export { default as ImportModulesMixin } from "./src/runtime/ImportModulesMixin.js";
14
13
  export { default as InvokeFunctionsTransform } from "./src/runtime/InvokeFunctionsTransform.js";
15
- export { default as jsGlobals } from "./src/runtime/jsGlobals.js";
16
14
  export * as moduleCache from "./src/runtime/moduleCache.js";
17
15
  export { default as OrigamiFiles } from "./src/runtime/OrigamiFiles.js";
18
16
  export * as symbols from "./src/runtime/symbols.js";
19
- export { default as taggedTemplateIndent } from "./src/runtime/templateIndent.js";
17
+ export { default as taggedTemplateIndent } from "./src/runtime/taggedTemplateIndent.js";
20
18
  export { default as TreeEvent } from "./src/runtime/TreeEvent.js";
21
19
  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-jse.3",
3
+ "version": "0.3.3",
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.3",
15
+ "@weborigami/types": "0.3.3",
16
16
  "watcher": "2.3.1",
17
17
  "yaml": "2.7.0"
18
18
  },
@@ -1,26 +1,18 @@
1
1
  import { createExpressionFunction } from "../runtime/expressionFunction.js";
2
- import jsGlobals from "../runtime/jsGlobals.js";
3
2
  import optimize from "./optimize.js";
4
3
  import { parse } from "./parse.js";
5
4
 
6
5
  function compile(source, options) {
7
- const { startRule } = options;
8
- const mode = options.mode ?? "shell";
9
- const globals = options.globals ?? jsGlobals;
6
+ const { macros, startRule } = options;
10
7
  const enableCaching = options.scopeCaching ?? true;
11
8
  if (typeof source === "string") {
12
9
  source = { text: source };
13
10
  }
14
11
  const code = parse(source.text, {
15
12
  grammarSource: source,
16
- mode,
17
13
  startRule,
18
14
  });
19
- const optimized = optimize(code, {
20
- enableCaching,
21
- globals,
22
- mode,
23
- });
15
+ const optimized = optimize(code, enableCaching, macros);
24
16
  const fn = createExpressionFunction(optimized);
25
17
  return fn;
26
18
  }
@@ -1,60 +1,40 @@
1
1
  import { pathFromKeys, trailingSlash } from "@weborigami/async-tree";
2
2
  import { ops } from "../runtime/internal.js";
3
- import jsGlobals from "../runtime/jsGlobals.js";
4
- import { annotate, markers } from "./parserHelpers.js";
3
+ import { annotate, undetermined } from "./parserHelpers.js";
5
4
 
6
5
  /**
7
6
  * Optimize an Origami code instruction:
8
7
  *
9
- * - Transform any remaining reference references to scope references.
8
+ * - Transform any remaining undetermined references to scope references.
10
9
  * - Transform those or explicit ops.scope calls to ops.external calls unless
11
10
  * they refer to local variables (variables defined by object literals or
12
11
  * lambda parameters).
12
+ * - Apply any macros to the code.
13
13
  *
14
14
  * @typedef {import("./parserHelpers.js").AnnotatedCode} AnnotatedCode
15
15
  * @typedef {import("./parserHelpers.js").Code} Code
16
16
  *
17
17
  * @param {AnnotatedCode} code
18
- * @param {any} options
18
+ * @param {boolean} enableCaching
19
+ * @param {Record<string, AnnotatedCode>} macros
20
+ * @param {Record<string, AnnotatedCode>} cache
21
+ * @param {Record<string, boolean>} locals
19
22
  * @returns {AnnotatedCode}
20
23
  */
21
- export default function optimize(code, options = {}) {
22
- const enableCaching = options.enableCaching ?? true;
23
- const globals = options.globals ?? jsGlobals;
24
- const mode = options.mode ?? "shell";
25
- const cache = options.cache ?? {};
26
-
27
- // The locals is an array, one item for each function or object context that
28
- // has been entered. The array grows to the right. The array items are
29
- // subarrays containing the names of local variables defined in that context.
30
- const locals = options.locals ? options.locals.slice() : [];
31
-
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
-
24
+ export default function optimize(
25
+ code,
26
+ enableCaching = true,
27
+ macros = {},
28
+ cache = {},
29
+ locals = {}
30
+ ) {
41
31
  // See if we can optimize this level of the code
42
32
  const [fn, ...args] = code;
43
- let optimized = code;
44
- let externalReference = fn instanceof Array && fn[0] === ops.scope;
45
- let depth;
33
+ let additionalLocalNames;
46
34
  switch (fn) {
47
- case markers.global:
48
- // Replace global op with the globals
49
- optimized = annotate([globals, args[0]], code.location);
50
- break;
51
-
52
35
  case ops.lambda:
53
36
  const parameters = args[0];
54
- if (parameters.length > 0) {
55
- const names = parameters.map((param) => param[1]);
56
- locals.push(names);
57
- }
37
+ additionalLocalNames = parameters.map((param) => param[1]);
58
38
  break;
59
39
 
60
40
  case ops.literal:
@@ -66,131 +46,97 @@ export default function optimize(code, options = {}) {
66
46
 
67
47
  case ops.object:
68
48
  const entries = args;
69
- const keys = entries.map(([key]) => propertyName(key));
70
- locals.push(keys);
49
+ additionalLocalNames = entries.map(([key]) => trailingSlash.remove(key));
71
50
  break;
72
51
 
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
- }
52
+ // Both of these are handled the same way
53
+ case undetermined:
54
+ case ops.scope:
55
+ const key = args[0];
80
56
  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;
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);
70
+ } else {
71
+ // Internal ops.scope call; leave as is
72
+ return code;
96
73
  }
97
- optimized = annotate([target, ...args], code.location);
98
- break;
99
74
 
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);
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
+ }
109
96
  }
110
97
  break;
111
98
  }
112
99
 
113
- // Optimize children
114
- optimized = annotate(
115
- optimized.map((child, index) => {
116
- // Don't optimize lambda parameter names
117
- if (fn === ops.lambda && index === 1) {
118
- 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.
122
- 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
- }
134
- return [
135
- key,
136
- optimize(/** @type {AnnotatedCode} */ (value), {
137
- ...options,
138
- locals: adjustedLocals,
139
- }),
140
- ];
141
- } else if (Array.isArray(child) && "location" in child) {
142
- // Review: Aside from ops.object (above), what non-instruction arrays
143
- // does this descend into?
144
- return optimize(/** @type {AnnotatedCode} */ (child), {
145
- ...options,
146
- locals,
147
- });
148
- } else {
149
- return child;
150
- }
151
- }),
152
- optimized.location
153
- );
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
+ }
108
+ } else {
109
+ updatedLocals = locals;
110
+ }
154
111
 
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
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
166
128
  );
167
- if (keys.some((key) => key === null)) {
168
- throw new Error("Internal error: scope reference with non-literal key");
129
+ } else {
130
+ return child;
169
131
  }
170
- const path = pathFromKeys(keys);
171
- optimized = annotate(
172
- [ops.cache, cache, path, optimized],
173
- optimized.location
174
- );
175
- }
132
+ });
176
133
 
177
134
  return annotate(optimized, code.location);
178
135
  }
179
136
 
180
- // Determine how many contexts up we need to go for a local
181
- function getLocalReferenceDepth(locals, key) {
182
- const contextIndex = locals.findLastIndex((names) => names.includes(key));
183
- if (contextIndex < 0) {
184
- return -1; // Not a local reference
185
- }
186
- const depth = locals.length - contextIndex - 1;
187
- return depth;
188
- }
189
-
190
- function propertyName(key) {
191
- if (key[0] === "(" && key[key.length - 1] === ")") {
192
- // Non-enumerable property, remove parentheses
193
- key = key.slice(1, -1);
194
- }
195
- return trailingSlash.remove(key);
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;
196
142
  }