@shapeshift-labs/frontier-lang-parser 0.3.45 → 0.3.46
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/dist/action-body.js +11 -8
- package/dist/action-expression.js +57 -7
- package/dist/action-syntax-children.js +13 -5
- package/package.json +1 -1
package/dist/action-body.js
CHANGED
|
@@ -78,9 +78,10 @@ function parseActionBodyLine(line, index) {
|
|
|
78
78
|
const rest = rawName?.startsWith('@') ? ` ${rawName}${rawRest ?? ''}` : (rawRest ?? '');
|
|
79
79
|
if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
|
|
80
80
|
const path = readInlineWord('path', rest);
|
|
81
|
-
const
|
|
81
|
+
const valueType = readInlineType(rest);
|
|
82
|
+
const value = readInlineActionValue('value', rest, { valueType });
|
|
82
83
|
if (!path || !value) return undefined;
|
|
83
|
-
return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, value });
|
|
84
|
+
return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, valueType, value });
|
|
84
85
|
}
|
|
85
86
|
if (rowKind === 'remove') {
|
|
86
87
|
const path = readInlineWord('path', rest);
|
|
@@ -94,9 +95,10 @@ function parseActionBodyLine(line, index) {
|
|
|
94
95
|
return compactRecord({ kind: 'callEffect', id: idFrom(rest, `action_body_callEffect_${name}`), name, capability: readInlineWord('capability', rest) ?? readInlineWord('effect', rest) ?? name, input });
|
|
95
96
|
}
|
|
96
97
|
if (rowKind === 'let') {
|
|
97
|
-
const
|
|
98
|
+
const valueType = readInlineType(rest);
|
|
99
|
+
const value = readInlineActionBindingValue('value', rest, { valueType });
|
|
98
100
|
if (!rawName || rawName.startsWith('@') || !isActionBindingName(name) || !value) return undefined;
|
|
99
|
-
return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, value });
|
|
101
|
+
return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, valueType, value });
|
|
100
102
|
}
|
|
101
103
|
if (rowKind === 'return') {
|
|
102
104
|
const valueText = stripIds(rawName?.startsWith('@') ? rest : `${rawName ?? ''}${rest ?? ''}`).trim();
|
|
@@ -171,13 +173,13 @@ function findMatchingBrace(source, open) {
|
|
|
171
173
|
return -1;
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
function readInlineActionValue(label, text) {
|
|
176
|
+
function readInlineActionValue(label, text, options = {}) {
|
|
175
177
|
const value = readInlineValue(label, text);
|
|
176
|
-
return value ? readActionValue(value) : undefined;
|
|
178
|
+
return value ? readActionValue(value, options) : undefined;
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
function readInlineActionBindingValue(label, text) {
|
|
180
|
-
const value = readInlineActionValue(label, text);
|
|
181
|
+
function readInlineActionBindingValue(label, text, options = {}) {
|
|
182
|
+
const value = readInlineActionValue(label, text, options);
|
|
181
183
|
return value ?? undefined;
|
|
182
184
|
}
|
|
183
185
|
|
|
@@ -186,6 +188,7 @@ function isActionBindingName(value) {
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
|
|
191
|
+
function readInlineType(text) { return readInlineWord('type', text) ?? readInlineWord('valueType', text); }
|
|
189
192
|
function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
|
|
190
193
|
function readInlineValue(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim(); }
|
|
191
194
|
function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const IDENTIFIER = /^[A-Za-z_$][\w$-]*$/;
|
|
2
2
|
const BLOCKED_REF_ROOTS = new Set(['globalThis', 'process', 'window', 'document', 'constructor', 'prototype', '__proto__', 'env']);
|
|
3
|
+
const NUMERIC_OPERATORS = new Set(['+', '-', '*', '/', '%']);
|
|
4
|
+
const NUMERIC_TYPES = new Set(['number', 'numeric', 'int', 'integer', 'float', 'double', 'decimal']);
|
|
3
5
|
|
|
4
6
|
export function readActionValue(value, options = {}) {
|
|
5
7
|
const parsed = parseActionValue(value, options);
|
|
@@ -11,8 +13,12 @@ export function parseActionValue(value, options = {}) {
|
|
|
11
13
|
if (!text) return fail('missing-action-expression');
|
|
12
14
|
const expression = parseActionExpression(text, options);
|
|
13
15
|
if (!expression.ok) return expression;
|
|
14
|
-
if (expression.
|
|
15
|
-
|
|
16
|
+
if (hasNumericOperator(expression.ast) && !isNumericType(options.valueType ?? options.type)) {
|
|
17
|
+
return fail((options.valueType ?? options.type) ? 'unsupported-action-expression-type' : 'missing-action-expression-type');
|
|
18
|
+
}
|
|
19
|
+
const valueType = options.valueType ?? options.type;
|
|
20
|
+
if (expression.literal) return { ok: true, value: compactRecord({ value: expression.ast.value, valueType }) };
|
|
21
|
+
return { ok: true, value: compactRecord({ expression: text, expressionAst: expression.ast, valueType }) };
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export function parseActionExpression(value, options = {}) {
|
|
@@ -47,7 +53,8 @@ function tokenizeActionExpression(text) {
|
|
|
47
53
|
index = read.next;
|
|
48
54
|
continue;
|
|
49
55
|
}
|
|
50
|
-
const
|
|
56
|
+
const canReadSignedNumber = char !== '-' || canStartSignedNumber(tokens);
|
|
57
|
+
const number = canReadSignedNumber ? /^-?\d+(?:\.\d+)?/.exec(text.slice(index)) : undefined;
|
|
51
58
|
if (number) {
|
|
52
59
|
tokens.push({ type: 'number', value: Number(number[0]), text: number[0], start: index, end: index + number[0].length });
|
|
53
60
|
index += number[0].length;
|
|
@@ -67,7 +74,7 @@ function tokenizeActionExpression(text) {
|
|
|
67
74
|
index += 2;
|
|
68
75
|
continue;
|
|
69
76
|
}
|
|
70
|
-
if (char === '!' || char === '>' || char === '<') {
|
|
77
|
+
if (char === '!' || char === '>' || char === '<' || NUMERIC_OPERATORS.has(char)) {
|
|
71
78
|
tokens.push({ type: 'operator', text: char, start: index, end: index + 1 });
|
|
72
79
|
index++;
|
|
73
80
|
continue;
|
|
@@ -77,7 +84,7 @@ function tokenizeActionExpression(text) {
|
|
|
77
84
|
index++;
|
|
78
85
|
continue;
|
|
79
86
|
}
|
|
80
|
-
if ('
|
|
87
|
+
if ('=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
|
|
81
88
|
return fail('malformed-action-expression');
|
|
82
89
|
}
|
|
83
90
|
tokens.push({ type: 'eof', text: '', start: text.length, end: text.length });
|
|
@@ -136,11 +143,11 @@ class ExpressionParser {
|
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
parseComparison() {
|
|
139
|
-
let left = this.
|
|
146
|
+
let left = this.parseAdditive();
|
|
140
147
|
const operator = this.peek();
|
|
141
148
|
if (operator.type === 'operator' && ['==', '!=', '>', '>=', '<', '<='].includes(operator.text)) {
|
|
142
149
|
this.index++;
|
|
143
|
-
left = { kind: 'binary', op: operator.text, left, right: this.
|
|
150
|
+
left = { kind: 'binary', op: operator.text, left, right: this.parseAdditive() };
|
|
144
151
|
if (this.peek().type === 'operator' && ['==', '!=', '>', '>=', '<', '<='].includes(this.peek().text)) {
|
|
145
152
|
this.reject('malformed-action-expression');
|
|
146
153
|
}
|
|
@@ -148,6 +155,26 @@ class ExpressionParser {
|
|
|
148
155
|
return left;
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
parseAdditive() {
|
|
159
|
+
let left = this.parseMultiplicative();
|
|
160
|
+
while (this.peek().type === 'operator' && (this.peek().text === '+' || this.peek().text === '-')) {
|
|
161
|
+
const operator = this.peek().text;
|
|
162
|
+
this.index++;
|
|
163
|
+
left = { kind: 'binary', op: operator, left, right: this.parseMultiplicative() };
|
|
164
|
+
}
|
|
165
|
+
return left;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
parseMultiplicative() {
|
|
169
|
+
let left = this.parseUnary();
|
|
170
|
+
while (this.peek().type === 'operator' && (this.peek().text === '*' || this.peek().text === '/' || this.peek().text === '%')) {
|
|
171
|
+
const operator = this.peek().text;
|
|
172
|
+
this.index++;
|
|
173
|
+
left = { kind: 'binary', op: operator, left, right: this.parseUnary() };
|
|
174
|
+
}
|
|
175
|
+
return left;
|
|
176
|
+
}
|
|
177
|
+
|
|
151
178
|
parseUnary() {
|
|
152
179
|
if (this.matchOperator('!')) return { kind: 'unary', op: '!', argument: this.parseUnary() };
|
|
153
180
|
return this.parsePrimary();
|
|
@@ -254,6 +281,25 @@ class ExpressionParser {
|
|
|
254
281
|
}
|
|
255
282
|
}
|
|
256
283
|
|
|
284
|
+
function canStartSignedNumber(tokens) {
|
|
285
|
+
const previous = tokens[tokens.length - 1];
|
|
286
|
+
if (!previous) return true;
|
|
287
|
+
if (previous.type === 'operator') return true;
|
|
288
|
+
return previous.type === 'punctuation' && previous.text === '(';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function hasNumericOperator(node) {
|
|
292
|
+
if (!node || typeof node !== 'object') return false;
|
|
293
|
+
if (node.kind === 'binary') return NUMERIC_OPERATORS.has(node.op) || hasNumericOperator(node.left) || hasNumericOperator(node.right);
|
|
294
|
+
if (node.kind === 'logical') return hasNumericOperator(node.left) || hasNumericOperator(node.right);
|
|
295
|
+
if (node.kind === 'unary') return hasNumericOperator(node.argument);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isNumericType(value) {
|
|
300
|
+
return NUMERIC_TYPES.has(String(value ?? '').trim().toLowerCase());
|
|
301
|
+
}
|
|
302
|
+
|
|
257
303
|
function refNode(parts) {
|
|
258
304
|
const [root, ...rest] = parts;
|
|
259
305
|
const scope = root === 'input' || root === 'state' || root === 'patches' ? root : 'local';
|
|
@@ -264,3 +310,7 @@ function refNode(parts) {
|
|
|
264
310
|
function fail(reason) {
|
|
265
311
|
return { ok: false, reason };
|
|
266
312
|
}
|
|
313
|
+
|
|
314
|
+
function compactRecord(record) {
|
|
315
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
|
|
316
|
+
}
|
|
@@ -94,7 +94,7 @@ function validateActionRow(rowKind, rawName, rest, header) {
|
|
|
94
94
|
if (rowKind === 'if') return validateActionExpressionText(readIfCondition(header));
|
|
95
95
|
if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
|
|
96
96
|
if (!readInlineWord('path', rest)) return { ok: false, reason: 'missing-action-path' };
|
|
97
|
-
return validateActionExpressionText(readInlineValue('value', rest));
|
|
97
|
+
return validateActionExpressionText(readInlineValue('value', rest), { valueType: readInlineType(rest) });
|
|
98
98
|
}
|
|
99
99
|
if (rowKind === 'remove') {
|
|
100
100
|
return readInlineWord('path', rest) ? { ok: true } : { ok: false, reason: 'missing-action-path' };
|
|
@@ -112,8 +112,12 @@ function validateActionRow(rowKind, rawName, rest, header) {
|
|
|
112
112
|
return { ok: false, reason: 'unsupported-action-binding-name' };
|
|
113
113
|
}
|
|
114
114
|
const value = readInlineValue('value', rest);
|
|
115
|
-
const parsed = value ? parseActionValue(value) : undefined;
|
|
116
|
-
|
|
115
|
+
const parsed = value ? parseActionValue(value, { valueType: readInlineType(rest) }) : undefined;
|
|
116
|
+
if (parsed?.ok) return { ok: true };
|
|
117
|
+
if (parsed?.reason === 'missing-action-expression-type' || parsed?.reason === 'unsupported-action-expression-type') {
|
|
118
|
+
return { ok: false, reason: parsed.reason };
|
|
119
|
+
}
|
|
120
|
+
return { ok: false, reason: 'unsupported-action-binding-value' };
|
|
117
121
|
}
|
|
118
122
|
return { ok: true };
|
|
119
123
|
}
|
|
@@ -126,6 +130,10 @@ function readInlineWord(label, text) {
|
|
|
126
130
|
return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim();
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
function readInlineType(text) {
|
|
134
|
+
return readInlineWord('type', text) ?? readInlineWord('valueType', text);
|
|
135
|
+
}
|
|
136
|
+
|
|
129
137
|
function readIfCondition(header) {
|
|
130
138
|
const text = header.replace(/^if\b/, '').replace(/\{\s*$/, '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
131
139
|
const explicit = /\bcondition\s+(.+)$/.exec(text);
|
|
@@ -141,8 +149,8 @@ function stripIds(text) {
|
|
|
141
149
|
return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
142
150
|
}
|
|
143
151
|
|
|
144
|
-
function validateActionExpressionText(text) {
|
|
145
|
-
const parsed = parseActionValue(text);
|
|
152
|
+
function validateActionExpressionText(text, options = {}) {
|
|
153
|
+
const parsed = parseActionValue(text, options);
|
|
146
154
|
return parsed.ok ? { ok: true } : { ok: false, reason: parsed.reason };
|
|
147
155
|
}
|
|
148
156
|
|