@weborigami/language 0.2.5 → 0.2.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.
- 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/runtime/evaluate.js +1 -1
- package/src/runtime/ops.js +19 -8
- package/test/compiler/compile.test.js +0 -24
- package/test/compiler/optimize.test.js +44 -0
- package/test/runtime/ops.test.js +13 -6
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.6",
|
|
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.6",
|
|
16
|
+
"@weborigami/types": "0.2.6",
|
|
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
|
+
}
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -20,7 +20,7 @@ export default async function evaluate(code) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
let evaluated;
|
|
23
|
-
const unevaluatedFns = [ops.lambda, ops.object, ops.literal];
|
|
23
|
+
const unevaluatedFns = [ops.external, ops.lambda, ops.object, ops.literal];
|
|
24
24
|
if (unevaluatedFns.includes(code[0])) {
|
|
25
25
|
// Don't evaluate instructions, use as is.
|
|
26
26
|
evaluated = code;
|
package/src/runtime/ops.js
CHANGED
|
@@ -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
|
|
|
@@ -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
|
+
});
|
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);
|