@weborigami/language 0.6.4 → 0.6.6

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.
@@ -9,20 +9,22 @@ import {
9
9
  import handleExtension from "./handleExtension.js";
10
10
  import { evaluate, ops } from "./internal.js";
11
11
 
12
+ export const KEY_TYPE = {
13
+ STRING: 0, // Simple string key: `a: 1`
14
+ COMPUTED: 1, // Computed key: `[code]: 1`
15
+ };
16
+
17
+ const VALUE_TYPE = {
18
+ PRIMITIVE: 0, // Primitive value: `a: 1`
19
+ EAGER: 1, // Calculated immediately: `a: 1 + 1`
20
+ GETTER: 2, // Calculated on demand: `a = fn()`
21
+ };
22
+
12
23
  /**
13
24
  * Given an array of entries with string keys and Origami code values (arrays of
14
25
  * ops and operands), return an object with the same keys defining properties
15
26
  * whose getters evaluate the code.
16
- *
17
- * The value can take three forms:
18
- *
19
- * 1. A primitive value (string, etc.). This will be defined directly as an
20
- * object property.
21
- * 1. An eager (as opposed to lazy) code entry. This will be evaluated during
22
- * this call and its result defined as an object property.
23
- * 1. A code entry that starts with ops.getter. This will be defined as a
24
- * property getter on the object.
25
- *
27
+
26
28
  * @param {*} entries
27
29
  * @param {import("../../index.ts").RuntimeState} [state]
28
30
  */
@@ -35,191 +37,201 @@ export default async function expressionObject(entries, state = {}) {
35
37
  }
36
38
  setParent(object, parent);
37
39
 
38
- // Get the keys, which might included computed keys
39
- const computedKeys = await Promise.all(
40
- entries.map(async ([key]) =>
41
- key instanceof Array ? await evaluate(key, state) : key
42
- )
43
- );
44
-
45
- let tree;
46
- const eagerProperties = [];
47
- const propertyIsEnumerable = {};
48
- let hasLazyProperties = false;
49
- for (let i = 0; i < entries.length; i++) {
50
- let key = computedKeys[i];
51
- let value = entries[i][1];
52
-
53
- // Determine if we need to define a getter or a regular property. If the key
54
- // has an extension, we need to define a getter. If the value is code (an
55
- // array), we need to define a getter -- but if that code takes the form
56
- // [ops.getter, <primitive>] or [ops.literal, <value>], we can define a
57
- // regular property.
58
- let defineProperty;
59
- const extname = extension.extname(key);
60
- if (extname) {
61
- defineProperty = false;
62
- } else if (!(value instanceof Array)) {
63
- defineProperty = true;
64
- } else if (value[0] === ops.getter && !(value[1] instanceof Array)) {
65
- defineProperty = true;
66
- value = value[1];
67
- } else if (value[0] === ops.literal) {
68
- defineProperty = true;
69
- value = value[1];
70
- } else {
71
- defineProperty = false;
40
+ // The object in Map form for use on the stack
41
+ const map = new ObjectMap(object);
42
+
43
+ // Preparation: gather information about all properties
44
+ const infos = entries.map(([key, value]) => propertyInfo(key, value));
45
+
46
+ // First pass: define all properties with plain string keys
47
+ for (const info of infos) {
48
+ if (info.keyType === KEY_TYPE.STRING) {
49
+ defineProperty(object, info, state, map);
72
50
  }
51
+ }
73
52
 
74
- // If the key is wrapped in parentheses, it is not enumerable.
75
- let enumerable = true;
76
- if (key[0] === "(" && key[key.length - 1] === ")") {
77
- key = key.slice(1, -1);
78
- enumerable = false;
53
+ // Second pass: redefine eager string-keyed properties with actual values.
54
+ for (const info of infos) {
55
+ if (
56
+ info.keyType === KEY_TYPE.STRING &&
57
+ info.valueType === VALUE_TYPE.EAGER
58
+ ) {
59
+ await redefineProperty(object, info);
79
60
  }
80
- propertyIsEnumerable[key] = enumerable;
81
-
82
- if (defineProperty) {
83
- // Define simple property
84
- Object.defineProperty(object, key, {
85
- configurable: true,
86
- enumerable,
87
- value,
88
- writable: true,
89
- });
90
- } else {
91
- // Property getter
92
- let code;
93
- if (value[0] === ops.getter) {
94
- hasLazyProperties = true;
95
- code = value[1];
96
- } else {
97
- eagerProperties.push(key);
98
- code = value;
99
- }
100
-
101
- const get = async () => {
102
- tree ??= new ObjectMap(object);
103
- const newState = Object.assign({}, state, { object: tree });
104
- const result = await evaluate(code, newState);
105
- return extname ? handleExtension(result, key, tree) : result;
106
- };
107
-
108
- Object.defineProperty(object, key, {
109
- configurable: true,
110
- enumerable,
111
- get,
112
- });
61
+ }
62
+
63
+ // Third pass: define all computed properties. These may refer to the
64
+ // properties we just defined.
65
+ for (const info of infos) {
66
+ if (info.keyType === KEY_TYPE.COMPUTED) {
67
+ const newState = Object.assign({}, state, { object: map });
68
+ const key = await evaluate(/** @type {any} */ (info.key), newState);
69
+ // Destructively update the property info with the computed key
70
+ info.key = key;
71
+ defineProperty(object, info, state, map);
113
72
  }
114
73
  }
115
74
 
116
- // Attach a keys method
75
+ // Fourth pass: redefine eager computed-keyed properties with actual values.
76
+ for (const info of infos) {
77
+ if (
78
+ info.keyType === KEY_TYPE.COMPUTED &&
79
+ info.valueType === VALUE_TYPE.EAGER
80
+ ) {
81
+ await redefineProperty(object, info);
82
+ }
83
+ }
84
+
85
+ // Attach a keys method, where keys for primitive/eager properties with
86
+ // maplike values get a trailing slash.
117
87
  Object.defineProperty(object, symbols.keys, {
118
88
  configurable: true,
119
89
  enumerable: false,
120
90
  value: () =>
121
- objectKeys(
122
- object,
123
- computedKeys,
124
- eagerProperties,
125
- propertyIsEnumerable,
126
- entries
127
- ),
91
+ infos
92
+ .filter((info) => info.enumerable)
93
+ .map((info) => normalizeKey(info, object)),
128
94
  writable: true,
129
95
  });
130
96
 
131
- // Evaluate any properties that were declared as immediate: get their value
132
- // and overwrite the property getter with the actual value.
133
- for (const key of eagerProperties) {
134
- const value = await object[key];
135
- const enumerable = Object.getOwnPropertyDescriptor(object, key)?.enumerable;
97
+ // TODO: If there are any getters, mark the object as async. Note: this code
98
+ // was added so that Tree.from() could know whether to return an ObjectMap or
99
+ // a hypothetical AsyncObjectMap, which in turn would let a map operation know
100
+ // whether to expect async property values. const hasGetters =
101
+ // infos.some((info) => info.valueType === VALUE_TYPE.GETTER); if (hasGetters)
102
+ // { Object.defineProperty(object, symbols.async, { configurable: true,
103
+ // enumerable: false, value: true, writable: true,
104
+ // });
105
+ // }
106
+
107
+ return object;
108
+ }
109
+
110
+ /**
111
+ * Define a single property on the object
112
+ */
113
+ function defineProperty(object, propertyInfo, state, map) {
114
+ let { enumerable, hasExtension, key, value, valueType } = propertyInfo;
115
+ if (valueType == VALUE_TYPE.PRIMITIVE) {
116
+ // Define simple property
136
117
  Object.defineProperty(object, key, {
137
118
  configurable: true,
138
119
  enumerable,
139
120
  value,
140
121
  writable: true,
141
122
  });
142
- }
143
-
144
- // If there are any getters, mark the object as async
145
- if (hasLazyProperties) {
146
- Object.defineProperty(object, symbols.async, {
123
+ } else {
124
+ // Eager or getter; will evaluate eager property later
125
+ Object.defineProperty(object, key, {
147
126
  configurable: true,
148
- enumerable: false,
149
- value: true,
150
- writable: true,
127
+ enumerable,
128
+ get: async () => {
129
+ const newState = Object.assign({}, state, { object: map });
130
+ const result = await evaluate(value, newState);
131
+ return hasExtension ? handleExtension(result, key, map) : result;
132
+ },
151
133
  });
152
134
  }
153
-
154
- return object;
155
135
  }
156
136
 
157
- export function entryKey(entry, object = null, eagerProperties = []) {
158
- let [key, value] = entry;
159
-
160
- if (typeof key !== "string") {
161
- // Computed property key
162
- return null;
163
- }
164
-
165
- if (key[0] === "(" && key[key.length - 1] === ")") {
166
- // Non-enumerable property, remove parentheses. This doesn't come up in the
167
- // constructor, but can happen in situations encountered by the compiler's
168
- // optimizer.
169
- key = key.slice(1, -1);
170
- }
137
+ /**
138
+ * Return a normalized version of the property key for use in the keys() method.
139
+ * Among other things, this adds trailing slashes to keys that correspond to
140
+ * maplike values.
141
+ */
142
+ export function normalizeKey(propertyInfo, object = null) {
143
+ const { key, value, valueType } = propertyInfo;
171
144
 
172
145
  if (trailingSlash.has(key)) {
173
146
  // Explicit trailing slash, return as is
174
147
  return key;
175
148
  }
176
149
 
177
- // If eager property value is maplike, add slash to the key
178
- if (eagerProperties.includes(key) && Tree.isMaplike(object?.[key])) {
150
+ // If actual property value is maplike, add slash
151
+ if (
152
+ (valueType === VALUE_TYPE.EAGER || valueType === VALUE_TYPE.PRIMITIVE) &&
153
+ Tree.isMaplike(object?.[key])
154
+ ) {
179
155
  return trailingSlash.add(key);
180
156
  }
181
157
 
158
+ // Look at value code to see if it will produce a maplike value
182
159
  if (!(value instanceof Array)) {
183
160
  // Can't be a subtree
184
161
  return trailingSlash.remove(key);
185
162
  }
186
-
187
- // If we're dealing with a getter, work with what that gets
188
- if (value[0] === ops.getter) {
189
- value = value[1];
190
- }
191
-
192
- // If entry will definitely create a subtree, add a trailing slash
193
163
  if (value[0] === ops.object) {
194
- // Subtree
164
+ // Creates an object; maplike
195
165
  return trailingSlash.add(key);
196
166
  }
197
-
198
- // See if it looks a merged object
199
167
  if (value[1] === "_result" && value[0][0] === ops.object) {
200
- // Merge
168
+ // Merges an object; maplike
201
169
  return trailingSlash.add(key);
202
170
  }
203
171
 
172
+ // Return as is
204
173
  return key;
205
174
  }
206
175
 
207
- function objectKeys(
208
- object,
209
- computedKeys,
210
- eagerProperties,
211
- propertyIsEnumerable,
212
- entries
213
- ) {
214
- // If the key is a simple string key and it's enumerable, get the friendly
215
- // version of it; if it's a computed key used that.
216
- const keys = entries.map((entry, index) =>
217
- typeof entry[0] !== "string"
218
- ? computedKeys[index]
219
- : propertyIsEnumerable[entry[0]]
220
- ? entryKey(entry, object, eagerProperties)
221
- : null
222
- );
223
- // Return the enumerable keys
224
- return keys.filter((key) => key !== null);
176
+ /**
177
+ * Given a key and the code for its value, determine some basic aspects of the
178
+ * property. This may return an updated key and/or value as well.
179
+ */
180
+ export function propertyInfo(key, value) {
181
+ // If the key is wrapped in parentheses, it is not enumerable.
182
+ let enumerable = true;
183
+ if (
184
+ typeof key === "string" &&
185
+ key[0] === "(" &&
186
+ key[key.length - 1] === ")"
187
+ ) {
188
+ key = key.slice(1, -1);
189
+ enumerable = false;
190
+ }
191
+
192
+ const keyType = key instanceof Array ? KEY_TYPE.COMPUTED : KEY_TYPE.STRING;
193
+
194
+ let valueType;
195
+ if (!(value instanceof Array)) {
196
+ // Primitive, no code to evaluate
197
+ valueType = VALUE_TYPE.PRIMITIVE;
198
+ } else if (value[0] !== ops.getter) {
199
+ // Code will be eagerly evaluated when object is constructed
200
+ valueType = VALUE_TYPE.EAGER;
201
+ } else {
202
+ // Defined as a getter
203
+ value = value[1]; // The actual code
204
+ if (!(value instanceof Array)) {
205
+ // Getter returns a primitive value; treat as regular property
206
+ valueType = VALUE_TYPE.PRIMITIVE;
207
+ } else if (value[0] === ops.literal) {
208
+ // Getter returns a literal value; treat as eager property
209
+ valueType = VALUE_TYPE.EAGER;
210
+ } else {
211
+ valueType = VALUE_TYPE.GETTER;
212
+ }
213
+ }
214
+
215
+ // Special case: a key with an extension has to be a getter
216
+ const hasExtension =
217
+ typeof key === "string" && extension.extname(key).length > 0;
218
+ if (hasExtension) {
219
+ valueType = VALUE_TYPE.GETTER;
220
+ }
221
+
222
+ return { enumerable, hasExtension, key, keyType, value, valueType };
223
+ }
224
+
225
+ /**
226
+ * Get the value of the indicated eager property and overwrite the property
227
+ * definition with the actual value.
228
+ */
229
+ async function redefineProperty(object, info) {
230
+ const value = await object[info.key];
231
+ Object.defineProperty(object, info.key, {
232
+ configurable: true,
233
+ enumerable: info.enumerable,
234
+ value,
235
+ writable: true,
236
+ });
225
237
  }
@@ -21,7 +21,6 @@ let projectGlobals;
21
21
  * @param {import("@weborigami/async-tree").SyncOrAsyncMap} [parent]
22
22
  */
23
23
  export default async function handleExtension(value, key, parent) {
24
- projectGlobals ??= await globals();
25
24
  if (isPacked(value) && isStringlike(key) && value.unpack === undefined) {
26
25
  const hasSlash = trailingSlash.has(key);
27
26
  if (hasSlash) {
@@ -34,7 +33,8 @@ export default async function handleExtension(value, key, parent) {
34
33
  : extension.extname(key);
35
34
  if (extname) {
36
35
  const handlerName = `${extname.slice(1)}_handler`;
37
- let handler = await projectGlobals[handlerName];
36
+ const handlers = await getHandlers(parent);
37
+ let handler = await handlers[handlerName];
38
38
  if (handler) {
39
39
  if (isUnpackable(handler)) {
40
40
  // The extension handler itself needs to be unpacked
@@ -66,3 +66,19 @@ export default async function handleExtension(value, key, parent) {
66
66
  }
67
67
  return value;
68
68
  }
69
+
70
+ async function getHandlers(parent) {
71
+ // Walk up the parent chain to find first `handlers` property
72
+ let current = parent;
73
+
74
+ while (current) {
75
+ if (current.handlers) {
76
+ return current.handlers;
77
+ }
78
+ current = current.parent;
79
+ }
80
+
81
+ // Fall back to project globals
82
+ projectGlobals ??= await globals();
83
+ return projectGlobals;
84
+ }
@@ -123,6 +123,18 @@ export async function construct(constructor, ...args) {
123
123
  return Reflect.construct(constructor, args);
124
124
  }
125
125
 
126
+ /**
127
+ * Default value for a parameter: if the value is defined, return that;
128
+ * otherwise, return the result of invoking the initializer.
129
+ */
130
+ export async function defaultValue(value, initializer) {
131
+ if (value !== undefined) {
132
+ return value;
133
+ }
134
+ return initializer instanceof Function ? await initializer() : initializer;
135
+ }
136
+ addOpLabel(defaultValue, "«ops.defaultValue»");
137
+
126
138
  export function division(a, b) {
127
139
  return a / b;
128
140
  }
@@ -219,7 +231,7 @@ addOpLabel(instanceOf, "«ops.instanceOf»");
219
231
  * @param {string[]} parameters
220
232
  * @param {AnnotatedCode} code
221
233
  */
222
- export function lambda(parameters, code, state = {}) {
234
+ export function lambda(length, parameters, code, state = {}) {
223
235
  const stack = state.stack ?? [];
224
236
 
225
237
  async function invoke(...args) {
@@ -228,12 +240,11 @@ export function lambda(parameters, code, state = {}) {
228
240
  // No parameters
229
241
  newState = state;
230
242
  } else {
231
- // Create a stack frame for the parameters
232
- const frame = {};
233
- for (const parameter of parameters) {
234
- const parameterName = parameter[1];
235
- frame[parameterName] = args.shift();
236
- }
243
+ // Create a stack frame for the parameters. Add the arguments as an
244
+ // interim stack frame.
245
+ const interimStack = stack.slice();
246
+ interimStack.push(args);
247
+ const frame = await expressionObject(parameters, { stack: interimStack });
237
248
  // Record which code this stack frame is associated with
238
249
  Object.defineProperty(frame, codeSymbol, {
239
250
  value: code,
@@ -255,9 +266,8 @@ export function lambda(parameters, code, state = {}) {
255
266
  // We set the `length` property on the function so that Tree.traverseOrThrow()
256
267
  // will correctly identify how many parameters it wants. This is unorthodox
257
268
  // but doesn't appear to affect other behavior.
258
- const fnLength = parameters.length;
259
269
  Object.defineProperty(invoke, "length", {
260
- value: fnLength,
270
+ value: length,
261
271
  });
262
272
 
263
273
  return invoke;
@@ -403,6 +413,17 @@ addOpLabel(object, "«ops.object»");
403
413
  object.unevaluatedArgs = true;
404
414
  object.needsState = true;
405
415
 
416
+ export async function objectRest(source, excludeKeys) {
417
+ const result = {};
418
+ for (const [key, value] of Object.entries(source)) {
419
+ if (!excludeKeys.includes(key)) {
420
+ result[key] = value; // might be a promise
421
+ }
422
+ }
423
+ return result;
424
+ }
425
+ addOpLabel(objectRest, "«ops.objectRest»");
426
+
406
427
  /**
407
428
  * Return the stack frame that's `depth` levels up the stack.
408
429
  *
@@ -100,6 +100,45 @@ describe("compile", () => {
100
100
  assert.deepEqual(await object.a.b, "Alice");
101
101
  });
102
102
 
103
+ test("lambda", async () => {
104
+ const fn = compile.expression("(name) => greet(name)", { globals });
105
+ const lambda = await fn();
106
+ const result = await lambda("Bob");
107
+ assert.equal(result, "Hello, Bob!");
108
+ });
109
+
110
+ test("lambda with default parameter", async () => {
111
+ const fn = compile.expression("(name = 'Guest') => greet(name)", {
112
+ globals,
113
+ });
114
+ const lambda = await fn();
115
+ const result1 = await lambda();
116
+ assert.equal(result1, "Hello, Guest!");
117
+ const result2 = await lambda("Bob");
118
+ assert.equal(result2, "Hello, Bob!");
119
+ });
120
+
121
+ test("lambda with rest parameter", async () => {
122
+ const fn = compile.expression("(head, ...rest) => { head, rest }", {
123
+ globals,
124
+ });
125
+ const lambda = await fn();
126
+ const result = await lambda(1, 2, 3, 4);
127
+ assert.deepEqual(result, { head: 1, rest: [2, 3, 4] });
128
+ });
129
+
130
+ test("lambda with object destructuring", async () => {
131
+ const fn = compile.expression("({ name, ...rest }) => { name, rest }", {
132
+ globals,
133
+ });
134
+ const lambda = await fn();
135
+ const result = await lambda({ name: "Bob", age: 30, city: "New York" });
136
+ assert.deepEqual(result, {
137
+ name: "Bob",
138
+ rest: { age: 30, city: "New York" },
139
+ });
140
+ });
141
+
103
142
  test("templateDocument", async () => {
104
143
  const defineTemplateFn = compile.templateDocument(
105
144
  "Documents can contain ` backticks"
@@ -22,7 +22,8 @@ describe("optimize", () => {
22
22
  }`;
23
23
  const expected = [
24
24
  ops.lambda,
25
- [[ops.literal, "name"]],
25
+ 1,
26
+ [["name", [[ops.params, 0], 0]]],
26
27
  [
27
28
  ops.object,
28
29
  [