@shapeshift-labs/frontier-lang-parser 0.3.45 → 0.3.46

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.
@@ -78,9 +78,10 @@ function parseActionBodyLine(line, index) {
78
78
  const rest = rawName?.startsWith('@') ? ` ${rawName}${rawRest ?? ''}` : (rawRest ?? '');
79
79
  if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
80
80
  const path = readInlineWord('path', rest);
81
- const value = readInlineActionValue('value', rest);
81
+ const valueType = readInlineType(rest);
82
+ const value = readInlineActionValue('value', rest, { valueType });
82
83
  if (!path || !value) return undefined;
83
- return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, value });
84
+ return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, valueType, value });
84
85
  }
85
86
  if (rowKind === 'remove') {
86
87
  const path = readInlineWord('path', rest);
@@ -94,9 +95,10 @@ function parseActionBodyLine(line, index) {
94
95
  return compactRecord({ kind: 'callEffect', id: idFrom(rest, `action_body_callEffect_${name}`), name, capability: readInlineWord('capability', rest) ?? readInlineWord('effect', rest) ?? name, input });
95
96
  }
96
97
  if (rowKind === 'let') {
97
- const value = readInlineActionBindingValue('value', rest);
98
+ const valueType = readInlineType(rest);
99
+ const value = readInlineActionBindingValue('value', rest, { valueType });
98
100
  if (!rawName || rawName.startsWith('@') || !isActionBindingName(name) || !value) return undefined;
99
- return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, value });
101
+ return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, valueType, value });
100
102
  }
101
103
  if (rowKind === 'return') {
102
104
  const valueText = stripIds(rawName?.startsWith('@') ? rest : `${rawName ?? ''}${rest ?? ''}`).trim();
@@ -171,13 +173,13 @@ function findMatchingBrace(source, open) {
171
173
  return -1;
172
174
  }
173
175
 
174
- function readInlineActionValue(label, text) {
176
+ function readInlineActionValue(label, text, options = {}) {
175
177
  const value = readInlineValue(label, text);
176
- return value ? readActionValue(value) : undefined;
178
+ return value ? readActionValue(value, options) : undefined;
177
179
  }
178
180
 
179
- function readInlineActionBindingValue(label, text) {
180
- const value = readInlineActionValue(label, text);
181
+ function readInlineActionBindingValue(label, text, options = {}) {
182
+ const value = readInlineActionValue(label, text, options);
181
183
  return value ?? undefined;
182
184
  }
183
185
 
@@ -186,6 +188,7 @@ function isActionBindingName(value) {
186
188
  }
187
189
 
188
190
  function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
191
+ function readInlineType(text) { return readInlineWord('type', text) ?? readInlineWord('valueType', text); }
189
192
  function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
190
193
  function readInlineValue(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim(); }
191
194
  function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
@@ -1,5 +1,7 @@
1
1
  const IDENTIFIER = /^[A-Za-z_$][\w$-]*$/;
2
2
  const BLOCKED_REF_ROOTS = new Set(['globalThis', 'process', 'window', 'document', 'constructor', 'prototype', '__proto__', 'env']);
3
+ const NUMERIC_OPERATORS = new Set(['+', '-', '*', '/', '%']);
4
+ const NUMERIC_TYPES = new Set(['number', 'numeric', 'int', 'integer', 'float', 'double', 'decimal']);
3
5
 
4
6
  export function readActionValue(value, options = {}) {
5
7
  const parsed = parseActionValue(value, options);
@@ -11,8 +13,12 @@ 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
+ const valueType = options.valueType ?? options.type;
20
+ if (expression.literal) return { ok: true, value: compactRecord({ value: expression.ast.value, valueType }) };
21
+ return { ok: true, value: compactRecord({ expression: text, expressionAst: expression.ast, valueType }) };
16
22
  }
17
23
 
18
24
  export function parseActionExpression(value, options = {}) {
@@ -47,7 +53,8 @@ function tokenizeActionExpression(text) {
47
53
  index = read.next;
48
54
  continue;
49
55
  }
50
- const number = /^-?\d+(?:\.\d+)?/.exec(text.slice(index));
56
+ const canReadSignedNumber = char !== '-' || canStartSignedNumber(tokens);
57
+ const number = canReadSignedNumber ? /^-?\d+(?:\.\d+)?/.exec(text.slice(index)) : undefined;
51
58
  if (number) {
52
59
  tokens.push({ type: 'number', value: Number(number[0]), text: number[0], start: index, end: index + number[0].length });
53
60
  index += number[0].length;
@@ -67,7 +74,7 @@ function tokenizeActionExpression(text) {
67
74
  index += 2;
68
75
  continue;
69
76
  }
70
- if (char === '!' || char === '>' || char === '<') {
77
+ if (char === '!' || char === '>' || char === '<' || NUMERIC_OPERATORS.has(char)) {
71
78
  tokens.push({ type: 'operator', text: char, start: index, end: index + 1 });
72
79
  index++;
73
80
  continue;
@@ -77,7 +84,7 @@ function tokenizeActionExpression(text) {
77
84
  index++;
78
85
  continue;
79
86
  }
80
- if ('+-*/%=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
87
+ if ('=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
81
88
  return fail('malformed-action-expression');
82
89
  }
83
90
  tokens.push({ type: 'eof', text: '', start: text.length, end: text.length });
@@ -136,11 +143,11 @@ class ExpressionParser {
136
143
  }
137
144
 
138
145
  parseComparison() {
139
- let left = this.parseUnary();
146
+ let left = this.parseAdditive();
140
147
  const operator = this.peek();
141
148
  if (operator.type === 'operator' && ['==', '!=', '>', '>=', '<', '<='].includes(operator.text)) {
142
149
  this.index++;
143
- left = { kind: 'binary', op: operator.text, left, right: this.parseUnary() };
150
+ left = { kind: 'binary', op: operator.text, left, right: this.parseAdditive() };
144
151
  if (this.peek().type === 'operator' && ['==', '!=', '>', '>=', '<', '<='].includes(this.peek().text)) {
145
152
  this.reject('malformed-action-expression');
146
153
  }
@@ -148,6 +155,26 @@ class ExpressionParser {
148
155
  return left;
149
156
  }
150
157
 
158
+ parseAdditive() {
159
+ let left = this.parseMultiplicative();
160
+ while (this.peek().type === 'operator' && (this.peek().text === '+' || this.peek().text === '-')) {
161
+ const operator = this.peek().text;
162
+ this.index++;
163
+ left = { kind: 'binary', op: operator, left, right: this.parseMultiplicative() };
164
+ }
165
+ return left;
166
+ }
167
+
168
+ parseMultiplicative() {
169
+ let left = this.parseUnary();
170
+ while (this.peek().type === 'operator' && (this.peek().text === '*' || this.peek().text === '/' || this.peek().text === '%')) {
171
+ const operator = this.peek().text;
172
+ this.index++;
173
+ left = { kind: 'binary', op: operator, left, right: this.parseUnary() };
174
+ }
175
+ return left;
176
+ }
177
+
151
178
  parseUnary() {
152
179
  if (this.matchOperator('!')) return { kind: 'unary', op: '!', argument: this.parseUnary() };
153
180
  return this.parsePrimary();
@@ -254,6 +281,25 @@ class ExpressionParser {
254
281
  }
255
282
  }
256
283
 
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
+
257
303
  function refNode(parts) {
258
304
  const [root, ...rest] = parts;
259
305
  const scope = root === 'input' || root === 'state' || root === 'patches' ? root : 'local';
@@ -264,3 +310,7 @@ function refNode(parts) {
264
310
  function fail(reason) {
265
311
  return { ok: false, reason };
266
312
  }
313
+
314
+ function compactRecord(record) {
315
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
316
+ }
@@ -94,7 +94,7 @@ function validateActionRow(rowKind, rawName, rest, header) {
94
94
  if (rowKind === 'if') return validateActionExpressionText(readIfCondition(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) });
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) }) : undefined;
116
+ if (parsed?.ok) return { ok: true };
117
+ if (parsed?.reason === 'missing-action-expression-type' || parsed?.reason === 'unsupported-action-expression-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,10 @@ 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
+
129
137
  function readIfCondition(header) {
130
138
  const text = header.replace(/^if\b/, '').replace(/\{\s*$/, '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
131
139
  const explicit = /\bcondition\s+(.+)$/.exec(text);
@@ -141,8 +149,8 @@ function stripIds(text) {
141
149
  return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
142
150
  }
143
151
 
144
- function validateActionExpressionText(text) {
145
- const parsed = parseActionValue(text);
152
+ function validateActionExpressionText(text, options = {}) {
153
+ const parsed = parseActionValue(text, options);
146
154
  return parsed.ok ? { ok: true } : { ok: false, reason: parsed.reason };
147
155
  }
148
156
 
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.46",
4
4
  "description": "Parser for the first Frontier Lang .frontier syntax slice.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",