@valbuild/core 0.12.0 → 0.13.0

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.
Files changed (71) hide show
  1. package/jest.config.js +4 -0
  2. package/package.json +1 -1
  3. package/src/Json.ts +4 -0
  4. package/src/expr/README.md +193 -0
  5. package/src/expr/eval.test.ts +202 -0
  6. package/src/expr/eval.ts +248 -0
  7. package/src/expr/expr.ts +91 -0
  8. package/src/expr/index.ts +3 -0
  9. package/src/expr/parser.test.ts +158 -0
  10. package/src/expr/parser.ts +229 -0
  11. package/src/expr/repl.ts +93 -0
  12. package/src/expr/tokenizer.test.ts +539 -0
  13. package/src/expr/tokenizer.ts +117 -0
  14. package/src/fetchVal.test.ts +164 -0
  15. package/src/fetchVal.ts +211 -0
  16. package/src/fp/array.ts +30 -0
  17. package/src/fp/index.ts +3 -0
  18. package/src/fp/result.ts +214 -0
  19. package/src/fp/util.ts +52 -0
  20. package/src/index.ts +55 -0
  21. package/src/initSchema.ts +45 -0
  22. package/src/initVal.ts +96 -0
  23. package/src/module.test.ts +170 -0
  24. package/src/module.ts +333 -0
  25. package/src/patch/deref.test.ts +300 -0
  26. package/src/patch/deref.ts +128 -0
  27. package/src/patch/index.ts +11 -0
  28. package/src/patch/json.test.ts +583 -0
  29. package/src/patch/json.ts +304 -0
  30. package/src/patch/operation.ts +74 -0
  31. package/src/patch/ops.ts +83 -0
  32. package/src/patch/parse.test.ts +202 -0
  33. package/src/patch/parse.ts +187 -0
  34. package/src/patch/patch.ts +46 -0
  35. package/src/patch/util.ts +67 -0
  36. package/src/schema/array.ts +52 -0
  37. package/src/schema/boolean.ts +38 -0
  38. package/src/schema/i18n.ts +65 -0
  39. package/src/schema/image.ts +70 -0
  40. package/src/schema/index.ts +46 -0
  41. package/src/schema/literal.ts +42 -0
  42. package/src/schema/number.ts +45 -0
  43. package/src/schema/object.ts +67 -0
  44. package/src/schema/oneOf.ts +60 -0
  45. package/src/schema/richtext.ts +417 -0
  46. package/src/schema/string.ts +49 -0
  47. package/src/schema/union.ts +62 -0
  48. package/src/selector/ExprProxy.test.ts +203 -0
  49. package/src/selector/ExprProxy.ts +209 -0
  50. package/src/selector/SelectorProxy.test.ts +172 -0
  51. package/src/selector/SelectorProxy.ts +237 -0
  52. package/src/selector/array.ts +37 -0
  53. package/src/selector/boolean.ts +4 -0
  54. package/src/selector/file.ts +14 -0
  55. package/src/selector/i18n.ts +13 -0
  56. package/src/selector/index.ts +159 -0
  57. package/src/selector/number.ts +4 -0
  58. package/src/selector/object.ts +22 -0
  59. package/src/selector/primitive.ts +17 -0
  60. package/src/selector/remote.ts +9 -0
  61. package/src/selector/selector.test.ts +453 -0
  62. package/src/selector/selectorOf.ts +7 -0
  63. package/src/selector/string.ts +4 -0
  64. package/src/source/file.ts +45 -0
  65. package/src/source/i18n.ts +60 -0
  66. package/src/source/index.ts +50 -0
  67. package/src/source/remote.ts +54 -0
  68. package/src/val/array.ts +10 -0
  69. package/src/val/index.ts +90 -0
  70. package/src/val/object.ts +13 -0
  71. package/src/val/primitive.ts +8 -0
package/jest.config.js ADDED
@@ -0,0 +1,4 @@
1
+ /** @type {import("jest").Config} */
2
+ module.exports = {
3
+ preset: "../../jest.preset",
4
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valbuild/core",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "scripts": {
5
5
  "typecheck": "tsc --noEmit",
6
6
  "test": "jest",
package/src/Json.ts ADDED
@@ -0,0 +1,4 @@
1
+ export type Json = JsonPrimitive | JsonObject | JsonArray;
2
+ export type JsonPrimitive = string | number | boolean | null;
3
+ export type JsonArray = readonly Json[];
4
+ export type JsonObject = { readonly [key in string]: Json };
@@ -0,0 +1,193 @@
1
+ # Visp
2
+
3
+ Visp (as in Val Lisp or whisk in Norwegian) is a Lisp used to serialize Val `Selector`s.
4
+
5
+ It is an INTERNAL language - it is NOT designed to be used by end-users.
6
+ This document is architectural overview for this INTERNAL language - it is documentation for developers working on Val.
7
+
8
+ Visp exists since Val clients must be able to execute remote `Selector`s.
9
+ See the docs for remote `Schema`s for more about this.
10
+
11
+ The design goals are as follows:
12
+
13
+ - evaluate to `Val` objects
14
+ - easy to parse and serialize
15
+ - easy to evaluate in JavaScript
16
+ - readable by Vals developers (for internal debugging)
17
+ - stable language semantics to avoid breaking changes as Visp is part of the (internal) API versioning
18
+ - does not support more functionality than what is required to serialize `Selector`s
19
+
20
+ The non-goals are:
21
+
22
+ - Visp does not need to be convenient to write - `Selector`s are used to write it
23
+ - Visp does not need to be easily understandable by end-users of Val
24
+
25
+ ## Syntax
26
+
27
+ Visp is a Lisp which only can evaluate one expression at a time.
28
+
29
+ Read more about how it works in sections that follows.
30
+
31
+ ### Property access
32
+
33
+ ```visp
34
+ ('title' foo)
35
+ ```
36
+
37
+ corresponds to:
38
+
39
+ ```js
40
+ foo["title"];
41
+ ```
42
+
43
+ There are no numbers in Visp, so arrays are indexed in the same way:
44
+
45
+ ```visp
46
+ ('0' foo)
47
+ ```
48
+
49
+ corresponds to:
50
+
51
+ ```js
52
+ foo["0"]; // same as foo[0]
53
+ ```
54
+
55
+ ### Function calls
56
+
57
+ Function calls are similar to property access, but with arguments separated by whitespace:
58
+
59
+ ```visp
60
+ (fnname foo arg1 arg2)
61
+ ```
62
+
63
+ corresponds to:
64
+
65
+ ```js
66
+ foo["fnname"](arg1, arg2); // same as foo.fname(arg1, arg2)
67
+ ```
68
+
69
+ #### Higher order functions
70
+
71
+ Higher order functions must be prefixed with the `!` character.
72
+ Arguments can be accessed using the `@` character. The `@` must be suffixed with indexes, e.g. `@[0,0]`, the first one corresponding to the stack depth and the second corresponds to index of the argument list.
73
+
74
+ ```visp
75
+ !(map foo @[0,0])
76
+ ```
77
+
78
+ corresponds to:
79
+
80
+ ```js
81
+ foo.map((v) => v);
82
+ ```
83
+
84
+ Here we access the second argument of a function:
85
+
86
+ ```visp
87
+ !(map foo @[0,1])
88
+ ```
89
+
90
+ corresponds to:
91
+
92
+ ```js
93
+ foo.map((_, i) => i);
94
+ ```
95
+
96
+ This example shows how higher functions and arguments can be nested:
97
+
98
+ ```visp
99
+ !(map foo !(map @[0,0] (slice @[1,0] @[0,1])))
100
+ ```
101
+
102
+ corresponds to:
103
+
104
+ ```js
105
+ foo.map((v, i) => v.map((j) => j.slice(i)));
106
+ ```
107
+
108
+ ### Literals
109
+
110
+ Visp only supports string literals.
111
+
112
+ Example:
113
+
114
+ ```visp
115
+ 'foo'
116
+ ```
117
+
118
+ corresponds to:
119
+
120
+ ```js
121
+ "foo";
122
+ ```
123
+
124
+ ### String templates
125
+
126
+ Val has support for string templates similar to JavaScript.
127
+ They are denoted using single quotes `'` (as string literal), but can inject expressions using `${}`.
128
+
129
+ Example:
130
+
131
+ ```visp
132
+ 'foo ${('title' obj)} bar'
133
+ ```
134
+
135
+ corresponds to:
136
+
137
+ ```js
138
+ `foo ${obj["title"]} bar`;
139
+ ```
140
+
141
+ ### Special symbols
142
+
143
+ ### `()`
144
+
145
+ The `()` symbol evaluates to `undefined`.
146
+
147
+ #### `@`
148
+
149
+ This symbol can be used to access arguments in [higher order functions](#higher-order-functions).
150
+
151
+ ### `!`
152
+
153
+ This is a prefix to a [higher order function](#higher-order-functions).
154
+
155
+ ### val
156
+
157
+ The `val` symbol is used to access data from a Val module.
158
+
159
+ Example:
160
+
161
+ ```visp
162
+ (val '/foo/bar`)
163
+ ```
164
+
165
+ Returns the `Source` of a Val module of id `/foo/bar`.
166
+
167
+ ### json
168
+
169
+ The `json` symbol is used to parse json strings.
170
+
171
+ Example:
172
+
173
+ ```visp
174
+ (json '{"foo": "bar"}')
175
+ ```
176
+
177
+ To create numbers, lists etc, the `json` symbol can be used.
178
+
179
+ It is also possible to use `json` with string templates:
180
+
181
+ ```visp
182
+ (json '{ "foo": ${('title' obj)} }')
183
+ ```
184
+
185
+ corresponds to:
186
+
187
+ ```js
188
+ JSON.parse(`{ "foo": ${obj["title"]} }`);
189
+ ```
190
+
191
+ ### More
192
+
193
+ More examples can be found in the [eval.test](eval.test.ts)
@@ -0,0 +1,202 @@
1
+ import { pipe, result } from "../../fp";
2
+ import { Path } from "../selector";
3
+ import { newSelectorProxy, selectorToVal } from "../selector/SelectorProxy";
4
+ import { Source } from "../source";
5
+ import { SourcePath } from "../val";
6
+ import { evaluate } from "./eval";
7
+ import { parse } from "./parser";
8
+
9
+ const sources = {
10
+ "/app/text": "text1",
11
+ "/numbers": [0, 1, 2],
12
+ "/articles": [{ title: "title1" }, { title: "title2" }],
13
+ "/app/blogs": [
14
+ { title: "blog1", text: "text1" },
15
+ { title: "blog2", text: "text2" },
16
+ ],
17
+ };
18
+
19
+ const EvalTestCases: {
20
+ expr: string;
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ expected: result.Result<{ val: Source; [Path]: any }, any>;
23
+ focus?: boolean; // use focus to specify a single test case
24
+ }[] = [
25
+ {
26
+ expr: `'hello world'`,
27
+ expected: result.ok({ val: "hello world", [Path]: undefined }),
28
+ },
29
+ {
30
+ expr: `(val '/numbers')`,
31
+ expected: result.ok({ val: [0, 1, 2], [Path]: "/numbers" }),
32
+ },
33
+ {
34
+ expr: `('hello world')`,
35
+ expected: result.ok({ val: "hello world", [Path]: undefined }),
36
+ },
37
+ {
38
+ expr: `()`,
39
+ expected: result.ok({ val: null, [Path]: undefined }),
40
+ },
41
+ {
42
+ expr: `(eq 'value' 'show me')`,
43
+ expected: result.ok({ val: false, [Path]: undefined }),
44
+ },
45
+ {
46
+ expr: `(eq 'value' 'value')`,
47
+ expected: result.ok({ val: true, [Path]: undefined }),
48
+ },
49
+ {
50
+ expr: `!(andThen 'value' 'show me')`,
51
+ expected: result.ok({ val: "show me", [Path]: undefined }),
52
+ },
53
+ {
54
+ expr: `!(andThen '' ('do NOT show me'))`,
55
+ expected: result.ok({ val: "", [Path]: undefined }),
56
+ },
57
+ {
58
+ expr: `!(andThen 'text1' @[0,0])`,
59
+ expected: result.ok({ val: "text1", [Path]: undefined }),
60
+ },
61
+ {
62
+ expr: `(json '1')`,
63
+ expected: result.ok({ val: 1, [Path]: undefined }),
64
+ },
65
+ {
66
+ expr: `(json '"1"')`,
67
+ expected: result.ok({ val: "1", [Path]: undefined }),
68
+ },
69
+ {
70
+ expr: `(json '{"foo": "bar"}')`,
71
+ expected: result.ok({ val: { foo: "bar" }, [Path]: undefined }),
72
+ },
73
+ {
74
+ expr: `(json '\${(json '1')}')`,
75
+ expected: result.ok({ val: 1, [Path]: undefined }),
76
+ },
77
+ {
78
+ expr: `(json '\${(json '"1"')}')`,
79
+ expected: result.ok({ val: "1", [Path]: undefined }),
80
+ },
81
+ {
82
+ expr: `(json '{"foo": \${(json '"1"')}}')`,
83
+ expected: result.ok({
84
+ val: {
85
+ foo: "1",
86
+ },
87
+ [Path]: undefined,
88
+ }),
89
+ },
90
+ {
91
+ expr: `(json '\${(val '/numbers')}')`,
92
+ expected: result.ok({
93
+ val: sources["/numbers"],
94
+ [Path]: "/numbers",
95
+ }),
96
+ },
97
+ {
98
+ expr: `('test' (json '{ "test": \${((json '0') (val '/numbers'))} }'))`,
99
+ expected: result.ok({
100
+ val: 0,
101
+ [Path]: "/numbers.0",
102
+ }),
103
+ },
104
+ {
105
+ expr: `((json '1') ('foo' (json '{"foo": \${(val '/numbers')}}')))`,
106
+ expected: result.ok({ val: 1, [Path]: "/numbers.1" }),
107
+ },
108
+ {
109
+ expr: `(length (val '/numbers'))`,
110
+ expected: result.ok({
111
+ val: sources["/numbers"].length,
112
+ [Path]: undefined,
113
+ }),
114
+ },
115
+ {
116
+ expr: `('0' (val '/articles'))`,
117
+ expected: result.ok({
118
+ val: sources["/articles"][0],
119
+ [Path]: "/articles.0",
120
+ }),
121
+ },
122
+ {
123
+ expr: `!(map (val '/articles') @[0,0])`,
124
+ expected: result.ok({
125
+ val: sources["/articles"].map((v) => v),
126
+ [Path]: "/articles",
127
+ }),
128
+ },
129
+ {
130
+ expr: `('0' !(map (val '/articles') ('title' @[0,0])))`,
131
+ expected: result.ok({
132
+ val: sources["/articles"].map((v) => v["title"])[0],
133
+ [Path]: '/articles.0."title"',
134
+ }),
135
+ },
136
+ {
137
+ expr: `!(map (val '/articles') ('title' @[0,0]))`,
138
+ expected: result.ok({
139
+ val: sources["/articles"].map((v) => v["title"]),
140
+ [Path]: "/articles",
141
+ }),
142
+ },
143
+ {
144
+ expr: `(eq !(andThen (val '/app/text') ()) 'foo')`,
145
+ expected: result.ok({
146
+ val: false,
147
+ [Path]: undefined,
148
+ }),
149
+ },
150
+ {
151
+ expr: `!(filter (val '/app/blogs') (eq ('title' @[0,0]) 'blog1'))`,
152
+ expected: result.ok({
153
+ val: [
154
+ {
155
+ text: "text1",
156
+ title: "blog1",
157
+ },
158
+ ],
159
+ [Path]: "/app/blogs",
160
+ }),
161
+ },
162
+ {
163
+ expr: `(json '{"title": \${()}}')`,
164
+ expected: result.ok({
165
+ val: {
166
+ title: null,
167
+ },
168
+
169
+ [Path]: undefined,
170
+ }),
171
+ },
172
+ ];
173
+
174
+ describe("eval", () => {
175
+ test.each(
176
+ EvalTestCases.filter(({ focus }) =>
177
+ EvalTestCases.some((v) => v.focus) ? focus : true
178
+ )
179
+ )('evaluate: "$expr"', ({ expr, expected }) => {
180
+ const parseRes = parse(expr);
181
+ if (result.isErr(parseRes)) {
182
+ return expect(parseRes).toHaveProperty("value");
183
+ }
184
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
+ expect(
186
+ pipe(
187
+ evaluate(
188
+ parseRes.value,
189
+ (ref) => {
190
+ return newSelectorProxy(
191
+ sources[ref as keyof typeof sources],
192
+ ref as SourcePath
193
+ );
194
+ },
195
+ []
196
+ ),
197
+ result.map((v) => selectorToVal(v))
198
+ )
199
+ ).toStrictEqual(expected);
200
+ });
201
+ //
202
+ });
@@ -0,0 +1,248 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { Call, Expr, StringLiteral, StringTemplate, Sym } from "./expr";
3
+ import { Source } from "../source";
4
+ import { result } from "../fp";
5
+ import { Path, SourceOrExpr } from "../selector";
6
+ import { newSelectorProxy } from "../selector/SelectorProxy";
7
+ import { isSerializedVal, SourcePath } from "../val";
8
+
9
+ export class EvalError {
10
+ constructor(public readonly message: string, public readonly expr: Expr) {}
11
+
12
+ toString() {
13
+ return `${this.message} in: ${this.expr.transpile()}`;
14
+ }
15
+ }
16
+
17
+ type LocalSelector<S extends Source> = {
18
+ readonly [key: string | number]:
19
+ | LocalSelector<Source>
20
+ | ((...args: any[]) => any);
21
+ } & {
22
+ [SourceOrExpr]: S;
23
+ [Path]: SourcePath | undefined;
24
+ };
25
+
26
+ const MAX_STACK_SIZE = 100; // an arbitrary semi-large number
27
+ function evaluateSync(
28
+ expr: Expr,
29
+ getSource: (ref: string) => LocalSelector<Source>,
30
+ stack: readonly LocalSelector<Source>[][]
31
+ ): LocalSelector<Source> {
32
+ // TODO: amount of evaluates should be limited?
33
+ if (stack.length > MAX_STACK_SIZE) {
34
+ throw new EvalError(
35
+ `Stack overflow. Final frames: ${stack
36
+ .slice(-10)
37
+ .map((frame, i) =>
38
+ frame.map((s, j) => `@[${i},${j}]: ${JSON.stringify(s)}`).join(", ")
39
+ )
40
+ .join(" -> ")}`,
41
+ expr
42
+ );
43
+ }
44
+ if (expr instanceof Call) {
45
+ if (expr.children[0] instanceof Sym) {
46
+ if (expr.children[0].value === "val") {
47
+ if (expr.isAnon) {
48
+ throw new EvalError("cannot call 'val' as anonymous function", expr);
49
+ }
50
+ if (expr.children[1] instanceof StringLiteral) {
51
+ return getSource(expr.children[1].value);
52
+ } else {
53
+ throw new EvalError(
54
+ "argument of 'val' must be a string literal",
55
+ expr
56
+ );
57
+ }
58
+ } else if (expr.children[0].value === "json") {
59
+ if (expr.children.length !== 2) {
60
+ throw new EvalError(
61
+ "must call 'json' with exactly one argument",
62
+ expr
63
+ );
64
+ }
65
+ const value = evaluateSync(expr.children[1], getSource, stack);
66
+
67
+ const valObj = value[SourceOrExpr];
68
+ const valPath = value[Path];
69
+ if (typeof valObj !== "string") {
70
+ throw new EvalError(
71
+ `cannot parse JSON: ${JSON.stringify(valObj)}, expected string`,
72
+ expr.children[1]
73
+ );
74
+ }
75
+ try {
76
+ const serialized = JSON.parse(valObj);
77
+ if (isSerializedVal(serialized)) {
78
+ return newSelectorProxy(serialized.val, serialized.valPath);
79
+ }
80
+ const parsedValue = newSelectorProxy(JSON.parse(valObj), valPath);
81
+ return parsedValue;
82
+ } catch (e) {
83
+ if (e instanceof SyntaxError) {
84
+ throw new EvalError(
85
+ `cannot parse JSON: ${valObj}, ${
86
+ e.message
87
+ } - value: ${JSON.stringify(value)}`,
88
+ expr.children[1]
89
+ );
90
+ }
91
+ throw e;
92
+ }
93
+ } else if (expr.children[0].value === "stringify") {
94
+ // TODO: remove stringify
95
+ if (expr.children.length !== 2) {
96
+ throw new EvalError(
97
+ "must call 'stringify' with exactly one argument",
98
+ expr
99
+ );
100
+ }
101
+ const res = evaluateSync(expr.children[1], getSource, stack);
102
+ return newSelectorProxy(JSON.stringify(res[SourceOrExpr]));
103
+ }
104
+ }
105
+ const prop = evaluateSync(expr.children[0], getSource, stack)[SourceOrExpr];
106
+ if (expr.children.length === 1) {
107
+ // TODO: return if literal only?
108
+ return newSelectorProxy(prop);
109
+ }
110
+ const obj = evaluateSync(expr.children[1], getSource, stack);
111
+ if (typeof prop !== "string" && typeof prop !== "number") {
112
+ throw new EvalError(
113
+ `cannot access ${JSON.stringify(obj)} with property ${JSON.stringify(
114
+ prop
115
+ )}: is not a string or number`,
116
+ expr
117
+ );
118
+ }
119
+
120
+ if (prop in obj) {
121
+ if (expr.isAnon) {
122
+ // anon functions:
123
+ const maybeFunction = obj[prop];
124
+ if (typeof maybeFunction !== "function") {
125
+ throw new EvalError(
126
+ `cannot access property ${JSON.stringify(prop)} of ${JSON.stringify(
127
+ obj
128
+ )}: required higher ordered function got ${typeof obj[prop]}`,
129
+ expr
130
+ );
131
+ }
132
+ if (expr.children[0] instanceof Sym) {
133
+ return maybeFunction((...args: any[]) => {
134
+ return evaluateSync(
135
+ expr.children[2],
136
+ getSource,
137
+ stack.concat([args])
138
+ );
139
+ });
140
+ } else {
141
+ throw new EvalError(
142
+ `cannot call an expression that is not a symbol, got: '${expr.children[0].type}'`,
143
+ expr
144
+ );
145
+ }
146
+ } else {
147
+ // non-anon functions:
148
+ if (expr.children[0] instanceof Sym) {
149
+ if (expr.children[0].value === "val") {
150
+ if (expr.children[1] instanceof StringLiteral) {
151
+ return getSource(expr.children[1].value);
152
+ } else {
153
+ throw new EvalError(
154
+ "argument of 'val' must be a string literal",
155
+ expr
156
+ );
157
+ }
158
+ }
159
+ }
160
+ const args = expr.children.slice(2);
161
+ if (args.length > 0) {
162
+ const maybeFunction = obj[prop];
163
+ if (typeof maybeFunction !== "function") {
164
+ throw new EvalError(
165
+ `cannot access property ${JSON.stringify(
166
+ prop
167
+ )} of ${JSON.stringify(obj)}: required function got ${typeof obj[
168
+ prop
169
+ ]}`,
170
+ expr
171
+ );
172
+ }
173
+ return maybeFunction(
174
+ ...args.map((arg) => evaluateSync(arg, getSource, stack))
175
+ );
176
+ }
177
+ const maybeValue = obj[prop];
178
+ if (typeof maybeValue === "function") {
179
+ throw new EvalError(
180
+ `cannot access property ${JSON.stringify(prop)} of ${JSON.stringify(
181
+ obj
182
+ )}: required value got ${typeof obj[prop]}`,
183
+ expr
184
+ );
185
+ }
186
+ return maybeValue;
187
+ }
188
+ }
189
+ } else if (expr instanceof Sym) {
190
+ if (expr.value.startsWith("@")) {
191
+ const [i, j, rest] = expr.value.slice(2, -1).split(",");
192
+ if (rest) {
193
+ throw new EvalError(`cannot access stack: too many indices`, expr);
194
+ }
195
+ const stackValue = stack[Number(i)]?.[Number(j)];
196
+ if (stackValue === undefined) {
197
+ throw new EvalError(`cannot access stack: out of bounds`, expr);
198
+ }
199
+ return stackValue;
200
+ } else if (expr.value === "()") {
201
+ return newSelectorProxy(null);
202
+ }
203
+ return newSelectorProxy(expr.value);
204
+ } else if (expr instanceof StringLiteral) {
205
+ return newSelectorProxy(expr.value);
206
+ } else if (expr instanceof StringTemplate) {
207
+ return newSelectorProxy(
208
+ expr.children
209
+ .map((child) => {
210
+ if (child instanceof Sym && child.value === "()") {
211
+ return "null";
212
+ }
213
+ const evalRes = evaluateSync(child, getSource, stack);
214
+ if (
215
+ child.type === "StringLiteral" ||
216
+ child.type === "StringTemplate"
217
+ ) {
218
+ return evalRes[SourceOrExpr];
219
+ }
220
+ if (Path in evalRes) {
221
+ // a selector, so serialize to Val
222
+ return JSON.stringify({
223
+ val: evalRes[SourceOrExpr],
224
+ valPath: evalRes[Path],
225
+ });
226
+ }
227
+ return JSON.stringify(evalRes[SourceOrExpr]);
228
+ })
229
+ .join("")
230
+ );
231
+ }
232
+ throw new EvalError(`could not evaluate`, expr);
233
+ }
234
+
235
+ export function evaluate(
236
+ expr: Expr,
237
+ source: (ref: string) => LocalSelector<Source>,
238
+ stack: readonly LocalSelector<Source>[][]
239
+ ): result.Result<LocalSelector<Source>, EvalError> {
240
+ try {
241
+ return result.ok(evaluateSync(expr, source, stack));
242
+ } catch (err) {
243
+ if (err instanceof EvalError) {
244
+ return result.err(err);
245
+ }
246
+ throw err;
247
+ }
248
+ }