@weborigami/language 0.6.2 → 0.6.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 +2 -2
- package/src/compiler/optimize.js +16 -11
- package/src/compiler/origami.pegjs +54 -29
- package/src/compiler/parse.js +440 -255
- package/src/compiler/parserHelpers.js +116 -29
- package/src/project/jsGlobals.js +2 -2
- package/src/runtime/expressionObject.js +42 -6
- package/src/runtime/ops.js +14 -18
- package/test/compiler/compile.test.js +14 -0
- package/test/compiler/parse.test.js +202 -65
- package/test/runtime/expressionObject.test.js +11 -0
- package/test/runtime/ops.test.js +14 -4
|
@@ -20,6 +20,7 @@ export const markers = {
|
|
|
20
20
|
external: Symbol("external"), // External reference
|
|
21
21
|
property: Symbol("property"), // Property access
|
|
22
22
|
reference: Symbol("reference"), // Reference to local, scope, or global
|
|
23
|
+
spread: Symbol("spread"), // Spread operator
|
|
23
24
|
traverse: Symbol("traverse"), // Path traversal
|
|
24
25
|
};
|
|
25
26
|
|
|
@@ -79,7 +80,7 @@ export function makeArray(entries, location) {
|
|
|
79
80
|
const spreads = [];
|
|
80
81
|
|
|
81
82
|
for (const value of entries) {
|
|
82
|
-
if (Array.isArray(value) && value[0] ===
|
|
83
|
+
if (Array.isArray(value) && value[0] === markers.spread) {
|
|
83
84
|
if (currentEntries.length > 0) {
|
|
84
85
|
const location = { ...currentEntries[0].location };
|
|
85
86
|
location.end = currentEntries[currentEntries.length - 1].location.end;
|
|
@@ -95,22 +96,20 @@ export function makeArray(entries, location) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
if (spreads.length === 0) {
|
|
100
|
+
// No spreads, simple array
|
|
101
|
+
return annotate([ops.array, ...currentEntries], location);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Finish any current entries, add to spreads
|
|
99
105
|
if (currentEntries.length > 0) {
|
|
100
106
|
spreads.push([ops.array, ...currentEntries]);
|
|
101
107
|
currentEntries = [];
|
|
102
108
|
}
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} else if (spreads.length === 1) {
|
|
108
|
-
result = spreads[0];
|
|
109
|
-
} else {
|
|
110
|
-
result = [ops.array];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return annotate(result, location);
|
|
110
|
+
// We don't optimize for the single-spread case here because the object
|
|
111
|
+
// being spread might be a tree and we want ops.flat to handle that.
|
|
112
|
+
return annotate([ops.flat, ...spreads], location);
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
/**
|
|
@@ -171,27 +170,60 @@ export function makeCall(target, args, location) {
|
|
|
171
170
|
|
|
172
171
|
let fnCall;
|
|
173
172
|
const op = args[0];
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
173
|
+
switch (op) {
|
|
174
|
+
case markers.property:
|
|
175
|
+
// Property access
|
|
176
|
+
const property = args[1];
|
|
177
|
+
fnCall = [ops.property, target, property];
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case ops.templateText:
|
|
181
|
+
// Tagged template
|
|
182
|
+
const strings = args[1];
|
|
183
|
+
const values = args.slice(2);
|
|
184
|
+
fnCall = makeTaggedTemplateCall(target, strings, ...values);
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case markers.traverse:
|
|
188
|
+
// Traverse
|
|
189
|
+
const keys = args.slice(1);
|
|
190
|
+
fnCall = [target, ...keys];
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
default:
|
|
194
|
+
// Function call with explicit or implicit parentheses
|
|
195
|
+
fnCall = makePossibleSpreadCall(target, args, location);
|
|
196
|
+
break;
|
|
190
197
|
}
|
|
191
198
|
|
|
192
199
|
return annotate(fnCall, location);
|
|
193
200
|
}
|
|
194
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Create a chain of function calls, property accesses, or traversals.
|
|
204
|
+
*
|
|
205
|
+
* @param {AnnotatedCode} target
|
|
206
|
+
* @param {AnnotatedCode[]} chain
|
|
207
|
+
* @param {CodeLocation} location
|
|
208
|
+
*/
|
|
209
|
+
export function makeCallChain(target, chain, location) {
|
|
210
|
+
let result = target;
|
|
211
|
+
let args = chain.shift();
|
|
212
|
+
while (args) {
|
|
213
|
+
const op = args[0];
|
|
214
|
+
if (op === ops.optional) {
|
|
215
|
+
// Optional chaining short-circuits the rest of the call chain
|
|
216
|
+
const optionalChain = [args[1], ...chain];
|
|
217
|
+
return makeOptionalCall(result, optionalChain, location);
|
|
218
|
+
} else {
|
|
219
|
+
// Extend normal call chain
|
|
220
|
+
result = makeCall(result, args, location);
|
|
221
|
+
}
|
|
222
|
+
args = chain.shift();
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
195
227
|
/**
|
|
196
228
|
* For functions that short-circuit arguments, we need to defer evaluation of
|
|
197
229
|
* the arguments until the function is called. Exception: if the argument is a
|
|
@@ -301,7 +333,7 @@ export function makeObject(entries, location) {
|
|
|
301
333
|
|
|
302
334
|
for (let entry of entries) {
|
|
303
335
|
const [key, value] = entry;
|
|
304
|
-
if (key ===
|
|
336
|
+
if (key === markers.spread) {
|
|
305
337
|
if (value[0] === ops.object) {
|
|
306
338
|
// Spread of an object; fold into current object
|
|
307
339
|
currentEntries.push(...value.slice(1));
|
|
@@ -361,6 +393,39 @@ export function makeObject(entries, location) {
|
|
|
361
393
|
return annotate(code, location);
|
|
362
394
|
}
|
|
363
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Make an optional call: if the target is null or undefined, return undefined;
|
|
398
|
+
* otherwise, make the call.
|
|
399
|
+
*
|
|
400
|
+
* @param {AnnotatedCode} target
|
|
401
|
+
* @param {AnnotatedCode[]} chain
|
|
402
|
+
* @param {CodeLocation} location
|
|
403
|
+
*/
|
|
404
|
+
function makeOptionalCall(target, chain, location) {
|
|
405
|
+
const optionalKey = "__optional__";
|
|
406
|
+
// Create a reference to the __optional__ parameter
|
|
407
|
+
const optionalReference = annotate(
|
|
408
|
+
[markers.reference, optionalKey],
|
|
409
|
+
location
|
|
410
|
+
);
|
|
411
|
+
const optionalTraverse = annotate(
|
|
412
|
+
[markers.traverse, optionalReference],
|
|
413
|
+
location
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Create the call to be made if the target is not null/undefined
|
|
417
|
+
const call = makeCallChain(optionalTraverse, chain, location);
|
|
418
|
+
|
|
419
|
+
// Create a function that takes __optional__ and makes the call
|
|
420
|
+
const optionalLiteral = annotate([ops.literal, optionalKey], location);
|
|
421
|
+
const lambdaParameters = annotate([optionalLiteral], location);
|
|
422
|
+
const lambda = annotate([ops.lambda, lambdaParameters, call], location);
|
|
423
|
+
|
|
424
|
+
// Create the call to ops.optional
|
|
425
|
+
const optionalCall = annotate([ops.optional, target, lambda], location);
|
|
426
|
+
return optionalCall;
|
|
427
|
+
}
|
|
428
|
+
|
|
364
429
|
/**
|
|
365
430
|
* Handle a path with one or more segments separated by slashes.
|
|
366
431
|
*
|
|
@@ -398,6 +463,28 @@ export function makePipeline(arg, fn, location) {
|
|
|
398
463
|
return annotate(result, { start, source, end });
|
|
399
464
|
}
|
|
400
465
|
|
|
466
|
+
function makePossibleSpreadCall(target, args, location) {
|
|
467
|
+
const hasSpread = args.some(
|
|
468
|
+
(arg) => Array.isArray(arg) && arg[0] === markers.spread
|
|
469
|
+
);
|
|
470
|
+
if (!hasSpread) {
|
|
471
|
+
// No spreads, simple call
|
|
472
|
+
return [target, ...args];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Get function's apply method
|
|
476
|
+
const applyMethod = annotate([ops.property, target, "apply"], location);
|
|
477
|
+
const wrappedArgs = args.map((arg) => {
|
|
478
|
+
if (arg[0] === markers.spread) {
|
|
479
|
+
return arg[1];
|
|
480
|
+
} else {
|
|
481
|
+
return annotate([ops.array, arg], arg.location);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
const flatCall = annotate([ops.flat, ...wrappedArgs], location);
|
|
485
|
+
return [applyMethod, null, flatCall];
|
|
486
|
+
}
|
|
487
|
+
|
|
401
488
|
/**
|
|
402
489
|
* Make a tagged template call
|
|
403
490
|
*
|
package/src/project/jsGlobals.js
CHANGED
|
@@ -173,7 +173,7 @@ async function fetchWrapper(resource, options) {
|
|
|
173
173
|
*
|
|
174
174
|
* @this {AsyncMap|null|undefined}
|
|
175
175
|
*/
|
|
176
|
-
async function importWrapper(modulePath) {
|
|
176
|
+
async function importWrapper(modulePath, options = {}) {
|
|
177
177
|
// Walk up parent tree looking for a FileTree or other object with a `path`
|
|
178
178
|
/** @type {any} */
|
|
179
179
|
let current = this;
|
|
@@ -186,7 +186,7 @@ async function importWrapper(modulePath) {
|
|
|
186
186
|
);
|
|
187
187
|
}
|
|
188
188
|
const filePath = path.resolve(current.path, modulePath);
|
|
189
|
-
return import(filePath);
|
|
189
|
+
return import(filePath, options);
|
|
190
190
|
}
|
|
191
191
|
importWrapper.containerAsTarget = true;
|
|
192
192
|
|
|
@@ -35,11 +35,21 @@ export default async function expressionObject(entries, state = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
setParent(object, parent);
|
|
37
37
|
|
|
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
|
+
|
|
38
45
|
let tree;
|
|
39
46
|
const eagerProperties = [];
|
|
40
47
|
const propertyIsEnumerable = {};
|
|
41
48
|
let hasLazyProperties = false;
|
|
42
|
-
for (let
|
|
49
|
+
for (let i = 0; i < entries.length; i++) {
|
|
50
|
+
let key = computedKeys[i];
|
|
51
|
+
let value = entries[i][1];
|
|
52
|
+
|
|
43
53
|
// Determine if we need to define a getter or a regular property. If the key
|
|
44
54
|
// has an extension, we need to define a getter. If the value is code (an
|
|
45
55
|
// array), we need to define a getter -- but if that code takes the form
|
|
@@ -107,7 +117,14 @@ export default async function expressionObject(entries, state = {}) {
|
|
|
107
117
|
Object.defineProperty(object, symbols.keys, {
|
|
108
118
|
configurable: true,
|
|
109
119
|
enumerable: false,
|
|
110
|
-
value: () =>
|
|
120
|
+
value: () =>
|
|
121
|
+
objectKeys(
|
|
122
|
+
object,
|
|
123
|
+
computedKeys,
|
|
124
|
+
eagerProperties,
|
|
125
|
+
propertyIsEnumerable,
|
|
126
|
+
entries
|
|
127
|
+
),
|
|
111
128
|
writable: true,
|
|
112
129
|
});
|
|
113
130
|
|
|
@@ -140,6 +157,11 @@ export default async function expressionObject(entries, state = {}) {
|
|
|
140
157
|
export function entryKey(entry, object = null, eagerProperties = []) {
|
|
141
158
|
let [key, value] = entry;
|
|
142
159
|
|
|
160
|
+
if (typeof key !== "string") {
|
|
161
|
+
// Computed property key
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
143
165
|
if (key[0] === "(" && key[key.length - 1] === ")") {
|
|
144
166
|
// Non-enumerable property, remove parentheses. This doesn't come up in the
|
|
145
167
|
// constructor, but can happen in situations encountered by the compiler's
|
|
@@ -182,8 +204,22 @@ export function entryKey(entry, object = null, eagerProperties = []) {
|
|
|
182
204
|
return key;
|
|
183
205
|
}
|
|
184
206
|
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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);
|
|
189
225
|
}
|
package/src/runtime/ops.js
CHANGED
|
@@ -416,13 +416,20 @@ export async function params(depth, state = {}) {
|
|
|
416
416
|
addOpLabel(params, "«ops.params»");
|
|
417
417
|
params.needsState = true;
|
|
418
418
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
419
|
+
/**
|
|
420
|
+
* If the value is null or undefined, return undefined; otherwise, invoke the
|
|
421
|
+
* given function with the value.
|
|
422
|
+
*
|
|
423
|
+
* @param {any} value
|
|
424
|
+
* @param {Function} fn
|
|
425
|
+
*/
|
|
426
|
+
export function optional(value, fn) {
|
|
427
|
+
if (value == null) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
return fn(value);
|
|
431
|
+
}
|
|
432
|
+
addOpLabel(optional, "«ops.optional»");
|
|
426
433
|
|
|
427
434
|
/**
|
|
428
435
|
* Return the indicated property
|
|
@@ -505,17 +512,6 @@ export function shiftRightUnsigned(a, b) {
|
|
|
505
512
|
}
|
|
506
513
|
addOpLabel(shiftRightUnsigned, "«ops.shiftRightUnsigned»");
|
|
507
514
|
|
|
508
|
-
/**
|
|
509
|
-
* The spread operator is a placeholder during parsing. It should be replaced
|
|
510
|
-
* with an object merge.
|
|
511
|
-
*/
|
|
512
|
-
export function spread(arg) {
|
|
513
|
-
throw new Error(
|
|
514
|
-
"Internal error: a spread operation wasn't compiled correctly."
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
addOpLabel(spread, "«ops.spread»");
|
|
518
|
-
|
|
519
515
|
export function strictEqual(a, b) {
|
|
520
516
|
return a === b;
|
|
521
517
|
}
|
|
@@ -5,6 +5,7 @@ import * as compile from "../../src/compiler/compile.js";
|
|
|
5
5
|
import { assertCodeEqual } from "./codeHelpers.js";
|
|
6
6
|
|
|
7
7
|
const globals = {
|
|
8
|
+
concat: (...args) => args.join(""),
|
|
8
9
|
greet: (name) => `Hello, ${name}!`,
|
|
9
10
|
name: "Alice",
|
|
10
11
|
};
|
|
@@ -22,6 +23,13 @@ describe("compile", () => {
|
|
|
22
23
|
await assertCompile("greet 'world'", "Hello, world!", { mode: "shell" });
|
|
23
24
|
});
|
|
24
25
|
|
|
26
|
+
test("function call with spread", async () => {
|
|
27
|
+
await assertCompile(
|
|
28
|
+
`concat("Hello", ...[", ", name], "!")`,
|
|
29
|
+
"Hello, Alice!"
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
25
33
|
test("angle bracket path", async () => {
|
|
26
34
|
await assertCompile("<data>", "Bob", {
|
|
27
35
|
target: {
|
|
@@ -43,6 +51,12 @@ describe("compile", () => {
|
|
|
43
51
|
);
|
|
44
52
|
});
|
|
45
53
|
|
|
54
|
+
test("object with computed property key", async () => {
|
|
55
|
+
await assertCompile("{ [name] = greet(name) }", {
|
|
56
|
+
Alice: "Hello, Alice!",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
46
60
|
test("merge", async () => {
|
|
47
61
|
{
|
|
48
62
|
const globals = {
|