@shapeshift-labs/frontier-lang-parser 0.3.45 → 0.3.47
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
CHANGED
|
@@ -52,6 +52,7 @@ function parseActionIfBlock(header, body, index, state) {
|
|
|
52
52
|
kind: 'if',
|
|
53
53
|
id: details.id,
|
|
54
54
|
name: details.name,
|
|
55
|
+
comparisonType: details.comparisonType,
|
|
55
56
|
condition: details.condition,
|
|
56
57
|
body: parseActionBodyRecords(body, state)
|
|
57
58
|
});
|
|
@@ -63,10 +64,12 @@ function parseActionIfHeader(header, index) {
|
|
|
63
64
|
const nameText = explicitCondition ? withoutId.slice(0, explicitCondition.index).trim() : '';
|
|
64
65
|
const name = firstIdentifier(nameText) ?? `if_${index}`;
|
|
65
66
|
const conditionText = explicitCondition?.[1]?.trim() || withoutId;
|
|
67
|
+
const comparisonType = readInlineComparisonType(header);
|
|
66
68
|
return {
|
|
67
69
|
id: idFrom(header, `action_body_if_${name}`),
|
|
68
70
|
name,
|
|
69
|
-
|
|
71
|
+
comparisonType,
|
|
72
|
+
condition: conditionText ? readActionValue(conditionText, { comparisonType }) : undefined
|
|
70
73
|
};
|
|
71
74
|
}
|
|
72
75
|
|
|
@@ -78,9 +81,11 @@ function parseActionBodyLine(line, index) {
|
|
|
78
81
|
const rest = rawName?.startsWith('@') ? ` ${rawName}${rawRest ?? ''}` : (rawRest ?? '');
|
|
79
82
|
if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
|
|
80
83
|
const path = readInlineWord('path', rest);
|
|
81
|
-
const
|
|
84
|
+
const valueType = readInlineType(rest);
|
|
85
|
+
const comparisonType = readInlineComparisonType(rest);
|
|
86
|
+
const value = readInlineActionValue('value', rest, { valueType, comparisonType });
|
|
82
87
|
if (!path || !value) return undefined;
|
|
83
|
-
return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, value });
|
|
88
|
+
return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, valueType, comparisonType, value });
|
|
84
89
|
}
|
|
85
90
|
if (rowKind === 'remove') {
|
|
86
91
|
const path = readInlineWord('path', rest);
|
|
@@ -94,9 +99,11 @@ function parseActionBodyLine(line, index) {
|
|
|
94
99
|
return compactRecord({ kind: 'callEffect', id: idFrom(rest, `action_body_callEffect_${name}`), name, capability: readInlineWord('capability', rest) ?? readInlineWord('effect', rest) ?? name, input });
|
|
95
100
|
}
|
|
96
101
|
if (rowKind === 'let') {
|
|
97
|
-
const
|
|
102
|
+
const valueType = readInlineType(rest);
|
|
103
|
+
const comparisonType = readInlineComparisonType(rest);
|
|
104
|
+
const value = readInlineActionBindingValue('value', rest, { valueType, comparisonType });
|
|
98
105
|
if (!rawName || rawName.startsWith('@') || !isActionBindingName(name) || !value) return undefined;
|
|
99
|
-
return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, value });
|
|
106
|
+
return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, valueType, comparisonType, value });
|
|
100
107
|
}
|
|
101
108
|
if (rowKind === 'return') {
|
|
102
109
|
const valueText = stripIds(rawName?.startsWith('@') ? rest : `${rawName ?? ''}${rest ?? ''}`).trim();
|
|
@@ -171,13 +178,13 @@ function findMatchingBrace(source, open) {
|
|
|
171
178
|
return -1;
|
|
172
179
|
}
|
|
173
180
|
|
|
174
|
-
function readInlineActionValue(label, text) {
|
|
181
|
+
function readInlineActionValue(label, text, options = {}) {
|
|
175
182
|
const value = readInlineValue(label, text);
|
|
176
|
-
return value ? readActionValue(value) : undefined;
|
|
183
|
+
return value ? readActionValue(value, options) : undefined;
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
function readInlineActionBindingValue(label, text) {
|
|
180
|
-
const value = readInlineActionValue(label, text);
|
|
186
|
+
function readInlineActionBindingValue(label, text, options = {}) {
|
|
187
|
+
const value = readInlineActionValue(label, text, options);
|
|
181
188
|
return value ?? undefined;
|
|
182
189
|
}
|
|
183
190
|
|
|
@@ -186,6 +193,8 @@ function isActionBindingName(value) {
|
|
|
186
193
|
}
|
|
187
194
|
|
|
188
195
|
function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
|
|
196
|
+
function readInlineType(text) { return readInlineWord('type', text) ?? readInlineWord('valueType', text); }
|
|
197
|
+
function readInlineComparisonType(text) { return readInlineWord('compare', text) ?? readInlineWord('comparisonType', text) ?? readInlineWord('compareType', text); }
|
|
189
198
|
function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
|
|
190
199
|
function readInlineValue(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim(); }
|
|
191
200
|
function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const NUMERIC_OPERATORS = new Set(['+', '-', '*', '/', '%']);
|
|
2
|
+
|
|
3
|
+
const ORDERED_COMPARISON_OPERATORS = new Set(['>', '>=', '<', '<=']);
|
|
4
|
+
const NUMERIC_TYPES = new Set(['number', 'numeric', 'int', 'integer', 'float', 'double', 'decimal']);
|
|
5
|
+
|
|
6
|
+
export function hasNumericOperator(node) {
|
|
7
|
+
if (!node || typeof node !== 'object') return false;
|
|
8
|
+
if (node.kind === 'binary') return NUMERIC_OPERATORS.has(node.op) || hasNumericOperator(node.left) || hasNumericOperator(node.right);
|
|
9
|
+
if (node.kind === 'logical') return hasNumericOperator(node.left) || hasNumericOperator(node.right);
|
|
10
|
+
if (node.kind === 'unary') return hasNumericOperator(node.argument);
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasNonLiteralOrderedComparison(node) {
|
|
15
|
+
if (!node || typeof node !== 'object') return false;
|
|
16
|
+
if (node.kind === 'binary') {
|
|
17
|
+
const current = ORDERED_COMPARISON_OPERATORS.has(node.op) && !isLiteralNumericComparison(node);
|
|
18
|
+
return current || hasNonLiteralOrderedComparison(node.left) || hasNonLiteralOrderedComparison(node.right);
|
|
19
|
+
}
|
|
20
|
+
if (node.kind === 'logical') return hasNonLiteralOrderedComparison(node.left) || hasNonLiteralOrderedComparison(node.right);
|
|
21
|
+
if (node.kind === 'unary') return hasNonLiteralOrderedComparison(node.argument);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isNumericType(value) {
|
|
26
|
+
return NUMERIC_TYPES.has(String(value ?? '').trim().toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isLiteralNumericComparison(node) {
|
|
30
|
+
return node.left?.kind === 'literal' && typeof node.left.value === 'number' && node.right?.kind === 'literal' && typeof node.right.value === 'number';
|
|
31
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { NUMERIC_OPERATORS, hasNonLiteralOrderedComparison, hasNumericOperator, isNumericType } from './action-expression-semantics.js';
|
|
2
|
+
|
|
1
3
|
const IDENTIFIER = /^[A-Za-z_$][\w$-]*$/;
|
|
2
4
|
const BLOCKED_REF_ROOTS = new Set(['globalThis', 'process', 'window', 'document', 'constructor', 'prototype', '__proto__', 'env']);
|
|
3
5
|
|
|
@@ -11,8 +13,16 @@ 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
|
+
if (hasNonLiteralOrderedComparison(expression.ast) && !isNumericType(options.comparisonType ?? options.compareType)) {
|
|
20
|
+
return fail((options.comparisonType ?? options.compareType) ? 'unsupported-action-comparison-type' : 'missing-action-comparison-type');
|
|
21
|
+
}
|
|
22
|
+
const valueType = options.valueType ?? options.type;
|
|
23
|
+
const comparisonType = options.comparisonType ?? options.compareType;
|
|
24
|
+
if (expression.literal) return { ok: true, value: compactRecord({ value: expression.ast.value, valueType, comparisonType }) };
|
|
25
|
+
return { ok: true, value: compactRecord({ expression: text, expressionAst: expression.ast, valueType, comparisonType }) };
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
export function parseActionExpression(value, options = {}) {
|
|
@@ -47,7 +57,8 @@ function tokenizeActionExpression(text) {
|
|
|
47
57
|
index = read.next;
|
|
48
58
|
continue;
|
|
49
59
|
}
|
|
50
|
-
const
|
|
60
|
+
const canReadSignedNumber = char !== '-' || canStartSignedNumber(tokens);
|
|
61
|
+
const number = canReadSignedNumber ? /^-?\d+(?:\.\d+)?/.exec(text.slice(index)) : undefined;
|
|
51
62
|
if (number) {
|
|
52
63
|
tokens.push({ type: 'number', value: Number(number[0]), text: number[0], start: index, end: index + number[0].length });
|
|
53
64
|
index += number[0].length;
|
|
@@ -67,7 +78,7 @@ function tokenizeActionExpression(text) {
|
|
|
67
78
|
index += 2;
|
|
68
79
|
continue;
|
|
69
80
|
}
|
|
70
|
-
if (char === '!' || char === '>' || char === '<') {
|
|
81
|
+
if (char === '!' || char === '>' || char === '<' || NUMERIC_OPERATORS.has(char)) {
|
|
71
82
|
tokens.push({ type: 'operator', text: char, start: index, end: index + 1 });
|
|
72
83
|
index++;
|
|
73
84
|
continue;
|
|
@@ -77,7 +88,7 @@ function tokenizeActionExpression(text) {
|
|
|
77
88
|
index++;
|
|
78
89
|
continue;
|
|
79
90
|
}
|
|
80
|
-
if ('
|
|
91
|
+
if ('=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
|
|
81
92
|
return fail('malformed-action-expression');
|
|
82
93
|
}
|
|
83
94
|
tokens.push({ type: 'eof', text: '', start: text.length, end: text.length });
|
|
@@ -136,11 +147,11 @@ class ExpressionParser {
|
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
parseComparison() {
|
|
139
|
-
let left = this.
|
|
150
|
+
let left = this.parseAdditive();
|
|
140
151
|
const operator = this.peek();
|
|
141
152
|
if (operator.type === 'operator' && ['==', '!=', '>', '>=', '<', '<='].includes(operator.text)) {
|
|
142
153
|
this.index++;
|
|
143
|
-
left = { kind: 'binary', op: operator.text, left, right: this.
|
|
154
|
+
left = { kind: 'binary', op: operator.text, left, right: this.parseAdditive() };
|
|
144
155
|
if (this.peek().type === 'operator' && ['==', '!=', '>', '>=', '<', '<='].includes(this.peek().text)) {
|
|
145
156
|
this.reject('malformed-action-expression');
|
|
146
157
|
}
|
|
@@ -148,6 +159,26 @@ class ExpressionParser {
|
|
|
148
159
|
return left;
|
|
149
160
|
}
|
|
150
161
|
|
|
162
|
+
parseAdditive() {
|
|
163
|
+
let left = this.parseMultiplicative();
|
|
164
|
+
while (this.peek().type === 'operator' && (this.peek().text === '+' || this.peek().text === '-')) {
|
|
165
|
+
const operator = this.peek().text;
|
|
166
|
+
this.index++;
|
|
167
|
+
left = { kind: 'binary', op: operator, left, right: this.parseMultiplicative() };
|
|
168
|
+
}
|
|
169
|
+
return left;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
parseMultiplicative() {
|
|
173
|
+
let left = this.parseUnary();
|
|
174
|
+
while (this.peek().type === 'operator' && (this.peek().text === '*' || this.peek().text === '/' || this.peek().text === '%')) {
|
|
175
|
+
const operator = this.peek().text;
|
|
176
|
+
this.index++;
|
|
177
|
+
left = { kind: 'binary', op: operator, left, right: this.parseUnary() };
|
|
178
|
+
}
|
|
179
|
+
return left;
|
|
180
|
+
}
|
|
181
|
+
|
|
151
182
|
parseUnary() {
|
|
152
183
|
if (this.matchOperator('!')) return { kind: 'unary', op: '!', argument: this.parseUnary() };
|
|
153
184
|
return this.parsePrimary();
|
|
@@ -254,6 +285,13 @@ class ExpressionParser {
|
|
|
254
285
|
}
|
|
255
286
|
}
|
|
256
287
|
|
|
288
|
+
function canStartSignedNumber(tokens) {
|
|
289
|
+
const previous = tokens[tokens.length - 1];
|
|
290
|
+
if (!previous) return true;
|
|
291
|
+
if (previous.type === 'operator') return true;
|
|
292
|
+
return previous.type === 'punctuation' && previous.text === '(';
|
|
293
|
+
}
|
|
294
|
+
|
|
257
295
|
function refNode(parts) {
|
|
258
296
|
const [root, ...rest] = parts;
|
|
259
297
|
const scope = root === 'input' || root === 'state' || root === 'patches' ? root : 'local';
|
|
@@ -264,3 +302,7 @@ function refNode(parts) {
|
|
|
264
302
|
function fail(reason) {
|
|
265
303
|
return { ok: false, reason };
|
|
266
304
|
}
|
|
305
|
+
|
|
306
|
+
function compactRecord(record) {
|
|
307
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
|
|
308
|
+
}
|
|
@@ -91,10 +91,10 @@ function actionRowName(rowKind, rawName, rowIndex) {
|
|
|
91
91
|
|
|
92
92
|
function validateActionRow(rowKind, rawName, rest, header) {
|
|
93
93
|
if (!ACTION_BODY_ROWS.has(rowKind)) return { ok: false, reason: 'unsupported-action-body-row' };
|
|
94
|
-
if (rowKind === 'if') return validateActionExpressionText(readIfCondition(header));
|
|
94
|
+
if (rowKind === 'if') return validateActionExpressionText(readIfCondition(header), { comparisonType: readInlineComparisonType(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), comparisonType: readInlineComparisonType(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), comparisonType: readInlineComparisonType(rest) }) : undefined;
|
|
116
|
+
if (parsed?.ok) return { ok: true };
|
|
117
|
+
if (parsed?.reason === 'missing-action-expression-type' || parsed?.reason === 'unsupported-action-expression-type' || parsed?.reason === 'missing-action-comparison-type' || parsed?.reason === 'unsupported-action-comparison-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,14 @@ 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
|
+
|
|
137
|
+
function readInlineComparisonType(text) {
|
|
138
|
+
return readInlineWord('compare', text) ?? readInlineWord('comparisonType', text) ?? readInlineWord('compareType', text);
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
function readIfCondition(header) {
|
|
130
142
|
const text = header.replace(/^if\b/, '').replace(/\{\s*$/, '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
131
143
|
const explicit = /\bcondition\s+(.+)$/.exec(text);
|
|
@@ -141,8 +153,8 @@ function stripIds(text) {
|
|
|
141
153
|
return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
142
154
|
}
|
|
143
155
|
|
|
144
|
-
function validateActionExpressionText(text) {
|
|
145
|
-
const parsed = parseActionValue(text);
|
|
156
|
+
function validateActionExpressionText(text, options = {}) {
|
|
157
|
+
const parsed = parseActionValue(text, options);
|
|
146
158
|
return parsed.ok ? { ok: true } : { ok: false, reason: parsed.reason };
|
|
147
159
|
}
|
|
148
160
|
|