@shapeshift-labs/frontier-lang-parser 0.3.47 → 0.3.49
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
|
@@ -53,6 +53,7 @@ function parseActionIfBlock(header, body, index, state) {
|
|
|
53
53
|
id: details.id,
|
|
54
54
|
name: details.name,
|
|
55
55
|
comparisonType: details.comparisonType,
|
|
56
|
+
callType: details.callType,
|
|
56
57
|
condition: details.condition,
|
|
57
58
|
body: parseActionBodyRecords(body, state)
|
|
58
59
|
});
|
|
@@ -65,11 +66,13 @@ function parseActionIfHeader(header, index) {
|
|
|
65
66
|
const name = firstIdentifier(nameText) ?? `if_${index}`;
|
|
66
67
|
const conditionText = explicitCondition?.[1]?.trim() || withoutId;
|
|
67
68
|
const comparisonType = readInlineComparisonType(header);
|
|
69
|
+
const callType = readInlineCallType(header);
|
|
68
70
|
return {
|
|
69
71
|
id: idFrom(header, `action_body_if_${name}`),
|
|
70
72
|
name,
|
|
71
73
|
comparisonType,
|
|
72
|
-
|
|
74
|
+
callType,
|
|
75
|
+
condition: conditionText ? readActionValue(conditionText, { comparisonType, callType }) : undefined
|
|
73
76
|
};
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -83,9 +86,10 @@ function parseActionBodyLine(line, index) {
|
|
|
83
86
|
const path = readInlineWord('path', rest);
|
|
84
87
|
const valueType = readInlineType(rest);
|
|
85
88
|
const comparisonType = readInlineComparisonType(rest);
|
|
86
|
-
const
|
|
89
|
+
const callType = readInlineCallType(rest);
|
|
90
|
+
const value = readInlineActionValue('value', rest, { valueType, comparisonType, callType });
|
|
87
91
|
if (!path || !value) return undefined;
|
|
88
|
-
return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, valueType, comparisonType, value });
|
|
92
|
+
return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, valueType, comparisonType, callType, value });
|
|
89
93
|
}
|
|
90
94
|
if (rowKind === 'remove') {
|
|
91
95
|
const path = readInlineWord('path', rest);
|
|
@@ -101,19 +105,42 @@ function parseActionBodyLine(line, index) {
|
|
|
101
105
|
if (rowKind === 'let') {
|
|
102
106
|
const valueType = readInlineType(rest);
|
|
103
107
|
const comparisonType = readInlineComparisonType(rest);
|
|
104
|
-
const
|
|
108
|
+
const callType = readInlineCallType(rest);
|
|
109
|
+
const value = readInlineActionBindingValue('value', rest, { valueType, comparisonType, callType });
|
|
105
110
|
if (!rawName || rawName.startsWith('@') || !isActionBindingName(name) || !value) return undefined;
|
|
106
|
-
return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, valueType, comparisonType, value });
|
|
111
|
+
return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, valueType, comparisonType, callType, value });
|
|
107
112
|
}
|
|
108
113
|
if (rowKind === 'return') {
|
|
109
|
-
const
|
|
110
|
-
const value = valueText ? readActionValue(valueText) : undefined;
|
|
111
|
-
if (valueText && !value) return undefined;
|
|
112
|
-
return compactRecord({
|
|
114
|
+
const details = readReturnDetails(rawName, rest);
|
|
115
|
+
const value = details.valueText ? readActionValue(details.valueText, details) : undefined;
|
|
116
|
+
if (details.valueText && !value) return undefined;
|
|
117
|
+
return compactRecord({
|
|
118
|
+
kind: 'return',
|
|
119
|
+
id: idFrom(rest, `action_body_return_${index}`),
|
|
120
|
+
valueType: details.valueType,
|
|
121
|
+
comparisonType: details.comparisonType,
|
|
122
|
+
callType: details.callType,
|
|
123
|
+
value
|
|
124
|
+
});
|
|
113
125
|
}
|
|
114
126
|
return undefined;
|
|
115
127
|
}
|
|
116
128
|
|
|
129
|
+
function readReturnDetails(rawName, rest) {
|
|
130
|
+
const text = stripIds(rawName?.startsWith('@') ? rest : `${rawName ?? ''}${rest ?? ''}`).trim();
|
|
131
|
+
const explicitValue = /\bvalue\s+/.test(text);
|
|
132
|
+
const valueType = readInlineType(text);
|
|
133
|
+
const comparisonType = readInlineComparisonType(text);
|
|
134
|
+
const callType = readInlineCallType(text);
|
|
135
|
+
const valueText = explicitValue ? readInlineValue('value', text) : text;
|
|
136
|
+
return {
|
|
137
|
+
valueText,
|
|
138
|
+
valueType,
|
|
139
|
+
comparisonType,
|
|
140
|
+
callType
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
117
144
|
function skipWhitespaceAndComments(source, offset) {
|
|
118
145
|
let index = offset;
|
|
119
146
|
while (index < source.length) {
|
|
@@ -195,6 +222,7 @@ function isActionBindingName(value) {
|
|
|
195
222
|
function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
|
|
196
223
|
function readInlineType(text) { return readInlineWord('type', text) ?? readInlineWord('valueType', text); }
|
|
197
224
|
function readInlineComparisonType(text) { return readInlineWord('compare', text) ?? readInlineWord('comparisonType', text) ?? readInlineWord('compareType', text); }
|
|
225
|
+
function readInlineCallType(text) { return readInlineWord('call', text) ?? readInlineWord('callType', text); }
|
|
198
226
|
function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
|
|
199
227
|
function readInlineValue(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim(); }
|
|
200
228
|
function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
|
|
@@ -2,6 +2,7 @@ export const NUMERIC_OPERATORS = new Set(['+', '-', '*', '/', '%']);
|
|
|
2
2
|
|
|
3
3
|
const ORDERED_COMPARISON_OPERATORS = new Set(['>', '>=', '<', '<=']);
|
|
4
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']);
|
|
5
6
|
|
|
6
7
|
export function hasNumericOperator(node) {
|
|
7
8
|
if (!node || typeof node !== 'object') return false;
|
|
@@ -22,10 +23,22 @@ export function hasNonLiteralOrderedComparison(node) {
|
|
|
22
23
|
return false;
|
|
23
24
|
}
|
|
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
|
+
|
|
25
34
|
export function isNumericType(value) {
|
|
26
35
|
return NUMERIC_TYPES.has(String(value ?? '').trim().toLowerCase());
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
export function isSupportedCallType(value) {
|
|
39
|
+
return CALL_TYPES.has(String(value ?? '').trim().toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
function isLiteralNumericComparison(node) {
|
|
30
43
|
return node.left?.kind === 'literal' && typeof node.left.value === 'number' && node.right?.kind === 'literal' && typeof node.right.value === 'number';
|
|
31
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 {
|
|
1
|
+
import { hasCallExpression, hasNonLiteralOrderedComparison, hasNumericOperator, isNumericType, isSupportedCallType } from './action-expression-semantics.js';
|
|
2
|
+
import { tokenizeActionExpression } from './action-expression-tokenizer.js';
|
|
2
3
|
|
|
3
4
|
const IDENTIFIER = /^[A-Za-z_$][\w$-]*$/;
|
|
4
5
|
const BLOCKED_REF_ROOTS = new Set(['globalThis', 'process', 'window', 'document', 'constructor', 'prototype', '__proto__', 'env']);
|
|
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);
|
|
@@ -19,10 +21,15 @@ export function parseActionValue(value, options = {}) {
|
|
|
19
21
|
if (hasNonLiteralOrderedComparison(expression.ast) && !isNumericType(options.comparisonType ?? options.compareType)) {
|
|
20
22
|
return fail((options.comparisonType ?? options.compareType) ? 'unsupported-action-comparison-type' : 'missing-action-comparison-type');
|
|
21
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
|
+
}
|
|
22
27
|
const valueType = options.valueType ?? options.type;
|
|
23
28
|
const comparisonType = options.comparisonType ?? options.compareType;
|
|
24
|
-
|
|
25
|
-
|
|
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 }) };
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
export function parseActionExpression(value, options = {}) {
|
|
@@ -41,86 +48,6 @@ export function isSupportedActionValue(value, options = {}) {
|
|
|
41
48
|
return parseActionValue(value, options).ok;
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
function tokenizeActionExpression(text) {
|
|
45
|
-
const tokens = [];
|
|
46
|
-
let index = 0;
|
|
47
|
-
while (index < text.length) {
|
|
48
|
-
const char = text[index];
|
|
49
|
-
if (/\s/.test(char)) {
|
|
50
|
-
index++;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (char === '"' || char === "'") {
|
|
54
|
-
const read = readStringToken(text, index);
|
|
55
|
-
if (!read.ok) return read;
|
|
56
|
-
tokens.push(read.token);
|
|
57
|
-
index = read.next;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const canReadSignedNumber = char !== '-' || canStartSignedNumber(tokens);
|
|
61
|
-
const number = canReadSignedNumber ? /^-?\d+(?:\.\d+)?/.exec(text.slice(index)) : undefined;
|
|
62
|
-
if (number) {
|
|
63
|
-
tokens.push({ type: 'number', value: Number(number[0]), text: number[0], start: index, end: index + number[0].length });
|
|
64
|
-
index += number[0].length;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const identifier = /^[A-Za-z_$][\w$-]*/.exec(text.slice(index));
|
|
68
|
-
if (identifier) {
|
|
69
|
-
tokens.push({ type: 'identifier', text: identifier[0], start: index, end: index + identifier[0].length });
|
|
70
|
-
index += identifier[0].length;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const triple = text.slice(index, index + 3);
|
|
74
|
-
if (triple === '===' || triple === '!==') return fail('unsupported-action-expression-operator');
|
|
75
|
-
const double = text.slice(index, index + 2);
|
|
76
|
-
if (double === '&&' || double === '||' || double === '==' || double === '!=' || double === '>=' || double === '<=') {
|
|
77
|
-
tokens.push({ type: 'operator', text: double, start: index, end: index + 2 });
|
|
78
|
-
index += 2;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (char === '!' || char === '>' || char === '<' || NUMERIC_OPERATORS.has(char)) {
|
|
82
|
-
tokens.push({ type: 'operator', text: char, start: index, end: index + 1 });
|
|
83
|
-
index++;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
if (char === '(' || char === ')' || char === '.') {
|
|
87
|
-
tokens.push({ type: 'punctuation', text: char, start: index, end: index + 1 });
|
|
88
|
-
index++;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if ('=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
|
|
92
|
-
return fail('malformed-action-expression');
|
|
93
|
-
}
|
|
94
|
-
tokens.push({ type: 'eof', text: '', start: text.length, end: text.length });
|
|
95
|
-
return { ok: true, tokens };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function readStringToken(text, start) {
|
|
99
|
-
const quote = text[start];
|
|
100
|
-
let value = '';
|
|
101
|
-
let index = start + 1;
|
|
102
|
-
while (index < text.length) {
|
|
103
|
-
const char = text[index];
|
|
104
|
-
if (char === quote) {
|
|
105
|
-
return { ok: true, token: { type: 'string', value, text: text.slice(start, index + 1), start, end: index + 1 }, next: index + 1 };
|
|
106
|
-
}
|
|
107
|
-
if (char === '\\') {
|
|
108
|
-
const next = text[index + 1];
|
|
109
|
-
if (next === undefined) return fail('malformed-action-expression');
|
|
110
|
-
if (next === 'n') value += '\n';
|
|
111
|
-
else if (next === 'r') value += '\r';
|
|
112
|
-
else if (next === 't') value += '\t';
|
|
113
|
-
else if (next === '\\' || next === '"' || next === "'") value += next;
|
|
114
|
-
else return fail('malformed-action-expression');
|
|
115
|
-
index += 2;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
value += char;
|
|
119
|
-
index++;
|
|
120
|
-
}
|
|
121
|
-
return fail('malformed-action-expression');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
51
|
class ExpressionParser {
|
|
125
52
|
constructor(tokens, options) {
|
|
126
53
|
this.tokens = tokens;
|
|
@@ -220,6 +147,7 @@ class ExpressionParser {
|
|
|
220
147
|
const first = this.consumeIdentifier();
|
|
221
148
|
if (!first) return { kind: 'literal', value: null };
|
|
222
149
|
parts.push(first);
|
|
150
|
+
if (this.peek().type === 'punctuation' && this.peek().text === '(') return this.parseCall(first);
|
|
223
151
|
while (this.matchPunctuation('.')) {
|
|
224
152
|
const part = this.consumeIdentifier();
|
|
225
153
|
if (!part) {
|
|
@@ -229,7 +157,7 @@ class ExpressionParser {
|
|
|
229
157
|
parts.push(part);
|
|
230
158
|
}
|
|
231
159
|
if (this.peek().type === 'punctuation' && this.peek().text === '(') {
|
|
232
|
-
this.reject('unsupported-action-
|
|
160
|
+
this.reject('unsupported-action-call-callee');
|
|
233
161
|
return refNode(parts);
|
|
234
162
|
}
|
|
235
163
|
if (BLOCKED_REF_ROOTS.has(parts[0])) {
|
|
@@ -239,6 +167,31 @@ class ExpressionParser {
|
|
|
239
167
|
return refNode(parts);
|
|
240
168
|
}
|
|
241
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
|
+
|
|
242
195
|
consumeIdentifier() {
|
|
243
196
|
const token = this.peek();
|
|
244
197
|
if (token.type !== 'identifier' || !IDENTIFIER.test(token.text)) {
|
|
@@ -285,13 +238,6 @@ class ExpressionParser {
|
|
|
285
238
|
}
|
|
286
239
|
}
|
|
287
240
|
|
|
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
|
-
|
|
295
241
|
function refNode(parts) {
|
|
296
242
|
const [root, ...rest] = parts;
|
|
297
243
|
const scope = root === 'input' || root === 'state' || root === 'patches' ? root : 'local';
|
|
@@ -303,6 +249,14 @@ function fail(reason) {
|
|
|
303
249
|
return { ok: false, reason };
|
|
304
250
|
}
|
|
305
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
|
+
|
|
306
260
|
function compactRecord(record) {
|
|
307
261
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
|
|
308
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), { comparisonType: readInlineComparisonType(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), comparisonType: readInlineComparisonType(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' };
|
|
@@ -104,17 +104,21 @@ function validateActionRow(rowKind, rawName, rest, header) {
|
|
|
104
104
|
return input ? validateActionExpressionText(input) : { ok: true };
|
|
105
105
|
}
|
|
106
106
|
if (rowKind === 'return') {
|
|
107
|
-
const
|
|
108
|
-
|
|
107
|
+
const details = readReturnDetails(rawName, rest);
|
|
108
|
+
if (!details.valueText) return { ok: true };
|
|
109
|
+
const parsed = parseActionValue(details.valueText, details);
|
|
110
|
+
if (parsed.ok) return { ok: true };
|
|
111
|
+
if (isActionExpressionAdmissionReason(parsed.reason)) return { ok: false, reason: parsed.reason };
|
|
112
|
+
return { ok: false, reason: 'unsupported-action-return-value' };
|
|
109
113
|
}
|
|
110
114
|
if (rowKind === 'let') {
|
|
111
115
|
if (!rawName || rawName.startsWith('@') || !/^[A-Za-z_$][\w$-]*$/.test(rawName)) {
|
|
112
116
|
return { ok: false, reason: 'unsupported-action-binding-name' };
|
|
113
117
|
}
|
|
114
118
|
const value = readInlineValue('value', rest);
|
|
115
|
-
const parsed = value ? parseActionValue(value, { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest) }) : undefined;
|
|
119
|
+
const parsed = value ? parseActionValue(value, { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest), callType: readInlineCallType(rest) }) : undefined;
|
|
116
120
|
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') {
|
|
121
|
+
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
122
|
return { ok: false, reason: parsed.reason };
|
|
119
123
|
}
|
|
120
124
|
return { ok: false, reason: 'unsupported-action-binding-value' };
|
|
@@ -138,15 +142,25 @@ function readInlineComparisonType(text) {
|
|
|
138
142
|
return readInlineWord('compare', text) ?? readInlineWord('comparisonType', text) ?? readInlineWord('compareType', text);
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
function readInlineCallType(text) {
|
|
146
|
+
return readInlineWord('call', text) ?? readInlineWord('callType', text);
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
function readIfCondition(header) {
|
|
142
150
|
const text = header.replace(/^if\b/, '').replace(/\{\s*$/, '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
143
151
|
const explicit = /\bcondition\s+(.+)$/.exec(text);
|
|
144
152
|
return explicit ? explicit[1].trim() : text;
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
|
|
155
|
+
function readReturnDetails(rawName, rest) {
|
|
156
|
+
const text = stripIds(rawName?.startsWith('@') ? rest : `${rawName ?? ''}${rest ?? ''}`).trim();
|
|
157
|
+
const explicitValue = /\bvalue\s+/.test(text);
|
|
158
|
+
return {
|
|
159
|
+
valueText: explicitValue ? readInlineValue('value', text) : text,
|
|
160
|
+
valueType: readInlineType(text),
|
|
161
|
+
comparisonType: readInlineComparisonType(text),
|
|
162
|
+
callType: readInlineCallType(text)
|
|
163
|
+
};
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
function stripIds(text) {
|
|
@@ -158,6 +172,20 @@ function validateActionExpressionText(text, options = {}) {
|
|
|
158
172
|
return parsed.ok ? { ok: true } : { ok: false, reason: parsed.reason };
|
|
159
173
|
}
|
|
160
174
|
|
|
175
|
+
function isActionExpressionAdmissionReason(reason) {
|
|
176
|
+
return reason === 'missing-action-expression-type'
|
|
177
|
+
|| reason === 'unsupported-action-expression-type'
|
|
178
|
+
|| reason === 'missing-action-comparison-type'
|
|
179
|
+
|| reason === 'unsupported-action-comparison-type'
|
|
180
|
+
|| reason === 'missing-action-call-type'
|
|
181
|
+
|| reason === 'unsupported-action-call-type'
|
|
182
|
+
|| reason === 'unsupported-action-call-callee'
|
|
183
|
+
|| reason === 'unsupported-action-call-argument'
|
|
184
|
+
|| reason === 'unsupported-action-expression-ref'
|
|
185
|
+
|| reason === 'malformed-action-expression'
|
|
186
|
+
|| reason === 'missing-action-expression';
|
|
187
|
+
}
|
|
188
|
+
|
|
161
189
|
function readNestedBodyBlocks(kind, source) {
|
|
162
190
|
const blocks = [];
|
|
163
191
|
const header = new RegExp('\\b' + kind.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:\\s+([^{}\\n]+?))?\\s*\\{', 'g');
|
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.49",
|
|
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-return-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",
|