@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.
@@ -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
- condition: conditionText ? readActionValue(conditionText, { comparisonType }) : undefined
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 value = readInlineActionValue('value', rest, { valueType, comparisonType });
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 value = readInlineActionBindingValue('value', rest, { valueType, comparisonType });
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 valueText = stripIds(rawName?.startsWith('@') ? rest : `${rawName ?? ''}${rest ?? ''}`).trim();
110
- const value = valueText ? readActionValue(valueText) : undefined;
111
- if (valueText && !value) return undefined;
112
- return compactRecord({ kind: 'return', id: idFrom(rest, `action_body_return_${index}`), value });
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 { NUMERIC_OPERATORS, hasNonLiteralOrderedComparison, hasNumericOperator, isNumericType } from './action-expression-semantics.js';
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
- 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 }) };
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-expression-ref');
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 value = readReturnValue(rawName, rest);
108
- return value ? validateActionExpressionText(value) : { ok: true };
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 readReturnValue(rawName, rest) {
148
- if (rawName?.startsWith('@')) return stripIds(`${rawName}${rest ?? ''}`).trim();
149
- return stripIds(`${rawName ?? ''}${rest ?? ''}`).trim();
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.47",
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",