@weborigami/language 0.2.5 → 0.2.7
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/LICENSE +1 -1
- package/package.json +3 -3
- package/src/compiler/compile.js +3 -103
- package/src/compiler/optimize.js +124 -0
- package/src/compiler/parserHelpers.js +10 -14
- package/src/runtime/evaluate.js +7 -1
- package/src/runtime/expressionObject.js +12 -4
- package/src/runtime/mergeTrees.js +2 -0
- package/src/runtime/ops.js +65 -22
- package/src/runtime/typos.js +6 -0
- package/test/compiler/compile.test.js +0 -24
- package/test/compiler/optimize.test.js +44 -0
- package/test/compiler/parse.test.js +5 -0
- package/test/runtime/ops.test.js +52 -6
- package/test/runtime/typos.test.js +1 -0
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2025 Jan Miksovsky and other contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/language",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Web Origami expression language compiler and runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"yaml": "2.6.1"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@weborigami/async-tree": "0.2.
|
|
16
|
-
"@weborigami/types": "0.2.
|
|
15
|
+
"@weborigami/async-tree": "0.2.7",
|
|
16
|
+
"@weborigami/types": "0.2.7",
|
|
17
17
|
"watcher": "2.3.1"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
package/src/compiler/compile.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { trailingSlash } from "@weborigami/async-tree";
|
|
2
1
|
import { createExpressionFunction } from "../runtime/expressionFunction.js";
|
|
3
|
-
import
|
|
2
|
+
import optimize from "./optimize.js";
|
|
4
3
|
import { parse } from "./parse.js";
|
|
5
|
-
import { annotate, undetermined } from "./parserHelpers.js";
|
|
6
4
|
|
|
7
5
|
function compile(source, options) {
|
|
8
6
|
const { macros, startRule } = options;
|
|
@@ -14,9 +12,8 @@ function compile(source, options) {
|
|
|
14
12
|
grammarSource: source,
|
|
15
13
|
startRule,
|
|
16
14
|
});
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const fn = createExpressionFunction(modified);
|
|
15
|
+
const optimized = optimize(code, enableCaching, macros);
|
|
16
|
+
const fn = createExpressionFunction(optimized);
|
|
20
17
|
return fn;
|
|
21
18
|
}
|
|
22
19
|
|
|
@@ -40,100 +37,3 @@ export function templateDocument(source, options = {}) {
|
|
|
40
37
|
startRule: "templateDocument",
|
|
41
38
|
});
|
|
42
39
|
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Transform any remaining undetermined references to scope references.
|
|
46
|
-
*
|
|
47
|
-
* At the same time, transform those or explicit ops.scope calls to ops.external
|
|
48
|
-
* calls unless they refer to local variables (variables defined by object
|
|
49
|
-
* literals or lambda parameters).
|
|
50
|
-
*
|
|
51
|
-
* Also apply any macros to the code.
|
|
52
|
-
*/
|
|
53
|
-
export function transformReferences(
|
|
54
|
-
code,
|
|
55
|
-
cache,
|
|
56
|
-
enableCaching,
|
|
57
|
-
macros,
|
|
58
|
-
locals = {}
|
|
59
|
-
) {
|
|
60
|
-
const [fn, ...args] = code;
|
|
61
|
-
|
|
62
|
-
let additionalLocalNames;
|
|
63
|
-
switch (fn) {
|
|
64
|
-
case undetermined:
|
|
65
|
-
case ops.scope:
|
|
66
|
-
const key = args[0];
|
|
67
|
-
const normalizedKey = trailingSlash.remove(key);
|
|
68
|
-
if (macros?.[normalizedKey]) {
|
|
69
|
-
// Apply macro
|
|
70
|
-
const macroBody = macros[normalizedKey];
|
|
71
|
-
const modified = transformReferences(
|
|
72
|
-
macroBody,
|
|
73
|
-
cache,
|
|
74
|
-
enableCaching,
|
|
75
|
-
macros,
|
|
76
|
-
locals
|
|
77
|
-
);
|
|
78
|
-
// @ts-ignore
|
|
79
|
-
annotate(modified, code.location);
|
|
80
|
-
return modified;
|
|
81
|
-
} else if (enableCaching && !locals[normalizedKey]) {
|
|
82
|
-
// Upgrade to cached external reference
|
|
83
|
-
const modified = [ops.external, key, cache];
|
|
84
|
-
// @ts-ignore
|
|
85
|
-
annotate(modified, code.location);
|
|
86
|
-
return modified;
|
|
87
|
-
} else if (fn === undetermined) {
|
|
88
|
-
// Transform undetermined reference to regular scope call
|
|
89
|
-
const modified = [ops.scope, key];
|
|
90
|
-
// @ts-ignore
|
|
91
|
-
annotate(modified, code.location);
|
|
92
|
-
return modified;
|
|
93
|
-
} else {
|
|
94
|
-
// Internal ops.scope call; leave as is
|
|
95
|
-
return code;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
case ops.lambda:
|
|
99
|
-
const parameters = args[0];
|
|
100
|
-
additionalLocalNames = parameters;
|
|
101
|
-
break;
|
|
102
|
-
|
|
103
|
-
case ops.object:
|
|
104
|
-
const entries = args;
|
|
105
|
-
additionalLocalNames = entries.map(([key]) => trailingSlash.remove(key));
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
let updatedLocals = { ...locals };
|
|
110
|
-
if (additionalLocalNames) {
|
|
111
|
-
for (const key of additionalLocalNames) {
|
|
112
|
-
updatedLocals[key] = true;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const modified = code.map((child) => {
|
|
117
|
-
if (Array.isArray(child)) {
|
|
118
|
-
// Review: This currently descends into arrays that are not instructions,
|
|
119
|
-
// such as the parameters of a lambda. This should be harmless, but it'd
|
|
120
|
-
// be preferable to only descend into instructions. This would require
|
|
121
|
-
// surrounding ops.lambda parameters with ops.literal, and ops.object
|
|
122
|
-
// entries with ops.array.
|
|
123
|
-
return transformReferences(
|
|
124
|
-
child,
|
|
125
|
-
cache,
|
|
126
|
-
enableCaching,
|
|
127
|
-
macros,
|
|
128
|
-
updatedLocals
|
|
129
|
-
);
|
|
130
|
-
} else {
|
|
131
|
-
return child;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (code.location) {
|
|
136
|
-
annotate(modified, code.location);
|
|
137
|
-
}
|
|
138
|
-
return modified;
|
|
139
|
-
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { pathFromKeys, trailingSlash } from "@weborigami/async-tree";
|
|
2
|
+
import { ops } from "../runtime/internal.js";
|
|
3
|
+
import { annotate, undetermined } from "./parserHelpers.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Optimize an Origami code instruction:
|
|
7
|
+
*
|
|
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.
|
|
13
|
+
*/
|
|
14
|
+
export default function optimize(
|
|
15
|
+
code,
|
|
16
|
+
enableCaching = true,
|
|
17
|
+
macros = {},
|
|
18
|
+
cache = {},
|
|
19
|
+
locals = {}
|
|
20
|
+
) {
|
|
21
|
+
// See if we can optimize this level of the code
|
|
22
|
+
const [fn, ...args] = code;
|
|
23
|
+
let additionalLocalNames;
|
|
24
|
+
switch (fn) {
|
|
25
|
+
case ops.lambda:
|
|
26
|
+
const parameters = args[0];
|
|
27
|
+
additionalLocalNames = parameters;
|
|
28
|
+
break;
|
|
29
|
+
|
|
30
|
+
case ops.object:
|
|
31
|
+
const entries = args;
|
|
32
|
+
additionalLocalNames = entries.map(([key]) => trailingSlash.remove(key));
|
|
33
|
+
break;
|
|
34
|
+
|
|
35
|
+
// Both of these are handled the same way
|
|
36
|
+
case undetermined:
|
|
37
|
+
case ops.scope:
|
|
38
|
+
const key = args[0];
|
|
39
|
+
const normalizedKey = trailingSlash.remove(key);
|
|
40
|
+
if (macros?.[normalizedKey]) {
|
|
41
|
+
// Apply macro
|
|
42
|
+
const macro = macros?.[normalizedKey];
|
|
43
|
+
return applyMacro(macro, code, enableCaching, macros, cache, locals);
|
|
44
|
+
} else if (enableCaching && !locals[normalizedKey]) {
|
|
45
|
+
// Upgrade to cached external scope reference
|
|
46
|
+
const optimized = [ops.external, key, [ops.scope, key], cache];
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
annotate(optimized, code.location);
|
|
49
|
+
return optimized;
|
|
50
|
+
} else if (fn === undetermined) {
|
|
51
|
+
// Transform undetermined reference to regular scope call
|
|
52
|
+
const optimized = [ops.scope, key];
|
|
53
|
+
// @ts-ignore
|
|
54
|
+
annotate(optimized, code.location);
|
|
55
|
+
return optimized;
|
|
56
|
+
} else {
|
|
57
|
+
// Internal ops.scope call; leave as is
|
|
58
|
+
return code;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case ops.traverse:
|
|
62
|
+
// Is the first argument a nonscope/undetermined reference?
|
|
63
|
+
const isScopeRef =
|
|
64
|
+
args[0]?.[0] === ops.scope || args[0]?.[0] === undetermined;
|
|
65
|
+
if (enableCaching && isScopeRef) {
|
|
66
|
+
// Is the first argument a nonlocal reference?
|
|
67
|
+
const normalizedKey = trailingSlash.remove(args[0][1]);
|
|
68
|
+
if (!locals[normalizedKey]) {
|
|
69
|
+
// Are the remaining arguments all literals?
|
|
70
|
+
const allLiterals = args
|
|
71
|
+
.slice(1)
|
|
72
|
+
.every((arg) => arg[0] === ops.literal);
|
|
73
|
+
if (allLiterals) {
|
|
74
|
+
// Convert to ops.external
|
|
75
|
+
const keys = args.map((arg) => arg[1]);
|
|
76
|
+
const path = pathFromKeys(keys);
|
|
77
|
+
const optimized = [ops.external, path, code, cache];
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
annotate(optimized, code.location);
|
|
80
|
+
return optimized;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add any locals introduced by this code to the list that will be consulted
|
|
88
|
+
// when we descend into child nodes.
|
|
89
|
+
let updatedLocals;
|
|
90
|
+
if (additionalLocalNames) {
|
|
91
|
+
updatedLocals = { ...locals };
|
|
92
|
+
for (const key of additionalLocalNames) {
|
|
93
|
+
updatedLocals[key] = true;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
updatedLocals = locals;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Optimize children
|
|
100
|
+
const optimized = code.map((child) => {
|
|
101
|
+
if (Array.isArray(child)) {
|
|
102
|
+
// Review: This currently descends into arrays that are not instructions,
|
|
103
|
+
// such as the parameters of a lambda. This should be harmless, but it'd
|
|
104
|
+
// be preferable to only descend into instructions. This would require
|
|
105
|
+
// surrounding ops.lambda parameters with ops.literal, and ops.object
|
|
106
|
+
// entries with ops.array.
|
|
107
|
+
return optimize(child, enableCaching, macros, cache, updatedLocals);
|
|
108
|
+
} else {
|
|
109
|
+
return child;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (code.location) {
|
|
114
|
+
annotate(optimized, code.location);
|
|
115
|
+
}
|
|
116
|
+
return optimized;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function applyMacro(macro, code, enableCaching, macros, cache, locals) {
|
|
120
|
+
const optimized = optimize(macro, enableCaching, macros, cache, locals);
|
|
121
|
+
// @ts-ignore
|
|
122
|
+
annotate(optimized, code.location);
|
|
123
|
+
return optimized;
|
|
124
|
+
}
|
|
@@ -235,12 +235,17 @@ export function makeObject(entries, op) {
|
|
|
235
235
|
|
|
236
236
|
for (let [key, value] of entries) {
|
|
237
237
|
if (key === ops.spread) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
if (value[0] === ops.object) {
|
|
239
|
+
// Spread of an object; fold into current object
|
|
240
|
+
currentEntries.push(...value.slice(1));
|
|
241
|
+
} else {
|
|
242
|
+
// Spread of a tree; accumulate
|
|
243
|
+
if (currentEntries.length > 0) {
|
|
244
|
+
spreads.push([op, ...currentEntries]);
|
|
245
|
+
currentEntries = [];
|
|
246
|
+
}
|
|
247
|
+
spreads.push(value);
|
|
242
248
|
}
|
|
243
|
-
spreads.push(value);
|
|
244
249
|
continue;
|
|
245
250
|
}
|
|
246
251
|
|
|
@@ -253,15 +258,6 @@ export function makeObject(entries, op) {
|
|
|
253
258
|
// Optimize a getter for a primitive value to a regular property
|
|
254
259
|
value = value[1];
|
|
255
260
|
}
|
|
256
|
-
// else if (
|
|
257
|
-
// value[0] === ops.object ||
|
|
258
|
-
// (value[0] === ops.getter &&
|
|
259
|
-
// value[1] instanceof Array &&
|
|
260
|
-
// (value[1][0] === ops.object || value[1][0] === ops.merge))
|
|
261
|
-
// ) {
|
|
262
|
-
// // Add a trailing slash to key to indicate value is a subtree
|
|
263
|
-
// key = trailingSlash.add(key);
|
|
264
|
-
// }
|
|
265
261
|
}
|
|
266
262
|
|
|
267
263
|
currentEntries.push([key, value]);
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -20,7 +20,13 @@ export default async function evaluate(code) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
let evaluated;
|
|
23
|
-
const unevaluatedFns = [
|
|
23
|
+
const unevaluatedFns = [
|
|
24
|
+
ops.external,
|
|
25
|
+
ops.lambda,
|
|
26
|
+
ops.merge,
|
|
27
|
+
ops.object,
|
|
28
|
+
ops.literal,
|
|
29
|
+
];
|
|
24
30
|
if (unevaluatedFns.includes(code[0])) {
|
|
25
31
|
// Don't evaluate instructions, use as is.
|
|
26
32
|
evaluated = code;
|
|
@@ -34,11 +34,13 @@ export default async function expressionObject(entries, parent) {
|
|
|
34
34
|
|
|
35
35
|
let tree;
|
|
36
36
|
const eagerProperties = [];
|
|
37
|
+
const propertyIsEnumerable = {};
|
|
37
38
|
for (let [key, value] of entries) {
|
|
38
39
|
// Determine if we need to define a getter or a regular property. If the key
|
|
39
40
|
// has an extension, we need to define a getter. If the value is code (an
|
|
40
41
|
// array), we need to define a getter -- but if that code takes the form
|
|
41
|
-
// [ops.getter, <primitive>], we can define a
|
|
42
|
+
// [ops.getter, <primitive>] or [ops.literal, <value>], we can define a
|
|
43
|
+
// regular property.
|
|
42
44
|
let defineProperty;
|
|
43
45
|
const extname = extension.extname(key);
|
|
44
46
|
if (extname) {
|
|
@@ -48,6 +50,9 @@ export default async function expressionObject(entries, parent) {
|
|
|
48
50
|
} else if (value[0] === ops.getter && !(value[1] instanceof Array)) {
|
|
49
51
|
defineProperty = true;
|
|
50
52
|
value = value[1];
|
|
53
|
+
} else if (value[0] === ops.literal) {
|
|
54
|
+
defineProperty = true;
|
|
55
|
+
value = value[1];
|
|
51
56
|
} else {
|
|
52
57
|
defineProperty = false;
|
|
53
58
|
}
|
|
@@ -58,6 +63,7 @@ export default async function expressionObject(entries, parent) {
|
|
|
58
63
|
key = key.slice(1, -1);
|
|
59
64
|
enumerable = false;
|
|
60
65
|
}
|
|
66
|
+
propertyIsEnumerable[key] = enumerable;
|
|
61
67
|
|
|
62
68
|
if (defineProperty) {
|
|
63
69
|
// Define simple property
|
|
@@ -105,7 +111,7 @@ export default async function expressionObject(entries, parent) {
|
|
|
105
111
|
Object.defineProperty(object, symbols.keys, {
|
|
106
112
|
configurable: true,
|
|
107
113
|
enumerable: false,
|
|
108
|
-
value: () => keys(object, eagerProperties, entries),
|
|
114
|
+
value: () => keys(object, eagerProperties, propertyIsEnumerable, entries),
|
|
109
115
|
writable: true,
|
|
110
116
|
});
|
|
111
117
|
|
|
@@ -158,6 +164,8 @@ function entryKey(object, eagerProperties, entry) {
|
|
|
158
164
|
return trailingSlash.toggle(key, entryCreatesSubtree);
|
|
159
165
|
}
|
|
160
166
|
|
|
161
|
-
function keys(object, eagerProperties, entries) {
|
|
162
|
-
return entries
|
|
167
|
+
function keys(object, eagerProperties, propertyIsEnumerable, entries) {
|
|
168
|
+
return entries
|
|
169
|
+
.filter(([key]) => propertyIsEnumerable[key])
|
|
170
|
+
.map((entry) => entryKey(object, eagerProperties, entry));
|
|
163
171
|
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
isPlainObject,
|
|
3
3
|
isUnpackable,
|
|
4
4
|
merge,
|
|
5
|
+
setParent,
|
|
5
6
|
Tree,
|
|
6
7
|
} from "@weborigami/async-tree";
|
|
7
8
|
|
|
@@ -59,5 +60,6 @@ export default async function mergeTrees(...trees) {
|
|
|
59
60
|
|
|
60
61
|
// Merge the trees.
|
|
61
62
|
const result = merge(...unpacked);
|
|
63
|
+
setParent(result, this);
|
|
62
64
|
return result;
|
|
63
65
|
}
|
package/src/runtime/ops.js
CHANGED
|
@@ -33,6 +33,17 @@ export function addition(a, b) {
|
|
|
33
33
|
}
|
|
34
34
|
addOpLabel(addition, "«ops.addition»");
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Construct an array.
|
|
38
|
+
*
|
|
39
|
+
* @this {AsyncTree|null}
|
|
40
|
+
* @param {any[]} items
|
|
41
|
+
*/
|
|
42
|
+
export async function array(...items) {
|
|
43
|
+
return items;
|
|
44
|
+
}
|
|
45
|
+
addOpLabel(array, "«ops.array»");
|
|
46
|
+
|
|
36
47
|
export function bitwiseAnd(a, b) {
|
|
37
48
|
return a & b;
|
|
38
49
|
}
|
|
@@ -53,17 +64,6 @@ export function bitwiseXor(a, b) {
|
|
|
53
64
|
}
|
|
54
65
|
addOpLabel(bitwiseXor, "«ops.bitwiseXor»");
|
|
55
66
|
|
|
56
|
-
/**
|
|
57
|
-
* Construct an array.
|
|
58
|
-
*
|
|
59
|
-
* @this {AsyncTree|null}
|
|
60
|
-
* @param {any[]} items
|
|
61
|
-
*/
|
|
62
|
-
export async function array(...items) {
|
|
63
|
-
return items;
|
|
64
|
-
}
|
|
65
|
-
addOpLabel(array, "«ops.array»");
|
|
66
|
-
|
|
67
67
|
/**
|
|
68
68
|
* Like ops.scope, but only searches for a builtin at the top of the scope
|
|
69
69
|
* chain.
|
|
@@ -131,16 +131,27 @@ addOpLabel(exponentiation, "«ops.exponentiation»");
|
|
|
131
131
|
*
|
|
132
132
|
* @this {AsyncTree|null}
|
|
133
133
|
*/
|
|
134
|
-
export async function external(
|
|
135
|
-
if (
|
|
136
|
-
|
|
134
|
+
export async function external(path, code, cache) {
|
|
135
|
+
if (!this) {
|
|
136
|
+
throw new Error("Tried to get the scope of a null or undefined tree.");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (path in cache) {
|
|
140
|
+
// Cache hit
|
|
141
|
+
return cache[path];
|
|
137
142
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
|
|
144
|
+
// Don't await: might get another request for this before promise resolves
|
|
145
|
+
const promise = evaluate.call(this, code);
|
|
146
|
+
// Save promise so another request will get the same promise
|
|
147
|
+
cache[path] = promise;
|
|
148
|
+
|
|
149
|
+
// Now wait for the value
|
|
141
150
|
const value = await promise;
|
|
142
|
-
|
|
143
|
-
cache
|
|
151
|
+
|
|
152
|
+
// Update the cache with the actual value
|
|
153
|
+
cache[path] = value;
|
|
154
|
+
|
|
144
155
|
return value;
|
|
145
156
|
}
|
|
146
157
|
|
|
@@ -317,9 +328,41 @@ export async function logicalOr(head, ...tail) {
|
|
|
317
328
|
* Merge the given trees. If they are all plain objects, return a plain object.
|
|
318
329
|
*
|
|
319
330
|
* @this {AsyncTree|null}
|
|
320
|
-
* @param {
|
|
331
|
+
* @param {Code[]} codes
|
|
321
332
|
*/
|
|
322
|
-
export async function merge(...
|
|
333
|
+
export async function merge(...codes) {
|
|
334
|
+
// First pass: evaluate the direct property entries in a single object
|
|
335
|
+
let treeSpreads = false;
|
|
336
|
+
const directEntries = [];
|
|
337
|
+
for (const code of codes) {
|
|
338
|
+
if (code[0] === object) {
|
|
339
|
+
directEntries.push(...code.slice(1));
|
|
340
|
+
} else {
|
|
341
|
+
treeSpreads = true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const directObject = directEntries
|
|
346
|
+
? await expressionObject(directEntries, this)
|
|
347
|
+
: null;
|
|
348
|
+
if (!treeSpreads) {
|
|
349
|
+
// No tree spreads, we're done
|
|
350
|
+
return directObject;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Second pass: evaluate the trees with the direct properties object in scope
|
|
354
|
+
let context;
|
|
355
|
+
if (directObject) {
|
|
356
|
+
context = Tree.from(directObject);
|
|
357
|
+
context.parent = this;
|
|
358
|
+
} else {
|
|
359
|
+
context = this;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const trees = await Promise.all(
|
|
363
|
+
codes.map(async (code) => evaluate.call(context, code))
|
|
364
|
+
);
|
|
365
|
+
|
|
323
366
|
return mergeTrees.call(this, ...trees);
|
|
324
367
|
}
|
|
325
368
|
addOpLabel(merge, "«ops.merge»");
|
|
@@ -427,7 +470,7 @@ addOpLabel(shiftRightUnsigned, "«ops.shiftRightUnsigned»");
|
|
|
427
470
|
* The spread operator is a placeholder during parsing. It should be replaced
|
|
428
471
|
* with an object merge.
|
|
429
472
|
*/
|
|
430
|
-
export function spread(
|
|
473
|
+
export function spread(arg) {
|
|
431
474
|
throw new Error(
|
|
432
475
|
"Internal error: a spread operation wasn't compiled correctly."
|
|
433
476
|
);
|
package/src/runtime/typos.js
CHANGED
|
@@ -15,6 +15,12 @@ export function isTypo(s1, s2) {
|
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// If strings are both a single character, we don't want to consider them
|
|
19
|
+
// typos.
|
|
20
|
+
if (length1 === 1 && length2 === 1) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
// If length difference is more than 1, distance can't be 1
|
|
19
25
|
if (Math.abs(length1 - length2) > 1) {
|
|
20
26
|
return false;
|
|
@@ -87,30 +87,6 @@ describe("compile", () => {
|
|
|
87
87
|
assert.equal(bob, "Hello, Bob!");
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
test("converts non-local ops.scope calls to ops.external", async () => {
|
|
91
|
-
const expression = `
|
|
92
|
-
(name) => {
|
|
93
|
-
a: 1
|
|
94
|
-
b: a // local, should be left as ops.scope
|
|
95
|
-
c: nonLocal // non-local, should be converted to ops.cache
|
|
96
|
-
d: name // local, should be left as ops.scope
|
|
97
|
-
}
|
|
98
|
-
`;
|
|
99
|
-
const fn = compile.expression(expression);
|
|
100
|
-
const code = fn.code;
|
|
101
|
-
assert.deepEqual(stripCodeLocations(code), [
|
|
102
|
-
ops.lambda,
|
|
103
|
-
["name"],
|
|
104
|
-
[
|
|
105
|
-
ops.object,
|
|
106
|
-
["a", [ops.literal, 1]],
|
|
107
|
-
["b", [ops.scope, "a"]],
|
|
108
|
-
["c", [ops.external, "nonLocal", {}]],
|
|
109
|
-
["d", [ops.scope, "name"]],
|
|
110
|
-
],
|
|
111
|
-
]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
90
|
test("can apply a macro", async () => {
|
|
115
91
|
const literal = [ops.literal, 1];
|
|
116
92
|
const expression = `{ a: literal }`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import * as compile from "../../src/compiler/compile.js";
|
|
4
|
+
import optimize from "../../src/compiler/optimize.js";
|
|
5
|
+
import { ops } from "../../src/runtime/internal.js";
|
|
6
|
+
import { stripCodeLocations } from "./stripCodeLocations.js";
|
|
7
|
+
|
|
8
|
+
describe("optimize", () => {
|
|
9
|
+
test("optimize non-local ops.scope calls to ops.external", async () => {
|
|
10
|
+
const expression = `
|
|
11
|
+
(name) => {
|
|
12
|
+
a: 1
|
|
13
|
+
b: a // local, should be left as ops.scope
|
|
14
|
+
c: elsewhere // external, should be converted to ops.external
|
|
15
|
+
d: name // local, should be left as ops.scope
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
const fn = compile.expression(expression);
|
|
19
|
+
const code = fn.code;
|
|
20
|
+
assert.deepEqual(stripCodeLocations(code), [
|
|
21
|
+
ops.lambda,
|
|
22
|
+
["name"],
|
|
23
|
+
[
|
|
24
|
+
ops.object,
|
|
25
|
+
["a", [ops.literal, 1]],
|
|
26
|
+
["b", [ops.scope, "a"]],
|
|
27
|
+
["c", [ops.external, "elsewhere", [ops.scope, "elsewhere"], {}]],
|
|
28
|
+
["d", [ops.scope, "name"]],
|
|
29
|
+
],
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("optimize scope traversals with all literal keys", async () => {
|
|
34
|
+
// Compilation of `x/y.js`
|
|
35
|
+
const code = [ops.traverse, [ops.scope, "x/"], [ops.literal, "y.js"]];
|
|
36
|
+
const optimized = optimize(code);
|
|
37
|
+
assert.deepEqual(stripCodeLocations(optimized), [
|
|
38
|
+
ops.external,
|
|
39
|
+
"x/y.js",
|
|
40
|
+
code,
|
|
41
|
+
{},
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -701,6 +701,11 @@ describe("Origami parser", () => {
|
|
|
701
701
|
[ops.object, ["a", [ops.literal, 1]]],
|
|
702
702
|
[ops.scope, "b"],
|
|
703
703
|
]);
|
|
704
|
+
assertParse("objectLiteral", "{ a: 1, ...{ b: 2 } }", [
|
|
705
|
+
ops.object,
|
|
706
|
+
["a", [ops.literal, 1]],
|
|
707
|
+
["b", [ops.literal, 2]],
|
|
708
|
+
]);
|
|
704
709
|
assertParse("objectLiteral", "{ (a): 1 }", [
|
|
705
710
|
ops.object,
|
|
706
711
|
["(a)", [ops.literal, 1]],
|
package/test/runtime/ops.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ObjectTree } from "@weborigami/async-tree";
|
|
1
|
+
import { DeepObjectTree, ObjectTree } from "@weborigami/async-tree";
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import { describe, test } from "node:test";
|
|
4
4
|
|
|
@@ -99,14 +99,21 @@ describe("ops", () => {
|
|
|
99
99
|
assert.strictEqual(ops.exponentiation(2, 0), 1);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
test("ops.external
|
|
102
|
+
test("ops.external evaluates code and cache its result", async () => {
|
|
103
103
|
let count = 0;
|
|
104
|
-
const tree = new
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const tree = new DeepObjectTree({
|
|
105
|
+
group: {
|
|
106
|
+
get count() {
|
|
107
|
+
return ++count;
|
|
108
|
+
},
|
|
107
109
|
},
|
|
108
110
|
});
|
|
109
|
-
const code = createCode([
|
|
111
|
+
const code = createCode([
|
|
112
|
+
ops.external,
|
|
113
|
+
"group/count",
|
|
114
|
+
[ops.traverse, [ops.scope, "group"], [ops.literal, "count"]],
|
|
115
|
+
{},
|
|
116
|
+
]);
|
|
110
117
|
const result = await evaluate.call(tree, code);
|
|
111
118
|
assert.strictEqual(result, 1);
|
|
112
119
|
const result2 = await evaluate.call(tree, code);
|
|
@@ -213,6 +220,45 @@ describe("ops", () => {
|
|
|
213
220
|
assert.strictEqual(await ops.logicalOr(true, errorFn), true);
|
|
214
221
|
});
|
|
215
222
|
|
|
223
|
+
test("ops.merge", async () => {
|
|
224
|
+
// {
|
|
225
|
+
// a: 1
|
|
226
|
+
// …fn(a)
|
|
227
|
+
// }
|
|
228
|
+
const scope = new ObjectTree({
|
|
229
|
+
fn: (a) => ({ b: 2 * a }),
|
|
230
|
+
});
|
|
231
|
+
const code = createCode([
|
|
232
|
+
ops.merge,
|
|
233
|
+
[ops.object, ["a", [ops.literal, 1]]],
|
|
234
|
+
[
|
|
235
|
+
[ops.builtin, "fn"],
|
|
236
|
+
[ops.scope, "a"],
|
|
237
|
+
],
|
|
238
|
+
]);
|
|
239
|
+
const result = await evaluate.call(scope, code);
|
|
240
|
+
assert.deepEqual(result, { a: 1, b: 2 });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("ops.merge lets all direct properties see each other", async () => {
|
|
244
|
+
// {
|
|
245
|
+
// a: 1
|
|
246
|
+
// ...more
|
|
247
|
+
// c: a
|
|
248
|
+
// }
|
|
249
|
+
const scope = new ObjectTree({
|
|
250
|
+
more: { b: 2 },
|
|
251
|
+
});
|
|
252
|
+
const code = createCode([
|
|
253
|
+
ops.merge,
|
|
254
|
+
[ops.object, ["a", [ops.literal, 1]]],
|
|
255
|
+
[ops.scope, "more"],
|
|
256
|
+
[ops.object, ["c", [ops.scope, "a"]]],
|
|
257
|
+
]);
|
|
258
|
+
const result = await evaluate.call(scope, code);
|
|
259
|
+
assert.deepEqual(result, { a: 1, b: 2, c: 1 });
|
|
260
|
+
});
|
|
261
|
+
|
|
216
262
|
test("ops.multiplication multiplies two numbers", async () => {
|
|
217
263
|
assert.strictEqual(ops.multiplication(3, 4), 12);
|
|
218
264
|
assert.strictEqual(ops.multiplication(-3, 4), -12);
|
|
@@ -12,6 +12,7 @@ describe("typos", () => {
|
|
|
12
12
|
assert(isTypo("cat", "cta")); // transposition
|
|
13
13
|
assert(isTypo("cat", "act")); // transposition
|
|
14
14
|
assert(!isTypo("cat", "dog")); // more than 1 edit
|
|
15
|
+
assert(!isTypo("a", "b")); // single character
|
|
15
16
|
});
|
|
16
17
|
|
|
17
18
|
test("typos", () => {
|