@cirru/parser.ts 0.0.8 → 0.0.10
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/.yarn/install-state.gz +0 -0
- package/.yarnrc.yml +1 -0
- package/Agents.md +198 -0
- package/lib/index.js +270 -281
- package/lib/index.js.map +1 -1
- package/lib/tree.d.ts +2 -1
- package/lib/tree.js +68 -25
- package/lib/tree.js.map +1 -1
- package/package.json +7 -6
- package/src/bench.ts +45 -0
- package/src/index.ts +266 -277
- package/src/parser.test.ts +54 -0
- package/src/tree.ts +66 -26
- package/tsconfig-compile.json +1 -1
- package/tsconfig.bench.json +11 -0
package/src/index.ts
CHANGED
|
@@ -1,291 +1,287 @@
|
|
|
1
|
-
import { ELexState
|
|
1
|
+
import { ELexState } from "./types";
|
|
2
2
|
import * as types from "./types";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveDollarComma, isOdd } from "./tree";
|
|
4
4
|
|
|
5
5
|
export type ICirruNode = types.ICirruNode;
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// Single-pass lex + build: eliminates the intermediate tokens[] array entirely.
|
|
8
|
+
// The lexer drives a stack-based tree builder directly — no token buffer, no pullToken closure.
|
|
9
|
+
//
|
|
10
|
+
// Stack model:
|
|
11
|
+
// result[] — top-level expressions being collected
|
|
12
|
+
// stack[] — in-progress arrays at deeper nesting (from indent or '(')
|
|
13
|
+
// current — innermost array currently being populated; null when between top-level exprs
|
|
14
|
+
//
|
|
15
|
+
// emitOpen() → push current onto stack, start a fresh array as current
|
|
16
|
+
// (or, if current is null, start the first/next top-level expression)
|
|
17
|
+
// emitClose() → complete current array; if stack is empty push to result, else push to parent
|
|
18
|
+
// emitToken() → append a string leaf to current
|
|
19
|
+
//
|
|
20
|
+
// "first" tracks whether emitOpen() has been called for the outermost wrapper yet.
|
|
21
|
+
// The original code used acc.unshift(open) + trailing closes; here we emit that open
|
|
22
|
+
// lazily on the first non-whitespace character so we avoid any end-of-input special case.
|
|
23
|
+
let lexAndBuild = (code: string): ICirruNode[] => {
|
|
24
|
+
const CHAR_SPACE = 32;
|
|
25
|
+
const CHAR_NEWLINE = 10;
|
|
26
|
+
const CHAR_DQUOTE = 34;
|
|
27
|
+
const CHAR_APOSTROPHE = 39;
|
|
28
|
+
const CHAR_LPAREN = 40;
|
|
29
|
+
const CHAR_RPAREN = 41;
|
|
30
|
+
const CHAR_BACKSLASH = 92;
|
|
31
|
+
const CHAR_R = 114;
|
|
32
|
+
const CHAR_T = 116;
|
|
33
|
+
const CHAR_N = 110;
|
|
34
|
+
const CHAR_U = 117;
|
|
8
35
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
let
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
pointer.push(cursor);
|
|
34
|
-
break;
|
|
35
|
-
} else {
|
|
36
|
-
console.log(JSON.stringify(cursor));
|
|
37
|
-
throw new Error("Unknown cursor");
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
};
|
|
36
|
+
const result: ICirruNode[] = [];
|
|
37
|
+
const stack: ICirruNode[][] = [];
|
|
38
|
+
let current: ICirruNode[] | null = null;
|
|
39
|
+
let hasDollar = false;
|
|
40
|
+
let hasComma = false;
|
|
41
|
+
|
|
42
|
+
let state = ELexState.indent as ELexState;
|
|
43
|
+
let buffer = ""; // string content when escape sequences appear
|
|
44
|
+
let level = 0;
|
|
45
|
+
// Track explicit balance separately from indentation-generated frames.
|
|
46
|
+
let parenDepth = 0;
|
|
47
|
+
let indentCount = 0;
|
|
48
|
+
let tokenStart = 0;
|
|
49
|
+
let stringStart = 0;
|
|
50
|
+
let stringHasEscape = false;
|
|
51
|
+
// Whether we've emitted the very first emitOpen() (the outer wrapper)
|
|
52
|
+
let first = true;
|
|
53
|
+
|
|
54
|
+
const len = code.length;
|
|
55
|
+
|
|
56
|
+
const emitOpen = () => {
|
|
57
|
+
if (current !== null) stack.push(current);
|
|
58
|
+
current = [];
|
|
59
|
+
};
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let expr = graspeExprs(pullToken);
|
|
49
|
-
acc.push(expr);
|
|
50
|
-
continue;
|
|
51
|
-
} else if (chunk == null) {
|
|
52
|
-
return acc;
|
|
53
|
-
} else if (chunk === ELexControl.close) {
|
|
54
|
-
throw new Error(`Unexpected ")"`);
|
|
61
|
+
const emitClose = () => {
|
|
62
|
+
const completed = current!;
|
|
63
|
+
if (stack.length === 0) {
|
|
64
|
+
result.push(completed);
|
|
65
|
+
current = null;
|
|
55
66
|
} else {
|
|
56
|
-
|
|
67
|
+
current = stack.pop()!;
|
|
68
|
+
current.push(completed);
|
|
57
69
|
}
|
|
58
|
-
}
|
|
59
|
-
};
|
|
70
|
+
};
|
|
60
71
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
return size / 2;
|
|
67
|
-
};
|
|
72
|
+
const emitToken = (tok: string) => {
|
|
73
|
+
(current as ICirruNode[]).push(tok);
|
|
74
|
+
if (tok === "$") hasDollar = true;
|
|
75
|
+
else if (tok === ",") hasComma = true;
|
|
76
|
+
};
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let code = initialCode;
|
|
74
|
-
// let count = 0;
|
|
78
|
+
const openParen = () => {
|
|
79
|
+
parenDepth++;
|
|
80
|
+
emitOpen();
|
|
81
|
+
};
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
const closeParen = () => {
|
|
84
|
+
if (parenDepth === 0) throw new Error("Unexpected closing parenthesis ')'");
|
|
85
|
+
parenDepth--;
|
|
86
|
+
emitClose();
|
|
87
|
+
};
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
default:
|
|
98
|
-
console.log(state);
|
|
99
|
-
throw new Error("Unkown state");
|
|
100
|
-
}
|
|
89
|
+
// Called whenever a new line's first non-whitespace character is encountered.
|
|
90
|
+
// newLevel = indentCount >> 1 (already validated as even before calling).
|
|
91
|
+
// On the very first call: lazily emit the outer open, then any extra opens for depth.
|
|
92
|
+
// Subsequent calls: emit close/open boundaries as the indent level changes.
|
|
93
|
+
const flushIndent = (newLevel: number) => {
|
|
94
|
+
if (first) {
|
|
95
|
+
first = false;
|
|
96
|
+
emitOpen(); // outer wrapper open (replaces acc.unshift at end of original)
|
|
97
|
+
for (let i = 0; i < newLevel; i++) emitOpen();
|
|
98
|
+
level = newLevel;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (newLevel === level) {
|
|
102
|
+
emitClose();
|
|
103
|
+
emitOpen(); // sibling expression boundary
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (newLevel > level) {
|
|
107
|
+
for (let i = 0; i < newLevel - level; i++) emitOpen();
|
|
101
108
|
} else {
|
|
102
|
-
let
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
case ELexState.space:
|
|
106
|
-
switch (c) {
|
|
107
|
-
case " ":
|
|
108
|
-
[acc, state, buffer] = [acc, ELexState.space, ""];
|
|
109
|
-
break;
|
|
110
|
-
case "\n":
|
|
111
|
-
[acc, state, buffer] = [acc, ELexState.indent, ""];
|
|
112
|
-
break;
|
|
113
|
-
case "(":
|
|
114
|
-
acc.push(ELexControl.open);
|
|
115
|
-
[state, buffer] = [ELexState.space, ""];
|
|
116
|
-
break;
|
|
117
|
-
case ")":
|
|
118
|
-
acc.push(ELexControl.close);
|
|
119
|
-
[state, buffer] = [ELexState.space, ""];
|
|
120
|
-
break;
|
|
121
|
-
case '"':
|
|
122
|
-
[acc, state, buffer] = [acc, ELexState.string, ""];
|
|
123
|
-
break;
|
|
124
|
-
default:
|
|
125
|
-
[acc, state, buffer] = [acc, ELexState.token, c];
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
case ELexState.token:
|
|
130
|
-
switch (c) {
|
|
131
|
-
case " ":
|
|
132
|
-
acc.push(buffer);
|
|
133
|
-
[state, buffer] = [ELexState.space, ""];
|
|
134
|
-
break;
|
|
135
|
-
case '"':
|
|
136
|
-
acc.push(buffer);
|
|
137
|
-
[state, buffer] = [ELexState.string, ""];
|
|
138
|
-
break;
|
|
139
|
-
case "\n":
|
|
140
|
-
acc.push(buffer);
|
|
141
|
-
[state, buffer] = [ELexState.indent, ""];
|
|
142
|
-
break;
|
|
143
|
-
case "(":
|
|
144
|
-
acc.push(buffer, ELexControl.open);
|
|
145
|
-
[state, buffer] = [ELexState.space, ""];
|
|
146
|
-
break;
|
|
147
|
-
case ")":
|
|
148
|
-
acc.push(buffer, ELexControl.close);
|
|
149
|
-
[state, buffer] = [ELexState.space, ""];
|
|
150
|
-
break;
|
|
151
|
-
default:
|
|
152
|
-
[acc, state, buffer] = [acc, ELexState.token, `${buffer}${c}`];
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
break;
|
|
156
|
-
case ELexState.string:
|
|
157
|
-
switch (c) {
|
|
158
|
-
case '"':
|
|
159
|
-
acc.push(buffer);
|
|
160
|
-
[state, buffer] = [ELexState.space, ""];
|
|
161
|
-
break;
|
|
162
|
-
case "\\":
|
|
163
|
-
[acc, state, buffer] = [acc, ELexState.escape, buffer];
|
|
164
|
-
break;
|
|
165
|
-
case "\n":
|
|
166
|
-
throw new Error("Expected newline in string");
|
|
167
|
-
default:
|
|
168
|
-
[acc, state, buffer] = [acc, ELexState.string, `${buffer}${c}`];
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
172
|
-
case ELexState.escape:
|
|
173
|
-
switch (c) {
|
|
174
|
-
case '"':
|
|
175
|
-
[acc, state, buffer] = [acc, ELexState.string, `${buffer}"`];
|
|
176
|
-
break;
|
|
177
|
-
case "'":
|
|
178
|
-
[acc, state, buffer] = [acc, ELexState.string, `${buffer}'`];
|
|
179
|
-
break;
|
|
180
|
-
case "t":
|
|
181
|
-
[acc, state, buffer] = [acc, ELexState.string, `${buffer}\t`];
|
|
182
|
-
break;
|
|
183
|
-
case "n":
|
|
184
|
-
[acc, state, buffer] = [acc, ELexState.string, `${buffer}\n`];
|
|
185
|
-
break;
|
|
186
|
-
case "r":
|
|
187
|
-
[acc, state, buffer] = [acc, ELexState.string, `${buffer}\r`];
|
|
188
|
-
break;
|
|
189
|
-
case "\\":
|
|
190
|
-
[acc, state, buffer] = [acc, ELexState.string, `${buffer}\\`];
|
|
191
|
-
break;
|
|
192
|
-
case "u":
|
|
193
|
-
console.warn(`unicode escaping not supported yet, ${code.slice(pointer - 1, pointer + 10)}...`);
|
|
194
|
-
[acc, state, buffer] = [acc, ELexState.string, `${buffer}\\u`];
|
|
195
|
-
break;
|
|
196
|
-
default:
|
|
197
|
-
throw new Error(`Unknown \\${c} in escape`);
|
|
198
|
-
}
|
|
199
|
-
break;
|
|
200
|
-
case ELexState.indent:
|
|
201
|
-
switch (c) {
|
|
202
|
-
case " ":
|
|
203
|
-
[acc, state, buffer] = [acc, ELexState.indent, `${buffer}${c}`];
|
|
204
|
-
break;
|
|
205
|
-
case "\n":
|
|
206
|
-
[acc, state, buffer] = [acc, ELexState.indent, ""];
|
|
207
|
-
break;
|
|
208
|
-
case '"':
|
|
209
|
-
acc.push(parseIndentation(buffer));
|
|
210
|
-
[state, buffer] = [ELexState.string, ""];
|
|
211
|
-
break;
|
|
212
|
-
case "(":
|
|
213
|
-
acc.push(parseIndentation(buffer), ELexControl.open);
|
|
214
|
-
[state, buffer] = [ELexState.space, ""];
|
|
215
|
-
break;
|
|
216
|
-
default:
|
|
217
|
-
acc.push(parseIndentation(buffer));
|
|
218
|
-
[state, buffer] = [ELexState.token, c];
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
break;
|
|
222
|
-
default:
|
|
223
|
-
console.log("Unknown state:", c);
|
|
224
|
-
return acc;
|
|
225
|
-
}
|
|
109
|
+
for (let i = 0; i < level - newLevel; i++) emitClose();
|
|
110
|
+
emitClose();
|
|
111
|
+
emitOpen();
|
|
226
112
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
let repeat = <T>(times: number, x: T) => {
|
|
232
|
-
let xs: T[] = [];
|
|
233
|
-
for (let i = 1; i <= times; i++) {
|
|
234
|
-
xs.push(x);
|
|
235
|
-
}
|
|
236
|
-
return xs;
|
|
237
|
-
};
|
|
113
|
+
level = newLevel;
|
|
114
|
+
};
|
|
238
115
|
|
|
239
|
-
let resolveIndentations = (initialTokens: LexList) => {
|
|
240
|
-
let acc: LexList = [];
|
|
241
|
-
let level = 0;
|
|
242
|
-
let tokens: LexList = initialTokens;
|
|
243
116
|
let pointer = 0;
|
|
244
|
-
while (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
117
|
+
while (pointer < len) {
|
|
118
|
+
const c = code.charCodeAt(pointer++);
|
|
119
|
+
switch (state) {
|
|
120
|
+
case ELexState.space:
|
|
121
|
+
switch (c) {
|
|
122
|
+
case CHAR_SPACE:
|
|
123
|
+
break;
|
|
124
|
+
case CHAR_NEWLINE:
|
|
125
|
+
state = ELexState.indent;
|
|
126
|
+
indentCount = 0;
|
|
127
|
+
break;
|
|
128
|
+
case CHAR_LPAREN:
|
|
129
|
+
openParen();
|
|
130
|
+
break;
|
|
131
|
+
case CHAR_RPAREN:
|
|
132
|
+
closeParen();
|
|
133
|
+
break;
|
|
134
|
+
case CHAR_DQUOTE:
|
|
135
|
+
state = ELexState.string;
|
|
136
|
+
stringStart = pointer;
|
|
137
|
+
stringHasEscape = false;
|
|
138
|
+
buffer = "";
|
|
139
|
+
break;
|
|
140
|
+
default:
|
|
141
|
+
state = ELexState.token;
|
|
142
|
+
tokenStart = pointer - 1;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
case ELexState.token:
|
|
147
|
+
switch (c) {
|
|
148
|
+
case CHAR_SPACE:
|
|
149
|
+
emitToken(code.slice(tokenStart, pointer - 1));
|
|
150
|
+
state = ELexState.space;
|
|
151
|
+
break;
|
|
152
|
+
case CHAR_DQUOTE:
|
|
153
|
+
emitToken(code.slice(tokenStart, pointer - 1));
|
|
154
|
+
state = ELexState.string;
|
|
155
|
+
stringStart = pointer;
|
|
156
|
+
stringHasEscape = false;
|
|
157
|
+
buffer = "";
|
|
158
|
+
break;
|
|
159
|
+
case CHAR_NEWLINE:
|
|
160
|
+
emitToken(code.slice(tokenStart, pointer - 1));
|
|
161
|
+
state = ELexState.indent;
|
|
162
|
+
indentCount = 0;
|
|
163
|
+
break;
|
|
164
|
+
case CHAR_LPAREN:
|
|
165
|
+
emitToken(code.slice(tokenStart, pointer - 1));
|
|
166
|
+
openParen();
|
|
167
|
+
state = ELexState.space;
|
|
168
|
+
break;
|
|
169
|
+
case CHAR_RPAREN:
|
|
170
|
+
emitToken(code.slice(tokenStart, pointer - 1));
|
|
171
|
+
closeParen();
|
|
172
|
+
state = ELexState.space;
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
break;
|
|
282
176
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
177
|
+
break;
|
|
178
|
+
case ELexState.string:
|
|
179
|
+
switch (c) {
|
|
180
|
+
case CHAR_DQUOTE:
|
|
181
|
+
emitToken(stringHasEscape ? buffer : code.slice(stringStart, pointer - 1));
|
|
182
|
+
state = ELexState.space;
|
|
183
|
+
break;
|
|
184
|
+
case CHAR_BACKSLASH:
|
|
185
|
+
if (!stringHasEscape) {
|
|
186
|
+
buffer = code.slice(stringStart, pointer - 1);
|
|
187
|
+
stringHasEscape = true;
|
|
188
|
+
}
|
|
189
|
+
state = ELexState.escape;
|
|
190
|
+
break;
|
|
191
|
+
case CHAR_NEWLINE:
|
|
192
|
+
throw new Error("Unexpected newline in string");
|
|
193
|
+
default:
|
|
194
|
+
if (stringHasEscape) buffer = buffer + code[pointer - 1];
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case ELexState.escape:
|
|
199
|
+
switch (c) {
|
|
200
|
+
case CHAR_DQUOTE:
|
|
201
|
+
buffer = buffer + '"';
|
|
202
|
+
break;
|
|
203
|
+
case CHAR_APOSTROPHE:
|
|
204
|
+
buffer = buffer + "'";
|
|
205
|
+
break;
|
|
206
|
+
case CHAR_T:
|
|
207
|
+
buffer = buffer + "\t";
|
|
208
|
+
break;
|
|
209
|
+
case CHAR_N:
|
|
210
|
+
buffer = buffer + "\n";
|
|
211
|
+
break;
|
|
212
|
+
case CHAR_R:
|
|
213
|
+
buffer = buffer + "\r";
|
|
214
|
+
break;
|
|
215
|
+
case CHAR_BACKSLASH:
|
|
216
|
+
buffer = buffer + "\\";
|
|
217
|
+
break;
|
|
218
|
+
case CHAR_U:
|
|
219
|
+
console.warn(`unicode escaping not supported yet, ${code.slice(pointer - 1, pointer + 10)}...`);
|
|
220
|
+
buffer = buffer + "\\u";
|
|
221
|
+
break;
|
|
222
|
+
default:
|
|
223
|
+
throw new Error(`Unknown \\${code[pointer - 1]} in escape`);
|
|
224
|
+
}
|
|
225
|
+
state = ELexState.string;
|
|
226
|
+
break;
|
|
227
|
+
case ELexState.indent:
|
|
228
|
+
switch (c) {
|
|
229
|
+
case CHAR_SPACE:
|
|
230
|
+
indentCount++;
|
|
231
|
+
break;
|
|
232
|
+
case CHAR_NEWLINE:
|
|
233
|
+
indentCount = 0;
|
|
234
|
+
break;
|
|
235
|
+
case CHAR_DQUOTE:
|
|
236
|
+
if (isOdd(indentCount)) throw new Error(`Invalid indentation size ${indentCount}`);
|
|
237
|
+
flushIndent(indentCount >> 1);
|
|
238
|
+
state = ELexState.string;
|
|
239
|
+
stringStart = pointer;
|
|
240
|
+
stringHasEscape = false;
|
|
241
|
+
buffer = "";
|
|
242
|
+
indentCount = 0;
|
|
243
|
+
break;
|
|
244
|
+
case CHAR_LPAREN:
|
|
245
|
+
if (isOdd(indentCount)) throw new Error(`Invalid indentation size ${indentCount}`);
|
|
246
|
+
flushIndent(indentCount >> 1);
|
|
247
|
+
openParen();
|
|
248
|
+
state = ELexState.space;
|
|
249
|
+
indentCount = 0;
|
|
250
|
+
break;
|
|
251
|
+
case CHAR_RPAREN:
|
|
252
|
+
throw new Error("Unexpected closing parenthesis ')' at line start");
|
|
253
|
+
default:
|
|
254
|
+
if (isOdd(indentCount)) throw new Error(`Invalid indentation size ${indentCount}`);
|
|
255
|
+
flushIndent(indentCount >> 1);
|
|
256
|
+
state = ELexState.token;
|
|
257
|
+
tokenStart = pointer - 1;
|
|
258
|
+
indentCount = 0;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
287
262
|
}
|
|
288
263
|
}
|
|
264
|
+
|
|
265
|
+
// Flush any token still in progress at end of input
|
|
266
|
+
if (state === ELexState.token) {
|
|
267
|
+
emitToken(code.slice(tokenStart, len));
|
|
268
|
+
} else if (state === ELexState.escape) {
|
|
269
|
+
throw new Error("Should not be escape");
|
|
270
|
+
} else if (state === ELexState.string) {
|
|
271
|
+
throw new Error("Should not be string");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (first) return []; // no content was seen
|
|
275
|
+
if (parenDepth !== 0) throw new Error("Unclosed parenthesis at end of input");
|
|
276
|
+
|
|
277
|
+
// Close remaining open levels (mirrors the original: level closes + 1 final close)
|
|
278
|
+
for (let i = 0; i < level; i++) emitClose();
|
|
279
|
+
emitClose();
|
|
280
|
+
|
|
281
|
+
if (hasDollar || hasComma) {
|
|
282
|
+
return resolveDollarComma(result);
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
289
285
|
};
|
|
290
286
|
|
|
291
287
|
/**
|
|
@@ -301,14 +297,7 @@ let resolveIndentations = (initialTokens: LexList) => {
|
|
|
301
297
|
* ```
|
|
302
298
|
*/
|
|
303
299
|
export let parse = (code: string) => {
|
|
304
|
-
|
|
305
|
-
let pointer = 0;
|
|
306
|
-
let pullToken = () => {
|
|
307
|
-
let c = tokens[pointer];
|
|
308
|
-
pointer += 1;
|
|
309
|
-
return c;
|
|
310
|
-
};
|
|
311
|
-
return resolveComma(resolveDollar(buildExprs(pullToken)));
|
|
300
|
+
return lexAndBuild(code);
|
|
312
301
|
};
|
|
313
302
|
|
|
314
303
|
/**
|
package/src/parser.test.ts
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
let fs = require("fs");
|
|
2
2
|
let path = require("path");
|
|
3
3
|
let { parse, parseOneLiner } = require("./index");
|
|
4
|
+
let { resolveDollar, resolveComma, resolveDollarComma } = require("./tree");
|
|
4
5
|
|
|
5
6
|
test("single quote", () => expect(parse('a "\\\'"')).toEqual([["a", "'"]]));
|
|
6
7
|
|
|
8
|
+
describe("parenthesis syntax errors", () => {
|
|
9
|
+
test.each([")", " )", "\n)", "a)", "a )", "a (b))", "a (b) )", "a\n )"])("rejects unmatched closing parenthesis: %j", (code) => {
|
|
10
|
+
expect(() => parse(code)).toThrow(/Unexpected closing parenthesis/);
|
|
11
|
+
expect(() => parseOneLiner(code)).toThrow(/Unexpected closing parenthesis/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test.each(["(", "a (b", "a ((b)", "a (b\n c"])("rejects unfinished parentheses: %j", (code) => {
|
|
15
|
+
expect(() => parse(code)).toThrow(/Unclosed parenthesis/);
|
|
16
|
+
expect(() => parseOneLiner(code)).toThrow(/Unclosed parenthesis/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("keeps quoted parentheses, balanced nesting and empty input", () => {
|
|
20
|
+
expect(parse('a "(" ")"')).toEqual([["a", "(", ")"]]);
|
|
21
|
+
expect(parse('a "\\\"(\\\""')).toEqual([["a", '"("']]);
|
|
22
|
+
expect(parse("a (b (c))")).toEqual([["a", ["b", ["c"]]]]);
|
|
23
|
+
expect(parse("a (b\n c)")).toEqual([["a", ["b", ["c"]]]]);
|
|
24
|
+
expect(parse("\n ")).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("preserves Cirru multiline frame closure and following tokens", () => {
|
|
28
|
+
expect(parse("a (b\n c) d")).toEqual([["a", ["b", ["c"], "d"]]]);
|
|
29
|
+
expect(parse("a (b\n c)\n d")).toEqual([["a", ["b", ["c"]], ["d"]]]);
|
|
30
|
+
expect(parse("a (b\n c) d (e)\nf")).toEqual([["a", ["b", ["c"], "d", ["e"]]], ["f"]]);
|
|
31
|
+
expect(parse("a (b\nc)")).toEqual([["a", ["b"], ["c"]]]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
7
35
|
test("demo", () => {
|
|
8
36
|
let code = fs.readFileSync(path.join(__dirname, "../test/cirru/demo.cirru"), "utf8");
|
|
9
37
|
let data = require(path.join(__dirname, "../test/ast/demo.json"));
|
|
@@ -121,3 +149,29 @@ describe("parseOneLiner", () => {
|
|
|
121
149
|
expect(() => parseOneLiner("add 1 2\nmul 3 4")).toThrow("Expected single expression, got 2 expressions");
|
|
122
150
|
});
|
|
123
151
|
});
|
|
152
|
+
|
|
153
|
+
describe("resolveDollarComma equivalence", () => {
|
|
154
|
+
let check = (data) => {
|
|
155
|
+
expect(resolveDollarComma(data)).toEqual(resolveComma(resolveDollar(data)));
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
test("empty", () => {
|
|
159
|
+
check([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("comma expansion", () => {
|
|
163
|
+
check(["set", [",", "a", "b"], "c"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("dollar nesting", () => {
|
|
167
|
+
check(["add", "1", "$", ["mul", "2", "3"], "4"]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("mixed unfolding shape", () => {
|
|
171
|
+
check([["set", ["add", "1", "$", [",", "x", "y"], ["add", "5", "$", ["add", "2"]]]]]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("nested head array stays nested", () => {
|
|
175
|
+
check([[[",", "a", "b"]], "tail"]);
|
|
176
|
+
});
|
|
177
|
+
});
|