@shapeshift-labs/frontier-lang-parser 0.3.46 → 0.3.48
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,8 @@ function parseActionIfBlock(header, body, index, state) {
|
|
|
52
52
|
kind: 'if',
|
|
53
53
|
id: details.id,
|
|
54
54
|
name: details.name,
|
|
55
|
+
comparisonType: details.comparisonType,
|
|
56
|
+
callType: details.callType,
|
|
55
57
|
condition: details.condition,
|
|
56
58
|
body: parseActionBodyRecords(body, state)
|
|
57
59
|
});
|
|
@@ -63,10 +65,14 @@ function parseActionIfHeader(header, index) {
|
|
|
63
65
|
const nameText = explicitCondition ? withoutId.slice(0, explicitCondition.index).trim() : '';
|
|
64
66
|
const name = firstIdentifier(nameText) ?? `if_${index}`;
|
|
65
67
|
const conditionText = explicitCondition?.[1]?.trim() || withoutId;
|
|
68
|
+
const comparisonType = readInlineComparisonType(header);
|
|
69
|
+
const callType = readInlineCallType(header);
|
|
66
70
|
return {
|
|
67
71
|
id: idFrom(header, `action_body_if_${name}`),
|
|
68
72
|
name,
|
|
69
|
-
|
|
73
|
+
comparisonType,
|
|
74
|
+
callType,
|
|
75
|
+
condition: conditionText ? readActionValue(conditionText, { comparisonType, callType }) : undefined
|
|
70
76
|
};
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -79,9 +85,11 @@ function parseActionBodyLine(line, index) {
|
|
|
79
85
|
if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
|
|
80
86
|
const path = readInlineWord('path', rest);
|
|
81
87
|
const valueType = readInlineType(rest);
|
|
82
|
-
const
|
|
88
|
+
const comparisonType = readInlineComparisonType(rest);
|
|
89
|
+
const callType = readInlineCallType(rest);
|
|
90
|
+
const value = readInlineActionValue('value', rest, { valueType, comparisonType, callType });
|
|
83
91
|
if (!path || !value) return undefined;
|
|
84
|
-
return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, valueType, value });
|
|
92
|
+
return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, valueType, comparisonType, callType, value });
|
|
85
93
|
}
|
|
86
94
|
if (rowKind === 'remove') {
|
|
87
95
|
const path = readInlineWord('path', rest);
|
|
@@ -96,9 +104,11 @@ function parseActionBodyLine(line, index) {
|
|
|
96
104
|
}
|
|
97
105
|
if (rowKind === 'let') {
|
|
98
106
|
const valueType = readInlineType(rest);
|
|
99
|
-
const
|
|
107
|
+
const comparisonType = readInlineComparisonType(rest);
|
|
108
|
+
const callType = readInlineCallType(rest);
|
|
109
|
+
const value = readInlineActionBindingValue('value', rest, { valueType, comparisonType, callType });
|
|
100
110
|
if (!rawName || rawName.startsWith('@') || !isActionBindingName(name) || !value) return undefined;
|
|
101
|
-
return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, valueType, value });
|
|
111
|
+
return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, valueType, comparisonType, callType, value });
|
|
102
112
|
}
|
|
103
113
|
if (rowKind === 'return') {
|
|
104
114
|
const valueText = stripIds(rawName?.startsWith('@') ? rest : `${rawName ?? ''}${rest ?? ''}`).trim();
|
|
@@ -189,6 +199,8 @@ function isActionBindingName(value) {
|
|
|
189
199
|
|
|
190
200
|
function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
|
|
191
201
|
function readInlineType(text) { return readInlineWord('type', text) ?? readInlineWord('valueType', text); }
|
|
202
|
+
function readInlineComparisonType(text) { return readInlineWord('compare', text) ?? readInlineWord('comparisonType', text) ?? readInlineWord('compareType', text); }
|
|
203
|
+
function readInlineCallType(text) { return readInlineWord('call', text) ?? readInlineWord('callType', text); }
|
|
192
204
|
function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
|
|
193
205
|
function readInlineValue(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim(); }
|
|
194
206
|
function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
const CALL_TYPES = new Set(['text', 'string', 'bool', 'boolean', 'number', 'numeric', 'int', 'integer', 'float', 'double', 'decimal', 'json']);
|
|
6
|
+
|
|
7
|
+
export function hasNumericOperator(node) {
|
|
8
|
+
if (!node || typeof node !== 'object') return false;
|
|
9
|
+
if (node.kind === 'binary') return NUMERIC_OPERATORS.has(node.op) || hasNumericOperator(node.left) || hasNumericOperator(node.right);
|
|
10
|
+
if (node.kind === 'logical') return hasNumericOperator(node.left) || hasNumericOperator(node.right);
|
|
11
|
+
if (node.kind === 'unary') return hasNumericOperator(node.argument);
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function hasNonLiteralOrderedComparison(node) {
|
|
16
|
+
if (!node || typeof node !== 'object') return false;
|
|
17
|
+
if (node.kind === 'binary') {
|
|
18
|
+
const current = ORDERED_COMPARISON_OPERATORS.has(node.op) && !isLiteralNumericComparison(node);
|
|
19
|
+
return current || hasNonLiteralOrderedComparison(node.left) || hasNonLiteralOrderedComparison(node.right);
|
|
20
|
+
}
|
|
21
|
+
if (node.kind === 'logical') return hasNonLiteralOrderedComparison(node.left) || hasNonLiteralOrderedComparison(node.right);
|
|
22
|
+
if (node.kind === 'unary') return hasNonLiteralOrderedComparison(node.argument);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function hasCallExpression(node) {
|
|
27
|
+
if (!node || typeof node !== 'object') return false;
|
|
28
|
+
if (node.kind === 'call') return true;
|
|
29
|
+
if (node.kind === 'binary' || node.kind === 'logical') return hasCallExpression(node.left) || hasCallExpression(node.right);
|
|
30
|
+
if (node.kind === 'unary') return hasCallExpression(node.argument);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isNumericType(value) {
|
|
35
|
+
return NUMERIC_TYPES.has(String(value ?? '').trim().toLowerCase());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isSupportedCallType(value) {
|
|
39
|
+
return CALL_TYPES.has(String(value ?? '').trim().toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isLiteralNumericComparison(node) {
|
|
43
|
+
return node.left?.kind === 'literal' && typeof node.left.value === 'number' && node.right?.kind === 'literal' && typeof node.right.value === 'number';
|
|
44
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { NUMERIC_OPERATORS } from './action-expression-semantics.js';
|
|
2
|
+
|
|
3
|
+
export function tokenizeActionExpression(text) {
|
|
4
|
+
const tokens = [];
|
|
5
|
+
let index = 0;
|
|
6
|
+
while (index < text.length) {
|
|
7
|
+
const char = text[index];
|
|
8
|
+
if (/\s/.test(char)) {
|
|
9
|
+
index++;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (char === '"' || char === "'") {
|
|
13
|
+
const read = readStringToken(text, index);
|
|
14
|
+
if (!read.ok) return read;
|
|
15
|
+
tokens.push(read.token);
|
|
16
|
+
index = read.next;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const canReadSignedNumber = char !== '-' || canStartSignedNumber(tokens);
|
|
20
|
+
const number = canReadSignedNumber ? /^-?\d+(?:\.\d+)?/.exec(text.slice(index)) : undefined;
|
|
21
|
+
if (number) {
|
|
22
|
+
tokens.push({ type: 'number', value: Number(number[0]), text: number[0], start: index, end: index + number[0].length });
|
|
23
|
+
index += number[0].length;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const identifier = /^[A-Za-z_$][\w$-]*/.exec(text.slice(index));
|
|
27
|
+
if (identifier) {
|
|
28
|
+
tokens.push({ type: 'identifier', text: identifier[0], start: index, end: index + identifier[0].length });
|
|
29
|
+
index += identifier[0].length;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const triple = text.slice(index, index + 3);
|
|
33
|
+
if (triple === '===' || triple === '!==') return fail('unsupported-action-expression-operator');
|
|
34
|
+
const double = text.slice(index, index + 2);
|
|
35
|
+
if (double === '&&' || double === '||' || double === '==' || double === '!=' || double === '>=' || double === '<=') {
|
|
36
|
+
tokens.push({ type: 'operator', text: double, start: index, end: index + 2 });
|
|
37
|
+
index += 2;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (char === '!' || char === '>' || char === '<' || NUMERIC_OPERATORS.has(char)) {
|
|
41
|
+
tokens.push({ type: 'operator', text: char, start: index, end: index + 1 });
|
|
42
|
+
index++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === '(' || char === ')' || char === '.' || char === ',') {
|
|
46
|
+
tokens.push({ type: 'punctuation', text: char, start: index, end: index + 1 });
|
|
47
|
+
index++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if ('=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
|
|
51
|
+
return fail('malformed-action-expression');
|
|
52
|
+
}
|
|
53
|
+
tokens.push({ type: 'eof', text: '', start: text.length, end: text.length });
|
|
54
|
+
return { ok: true, tokens };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readStringToken(text, start) {
|
|
58
|
+
const quote = text[start];
|
|
59
|
+
let value = '';
|
|
60
|
+
let index = start + 1;
|
|
61
|
+
while (index < text.length) {
|
|
62
|
+
const char = text[index];
|
|
63
|
+
if (char === quote) {
|
|
64
|
+
return { ok: true, token: { type: 'string', value, text: text.slice(start, index + 1), start, end: index + 1 }, next: index + 1 };
|
|
65
|
+
}
|
|
66
|
+
if (char === '\\') {
|
|
67
|
+
const next = text[index + 1];
|
|
68
|
+
if (next === undefined) return fail('malformed-action-expression');
|
|
69
|
+
if (next === 'n') value += '\n';
|
|
70
|
+
else if (next === 'r') value += '\r';
|
|
71
|
+
else if (next === 't') value += '\t';
|
|
72
|
+
else if (next === '\\' || next === '"' || next === "'") value += next;
|
|
73
|
+
else return fail('malformed-action-expression');
|
|
74
|
+
index += 2;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
value += char;
|
|
78
|
+
index++;
|
|
79
|
+
}
|
|
80
|
+
return fail('malformed-action-expression');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function canStartSignedNumber(tokens) {
|
|
84
|
+
const previous = tokens[tokens.length - 1];
|
|
85
|
+
if (!previous) return true;
|
|
86
|
+
if (previous.type === 'operator') return true;
|
|
87
|
+
return previous.type === 'punctuation' && (previous.text === '(' || previous.text === ',');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function fail(reason) {
|
|
91
|
+
return { ok: false, reason };
|
|
92
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { hasCallExpression, hasNonLiteralOrderedComparison, hasNumericOperator, isNumericType, isSupportedCallType } from './action-expression-semantics.js';
|
|
2
|
+
import { tokenizeActionExpression } from './action-expression-tokenizer.js';
|
|
3
|
+
|
|
1
4
|
const IDENTIFIER = /^[A-Za-z_$][\w$-]*$/;
|
|
2
5
|
const BLOCKED_REF_ROOTS = new Set(['globalThis', 'process', 'window', 'document', 'constructor', 'prototype', '__proto__', 'env']);
|
|
3
|
-
const
|
|
4
|
-
const NUMERIC_TYPES = new Set(['number', 'numeric', 'int', 'integer', 'float', 'double', 'decimal']);
|
|
6
|
+
const BLOCKED_CALL_ROOTS = new Set([...BLOCKED_REF_ROOTS, 'input', 'state', 'patches']);
|
|
5
7
|
|
|
6
8
|
export function readActionValue(value, options = {}) {
|
|
7
9
|
const parsed = parseActionValue(value, options);
|
|
@@ -16,9 +18,18 @@ export function parseActionValue(value, options = {}) {
|
|
|
16
18
|
if (hasNumericOperator(expression.ast) && !isNumericType(options.valueType ?? options.type)) {
|
|
17
19
|
return fail((options.valueType ?? options.type) ? 'unsupported-action-expression-type' : 'missing-action-expression-type');
|
|
18
20
|
}
|
|
21
|
+
if (hasNonLiteralOrderedComparison(expression.ast) && !isNumericType(options.comparisonType ?? options.compareType)) {
|
|
22
|
+
return fail((options.comparisonType ?? options.compareType) ? 'unsupported-action-comparison-type' : 'missing-action-comparison-type');
|
|
23
|
+
}
|
|
24
|
+
if (hasCallExpression(expression.ast) && !isSupportedCallType(options.callType ?? options.call ?? options.returns)) {
|
|
25
|
+
return fail((options.callType ?? options.call ?? options.returns) ? 'unsupported-action-call-type' : 'missing-action-call-type');
|
|
26
|
+
}
|
|
19
27
|
const valueType = options.valueType ?? options.type;
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
const comparisonType = options.comparisonType ?? options.compareType;
|
|
29
|
+
const callType = options.callType ?? options.call ?? options.returns;
|
|
30
|
+
const ast = callType ? attachCallType(expression.ast, callType) : expression.ast;
|
|
31
|
+
if (expression.literal) return { ok: true, value: compactRecord({ value: ast.value, valueType, comparisonType, callType }) };
|
|
32
|
+
return { ok: true, value: compactRecord({ expression: text, expressionAst: ast, valueType, comparisonType, callType }) };
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
export function parseActionExpression(value, options = {}) {
|
|
@@ -37,86 +48,6 @@ export function isSupportedActionValue(value, options = {}) {
|
|
|
37
48
|
return parseActionValue(value, options).ok;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
function tokenizeActionExpression(text) {
|
|
41
|
-
const tokens = [];
|
|
42
|
-
let index = 0;
|
|
43
|
-
while (index < text.length) {
|
|
44
|
-
const char = text[index];
|
|
45
|
-
if (/\s/.test(char)) {
|
|
46
|
-
index++;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (char === '"' || char === "'") {
|
|
50
|
-
const read = readStringToken(text, index);
|
|
51
|
-
if (!read.ok) return read;
|
|
52
|
-
tokens.push(read.token);
|
|
53
|
-
index = read.next;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
const canReadSignedNumber = char !== '-' || canStartSignedNumber(tokens);
|
|
57
|
-
const number = canReadSignedNumber ? /^-?\d+(?:\.\d+)?/.exec(text.slice(index)) : undefined;
|
|
58
|
-
if (number) {
|
|
59
|
-
tokens.push({ type: 'number', value: Number(number[0]), text: number[0], start: index, end: index + number[0].length });
|
|
60
|
-
index += number[0].length;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
const identifier = /^[A-Za-z_$][\w$-]*/.exec(text.slice(index));
|
|
64
|
-
if (identifier) {
|
|
65
|
-
tokens.push({ type: 'identifier', text: identifier[0], start: index, end: index + identifier[0].length });
|
|
66
|
-
index += identifier[0].length;
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
const triple = text.slice(index, index + 3);
|
|
70
|
-
if (triple === '===' || triple === '!==') return fail('unsupported-action-expression-operator');
|
|
71
|
-
const double = text.slice(index, index + 2);
|
|
72
|
-
if (double === '&&' || double === '||' || double === '==' || double === '!=' || double === '>=' || double === '<=') {
|
|
73
|
-
tokens.push({ type: 'operator', text: double, start: index, end: index + 2 });
|
|
74
|
-
index += 2;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (char === '!' || char === '>' || char === '<' || NUMERIC_OPERATORS.has(char)) {
|
|
78
|
-
tokens.push({ type: 'operator', text: char, start: index, end: index + 1 });
|
|
79
|
-
index++;
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
if (char === '(' || char === ')' || char === '.') {
|
|
83
|
-
tokens.push({ type: 'punctuation', text: char, start: index, end: index + 1 });
|
|
84
|
-
index++;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if ('=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
|
|
88
|
-
return fail('malformed-action-expression');
|
|
89
|
-
}
|
|
90
|
-
tokens.push({ type: 'eof', text: '', start: text.length, end: text.length });
|
|
91
|
-
return { ok: true, tokens };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function readStringToken(text, start) {
|
|
95
|
-
const quote = text[start];
|
|
96
|
-
let value = '';
|
|
97
|
-
let index = start + 1;
|
|
98
|
-
while (index < text.length) {
|
|
99
|
-
const char = text[index];
|
|
100
|
-
if (char === quote) {
|
|
101
|
-
return { ok: true, token: { type: 'string', value, text: text.slice(start, index + 1), start, end: index + 1 }, next: index + 1 };
|
|
102
|
-
}
|
|
103
|
-
if (char === '\\') {
|
|
104
|
-
const next = text[index + 1];
|
|
105
|
-
if (next === undefined) return fail('malformed-action-expression');
|
|
106
|
-
if (next === 'n') value += '\n';
|
|
107
|
-
else if (next === 'r') value += '\r';
|
|
108
|
-
else if (next === 't') value += '\t';
|
|
109
|
-
else if (next === '\\' || next === '"' || next === "'") value += next;
|
|
110
|
-
else return fail('malformed-action-expression');
|
|
111
|
-
index += 2;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
value += char;
|
|
115
|
-
index++;
|
|
116
|
-
}
|
|
117
|
-
return fail('malformed-action-expression');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
51
|
class ExpressionParser {
|
|
121
52
|
constructor(tokens, options) {
|
|
122
53
|
this.tokens = tokens;
|
|
@@ -216,6 +147,7 @@ class ExpressionParser {
|
|
|
216
147
|
const first = this.consumeIdentifier();
|
|
217
148
|
if (!first) return { kind: 'literal', value: null };
|
|
218
149
|
parts.push(first);
|
|
150
|
+
if (this.peek().type === 'punctuation' && this.peek().text === '(') return this.parseCall(first);
|
|
219
151
|
while (this.matchPunctuation('.')) {
|
|
220
152
|
const part = this.consumeIdentifier();
|
|
221
153
|
if (!part) {
|
|
@@ -225,7 +157,7 @@ class ExpressionParser {
|
|
|
225
157
|
parts.push(part);
|
|
226
158
|
}
|
|
227
159
|
if (this.peek().type === 'punctuation' && this.peek().text === '(') {
|
|
228
|
-
this.reject('unsupported-action-
|
|
160
|
+
this.reject('unsupported-action-call-callee');
|
|
229
161
|
return refNode(parts);
|
|
230
162
|
}
|
|
231
163
|
if (BLOCKED_REF_ROOTS.has(parts[0])) {
|
|
@@ -235,6 +167,31 @@ class ExpressionParser {
|
|
|
235
167
|
return refNode(parts);
|
|
236
168
|
}
|
|
237
169
|
|
|
170
|
+
parseCall(callee) {
|
|
171
|
+
if (!IDENTIFIER.test(callee) || BLOCKED_CALL_ROOTS.has(callee)) {
|
|
172
|
+
this.reject('unsupported-action-call-callee');
|
|
173
|
+
return { kind: 'call', callee, args: [] };
|
|
174
|
+
}
|
|
175
|
+
this.matchPunctuation('(');
|
|
176
|
+
const args = [];
|
|
177
|
+
if (this.matchPunctuation(')')) return compactRecord({ kind: 'call', callee, args, callType: this.options.callType ?? this.options.call ?? this.options.returns });
|
|
178
|
+
while (this.ok) {
|
|
179
|
+
const argument = this.parseExpression();
|
|
180
|
+
if (hasCallExpression(argument)) this.reject('unsupported-action-call-argument');
|
|
181
|
+
args.push(argument);
|
|
182
|
+
if (this.matchPunctuation(')')) break;
|
|
183
|
+
if (!this.matchPunctuation(',')) {
|
|
184
|
+
this.reject('malformed-action-expression');
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
if (this.peek().type === 'punctuation' && this.peek().text === ')') {
|
|
188
|
+
this.reject('malformed-action-expression');
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return compactRecord({ kind: 'call', callee, args, callType: this.options.callType ?? this.options.call ?? this.options.returns });
|
|
193
|
+
}
|
|
194
|
+
|
|
238
195
|
consumeIdentifier() {
|
|
239
196
|
const token = this.peek();
|
|
240
197
|
if (token.type !== 'identifier' || !IDENTIFIER.test(token.text)) {
|
|
@@ -281,25 +238,6 @@ class ExpressionParser {
|
|
|
281
238
|
}
|
|
282
239
|
}
|
|
283
240
|
|
|
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
|
-
|
|
303
241
|
function refNode(parts) {
|
|
304
242
|
const [root, ...rest] = parts;
|
|
305
243
|
const scope = root === 'input' || root === 'state' || root === 'patches' ? root : 'local';
|
|
@@ -311,6 +249,14 @@ function fail(reason) {
|
|
|
311
249
|
return { ok: false, reason };
|
|
312
250
|
}
|
|
313
251
|
|
|
252
|
+
function attachCallType(node, callType) {
|
|
253
|
+
if (!node || typeof node !== 'object') return node;
|
|
254
|
+
if (node.kind === 'call') return compactRecord({ ...node, callType });
|
|
255
|
+
if (node.kind === 'binary' || node.kind === 'logical') return { ...node, left: attachCallType(node.left, callType), right: attachCallType(node.right, callType) };
|
|
256
|
+
if (node.kind === 'unary') return { ...node, argument: attachCallType(node.argument, callType) };
|
|
257
|
+
return node;
|
|
258
|
+
}
|
|
259
|
+
|
|
314
260
|
function compactRecord(record) {
|
|
315
261
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
|
|
316
262
|
}
|
|
@@ -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), callType: readInlineCallType(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), { valueType: readInlineType(rest) });
|
|
97
|
+
return validateActionExpressionText(readInlineValue('value', rest), { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest), callType: readInlineCallType(rest) });
|
|
98
98
|
}
|
|
99
99
|
if (rowKind === 'remove') {
|
|
100
100
|
return readInlineWord('path', rest) ? { ok: true } : { ok: false, reason: 'missing-action-path' };
|
|
@@ -112,9 +112,9 @@ 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, { valueType: readInlineType(rest) }) : undefined;
|
|
115
|
+
const parsed = value ? parseActionValue(value, { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest), callType: readInlineCallType(rest) }) : undefined;
|
|
116
116
|
if (parsed?.ok) return { ok: true };
|
|
117
|
-
if (parsed?.reason === 'missing-action-expression-type' || parsed?.reason === 'unsupported-action-expression-type') {
|
|
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' || parsed?.reason === 'missing-action-call-type' || parsed?.reason === 'unsupported-action-call-type' || parsed?.reason === 'unsupported-action-call-callee' || parsed?.reason === 'unsupported-action-call-argument') {
|
|
118
118
|
return { ok: false, reason: parsed.reason };
|
|
119
119
|
}
|
|
120
120
|
return { ok: false, reason: 'unsupported-action-binding-value' };
|
|
@@ -134,6 +134,14 @@ function readInlineType(text) {
|
|
|
134
134
|
return readInlineWord('type', text) ?? readInlineWord('valueType', text);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
function readInlineComparisonType(text) {
|
|
138
|
+
return readInlineWord('compare', text) ?? readInlineWord('comparisonType', text) ?? readInlineWord('compareType', text);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readInlineCallType(text) {
|
|
142
|
+
return readInlineWord('call', text) ?? readInlineWord('callType', text);
|
|
143
|
+
}
|
|
144
|
+
|
|
137
145
|
function readIfCondition(header) {
|
|
138
146
|
const text = header.replace(/^if\b/, '').replace(/\{\s*$/, '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
139
147
|
const explicit = /\bcondition\s+(.+)$/.exec(text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shapeshift-labs/frontier-lang-parser",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.48",
|
|
4
4
|
"description": "Parser for the first Frontier Lang .frontier syntax slice.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "node scripts/build.mjs",
|
|
25
|
-
"test": "npm run build && node test/smoke.mjs && node test/action-body-smoke.mjs && node test/target-projection-aggregate-smoke.mjs && node test/conversion-constraint-fields-smoke.mjs && node test/conversion-evidence-smoke.mjs && node test/package-canvas-surface-smoke.mjs && node test/view-render-graph-smoke.mjs && node test/resource-graph-smoke.mjs && node test/interlingua-smoke.mjs && node test/dialect-registry-smoke.mjs",
|
|
25
|
+
"test": "npm run build && node test/smoke.mjs && node test/action-body-smoke.mjs && node test/action-call-smoke.mjs && node test/target-projection-aggregate-smoke.mjs && node test/conversion-constraint-fields-smoke.mjs && node test/conversion-evidence-smoke.mjs && node test/package-canvas-surface-smoke.mjs && node test/view-render-graph-smoke.mjs && node test/resource-graph-smoke.mjs && node test/interlingua-smoke.mjs && node test/dialect-registry-smoke.mjs",
|
|
26
26
|
"typecheck": "node ./node_modules/typescript/bin/tsc --noEmit -p test/tsconfig.json",
|
|
27
27
|
"fuzz": "npm run build && node fuzz/smoke.mjs",
|
|
28
28
|
"bench": "npm run build && node bench/smoke.mjs",
|