exprify 1.0.4 → 1.0.6

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.
@@ -1,16 +1,45 @@
1
+ /** @param {string | any[]} tokens */
1
2
  export function buildAST(tokens) {
2
3
  let current = 0;
3
4
 
4
5
  const peek = () => tokens[current];
5
6
  const consume = () => tokens[current++];
7
+ const lastPos = () => {
8
+ const t = current > 0 ? tokens[current - 1] : null;
9
+ return t && t.pos !== undefined ? t.pos : -1;
10
+ };
11
+ const tokenPos = () => {
12
+ const t = peek();
13
+ return t && t.pos !== undefined ? t.pos : -1;
14
+ };
15
+
16
+ const nodeAt = (/** @type {any} */ node) => {
17
+ const pos = lastPos();
18
+ if (pos >= 0) {
19
+ node.pos = pos;
20
+ }
21
+ return node;
22
+ };
23
+
24
+ const syntaxError = (/** @type {string} */ msg) => {
25
+ const pos = tokenPos() >= 0 ? tokenPos() : lastPos();
26
+ const at = pos >= 0 ? ` at position ${pos}` : '';
27
+ throw new Error(`${msg}${at}`);
28
+ };
6
29
 
7
- const match = (type, value) => {
30
+ const match = (/** @type {string} */ type, /** @type {string | undefined} */ value) => {
8
31
  const t = peek();
9
- if (!t) return false;
32
+ if (!t) {
33
+ return false;
34
+ }
10
35
 
11
- if (t.type !== type) return false;
36
+ if (t.type !== type) {
37
+ return false;
38
+ }
12
39
 
13
- if (value !== undefined && t.value !== value) return false;
40
+ if (value !== undefined && t.value !== value) {
41
+ return false;
42
+ }
14
43
 
15
44
  current++;
16
45
  return true;
@@ -19,203 +48,208 @@ export function buildAST(tokens) {
19
48
  const parseSliceOrIndex = () => {
20
49
  let start = null;
21
50
 
22
- if (!(peek()?.type === "Colon" || peek()?.type === "Comma" || peek()?.type === "ArrayEnd")) {
51
+ if (!(peek()?.type === 'Colon' || peek()?.type === 'Comma' || peek()?.type === 'ArrayEnd')) {
23
52
  start = parseExpression();
24
53
  }
25
54
 
26
- if (match("Colon")) {
55
+ if (match('Colon', undefined)) {
27
56
  let end = null;
28
57
 
29
- if (!(peek()?.type === "Comma" || peek()?.type === "ArrayEnd")) {
58
+ if (!(peek()?.type === 'Comma' || peek()?.type === 'ArrayEnd')) {
30
59
  end = parseExpression();
31
60
  }
32
61
 
33
62
  return {
34
- type: "SliceExpression",
63
+ type: 'SliceExpression',
35
64
  start,
36
- end
65
+ end,
37
66
  };
38
67
  }
39
68
 
40
69
  return start;
41
70
  };
42
71
 
43
- /* ================= PRIMARY ================= */
44
72
  function parsePrimary() {
45
73
  const token = consume();
46
- if (!token) throw new Error("Unexpected end of input");
74
+ if (!token) {
75
+ syntaxError('Unexpected end of input');
76
+ }
77
+
78
+ const withPos = (/** @type {any} */ node) => {
79
+ if (token.pos !== undefined) {
80
+ node.pos = token.pos;
81
+ }
82
+ return node;
83
+ };
47
84
 
48
85
  switch (token.type) {
49
- case "Number":
50
- case "BigInt":
51
- case "Boolean":
52
- case "String":
53
- return { type: "Literal", value: token.value };
54
-
55
- case "ImaginaryLiteral":
56
- return { type: "ImaginaryLiteral", value: token.value };
57
-
58
- case "NumberWithUnit":
59
- return {
60
- type: "UnitLiteral",
86
+ case 'Number':
87
+ case 'BigInt':
88
+ case 'Boolean':
89
+ case 'String':
90
+ return withPos({ type: 'Literal', value: token.value });
91
+
92
+ case 'ImaginaryLiteral':
93
+ return withPos({ type: 'ImaginaryLiteral', value: token.value });
94
+
95
+ case 'NumberWithUnit':
96
+ return withPos({
97
+ type: 'UnitLiteral',
61
98
  value: token.value,
62
- unit: token.unit
63
- };
64
-
65
- case "Identifier":
66
- return { type: "Identifier", name: token.name };
67
-
68
- case "Function":
69
- return {
70
- type: "Identifier",
71
- name: token.name
72
- };
73
-
74
- case "Parenthesis":
75
- if (token.value === "(") {
99
+ unit: token.unit,
100
+ });
101
+
102
+ case 'Identifier':
103
+ return withPos({ type: 'Identifier', name: token.name });
104
+
105
+ case 'Function':
106
+ return withPos({
107
+ type: 'Identifier',
108
+ name: token.name,
109
+ });
110
+
111
+ case 'Parenthesis':
112
+ if (token.value === '(') {
76
113
  const expr = parseExpression();
77
114
 
78
- if (!match("Parenthesis", ")")) {
79
- throw new Error(`Expected ')'`);
115
+ if (!match('Parenthesis', ')')) {
116
+ syntaxError("Expected ')'");
80
117
  }
81
118
 
82
119
  return expr;
83
120
  }
84
-
85
- case "ArrayStart": {
121
+
122
+ // falls through
123
+
124
+ case 'ArrayStart': {
86
125
  const rows = [];
87
126
  let currentRow = [];
88
127
 
89
- if (!match("ArrayEnd")) {
128
+ if (!match('ArrayEnd', undefined)) {
90
129
  while (true) {
91
130
  currentRow.push(parseExpression());
92
131
 
93
- if (match("Comma")) {
132
+ if (match('Comma', undefined)) {
94
133
  continue;
95
134
  }
96
135
 
97
- if (match("Semicolon")) {
136
+ if (match('Semicolon', undefined)) {
98
137
  rows.push(currentRow);
99
138
  currentRow = [];
100
139
  continue;
101
140
  }
102
141
 
103
- if (match("ArrayEnd")) {
142
+ if (match('ArrayEnd', undefined)) {
104
143
  rows.push(currentRow);
105
144
  break;
106
145
  }
107
146
 
108
- throw new Error(`Expected ',', ';', or ']' at ${current}`);
147
+ syntaxError("Expected ',', ';', or ']'");
109
148
  }
110
149
  }
111
150
 
112
151
  if (!rows.length) {
113
- return { type: "ArrayExpression", elements: [] };
152
+ return withPos({ type: 'ArrayExpression', elements: [] });
114
153
  }
115
154
 
116
155
  if (rows.length === 1) {
117
- return { type: "ArrayExpression", elements: rows[0] };
156
+ return withPos({ type: 'ArrayExpression', elements: rows[0] });
118
157
  }
119
158
 
120
- return {
121
- type: "ArrayExpression",
159
+ return withPos({
160
+ type: 'ArrayExpression',
122
161
  elements: rows.map((elements) => ({
123
- type: "ArrayExpression",
124
- elements
125
- }))
126
- };
162
+ type: 'ArrayExpression',
163
+ elements,
164
+ })),
165
+ });
127
166
  }
128
167
 
129
- case "BlockStart": {
168
+ case 'BlockStart': {
130
169
  const properties = [];
131
170
 
132
- if (!match("BlockEnd")) {
171
+ if (!match('BlockEnd', undefined)) {
133
172
  do {
134
173
  const keyToken = consume();
135
174
 
136
- if (
137
- keyToken.type !== "Identifier" &&
138
- keyToken.type !== "String"
139
- ) {
140
- throw new Error("Invalid object key");
175
+ if (keyToken.type !== 'Identifier' && keyToken.type !== 'String') {
176
+ syntaxError('Invalid object key');
141
177
  }
142
178
 
143
- if (!match("Colon")) {
144
- throw new Error("Expected ':' after key");
179
+ if (!match('Colon', undefined)) {
180
+ syntaxError("Expected ':' after key");
145
181
  }
146
182
 
147
183
  const value = parseExpression();
148
184
 
149
185
  properties.push({
150
186
  key: keyToken.value,
151
- value
187
+ value,
152
188
  });
189
+ } while (match('Comma', undefined));
153
190
 
154
- } while (match("Comma"));
155
-
156
- if (!match("BlockEnd")) {
157
- throw new Error(`Expected '}' at ${current}`);
191
+ if (!match('BlockEnd', undefined)) {
192
+ syntaxError("Expected '}'");
158
193
  }
159
194
  }
160
195
 
161
- return { type: "ObjectExpression", properties };
196
+ return withPos({ type: 'ObjectExpression', properties });
162
197
  }
163
198
  }
164
199
 
165
- throw new Error(`Unexpected token: ${JSON.stringify(token)}`);
200
+ syntaxError(`Unexpected token: ${JSON.stringify(token.value || token.name || token.type)}`);
166
201
  }
167
202
 
168
- /* ================= MEMBER ================= */
169
203
  function parseMember() {
170
204
  let object = parsePrimary();
171
205
 
172
206
  while (true) {
173
- if (match("ArrayStart")) {
207
+ if (match('ArrayStart', undefined)) {
174
208
  const selectors = [];
175
209
 
176
- if (!match("ArrayEnd")) {
210
+ if (!match('ArrayEnd', undefined)) {
177
211
  do {
178
212
  selectors.push(parseSliceOrIndex());
179
- } while (match("Comma"));
213
+ } while (match('Comma', undefined));
180
214
 
181
- if (!match("ArrayEnd")) {
182
- throw new Error(`Expected ']' at ${current}`);
215
+ if (!match('ArrayEnd', undefined)) {
216
+ syntaxError("Expected ']'");
183
217
  }
184
218
  }
185
219
 
186
- object = {
187
- type: "IndexExpression",
220
+ object = nodeAt({
221
+ type: 'IndexExpression',
188
222
  object,
189
- selectors
190
- };
223
+ selectors,
224
+ });
191
225
  continue;
192
226
  }
193
227
 
194
- if (match("Dot")) {
228
+ if (match('Dot', undefined)) {
195
229
  const property = consume();
196
230
 
197
- if (property.type !== "Identifier") {
198
- throw new Error("Expected property after '.'");
231
+ if (property.type !== 'Identifier') {
232
+ syntaxError("Expected property after '.'");
199
233
  }
200
234
 
201
- object = {
202
- type: "MemberExpression",
235
+ object = nodeAt({
236
+ type: 'MemberExpression',
203
237
  object,
204
- property: { type: "Identifier", name: property.value },
205
- optional: false
206
- };
238
+ property: { type: 'Identifier', name: property.value },
239
+ optional: false,
240
+ });
207
241
  continue;
208
242
  }
209
243
 
210
- if (match("Operator", "?.")) {
244
+ if (match('Operator', '?.')) {
211
245
  const property = consume();
212
246
 
213
- object = {
214
- type: "MemberExpression",
247
+ object = nodeAt({
248
+ type: 'MemberExpression',
215
249
  object,
216
- property: { type: "Identifier", name: property.value },
217
- optional: true
218
- };
250
+ property: { type: 'Identifier', name: property.value },
251
+ optional: true,
252
+ });
219
253
  continue;
220
254
  }
221
255
 
@@ -225,296 +259,322 @@ export function buildAST(tokens) {
225
259
  return object;
226
260
  }
227
261
 
228
- /* ================= CALL ================= */
229
262
  function parseCallChain() {
230
263
  let expr = parseMember();
231
264
 
232
- while (peek()?.type === "Parenthesis" && peek()?.value === "(") {
233
- consume(); // '('
265
+ while (peek()?.type === 'Parenthesis' && peek()?.value === '(') {
266
+ consume();
234
267
 
235
268
  const args = [];
236
269
 
237
- if (!(peek()?.type === "Parenthesis" && peek()?.value === ")")) {
270
+ if (!(peek()?.type === 'Parenthesis' && peek()?.value === ')')) {
238
271
  do {
239
- args.push(parseExpression());
240
- } while (match("Comma"));
272
+ if (match('Spread', undefined)) {
273
+ const arg = parseExpression();
274
+ args.push({ type: 'SpreadElement', argument: arg });
275
+ } else {
276
+ args.push(parseExpression());
277
+ }
278
+ } while (match('Comma', undefined));
241
279
  }
242
280
 
243
- if (!match("Parenthesis", ")")) {
244
- throw new Error(`Expected ')' at ${current}`);
281
+ if (!match('Parenthesis', ')')) {
282
+ syntaxError("Expected ')'");
245
283
  }
246
284
 
247
- expr = {
248
- type: "CallExpression",
285
+ expr = nodeAt({
286
+ type: 'CallExpression',
249
287
  callee: expr,
250
- arguments: args
251
- };
288
+ arguments: args,
289
+ });
252
290
  }
253
291
 
254
292
  return expr;
255
293
  }
256
294
 
257
- /* ================= UNARY ================= */
258
295
  function parseUnary() {
259
- if (match("UnaryOperator")) {
296
+ if (match('UnaryOperator', undefined)) {
260
297
  const operator = tokens[current - 1].value;
261
298
 
262
- return {
263
- type: "UnaryExpression",
299
+ return nodeAt({
300
+ type: 'UnaryExpression',
264
301
  operator,
265
- argument: parseUnary()
266
- };
302
+ argument: parseUnary(),
303
+ });
267
304
  }
268
305
 
269
306
  return parseCallChain();
270
307
  }
271
308
 
272
- /* ================= POWER ================= */
273
309
  function parsePower() {
274
- let left = parseUnary();
310
+ const left = parseUnary();
275
311
 
276
- if (match("Operator", "^")) {
312
+ if (match('Operator', '^')) {
277
313
  const right = parsePower();
278
- return {
279
- type: "BinaryExpression",
280
- operator: "^",
314
+ return nodeAt({
315
+ type: 'BinaryExpression',
316
+ operator: '^',
281
317
  left,
282
- right
283
- };
318
+ right,
319
+ });
284
320
  }
285
321
 
286
322
  return left;
287
323
  }
288
324
 
289
- /* ================= MULT ================= */
290
325
  function parseMultiplication() {
291
326
  let left = parsePower();
292
327
 
293
- while (
294
- match("Operator", "*") ||
295
- match("Operator", "/") ||
296
- match("Operator", "%")
297
- ) {
328
+ while (match('Operator', '*') || match('Operator', '/') || match('Operator', '%')) {
298
329
  const operator = tokens[current - 1].value;
299
330
  const right = parsePower();
300
331
 
301
- left = {
302
- type: "BinaryExpression",
332
+ left = nodeAt({
333
+ type: 'BinaryExpression',
303
334
  operator,
304
335
  left,
305
- right
306
- };
336
+ right,
337
+ });
307
338
  }
308
339
 
309
340
  return left;
310
341
  }
311
342
 
312
- /* ================= ADD ================= */
313
343
  function parseAddition() {
314
344
  let left = parseMultiplication();
315
345
 
316
- while (match("Operator", "+") || match("Operator", "-")) {
346
+ while (match('Operator', '+') || match('Operator', '-')) {
317
347
  const operator = tokens[current - 1].value;
318
348
  const right = parseMultiplication();
319
349
 
320
- left = {
321
- type: "BinaryExpression",
350
+ left = nodeAt({
351
+ type: 'BinaryExpression',
322
352
  operator,
323
353
  left,
324
- right
325
- };
354
+ right,
355
+ });
326
356
  }
327
357
 
328
358
  return left;
329
359
  }
330
360
 
331
- /* ================= UNIT CONVERSION ================= */
332
361
  function parseUnitConversion() {
333
- let left = parseAddition();
362
+ const left = parseAddition();
334
363
 
335
364
  const nextKeyword = peek();
336
- if (nextKeyword?.type === "Keyword" && ["to", "in"].includes(nextKeyword.value)) {
365
+ if (nextKeyword?.type === 'Keyword' && ['to', 'in'].includes(nextKeyword.value)) {
337
366
  consume();
338
367
  const next = consume();
339
368
 
340
- if (!next || next.type !== "Unit") {
341
- throw new Error(`Expected unit after '${nextKeyword.value}'`);
369
+ if (!next || next.type !== 'Unit') {
370
+ syntaxError(`Expected unit after '${nextKeyword.value}'`);
342
371
  }
343
372
 
344
- return {
345
- type: "UnitConversion",
373
+ return nodeAt({
374
+ type: 'UnitConversion',
346
375
  from: left,
347
- to: next.value
348
- };
376
+ to: next.value,
377
+ });
349
378
  }
350
379
 
351
380
  return left;
352
381
  }
353
382
 
354
- /* ================= COMPARISON ================= */
355
383
  function parseComparison() {
356
384
  let left = parseUnitConversion();
357
385
 
358
386
  while (
359
- match("Operator", ">") ||
360
- match("Operator", "<") ||
361
- match("Operator", ">=") ||
362
- match("Operator", "<=") ||
363
- match("Operator", "==")
387
+ match('Operator', '>') ||
388
+ match('Operator', '<') ||
389
+ match('Operator', '>=') ||
390
+ match('Operator', '<=') ||
391
+ match('Operator', '==')
364
392
  ) {
365
393
  const operator = tokens[current - 1].value;
366
394
  const right = parseUnitConversion();
367
395
 
368
- left = {
369
- type: "BinaryExpression",
396
+ left = nodeAt({
397
+ type: 'BinaryExpression',
370
398
  operator,
371
399
  left,
372
- right
373
- };
400
+ right,
401
+ });
374
402
  }
375
403
 
376
404
  return left;
377
405
  }
378
406
 
379
- /* ================= LOGICAL ================= */
380
407
  function parseLogical() {
381
408
  let left = parseComparison();
382
409
 
383
- while (
384
- match("Operator", "&&") ||
385
- match("Operator", "||")
386
- ) {
410
+ while (match('Operator', '&&') || match('Operator', '||')) {
387
411
  const operator = tokens[current - 1].value;
388
412
  const right = parseComparison();
389
413
 
390
- left = {
391
- type: "LogicalExpression",
414
+ left = nodeAt({
415
+ type: 'LogicalExpression',
392
416
  operator,
393
417
  left,
394
- right
395
- };
418
+ right,
419
+ });
396
420
  }
397
421
 
398
422
  return left;
399
423
  }
400
424
 
401
- /* ================= NULLISH ================= */
402
425
  function parseNullish() {
403
426
  let left = parseLogical();
404
427
 
405
- while (match("Operator", "??")) {
428
+ while (match('Operator', '??')) {
406
429
  const right = parseLogical();
407
430
 
408
- left = {
409
- type: "LogicalExpression",
410
- operator: "??",
431
+ left = nodeAt({
432
+ type: 'LogicalExpression',
433
+ operator: '??',
411
434
  left,
412
- right
413
- };
435
+ right,
436
+ });
414
437
  }
415
438
 
416
439
  return left;
417
440
  }
418
441
 
419
- /* ================= TERNARY ================= */
420
442
  function parseTernary() {
421
- let test = parseNullish();
443
+ const test = parseNullish();
422
444
 
423
- if (match("Ternary", "?")) {
445
+ if (match('Ternary', '?')) {
424
446
  const consequent = parseExpression();
425
447
 
426
- if (!match("Ternary", ":")) {
427
- throw new Error("Expected ':' in ternary");
448
+ if (!match('Ternary', ':')) {
449
+ syntaxError("Expected ':' in ternary");
428
450
  }
429
451
 
430
452
  const alternate = parseExpression();
431
453
 
432
- return {
433
- type: "ConditionalExpression",
454
+ return nodeAt({
455
+ type: 'ConditionalExpression',
434
456
  test,
435
457
  consequent,
436
- alternate
437
- };
458
+ alternate,
459
+ });
460
+ }
461
+
462
+ if (match('Colon', undefined)) {
463
+ const end = parseNullish();
464
+
465
+ return nodeAt({
466
+ type: 'RangeExpression',
467
+ start: test,
468
+ end,
469
+ });
438
470
  }
439
471
 
440
472
  return test;
441
473
  }
442
474
 
443
- /* ================= PIPELINE ================= */
475
+ function parseLambda() {
476
+ const left = parsePipeline();
477
+
478
+ if (match('Operator', '->')) {
479
+ let params;
480
+ if (left.type === 'Identifier') {
481
+ params = [left.name];
482
+ } else if (left.type === 'ArrayExpression') {
483
+ params = left.elements.map((/** @type {{ type: string; name: any; }} */ el) => {
484
+ if (el.type !== 'Identifier') {
485
+ syntaxError('Lambda parameter must be an identifier');
486
+ }
487
+ return el.name;
488
+ });
489
+ } else {
490
+ syntaxError('Invalid lambda parameter');
491
+ }
492
+
493
+ const body = parseLambda();
494
+
495
+ return nodeAt({
496
+ type: 'ArrowFunctionExpression',
497
+ params,
498
+ body,
499
+ });
500
+ }
501
+
502
+ return left;
503
+ }
504
+
444
505
  function parsePipeline() {
445
506
  let left = parseTernary();
446
507
 
447
- while (match("Operator", "|>")) {
508
+ while (match('Operator', '|>')) {
448
509
  const right = parseTernary();
449
510
 
450
- left = {
451
- type: "PipelineExpression",
511
+ left = nodeAt({
512
+ type: 'PipelineExpression',
452
513
  left,
453
- right
454
- };
514
+ right,
515
+ });
455
516
  }
456
517
 
457
518
  return left;
458
519
  }
459
520
 
460
- /* ================= ASSIGNMENT ================= */
461
521
  function parseAssignment() {
462
- let left = parsePipeline();
522
+ const left = parseLambda();
463
523
 
464
524
  if (
465
- match("Operator", "=") ||
466
- match("Operator", "+=") ||
467
- match("Operator", "-=") ||
468
- match("Operator", "*=") ||
469
- match("Operator", "/=")
525
+ match('Operator', '=') ||
526
+ match('Operator', '+=') ||
527
+ match('Operator', '-=') ||
528
+ match('Operator', '*=') ||
529
+ match('Operator', '/=')
470
530
  ) {
471
531
  const operator = tokens[current - 1].value;
472
532
 
473
- if (left.type === "CallExpression") {
533
+ // f(a,b) = expr: treat as function definition, not assignment
534
+ if (left.type === 'CallExpression') {
474
535
  const isFunctionTarget =
475
- left.callee?.type === "Identifier" &&
476
- left.arguments.every((arg) => arg.type === "Identifier");
536
+ left.callee?.type === 'Identifier' &&
537
+ left.arguments.every((/** @type {{ type: string; }} */ arg) => arg.type === 'Identifier');
477
538
 
478
539
  if (!isFunctionTarget) {
479
- throw new Error("Invalid function definition");
540
+ syntaxError('Invalid function definition');
480
541
  }
481
542
 
482
543
  const right = parseAssignment();
483
544
 
484
- return {
485
- type: "FunctionAssignmentExpression",
545
+ return nodeAt({
546
+ type: 'FunctionAssignmentExpression',
486
547
  operator,
487
548
  left: {
488
- type: "Identifier",
489
- name: left.callee.name
549
+ type: 'Identifier',
550
+ name: left.callee.name,
490
551
  },
491
- params: left.arguments.map((arg) => arg.name),
492
- right
493
- };
552
+ params: left.arguments.map((/** @type {{ name: any; }} */ arg) => arg.name),
553
+ right,
554
+ });
494
555
  }
495
556
 
496
557
  if (
497
- left.type !== "Identifier" &&
498
- left.type !== "MemberExpression" &&
499
- left.type !== "IndexExpression"
558
+ left.type !== 'Identifier' &&
559
+ left.type !== 'MemberExpression' &&
560
+ left.type !== 'IndexExpression'
500
561
  ) {
501
- throw new Error("Invalid assignment target");
562
+ syntaxError('Invalid assignment target');
502
563
  }
503
564
 
504
565
  const right = parseAssignment();
505
566
 
506
- return {
507
- type: "AssignmentExpression",
567
+ return nodeAt({
568
+ type: 'AssignmentExpression',
508
569
  operator,
509
570
  left,
510
- right
511
- };
571
+ right,
572
+ });
512
573
  }
513
574
 
514
575
  return left;
515
576
  }
516
577
 
517
- /* ================= ENTRY ================= */
518
578
  function parseExpression() {
519
579
  return parseAssignment();
520
580
  }
@@ -522,8 +582,10 @@ export function buildAST(tokens) {
522
582
  const ast = parseExpression();
523
583
 
524
584
  if (current < tokens.length) {
585
+ const t = peek();
586
+ const pos = t && t.pos !== undefined ? ` at position ${t.pos}` : '';
525
587
  throw new Error(
526
- `Unexpected token at end: ${JSON.stringify(peek())}`
588
+ `Unexpected token "${t ? JSON.stringify(t.value || t.name || t.type) : '?'}"${pos}`
527
589
  );
528
590
  }
529
591