@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.
@@ -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
- condition: conditionText ? readActionValue(conditionText) : undefined
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 value = readInlineActionValue('value', rest);
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 value = readInlineActionBindingValue('value', rest);
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.literal) return { ok: true, value: { value: expression.ast.value } };
15
- return { ok: true, value: { expression: text, expressionAst: expression.ast } };
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 number = /^-?\d+(?:\.\d+)?/.exec(text.slice(index));
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 ('+-*/%=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
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.parseUnary();
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.parseUnary() };
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
- return parsed?.ok ? { ok: true } : { ok: false, reason: 'unsupported-action-binding-value' };
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.45",
3
+ "version": "0.3.47",
4
4
  "description": "Parser for the first Frontier Lang .frontier syntax slice.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",