langsagne 0.1.0
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/README.md +158 -0
- package/dist/index.js +715 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# langsagne
|
|
2
|
+
> Minimal programming language parser and execution experiment
|
|
3
|
+
|
|
4
|
+
## Play
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
bun run test
|
|
8
|
+
bun run exec examples/assignment
|
|
9
|
+
bun run exec -- --trace examples/loop
|
|
10
|
+
bun run exec -- --step examples/if-else
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Programs live in `examples/`. Use Enter or `n` for next, `p` for previous, and `q` to quit in step mode.
|
|
14
|
+
|
|
15
|
+
## Language
|
|
16
|
+
|
|
17
|
+
### Syntax
|
|
18
|
+
|
|
19
|
+
- numbers: `1`, `42`
|
|
20
|
+
- strings: `"hello"`, `'path/to/file'`
|
|
21
|
+
- variables: `a`, `count_1`
|
|
22
|
+
- assignment: `a = 1 + 2`
|
|
23
|
+
- arithmetic: `+`, `-`, `*`, `/` (numeric only)
|
|
24
|
+
- comparison: `<` (returns `1` or `0`)
|
|
25
|
+
- parentheses: `(1 + 2) * 3`
|
|
26
|
+
- blocks: `{ ... }`
|
|
27
|
+
- `if (expr) { ... } else { ... }`
|
|
28
|
+
- `while (expr) { ... }`
|
|
29
|
+
- `fn name(a, b) { ... }`
|
|
30
|
+
- `return expr;`
|
|
31
|
+
- function calls: `add(1, 2)`
|
|
32
|
+
- statements end with `;`
|
|
33
|
+
- last expression value is the program result
|
|
34
|
+
|
|
35
|
+
Not supported yet: string `+`, `==`, arrays, comments, forward references.
|
|
36
|
+
|
|
37
|
+
### System calls
|
|
38
|
+
|
|
39
|
+
| Call | Args | Result |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `print(...)` | zero or more expressions | prints to stdout, leaves last arg in `ax` |
|
|
42
|
+
| `assert(expr)` | one expression | throws if falsy |
|
|
43
|
+
| `clock()` | none | `Date.now()` |
|
|
44
|
+
|
|
45
|
+
Functions must be declared before use. Calls check arity (`add(1)` is an error).
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
<details>
|
|
50
|
+
<summary>Assignment and arithmetic</summary>
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
a = 1;
|
|
54
|
+
b = 2 + a;
|
|
55
|
+
b;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
→ 3
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
</details>
|
|
63
|
+
|
|
64
|
+
<details>
|
|
65
|
+
<summary>Loop</summary>
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
i = 0;
|
|
69
|
+
sum = 0;
|
|
70
|
+
while (i < 2) {
|
|
71
|
+
sum = sum + i;
|
|
72
|
+
i = i + 1;
|
|
73
|
+
}
|
|
74
|
+
sum;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
→ 1
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
<details>
|
|
84
|
+
<summary>If / else and multi-arg print</summary>
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
value = 0;
|
|
88
|
+
if (value) {
|
|
89
|
+
result = 1;
|
|
90
|
+
} else {
|
|
91
|
+
result = 2;
|
|
92
|
+
}
|
|
93
|
+
print('result =', result);
|
|
94
|
+
result;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
→ prints: result = 2
|
|
99
|
+
→ returns: 2
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
</details>
|
|
103
|
+
|
|
104
|
+
<details>
|
|
105
|
+
<summary>Functions</summary>
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
fn add(a, b) {
|
|
109
|
+
return a + b;
|
|
110
|
+
}
|
|
111
|
+
add(1, 2);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
→ 3
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
</details>
|
|
119
|
+
|
|
120
|
+
<details>
|
|
121
|
+
<summary>Failed assertion</summary>
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
assert(1);
|
|
125
|
+
assert(1 + 2 < 4);
|
|
126
|
+
assert(2 + 2 < 4);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
→ RUNTIME ERR: assert failed
|
|
131
|
+
at line 3
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
</details>
|
|
135
|
+
|
|
136
|
+
## API
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { execute } from './src/index.ts'
|
|
140
|
+
|
|
141
|
+
execute('a = 1; b = a + 2; b;') // 3
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`execute(code)` compiles and runs source, then returns the final value in `ax`.
|
|
145
|
+
|
|
146
|
+
For directive-level debugging:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { inspect } from './src/index.ts'
|
|
150
|
+
|
|
151
|
+
const { directives, trace, result } = inspect('a = 1; a;')
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Runtime code is under `src/` (`lexer/`, `compiler/`, `runtime/`, `debug/`). Tests are in `test/`.
|
|
155
|
+
|
|
156
|
+
## Plan
|
|
157
|
+
|
|
158
|
+
See [docs/design.md](docs/design.md) for the current gaps between this project and the c4-inspired language target.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
// src/lexer/source.ts
|
|
2
|
+
var i = 0;
|
|
3
|
+
var line = 1;
|
|
4
|
+
var column = 1;
|
|
5
|
+
var str = "";
|
|
6
|
+
var Source = {
|
|
7
|
+
get line() {
|
|
8
|
+
return line;
|
|
9
|
+
},
|
|
10
|
+
get column() {
|
|
11
|
+
return column;
|
|
12
|
+
},
|
|
13
|
+
get val() {
|
|
14
|
+
return str[i];
|
|
15
|
+
},
|
|
16
|
+
read() {
|
|
17
|
+
const ch = str[i++];
|
|
18
|
+
if (ch === `
|
|
19
|
+
`) {
|
|
20
|
+
line += 1;
|
|
21
|
+
column = 1;
|
|
22
|
+
} else {
|
|
23
|
+
column += 1;
|
|
24
|
+
}
|
|
25
|
+
return ch;
|
|
26
|
+
},
|
|
27
|
+
eof: () => i === str.length,
|
|
28
|
+
initialize(text) {
|
|
29
|
+
str = text;
|
|
30
|
+
i = 0;
|
|
31
|
+
line = 1;
|
|
32
|
+
column = 1;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/lexer/token-state.ts
|
|
37
|
+
var TokenState = {
|
|
38
|
+
token: null,
|
|
39
|
+
value: null,
|
|
40
|
+
startLine: 1,
|
|
41
|
+
startColumn: 1,
|
|
42
|
+
length: 0,
|
|
43
|
+
reset() {
|
|
44
|
+
TokenState.token = null;
|
|
45
|
+
TokenState.value = null;
|
|
46
|
+
TokenState.startLine = 1;
|
|
47
|
+
TokenState.startColumn = 1;
|
|
48
|
+
TokenState.length = 0;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/lexer/token-kind.ts
|
|
53
|
+
var tokenKindLabels = new Map;
|
|
54
|
+
var TokenKind = {
|
|
55
|
+
Number: 1,
|
|
56
|
+
Identifier: 2,
|
|
57
|
+
String: 3,
|
|
58
|
+
Else: 4,
|
|
59
|
+
If: 5,
|
|
60
|
+
Function: 6,
|
|
61
|
+
Return: 7,
|
|
62
|
+
While: 8,
|
|
63
|
+
Assign: 9,
|
|
64
|
+
LessThan: 10,
|
|
65
|
+
Add: 11,
|
|
66
|
+
Subtract: 12,
|
|
67
|
+
Multiply: 13,
|
|
68
|
+
Divide: 14,
|
|
69
|
+
label(value) {
|
|
70
|
+
return tokenKindLabels.get(value);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
for (const [name, value] of Object.entries(TokenKind)) {
|
|
74
|
+
if (typeof value === "number")
|
|
75
|
+
tokenKindLabels.set(value, name);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/lexer/tokenize.ts
|
|
79
|
+
function isDigit(chr) {
|
|
80
|
+
return chr != null && chr >= "0" && chr <= "9";
|
|
81
|
+
}
|
|
82
|
+
function isAlpha(chr) {
|
|
83
|
+
return chr != null && (chr >= "a" && chr <= "z" || chr >= "A" && chr <= "Z");
|
|
84
|
+
}
|
|
85
|
+
function setToken(token, value = null) {
|
|
86
|
+
TokenState.token = token;
|
|
87
|
+
TokenState.value = value;
|
|
88
|
+
}
|
|
89
|
+
function finishToken(startLine, startColumn) {
|
|
90
|
+
TokenState.startLine = startLine;
|
|
91
|
+
TokenState.startColumn = startColumn;
|
|
92
|
+
TokenState.length = Math.max(1, Source.column - startColumn);
|
|
93
|
+
}
|
|
94
|
+
function next() {
|
|
95
|
+
while (!Source.eof()) {
|
|
96
|
+
const startLine = Source.line;
|
|
97
|
+
const startColumn = Source.column;
|
|
98
|
+
const ch = Source.read();
|
|
99
|
+
if (isDigit(ch)) {
|
|
100
|
+
let value = +ch;
|
|
101
|
+
while (isDigit(Source.val)) {
|
|
102
|
+
value = value * 10 + +Source.read();
|
|
103
|
+
}
|
|
104
|
+
setToken(TokenKind.Number, value);
|
|
105
|
+
finishToken(startLine, startColumn);
|
|
106
|
+
return TokenState;
|
|
107
|
+
}
|
|
108
|
+
if (ch === '"' || ch === "'") {
|
|
109
|
+
let value = "";
|
|
110
|
+
while (!Source.eof() && Source.val !== ch) {
|
|
111
|
+
value += Source.read();
|
|
112
|
+
}
|
|
113
|
+
Source.read();
|
|
114
|
+
setToken(TokenKind.String, value);
|
|
115
|
+
finishToken(startLine, startColumn);
|
|
116
|
+
return TokenState;
|
|
117
|
+
}
|
|
118
|
+
if (isAlpha(ch) || ch === "_") {
|
|
119
|
+
let ident = ch;
|
|
120
|
+
while (isAlpha(Source.val) || Source.val === "_" || isDigit(Source.val)) {
|
|
121
|
+
ident += Source.read();
|
|
122
|
+
}
|
|
123
|
+
if (ident === "while")
|
|
124
|
+
setToken(TokenKind.While);
|
|
125
|
+
else if (ident === "if")
|
|
126
|
+
setToken(TokenKind.If);
|
|
127
|
+
else if (ident === "else")
|
|
128
|
+
setToken(TokenKind.Else);
|
|
129
|
+
else if (ident === "fn")
|
|
130
|
+
setToken(TokenKind.Function);
|
|
131
|
+
else if (ident === "return")
|
|
132
|
+
setToken(TokenKind.Return);
|
|
133
|
+
else
|
|
134
|
+
setToken(TokenKind.Identifier, ident);
|
|
135
|
+
finishToken(startLine, startColumn);
|
|
136
|
+
return TokenState;
|
|
137
|
+
}
|
|
138
|
+
if (ch === "+") {
|
|
139
|
+
setToken(TokenKind.Add);
|
|
140
|
+
finishToken(startLine, startColumn);
|
|
141
|
+
return TokenState;
|
|
142
|
+
}
|
|
143
|
+
if (ch === "-") {
|
|
144
|
+
setToken(TokenKind.Subtract);
|
|
145
|
+
finishToken(startLine, startColumn);
|
|
146
|
+
return TokenState;
|
|
147
|
+
}
|
|
148
|
+
if (ch === "*") {
|
|
149
|
+
setToken(TokenKind.Multiply);
|
|
150
|
+
finishToken(startLine, startColumn);
|
|
151
|
+
return TokenState;
|
|
152
|
+
}
|
|
153
|
+
if (ch === "/") {
|
|
154
|
+
setToken(TokenKind.Divide);
|
|
155
|
+
finishToken(startLine, startColumn);
|
|
156
|
+
return TokenState;
|
|
157
|
+
}
|
|
158
|
+
if (ch === "=") {
|
|
159
|
+
setToken(TokenKind.Assign);
|
|
160
|
+
finishToken(startLine, startColumn);
|
|
161
|
+
return TokenState;
|
|
162
|
+
}
|
|
163
|
+
if (ch === "<") {
|
|
164
|
+
setToken(TokenKind.LessThan);
|
|
165
|
+
finishToken(startLine, startColumn);
|
|
166
|
+
return TokenState;
|
|
167
|
+
}
|
|
168
|
+
if (ch === "(" || ch === ")" || ch === "{" || ch === "}" || ch === ";" || ch === ",") {
|
|
169
|
+
setToken(ch);
|
|
170
|
+
finishToken(startLine, startColumn);
|
|
171
|
+
return TokenState;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return TokenState;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/runtime/directive.ts
|
|
178
|
+
var Directive = {
|
|
179
|
+
CONST: "CONST",
|
|
180
|
+
LOAD: "LOAD",
|
|
181
|
+
STORE: "STORE",
|
|
182
|
+
PUSH: "PUSH",
|
|
183
|
+
JMP: "JMP",
|
|
184
|
+
BZ: "BZ",
|
|
185
|
+
ADD: "ADD",
|
|
186
|
+
SUB: "SUB",
|
|
187
|
+
MUL: "MUL",
|
|
188
|
+
DIV: "DIV",
|
|
189
|
+
LT: "LT",
|
|
190
|
+
PRINT: "PRINT",
|
|
191
|
+
ASSERT: "ASSERT",
|
|
192
|
+
CLOCK: "CLOCK",
|
|
193
|
+
CALL: "CALL",
|
|
194
|
+
RET: "RET",
|
|
195
|
+
EXIT: "EXIT"
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/runtime/storage.ts
|
|
199
|
+
var Store = {
|
|
200
|
+
ax: 0,
|
|
201
|
+
pc: 0,
|
|
202
|
+
vs: [],
|
|
203
|
+
env: new Map,
|
|
204
|
+
fns: new Map,
|
|
205
|
+
cs: [],
|
|
206
|
+
reset() {
|
|
207
|
+
this.ax = 0;
|
|
208
|
+
this.pc = 0;
|
|
209
|
+
this.vs = [];
|
|
210
|
+
this.env = new Map;
|
|
211
|
+
this.fns = new Map;
|
|
212
|
+
this.cs = [];
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/error.ts
|
|
217
|
+
class LangError extends Error {
|
|
218
|
+
scope;
|
|
219
|
+
detail;
|
|
220
|
+
sourceLine;
|
|
221
|
+
pc = null;
|
|
222
|
+
constructor(scope, detail, sourceLine = null) {
|
|
223
|
+
super(`${scope} ERR: ${detail}`);
|
|
224
|
+
this.scope = scope;
|
|
225
|
+
this.detail = detail;
|
|
226
|
+
this.sourceLine = sourceLine;
|
|
227
|
+
this.name = "LangError";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function error(scope, message, sourceLine = null) {
|
|
231
|
+
throw new LangError(scope, message, sourceLine);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/runtime/vm.ts
|
|
235
|
+
var directiveOperands = {
|
|
236
|
+
[Directive.CONST]: 1,
|
|
237
|
+
[Directive.LOAD]: 1,
|
|
238
|
+
[Directive.STORE]: 1,
|
|
239
|
+
[Directive.JMP]: 1,
|
|
240
|
+
[Directive.BZ]: 1,
|
|
241
|
+
[Directive.CALL]: 2,
|
|
242
|
+
[Directive.PRINT]: 1
|
|
243
|
+
};
|
|
244
|
+
function numberValue(value) {
|
|
245
|
+
return Number(value);
|
|
246
|
+
}
|
|
247
|
+
function popNumber() {
|
|
248
|
+
return numberValue(Store.vs.pop());
|
|
249
|
+
}
|
|
250
|
+
function currentFrame() {
|
|
251
|
+
const index = Store.cs.length - 1;
|
|
252
|
+
if (index < 0)
|
|
253
|
+
return null;
|
|
254
|
+
return Store.cs[index];
|
|
255
|
+
}
|
|
256
|
+
function snapshotEnv() {
|
|
257
|
+
const env = Object.fromEntries(Store.env);
|
|
258
|
+
const frame = currentFrame();
|
|
259
|
+
if (frame !== null) {
|
|
260
|
+
for (const [key, value] of frame.locals) {
|
|
261
|
+
env[key] = value;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return env;
|
|
265
|
+
}
|
|
266
|
+
function snapshot(pc = Store.pc) {
|
|
267
|
+
return {
|
|
268
|
+
pc,
|
|
269
|
+
ax: Store.ax,
|
|
270
|
+
vs: [...Store.vs],
|
|
271
|
+
env: snapshotEnv()
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function execute(text, shouldTrace) {
|
|
275
|
+
Store.pc = 0;
|
|
276
|
+
const trace = [];
|
|
277
|
+
let op;
|
|
278
|
+
while (true) {
|
|
279
|
+
const pc = Store.pc;
|
|
280
|
+
const before = snapshot(pc);
|
|
281
|
+
op = text[Store.pc++];
|
|
282
|
+
if (op == null)
|
|
283
|
+
break;
|
|
284
|
+
const operandCount = typeof op === "string" ? directiveOperands[op] ?? 0 : 0;
|
|
285
|
+
const operands = text.slice(Store.pc, Store.pc + operandCount);
|
|
286
|
+
if (op === Directive.CONST) {
|
|
287
|
+
Store.ax = text[Store.pc++];
|
|
288
|
+
} else if (op === Directive.LOAD) {
|
|
289
|
+
const name = String(text[Store.pc++]);
|
|
290
|
+
const frame = currentFrame();
|
|
291
|
+
if (frame !== null && frame.locals.has(name)) {
|
|
292
|
+
Store.ax = frame.locals.get(name);
|
|
293
|
+
} else {
|
|
294
|
+
Store.ax = Store.env.get(name);
|
|
295
|
+
}
|
|
296
|
+
} else if (op === Directive.STORE) {
|
|
297
|
+
const name = String(text[Store.pc++]);
|
|
298
|
+
const frame = currentFrame();
|
|
299
|
+
if (frame !== null) {
|
|
300
|
+
frame.locals.set(name, Store.ax);
|
|
301
|
+
} else {
|
|
302
|
+
Store.env.set(name, Store.ax);
|
|
303
|
+
}
|
|
304
|
+
} else if (op === Directive.PUSH) {
|
|
305
|
+
Store.vs.push(Store.ax);
|
|
306
|
+
} else if (op === Directive.JMP) {
|
|
307
|
+
Store.pc = Number(text[Store.pc]);
|
|
308
|
+
} else if (op === Directive.BZ) {
|
|
309
|
+
const target = Number(text[Store.pc++]);
|
|
310
|
+
if (!Store.ax)
|
|
311
|
+
Store.pc = target;
|
|
312
|
+
} else if (op === Directive.ADD) {
|
|
313
|
+
Store.ax = popNumber() + numberValue(Store.ax);
|
|
314
|
+
} else if (op === Directive.SUB) {
|
|
315
|
+
Store.ax = popNumber() - numberValue(Store.ax);
|
|
316
|
+
} else if (op === Directive.MUL) {
|
|
317
|
+
Store.ax = popNumber() * numberValue(Store.ax);
|
|
318
|
+
} else if (op === Directive.DIV) {
|
|
319
|
+
Store.ax = popNumber() / numberValue(Store.ax);
|
|
320
|
+
} else if (op === Directive.LT) {
|
|
321
|
+
Store.ax = popNumber() < numberValue(Store.ax) ? 1 : 0;
|
|
322
|
+
} else if (op === Directive.PRINT) {
|
|
323
|
+
const argc = Number(text[Store.pc++]);
|
|
324
|
+
if (argc > 0) {
|
|
325
|
+
const args = Store.vs.splice(-argc, argc);
|
|
326
|
+
console.log(...args);
|
|
327
|
+
} else {
|
|
328
|
+
console.log();
|
|
329
|
+
}
|
|
330
|
+
} else if (op === Directive.ASSERT) {
|
|
331
|
+
if (!Store.ax)
|
|
332
|
+
error("RUNTIME", "assert failed", directiveSites.get(pc)?.line ?? null);
|
|
333
|
+
} else if (op === Directive.CLOCK) {
|
|
334
|
+
Store.ax = Date.now();
|
|
335
|
+
} else if (op === Directive.CALL) {
|
|
336
|
+
const name = String(text[Store.pc++]);
|
|
337
|
+
const argc = Number(text[Store.pc++]);
|
|
338
|
+
const fn = Store.fns.get(name) ?? error("RUNTIME", `unknown function ${name}`, directiveSites.get(pc)?.line ?? null);
|
|
339
|
+
if (argc !== fn.params.length) {
|
|
340
|
+
error("RUNTIME", `${name} expected ${fn.params.length} args but got ${argc}`, directiveSites.get(pc)?.line ?? null);
|
|
341
|
+
}
|
|
342
|
+
const locals = new Map;
|
|
343
|
+
for (let index = fn.params.length - 1;index >= 0; index -= 1) {
|
|
344
|
+
const param = fn.params[index];
|
|
345
|
+
const value = Store.vs.pop();
|
|
346
|
+
locals.set(param, value);
|
|
347
|
+
}
|
|
348
|
+
Store.cs.push({ ret: Store.pc, locals });
|
|
349
|
+
Store.pc = fn.entry;
|
|
350
|
+
} else if (op === Directive.RET) {
|
|
351
|
+
const frame = Store.cs.pop() ?? error("RUNTIME", "RET without call frame", directiveSites.get(pc)?.line ?? null);
|
|
352
|
+
Store.pc = frame.ret;
|
|
353
|
+
} else if (op === Directive.EXIT) {
|
|
354
|
+
if (shouldTrace)
|
|
355
|
+
trace.push(traceStep(pc, op, operands, before));
|
|
356
|
+
break;
|
|
357
|
+
} else {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (shouldTrace)
|
|
361
|
+
trace.push(traceStep(pc, op, operands, before));
|
|
362
|
+
}
|
|
363
|
+
return trace;
|
|
364
|
+
}
|
|
365
|
+
var directiveItems = [];
|
|
366
|
+
var directiveSites = new Map;
|
|
367
|
+
function traceStep(pc, op, operands, before) {
|
|
368
|
+
const site = directiveSites.get(pc) ?? null;
|
|
369
|
+
return {
|
|
370
|
+
pc,
|
|
371
|
+
op,
|
|
372
|
+
operands,
|
|
373
|
+
sourceLine: site?.line ?? null,
|
|
374
|
+
sourceColumn: site?.column ?? null,
|
|
375
|
+
sourceLength: site?.length ?? null,
|
|
376
|
+
before,
|
|
377
|
+
after: snapshot()
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
var VM = {
|
|
381
|
+
directives: () => Object.freeze(directiveItems),
|
|
382
|
+
emit: (...items) => {
|
|
383
|
+
directiveItems.push(...items);
|
|
384
|
+
},
|
|
385
|
+
emitAll: (items) => {
|
|
386
|
+
directiveItems.push(...items);
|
|
387
|
+
},
|
|
388
|
+
execute: () => execute(directiveItems, false),
|
|
389
|
+
patch: (index, value) => {
|
|
390
|
+
directiveItems[index] = value;
|
|
391
|
+
},
|
|
392
|
+
pop: () => directiveItems.pop(),
|
|
393
|
+
position: () => directiveItems.length,
|
|
394
|
+
mark: (line2, column2, length) => {
|
|
395
|
+
directiveSites.set(directiveItems.length, { line: line2, column: column2, length });
|
|
396
|
+
},
|
|
397
|
+
registerFn: (name, params, entry) => {
|
|
398
|
+
Store.fns.set(name, { entry, params });
|
|
399
|
+
},
|
|
400
|
+
reset: () => {
|
|
401
|
+
directiveItems = [];
|
|
402
|
+
directiveSites = new Map;
|
|
403
|
+
},
|
|
404
|
+
trace: () => execute(directiveItems, true)
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// src/compiler/parse.ts
|
|
408
|
+
function emit(...items) {
|
|
409
|
+
VM.mark(TokenState.startLine, TokenState.startColumn, TokenState.length);
|
|
410
|
+
VM.emit(...items);
|
|
411
|
+
}
|
|
412
|
+
function expect(expected) {
|
|
413
|
+
if (TokenState.token !== expected) {
|
|
414
|
+
const actual = typeof TokenState.token === "number" ? TokenKind.label(TokenState.token) : TokenState.token;
|
|
415
|
+
error("PARSE", `expected ${expected} but get ${actual}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function token() {
|
|
419
|
+
return TokenState.token;
|
|
420
|
+
}
|
|
421
|
+
function param() {
|
|
422
|
+
expect(TokenKind.Identifier);
|
|
423
|
+
const name = String(TokenState.value);
|
|
424
|
+
next();
|
|
425
|
+
return name;
|
|
426
|
+
}
|
|
427
|
+
function paramList() {
|
|
428
|
+
const params = [];
|
|
429
|
+
expect("(");
|
|
430
|
+
next();
|
|
431
|
+
if (TokenState.token !== ")") {
|
|
432
|
+
params.push(param());
|
|
433
|
+
while (TokenState.token === ",") {
|
|
434
|
+
next();
|
|
435
|
+
params.push(param());
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
expect(")");
|
|
439
|
+
next();
|
|
440
|
+
return params;
|
|
441
|
+
}
|
|
442
|
+
function argList() {
|
|
443
|
+
let argc = 0;
|
|
444
|
+
expect("(");
|
|
445
|
+
next();
|
|
446
|
+
if (TokenState.token !== ")") {
|
|
447
|
+
expr(TokenKind.Assign);
|
|
448
|
+
argc += 1;
|
|
449
|
+
while (TokenState.token === ",") {
|
|
450
|
+
next();
|
|
451
|
+
emit(Directive.PUSH);
|
|
452
|
+
expr(TokenKind.Assign);
|
|
453
|
+
argc += 1;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
expect(")");
|
|
457
|
+
next();
|
|
458
|
+
if (argc > 0) {
|
|
459
|
+
emit(Directive.PUSH);
|
|
460
|
+
}
|
|
461
|
+
return argc;
|
|
462
|
+
}
|
|
463
|
+
function emptyArgList() {
|
|
464
|
+
expect("(");
|
|
465
|
+
next();
|
|
466
|
+
expect(")");
|
|
467
|
+
next();
|
|
468
|
+
}
|
|
469
|
+
function fnDecl() {
|
|
470
|
+
expect(TokenKind.Function);
|
|
471
|
+
next();
|
|
472
|
+
if (TokenState.token !== TokenKind.Identifier)
|
|
473
|
+
error("PARSE", "expected function name");
|
|
474
|
+
const name = String(TokenState.value);
|
|
475
|
+
next();
|
|
476
|
+
const params = paramList();
|
|
477
|
+
emit(Directive.JMP);
|
|
478
|
+
const skipTarget = VM.position();
|
|
479
|
+
emit(null);
|
|
480
|
+
const fnStart = VM.position();
|
|
481
|
+
VM.registerFn(name, params, fnStart);
|
|
482
|
+
block();
|
|
483
|
+
VM.patch(skipTarget, VM.position());
|
|
484
|
+
}
|
|
485
|
+
function statement() {
|
|
486
|
+
if (!TokenState.token && !Source.eof())
|
|
487
|
+
next();
|
|
488
|
+
if (!Source.eof() && TokenState.token === TokenKind.If) {
|
|
489
|
+
next();
|
|
490
|
+
expect("(");
|
|
491
|
+
next();
|
|
492
|
+
expr(TokenKind.Assign);
|
|
493
|
+
expect(")");
|
|
494
|
+
next();
|
|
495
|
+
emit(Directive.BZ);
|
|
496
|
+
const elseTarget = VM.position();
|
|
497
|
+
emit(null);
|
|
498
|
+
block();
|
|
499
|
+
if (TokenState.token === TokenKind.Else) {
|
|
500
|
+
emit(Directive.JMP);
|
|
501
|
+
const endTarget = VM.position();
|
|
502
|
+
emit(null);
|
|
503
|
+
VM.patch(elseTarget, VM.position());
|
|
504
|
+
next();
|
|
505
|
+
block();
|
|
506
|
+
VM.patch(endTarget, VM.position());
|
|
507
|
+
} else {
|
|
508
|
+
VM.patch(elseTarget, VM.position());
|
|
509
|
+
}
|
|
510
|
+
} else if (!Source.eof() && TokenState.token === TokenKind.While) {
|
|
511
|
+
next();
|
|
512
|
+
expect("(");
|
|
513
|
+
next();
|
|
514
|
+
const loopStart = VM.position();
|
|
515
|
+
expr(TokenKind.Assign);
|
|
516
|
+
expect(")");
|
|
517
|
+
next();
|
|
518
|
+
emit(Directive.BZ);
|
|
519
|
+
const exitTarget = VM.position();
|
|
520
|
+
emit(null);
|
|
521
|
+
block();
|
|
522
|
+
emit(Directive.JMP, loopStart);
|
|
523
|
+
VM.patch(exitTarget, VM.position());
|
|
524
|
+
} else if (TokenState.token === TokenKind.Return) {
|
|
525
|
+
next();
|
|
526
|
+
if (token() !== ";" && token() !== "}") {
|
|
527
|
+
expr(TokenKind.Assign);
|
|
528
|
+
}
|
|
529
|
+
emit(Directive.RET);
|
|
530
|
+
if (token() === ";") {
|
|
531
|
+
next();
|
|
532
|
+
}
|
|
533
|
+
} else if (TokenState.token === "{") {
|
|
534
|
+
block();
|
|
535
|
+
} else if (TokenState.token === ";") {
|
|
536
|
+
next();
|
|
537
|
+
} else {
|
|
538
|
+
expr(TokenKind.Assign);
|
|
539
|
+
if (TokenState.token === ";") {
|
|
540
|
+
next();
|
|
541
|
+
} else {
|
|
542
|
+
error("PARSE", `expected ; but get ${TokenKind.label(TokenState.token)}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function block() {
|
|
547
|
+
expect("{");
|
|
548
|
+
next();
|
|
549
|
+
while (!Source.eof() && TokenState.token !== "}") {
|
|
550
|
+
statement();
|
|
551
|
+
}
|
|
552
|
+
expect("}");
|
|
553
|
+
next();
|
|
554
|
+
}
|
|
555
|
+
function expr(level = 0) {
|
|
556
|
+
if (Source.eof())
|
|
557
|
+
return;
|
|
558
|
+
if (TokenState.token === TokenKind.Number) {
|
|
559
|
+
emit(Directive.CONST, TokenState.value);
|
|
560
|
+
next();
|
|
561
|
+
} else if (TokenState.token === TokenKind.String) {
|
|
562
|
+
emit(Directive.CONST, TokenState.value);
|
|
563
|
+
next();
|
|
564
|
+
} else if (TokenState.token === "(") {
|
|
565
|
+
expect("(");
|
|
566
|
+
next();
|
|
567
|
+
expr(TokenKind.Assign);
|
|
568
|
+
expect(")");
|
|
569
|
+
next();
|
|
570
|
+
} else if (TokenState.token === TokenKind.Identifier) {
|
|
571
|
+
const ident = String(TokenState.value);
|
|
572
|
+
next();
|
|
573
|
+
if (TokenState.token === "(") {
|
|
574
|
+
if (ident === "print") {
|
|
575
|
+
const argc = argList();
|
|
576
|
+
emit(Directive.PRINT, argc);
|
|
577
|
+
} else if (ident === "assert") {
|
|
578
|
+
expect("(");
|
|
579
|
+
next();
|
|
580
|
+
expr(TokenKind.Assign);
|
|
581
|
+
expect(")");
|
|
582
|
+
next();
|
|
583
|
+
emit(Directive.ASSERT);
|
|
584
|
+
} else if (ident === "clock") {
|
|
585
|
+
emptyArgList();
|
|
586
|
+
emit(Directive.CLOCK);
|
|
587
|
+
} else {
|
|
588
|
+
const argc = argList();
|
|
589
|
+
emit(Directive.CALL, ident, argc);
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
emit(Directive.LOAD, ident);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
while (TokenState.token >= level) {
|
|
596
|
+
if (TokenState.token === TokenKind.Add) {
|
|
597
|
+
next();
|
|
598
|
+
emit(Directive.PUSH);
|
|
599
|
+
expr(TokenKind.Multiply);
|
|
600
|
+
emit(Directive.ADD);
|
|
601
|
+
} else if (TokenState.token === TokenKind.Subtract) {
|
|
602
|
+
next();
|
|
603
|
+
emit(Directive.PUSH);
|
|
604
|
+
expr(TokenKind.Multiply);
|
|
605
|
+
emit(Directive.SUB);
|
|
606
|
+
} else if (TokenState.token === TokenKind.Multiply) {
|
|
607
|
+
next();
|
|
608
|
+
emit(Directive.PUSH);
|
|
609
|
+
expr(TokenKind.Multiply + 1);
|
|
610
|
+
emit(Directive.MUL);
|
|
611
|
+
} else if (TokenState.token === TokenKind.Divide) {
|
|
612
|
+
next();
|
|
613
|
+
emit(Directive.PUSH);
|
|
614
|
+
expr(TokenKind.Multiply + 1);
|
|
615
|
+
emit(Directive.DIV);
|
|
616
|
+
} else if (TokenState.token === TokenKind.LessThan) {
|
|
617
|
+
next();
|
|
618
|
+
emit(Directive.PUSH);
|
|
619
|
+
expr(TokenKind.Add);
|
|
620
|
+
emit(Directive.LT);
|
|
621
|
+
} else if (TokenState.token === ";") {
|
|
622
|
+
next();
|
|
623
|
+
} else if (TokenState.token === TokenKind.Assign) {
|
|
624
|
+
next();
|
|
625
|
+
const target = VM.pop();
|
|
626
|
+
const load = VM.pop();
|
|
627
|
+
if (load !== Directive.LOAD || typeof target !== "string") {
|
|
628
|
+
error("PARSE", "bad lvalue in assignment");
|
|
629
|
+
}
|
|
630
|
+
expr(TokenKind.Assign);
|
|
631
|
+
emit(Directive.STORE, target);
|
|
632
|
+
} else {
|
|
633
|
+
error("PARSE", "parsing fail " + TokenState.token);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function parse() {
|
|
638
|
+
emit(Directive.JMP);
|
|
639
|
+
const mainTarget = VM.position();
|
|
640
|
+
emit(null);
|
|
641
|
+
let mainStart = null;
|
|
642
|
+
while (!Source.eof()) {
|
|
643
|
+
if (!TokenState.token)
|
|
644
|
+
next();
|
|
645
|
+
if (TokenState.token === TokenKind.Function) {
|
|
646
|
+
fnDecl();
|
|
647
|
+
} else {
|
|
648
|
+
if (mainStart === null)
|
|
649
|
+
mainStart = VM.position();
|
|
650
|
+
statement();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (mainStart === null)
|
|
654
|
+
mainStart = VM.position();
|
|
655
|
+
VM.patch(mainTarget, mainStart);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/runtime/runtime.ts
|
|
659
|
+
function resetRuntime(code) {
|
|
660
|
+
TokenState.reset();
|
|
661
|
+
Store.reset();
|
|
662
|
+
VM.reset();
|
|
663
|
+
Source.initialize(code);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/debug/inspect.ts
|
|
667
|
+
function tokenLabel(token2) {
|
|
668
|
+
if (typeof token2 === "number")
|
|
669
|
+
return TokenKind.label(token2) ?? String(token2);
|
|
670
|
+
if (token2 === null)
|
|
671
|
+
return "EOF";
|
|
672
|
+
return token2;
|
|
673
|
+
}
|
|
674
|
+
function scanTokens(code) {
|
|
675
|
+
resetRuntime(code);
|
|
676
|
+
const tokens = [];
|
|
677
|
+
while (!Source.eof()) {
|
|
678
|
+
const state = next();
|
|
679
|
+
if (state.token !== null) {
|
|
680
|
+
tokens.push({
|
|
681
|
+
label: tokenLabel(state.token),
|
|
682
|
+
value: state.value,
|
|
683
|
+
line: state.startLine,
|
|
684
|
+
column: state.startColumn,
|
|
685
|
+
length: state.length
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return tokens;
|
|
690
|
+
}
|
|
691
|
+
function inspect(code) {
|
|
692
|
+
const tokens = scanTokens(code);
|
|
693
|
+
resetRuntime(code);
|
|
694
|
+
parse();
|
|
695
|
+
const trace = VM.trace();
|
|
696
|
+
return {
|
|
697
|
+
directives: VM.directives(),
|
|
698
|
+
env: Object.fromEntries(Store.env),
|
|
699
|
+
result: Store.ax,
|
|
700
|
+
tokens,
|
|
701
|
+
trace
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/index.ts
|
|
706
|
+
function execute2(code) {
|
|
707
|
+
resetRuntime(code);
|
|
708
|
+
parse();
|
|
709
|
+
VM.execute();
|
|
710
|
+
return Store.ax;
|
|
711
|
+
}
|
|
712
|
+
export {
|
|
713
|
+
inspect,
|
|
714
|
+
execute2 as execute
|
|
715
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "langsagne",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "minimal programming language parser and execution experiment",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"directories": {
|
|
12
|
+
"test": "test"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "rm -rf dist && bun build ./src/index.ts --outdir ./dist --target node --format esm",
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"exec": "bun scripts/exec.ts",
|
|
18
|
+
"web:build": "next build web",
|
|
19
|
+
"web:dev": "next dev web",
|
|
20
|
+
"web:start": "next start web",
|
|
21
|
+
"prepublishOnly": "bun run build"
|
|
22
|
+
},
|
|
23
|
+
"packageManager": "bun@1.3.13",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/huozhi/langsagne.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [],
|
|
29
|
+
"author": "",
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/huozhi/langsagne/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/huozhi/langsagne#readme",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@tailwindcss/postcss": "^4.3.0",
|
|
37
|
+
"@types/bun": "^1.3.14",
|
|
38
|
+
"@types/react": "^19.2.14",
|
|
39
|
+
"@types/react-dom": "^19.2.3",
|
|
40
|
+
"next": "^16.2.6",
|
|
41
|
+
"postcss": "^8.5.14",
|
|
42
|
+
"react": "^19.2.6",
|
|
43
|
+
"react-dom": "^19.2.6",
|
|
44
|
+
"tailwindcss": "^4.3.0",
|
|
45
|
+
"typescript": "^6.0.3"
|
|
46
|
+
}
|
|
47
|
+
}
|