arc-lang 0.5.2 → 0.5.4
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 +1 -1
- package/dist/ast.d.ts +6 -1
- package/dist/build.js +2 -2
- package/dist/codegen-js.js +79 -6
- package/dist/formatter.js +37 -5
- package/dist/interpreter.js +59 -14
- package/dist/ir.d.ts +6 -0
- package/dist/ir.js +72 -12
- package/dist/lexer.js +77 -1
- package/dist/modules.js +9 -1
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +58 -6
- package/dist/repl.js +25 -6
- package/dist/security.js +1 -1
- package/dist/version.js +29 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ Plus many built-in functions available without imports: `map`, `filter`, `reduce
|
|
|
96
96
|
|
|
97
97
|
## Status
|
|
98
98
|
|
|
99
|
-
🚀 **In Active Development** — Arc has a working compiler (lexer, parser, IR, optimizer, JS/WAT codegen), interpreter, REPL,
|
|
99
|
+
🚀 **In Active Development** — Arc has a working compiler (lexer, parser, IR, optimizer, JS/WAT codegen), interpreter, REPL, 17 stdlib modules, LSP, VS Code extension, package manager, build system, formatter, linter, security sandbox, rich error reporting, benchmarking framework, and migration tools. 508+ tests passing.
|
|
100
100
|
|
|
101
101
|
Current phase: **Phase 6 — Community & Adoption**
|
|
102
102
|
|
package/dist/ast.d.ts
CHANGED
|
@@ -177,7 +177,7 @@ export interface OrPattern {
|
|
|
177
177
|
patterns: Pattern[];
|
|
178
178
|
loc: Loc;
|
|
179
179
|
}
|
|
180
|
-
export type Stmt = LetStmt | FnStmt | ForStmt | DoStmt | ExprStmt | UseStmt | TypeStmt | AssignStmt | MemberAssignStmt | IndexAssignStmt;
|
|
180
|
+
export type Stmt = LetStmt | FnStmt | ForStmt | DoStmt | ExprStmt | UseStmt | TypeStmt | AssignStmt | MemberAssignStmt | IndexAssignStmt | RetStmt;
|
|
181
181
|
export interface AssignStmt {
|
|
182
182
|
kind: "AssignStmt";
|
|
183
183
|
target: string;
|
|
@@ -198,6 +198,11 @@ export interface IndexAssignStmt {
|
|
|
198
198
|
value: Expr;
|
|
199
199
|
loc: Loc;
|
|
200
200
|
}
|
|
201
|
+
export interface RetStmt {
|
|
202
|
+
kind: "RetStmt";
|
|
203
|
+
value?: Expr;
|
|
204
|
+
loc: Loc;
|
|
205
|
+
}
|
|
201
206
|
export interface LetStmt {
|
|
202
207
|
kind: "LetStmt";
|
|
203
208
|
name: string | DestructureTarget;
|
package/dist/build.js
CHANGED
|
@@ -127,8 +127,8 @@ export function newProject(name, parentDir) {
|
|
|
127
127
|
"dev-dependencies": {},
|
|
128
128
|
};
|
|
129
129
|
writeFileSync(resolve(projectDir, "arc.toml"), serializeArcToml(toml));
|
|
130
|
-
writeFileSync(resolve(projectDir, "src", "main.arc"), `fn main()
|
|
131
|
-
writeFileSync(resolve(projectDir, "tests", "main.test.arc"), `fn test_main()
|
|
130
|
+
writeFileSync(resolve(projectDir, "src", "main.arc"), `fn main() {\n let msg = "Hello from ${name}!"\n print(msg)\n}\n`);
|
|
131
|
+
writeFileSync(resolve(projectDir, "tests", "main.test.arc"), `fn test_main() {\n let x = 1 + 1\n print(x)\n}\n`);
|
|
132
132
|
writeFileSync(resolve(projectDir, "README.md"), `# ${name}\n\nAn Arc project.\n\n## Getting Started\n\n\`\`\`bash\narc build\narc run\n\`\`\`\n`);
|
|
133
133
|
console.log(`Created project '${name}'`);
|
|
134
134
|
console.log(` ${name}/arc.toml`);
|
package/dist/codegen-js.js
CHANGED
|
@@ -9,7 +9,8 @@ export function generateJS(module) {
|
|
|
9
9
|
lines.push(` len(a) { return Array.isArray(a) ? a.length : typeof a === "string" ? a.length : 0; },`);
|
|
10
10
|
lines.push(` str(v) { return String(v); },`);
|
|
11
11
|
lines.push(` push(arr, v) { arr.push(v); return arr; },`);
|
|
12
|
-
lines.push(`
|
|
12
|
+
lines.push(` __toStr(v) { if (v === null) return "nil"; if (typeof v === "boolean") return v ? "true" : "false"; if (typeof v === "number" || typeof v === "string") return String(v); if (Array.isArray(v)) return "[" + v.map(x => this.__toStr(x)).join(", ") + "]"; if (v && v.__map) { const entries = [...v.entries.entries()].map(([k, val]) => k + ": " + this.__toStr(val)); return "{" + entries.join(", ") + "}"; } return String(v); },`);
|
|
13
|
+
lines.push(` print(v) { console.log(this.__toStr(v)); },`);
|
|
13
14
|
lines.push(` head(a) { return a[0]; },`);
|
|
14
15
|
lines.push(` tail(a) { return a.slice(1); },`);
|
|
15
16
|
lines.push(` map(a, f) { return a.map(f); },`);
|
|
@@ -34,6 +35,9 @@ export function generateJS(module) {
|
|
|
34
35
|
lines.push(` replace(s, old, nw) { return s.replaceAll(old, nw); },`);
|
|
35
36
|
lines.push(` uppercase(s) { return s.toUpperCase(); },`);
|
|
36
37
|
lines.push(` lowercase(s) { return s.toLowerCase(); },`);
|
|
38
|
+
lines.push(` upper(s) { return s.toUpperCase(); },`);
|
|
39
|
+
lines.push(` lower(s) { return s.toLowerCase(); },`);
|
|
40
|
+
lines.push(` type_of(v) { if (v === null) return "nil"; if (typeof v === "boolean") return "bool"; if (typeof v === "number") return Number.isInteger(v) ? "int" : "float"; if (typeof v === "string") return "string"; if (Array.isArray(v)) return "list"; if (v && v.__map) return "map"; return "unknown"; },`);
|
|
37
41
|
lines.push(` sum(a) { return a.reduce((s, x) => s + x, 0); },`);
|
|
38
42
|
lines.push(` flat(a) { return a.flat(); },`);
|
|
39
43
|
lines.push(` zip(a, b) { return a.map((x, i) => [x, b[i]]); },`);
|
|
@@ -74,9 +78,71 @@ function emitFunction(fn, baseIndent = "") {
|
|
|
74
78
|
}
|
|
75
79
|
function emitBlock(block, indent) {
|
|
76
80
|
const lines = [];
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
const instrs = block.instrs;
|
|
82
|
+
const sections = [];
|
|
83
|
+
let currentSection = { label: "__start", instrs: [] };
|
|
84
|
+
sections.push(currentSection);
|
|
85
|
+
for (const instr of instrs) {
|
|
86
|
+
if (instr.op === "label") {
|
|
87
|
+
currentSection = { label: instr.name, instrs: [] };
|
|
88
|
+
sections.push(currentSection);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
currentSection.instrs.push(instr);
|
|
92
|
+
}
|
|
79
93
|
}
|
|
94
|
+
// If no labels exist, emit linearly (simple case)
|
|
95
|
+
if (sections.length === 1) {
|
|
96
|
+
for (const instr of instrs) {
|
|
97
|
+
lines.push(indent + emitInstr(instr));
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
101
|
+
// Use a state-machine approach with a __pc variable and a while/switch loop
|
|
102
|
+
// to properly handle branches and jumps
|
|
103
|
+
const labelToIdx = new Map();
|
|
104
|
+
for (let i = 0; i < sections.length; i++) {
|
|
105
|
+
labelToIdx.set(sections[i].label, i);
|
|
106
|
+
}
|
|
107
|
+
lines.push(`${indent}var __pc = 0;`);
|
|
108
|
+
lines.push(`${indent}__control: while (true) {`);
|
|
109
|
+
lines.push(`${indent} switch (__pc) {`);
|
|
110
|
+
for (let i = 0; i < sections.length; i++) {
|
|
111
|
+
const section = sections[i];
|
|
112
|
+
lines.push(`${indent} case ${i}: /* ${section.label} */`);
|
|
113
|
+
for (const instr of section.instrs) {
|
|
114
|
+
if (instr.op === "jump") {
|
|
115
|
+
const target = labelToIdx.get(instr.target);
|
|
116
|
+
if (target !== undefined) {
|
|
117
|
+
lines.push(`${indent} __pc = ${target}; continue __control;`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (instr.op === "branch") {
|
|
121
|
+
const ifTrue = labelToIdx.get(instr.ifTrue);
|
|
122
|
+
const ifFalse = labelToIdx.get(instr.ifFalse);
|
|
123
|
+
lines.push(`${indent} if (${S(instr.cond)}) { __pc = ${ifTrue}; } else { __pc = ${ifFalse}; } continue __control;`);
|
|
124
|
+
}
|
|
125
|
+
else if (instr.op === "ret") {
|
|
126
|
+
lines.push(`${indent} ` + emitInstr(instr));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
lines.push(`${indent} ` + emitInstr(instr));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Fall through to next section if no jump/branch/ret at end
|
|
133
|
+
const lastInstr = section.instrs[section.instrs.length - 1];
|
|
134
|
+
if (!lastInstr || (lastInstr.op !== "jump" && lastInstr.op !== "branch" && lastInstr.op !== "ret")) {
|
|
135
|
+
if (i + 1 < sections.length) {
|
|
136
|
+
lines.push(`${indent} __pc = ${i + 1}; continue __control;`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
lines.push(`${indent} break __control;`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
lines.push(`${indent} default: break __control;`);
|
|
144
|
+
lines.push(`${indent} }`);
|
|
145
|
+
lines.push(`${indent}}`);
|
|
80
146
|
return lines;
|
|
81
147
|
}
|
|
82
148
|
function emitInstr(instr) {
|
|
@@ -90,7 +156,13 @@ function emitInstr(instr) {
|
|
|
90
156
|
return `var ${S(instr.dest)} = ${S(instr.name)};`;
|
|
91
157
|
case "store":
|
|
92
158
|
if (instr.src.startsWith("@fn:")) {
|
|
93
|
-
|
|
159
|
+
const fnName = instr.src.slice(4);
|
|
160
|
+
// If storing a function reference to a variable with the same name,
|
|
161
|
+
// the hoisted function definition already provides it — skip redundant var
|
|
162
|
+
if (S(instr.name) === S(fnName)) {
|
|
163
|
+
return `/* ${S(instr.name)} = ${S(fnName)} (hoisted) */`;
|
|
164
|
+
}
|
|
165
|
+
return `var ${S(instr.name)} = ${S(fnName)};`;
|
|
94
166
|
}
|
|
95
167
|
return `var ${S(instr.name)} = ${S(instr.src)};`;
|
|
96
168
|
case "binop":
|
|
@@ -102,7 +174,7 @@ function emitInstr(instr) {
|
|
|
102
174
|
case "toolcall":
|
|
103
175
|
return `var ${S(instr.dest)} = await fetch(${S(instr.url)}, { method: ${JSON.stringify(instr.method)}${instr.body ? `, body: JSON.stringify(${S(instr.body)})` : ""} }).then(r => r.json());`;
|
|
104
176
|
case "field":
|
|
105
|
-
return `var ${S(instr.dest)} = (${S(instr.obj)}
|
|
177
|
+
return `var ${S(instr.dest)} = (${S(instr.obj)} == null) ? null : (${S(instr.obj)}.__map) ? ${S(instr.obj)}.entries.get(${JSON.stringify(instr.prop)}) : ${S(instr.obj)}[${JSON.stringify(instr.prop)}];`;
|
|
106
178
|
case "index":
|
|
107
179
|
return `var ${S(instr.dest)} = ${S(instr.obj)}[${S(instr.idx)}];`;
|
|
108
180
|
case "setfield":
|
|
@@ -155,7 +227,8 @@ function emitCall(fn, args) {
|
|
|
155
227
|
"range", "keys", "values", "type", "abs", "max", "min", "floor", "ceil",
|
|
156
228
|
"round", "sort", "reverse", "contains", "join", "split", "trim", "replace",
|
|
157
229
|
"uppercase", "lowercase", "sum", "flat", "zip", "enumerate", "slice",
|
|
158
|
-
"append", "concat", "unique", "int", "float", "__await", "__fetch_parallel"
|
|
230
|
+
"append", "concat", "unique", "int", "float", "__await", "__fetch_parallel",
|
|
231
|
+
"upper", "lower", "type_of", "__toStr"
|
|
159
232
|
];
|
|
160
233
|
if (builtins.includes(fn)) {
|
|
161
234
|
return `__arc_runtime.${fn}(${args.join(", ")})`;
|
package/dist/formatter.js
CHANGED
|
@@ -141,15 +141,39 @@ export function format(source, options) {
|
|
|
141
141
|
}
|
|
142
142
|
case "IfExpr": {
|
|
143
143
|
const cond = formatExpr(expr.condition, depth);
|
|
144
|
-
const
|
|
144
|
+
const thenInline = formatBlockExpr(expr.then, depth);
|
|
145
145
|
if (expr.else_) {
|
|
146
|
+
let elInline;
|
|
146
147
|
if (expr.else_.kind === "IfExpr") {
|
|
147
|
-
|
|
148
|
+
elInline = formatExpr(expr.else_, depth);
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
else {
|
|
151
|
+
elInline = formatBlockExpr(expr.else_, depth);
|
|
152
|
+
}
|
|
153
|
+
const single = `if ${cond} ${thenInline} el ${elInline}`;
|
|
154
|
+
if (single.length + depth * opts.indentSize <= opts.maxLineLength) {
|
|
155
|
+
return single;
|
|
156
|
+
}
|
|
157
|
+
// Force multi-line blocks when single-line is too long
|
|
158
|
+
const thenMulti = expr.then.kind === "BlockExpr"
|
|
159
|
+
? formatBlockMultiline(expr.then, depth)
|
|
160
|
+
: thenInline;
|
|
161
|
+
if (expr.else_.kind === "IfExpr") {
|
|
162
|
+
return `if ${cond} ${thenMulti} el ${formatExpr(expr.else_, depth)}`;
|
|
163
|
+
}
|
|
164
|
+
const elMulti = expr.else_.kind === "BlockExpr"
|
|
165
|
+
? formatBlockMultiline(expr.else_, depth)
|
|
166
|
+
: elInline;
|
|
167
|
+
return `if ${cond} ${thenMulti} el ${elMulti}`;
|
|
168
|
+
}
|
|
169
|
+
const single = `if ${cond} ${thenInline}`;
|
|
170
|
+
if (single.length + depth * opts.indentSize <= opts.maxLineLength) {
|
|
171
|
+
return single;
|
|
151
172
|
}
|
|
152
|
-
|
|
173
|
+
const thenMulti = expr.then.kind === "BlockExpr"
|
|
174
|
+
? formatBlockMultiline(expr.then, depth)
|
|
175
|
+
: thenInline;
|
|
176
|
+
return `if ${cond} ${thenMulti}`;
|
|
153
177
|
}
|
|
154
178
|
case "MatchExpr": {
|
|
155
179
|
const subject = formatExpr(expr.subject, depth);
|
|
@@ -218,6 +242,12 @@ export function format(source, options) {
|
|
|
218
242
|
return formatBlockInline(expr, depth);
|
|
219
243
|
return formatExpr(expr, depth);
|
|
220
244
|
}
|
|
245
|
+
function formatBlockMultiline(block, depth) {
|
|
246
|
+
if (block.stmts.length === 0)
|
|
247
|
+
return "{}";
|
|
248
|
+
const body = block.stmts.map(s => `${indent(depth + 1)}${formatStmtStr(s, depth + 1)}`).join('\n');
|
|
249
|
+
return `{\n${body}\n${indent(depth)}}`;
|
|
250
|
+
}
|
|
221
251
|
function formatBlockInline(block, depth) {
|
|
222
252
|
if (block.stmts.length === 0)
|
|
223
253
|
return "{}";
|
|
@@ -315,6 +345,8 @@ export function format(source, options) {
|
|
|
315
345
|
return `${formatExpr(stmt.object, depth)}.${stmt.property} = ${formatExpr(stmt.value, depth)}`;
|
|
316
346
|
case "IndexAssignStmt":
|
|
317
347
|
return `${formatExpr(stmt.object, depth)}[${formatExpr(stmt.index, depth)}] = ${formatExpr(stmt.value, depth)}`;
|
|
348
|
+
default:
|
|
349
|
+
return `/* unknown stmt: ${stmt.kind} */`;
|
|
318
350
|
}
|
|
319
351
|
}
|
|
320
352
|
// Emit top-level statements with comments and blank lines between declarations
|
package/dist/interpreter.js
CHANGED
|
@@ -107,6 +107,15 @@ function makePrelude(env) {
|
|
|
107
107
|
}
|
|
108
108
|
return acc;
|
|
109
109
|
},
|
|
110
|
+
fold: (list, init, fn) => {
|
|
111
|
+
if (!Array.isArray(list))
|
|
112
|
+
throw new Error("fold expects a list");
|
|
113
|
+
let acc = init;
|
|
114
|
+
for (let i = 0; i < list.length; i++) {
|
|
115
|
+
acc = callFn(fn, [acc, list[i]]);
|
|
116
|
+
}
|
|
117
|
+
return acc;
|
|
118
|
+
},
|
|
110
119
|
sort: (list) => {
|
|
111
120
|
if (!Array.isArray(list))
|
|
112
121
|
throw new Error("sort expects a list");
|
|
@@ -243,6 +252,13 @@ function makePrelude(env) {
|
|
|
243
252
|
env.set(name, fn);
|
|
244
253
|
}
|
|
245
254
|
}
|
|
255
|
+
// Return signal — thrown to unwind the stack on `ret`
|
|
256
|
+
class ReturnSignal {
|
|
257
|
+
value;
|
|
258
|
+
constructor(value) {
|
|
259
|
+
this.value = value;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
246
262
|
// Evaluate expression in tail position — returns TCOSignal for self-recursive tail calls
|
|
247
263
|
function evalExprTCO(expr, env, fnName) {
|
|
248
264
|
// Only handle tail-position expressions specially
|
|
@@ -314,11 +330,24 @@ function evalExpr(expr, env) {
|
|
|
314
330
|
const left = evalExpr(expr.left, env);
|
|
315
331
|
const right = evalExpr(expr.right, env);
|
|
316
332
|
switch (expr.op) {
|
|
317
|
-
case "+":
|
|
333
|
+
case "+": {
|
|
334
|
+
if (typeof left === "string" || typeof right === "string") {
|
|
335
|
+
return toStr(left) + toStr(right);
|
|
336
|
+
}
|
|
337
|
+
return left + right;
|
|
338
|
+
}
|
|
318
339
|
case "-": return left - right;
|
|
319
340
|
case "*": return left * right;
|
|
320
|
-
case "/":
|
|
321
|
-
|
|
341
|
+
case "/": {
|
|
342
|
+
if (right === 0)
|
|
343
|
+
throw new Error(`Division by zero at line ${expr.loc.line}`);
|
|
344
|
+
return left / right;
|
|
345
|
+
}
|
|
346
|
+
case "%": {
|
|
347
|
+
if (right === 0)
|
|
348
|
+
throw new Error(`Modulo by zero at line ${expr.loc.line}`);
|
|
349
|
+
return left % right;
|
|
350
|
+
}
|
|
322
351
|
case "**": return Math.pow(left, right);
|
|
323
352
|
case "==": return left === right;
|
|
324
353
|
case "!=": return left !== right;
|
|
@@ -353,18 +382,28 @@ function evalExpr(expr, env) {
|
|
|
353
382
|
let fn = callee;
|
|
354
383
|
// Tail call optimization loop: if the function body resolves to
|
|
355
384
|
// a tail call back to itself, reuse the frame instead of recursing
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
385
|
+
try {
|
|
386
|
+
tailLoop: while (true) {
|
|
387
|
+
const fnEnv = new Env(fn.closure);
|
|
388
|
+
fn.params.forEach((p, i) => fnEnv.set(p, args[i] ?? null));
|
|
389
|
+
const bodyResult = evalExprTCO(fn.body, fnEnv, fn.name);
|
|
390
|
+
if (bodyResult && typeof bodyResult === "object" && "__tco" in bodyResult) {
|
|
391
|
+
const tco = bodyResult;
|
|
392
|
+
args = tco.args;
|
|
393
|
+
// fn stays the same — it's a self-recursive tail call
|
|
394
|
+
continue tailLoop;
|
|
395
|
+
}
|
|
396
|
+
result = bodyResult;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (e) {
|
|
401
|
+
if (e instanceof ReturnSignal) {
|
|
402
|
+
result = e.value;
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
throw e;
|
|
365
406
|
}
|
|
366
|
-
result = bodyResult;
|
|
367
|
-
break;
|
|
368
407
|
}
|
|
369
408
|
}
|
|
370
409
|
else {
|
|
@@ -375,6 +414,8 @@ function evalExpr(expr, env) {
|
|
|
375
414
|
}
|
|
376
415
|
case "MemberExpr": {
|
|
377
416
|
const obj = evalExpr(expr.object, env);
|
|
417
|
+
if (obj === null)
|
|
418
|
+
return null;
|
|
378
419
|
if (obj && typeof obj === "object" && "__map" in obj) {
|
|
379
420
|
return obj.entries.get(expr.property) ?? null;
|
|
380
421
|
}
|
|
@@ -615,6 +656,10 @@ function evalStmt(stmt, env) {
|
|
|
615
656
|
}
|
|
616
657
|
throw new Error(`Cannot assign index on ${toStr(obj)}`);
|
|
617
658
|
}
|
|
659
|
+
case "RetStmt": {
|
|
660
|
+
const value = stmt.value ? evalExpr(stmt.value, env) : null;
|
|
661
|
+
throw new ReturnSignal(value);
|
|
662
|
+
}
|
|
618
663
|
case "ExprStmt": return evalExpr(stmt.expr, env);
|
|
619
664
|
case "UseStmt": {
|
|
620
665
|
// Module imports handled by interpretWithFile; no-op if no file context
|
package/dist/ir.d.ts
CHANGED
|
@@ -112,9 +112,15 @@ export declare class IRGenerator {
|
|
|
112
112
|
private labelCount;
|
|
113
113
|
private functions;
|
|
114
114
|
private currentInstrs;
|
|
115
|
+
private scopeStack;
|
|
116
|
+
private scopeCount;
|
|
115
117
|
private temp;
|
|
116
118
|
private label;
|
|
117
119
|
private emit;
|
|
120
|
+
private pushScope;
|
|
121
|
+
private popScope;
|
|
122
|
+
private defineVar;
|
|
123
|
+
private resolveVar;
|
|
118
124
|
generateIR(program: AST.Program): IRModule;
|
|
119
125
|
private lowerStmt;
|
|
120
126
|
private lowerExpr;
|
package/dist/ir.js
CHANGED
|
@@ -7,6 +7,8 @@ export class IRGenerator {
|
|
|
7
7
|
labelCount = 0;
|
|
8
8
|
functions = [];
|
|
9
9
|
currentInstrs = [];
|
|
10
|
+
scopeStack = [new Map()];
|
|
11
|
+
scopeCount = 0;
|
|
10
12
|
temp() {
|
|
11
13
|
return `%${this.tempCount++}`;
|
|
12
14
|
}
|
|
@@ -16,6 +18,38 @@ export class IRGenerator {
|
|
|
16
18
|
emit(instr) {
|
|
17
19
|
this.currentInstrs.push(instr);
|
|
18
20
|
}
|
|
21
|
+
pushScope() {
|
|
22
|
+
this.scopeStack.push(new Map());
|
|
23
|
+
}
|
|
24
|
+
popScope() {
|
|
25
|
+
this.scopeStack.pop();
|
|
26
|
+
}
|
|
27
|
+
defineVar(name) {
|
|
28
|
+
const scope = this.scopeStack[this.scopeStack.length - 1];
|
|
29
|
+
// Check if already defined in ANY scope (including this one)
|
|
30
|
+
let existsInAnyScope = false;
|
|
31
|
+
for (let i = this.scopeStack.length - 1; i >= 0; i--) {
|
|
32
|
+
if (this.scopeStack[i].has(name)) {
|
|
33
|
+
existsInAnyScope = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (existsInAnyScope) {
|
|
38
|
+
const mangled = `${name}__s${this.scopeCount++}`;
|
|
39
|
+
scope.set(name, mangled);
|
|
40
|
+
return mangled;
|
|
41
|
+
}
|
|
42
|
+
scope.set(name, name);
|
|
43
|
+
return name;
|
|
44
|
+
}
|
|
45
|
+
resolveVar(name) {
|
|
46
|
+
for (let i = this.scopeStack.length - 1; i >= 0; i--) {
|
|
47
|
+
const v = this.scopeStack[i].get(name);
|
|
48
|
+
if (v !== undefined)
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
51
|
+
return name;
|
|
52
|
+
}
|
|
19
53
|
generateIR(program) {
|
|
20
54
|
this.functions = [];
|
|
21
55
|
this.currentInstrs = [];
|
|
@@ -35,7 +69,8 @@ export class IRGenerator {
|
|
|
35
69
|
case "LetStmt": {
|
|
36
70
|
const val = this.lowerExpr(stmt.value);
|
|
37
71
|
if (typeof stmt.name === "string") {
|
|
38
|
-
this.
|
|
72
|
+
const mangled = this.defineVar(stmt.name);
|
|
73
|
+
this.emit({ op: "store", name: mangled, src: val });
|
|
39
74
|
}
|
|
40
75
|
else {
|
|
41
76
|
// Destructuring — store temp then extract fields
|
|
@@ -50,14 +85,21 @@ export class IRGenerator {
|
|
|
50
85
|
this.emit({ op: "const", dest: idx, value: i });
|
|
51
86
|
this.emit({ op: "index", dest, obj: val, idx });
|
|
52
87
|
}
|
|
53
|
-
this.
|
|
88
|
+
const mangled = this.defineVar(dt.names[i]);
|
|
89
|
+
this.emit({ op: "store", name: mangled, src: dest });
|
|
54
90
|
}
|
|
55
91
|
}
|
|
56
92
|
break;
|
|
57
93
|
}
|
|
58
94
|
case "FnStmt": {
|
|
59
95
|
const savedInstrs = this.currentInstrs;
|
|
96
|
+
const savedScope = this.scopeStack;
|
|
60
97
|
this.currentInstrs = [];
|
|
98
|
+
// Function gets its own scope with params
|
|
99
|
+
this.scopeStack = [new Map()];
|
|
100
|
+
for (const p of stmt.params) {
|
|
101
|
+
this.defineVar(p);
|
|
102
|
+
}
|
|
61
103
|
// Lower function body
|
|
62
104
|
const result = this.lowerExpr(stmt.body);
|
|
63
105
|
this.emit({ op: "ret", value: result });
|
|
@@ -67,8 +109,10 @@ export class IRGenerator {
|
|
|
67
109
|
blocks: [{ label: "entry", instrs: this.currentInstrs }],
|
|
68
110
|
});
|
|
69
111
|
this.currentInstrs = savedInstrs;
|
|
112
|
+
this.scopeStack = savedScope;
|
|
70
113
|
// Store function reference in main scope
|
|
71
|
-
|
|
114
|
+
const fnMangled = this.defineVar(stmt.name);
|
|
115
|
+
this.emit({ op: "store", name: fnMangled, src: `@fn:${stmt.name}` });
|
|
72
116
|
break;
|
|
73
117
|
}
|
|
74
118
|
case "ForStmt": {
|
|
@@ -89,10 +133,13 @@ export class IRGenerator {
|
|
|
89
133
|
this.emit({ op: "binop", dest: cond, operator: "<", left: counter, right: lenTemp });
|
|
90
134
|
this.emit({ op: "branch", cond, ifTrue: bodyLabel, ifFalse: endLabel });
|
|
91
135
|
this.emit({ op: "label", name: bodyLabel });
|
|
136
|
+
this.pushScope();
|
|
92
137
|
const elem = this.temp();
|
|
93
138
|
this.emit({ op: "index", dest: elem, obj: iter, idx: counter });
|
|
94
|
-
this.
|
|
139
|
+
const loopVarName = this.defineVar(stmt.variable);
|
|
140
|
+
this.emit({ op: "store", name: loopVarName, src: elem });
|
|
95
141
|
this.lowerExpr(stmt.body);
|
|
142
|
+
this.popScope();
|
|
96
143
|
const next = this.temp();
|
|
97
144
|
const one = this.temp();
|
|
98
145
|
this.emit({ op: "const", dest: one, value: 1 });
|
|
@@ -122,7 +169,7 @@ export class IRGenerator {
|
|
|
122
169
|
}
|
|
123
170
|
case "AssignStmt": {
|
|
124
171
|
const val = this.lowerExpr(stmt.value);
|
|
125
|
-
this.emit({ op: "store", name: stmt.target, src: val });
|
|
172
|
+
this.emit({ op: "store", name: this.resolveVar(stmt.target), src: val });
|
|
126
173
|
break;
|
|
127
174
|
}
|
|
128
175
|
case "MemberAssignStmt": {
|
|
@@ -200,7 +247,7 @@ export class IRGenerator {
|
|
|
200
247
|
}
|
|
201
248
|
case "Identifier": {
|
|
202
249
|
const dest = this.temp();
|
|
203
|
-
this.emit({ op: "load", dest, name: expr.name });
|
|
250
|
+
this.emit({ op: "load", dest, name: this.resolveVar(expr.name) });
|
|
204
251
|
return dest;
|
|
205
252
|
}
|
|
206
253
|
case "BinaryExpr": {
|
|
@@ -303,18 +350,23 @@ export class IRGenerator {
|
|
|
303
350
|
// Lower pattern to condition
|
|
304
351
|
const cond = this.lowerPattern(arm.pattern, subject);
|
|
305
352
|
if (arm.guard) {
|
|
306
|
-
//
|
|
353
|
+
// For guards, we need to bind pattern variables BEFORE evaluating the guard
|
|
354
|
+
// because the guard may reference bound variables (e.g., `n if n > 10`)
|
|
355
|
+
const guardLabel = this.label("match_guard");
|
|
356
|
+
this.emit({ op: "branch", cond, ifTrue: guardLabel, ifFalse: nextLabel });
|
|
357
|
+
this.emit({ op: "label", name: guardLabel });
|
|
358
|
+
this.bindPattern(arm.pattern, subject);
|
|
307
359
|
const guardCond = this.lowerExpr(arm.guard);
|
|
308
|
-
|
|
309
|
-
this.emit({ op: "binop", dest: combined, operator: "and", left: cond, right: guardCond });
|
|
310
|
-
this.emit({ op: "branch", cond: combined, ifTrue: armLabel, ifFalse: nextLabel });
|
|
360
|
+
this.emit({ op: "branch", cond: guardCond, ifTrue: armLabel, ifFalse: nextLabel });
|
|
311
361
|
}
|
|
312
362
|
else {
|
|
313
363
|
this.emit({ op: "branch", cond, ifTrue: armLabel, ifFalse: nextLabel });
|
|
314
364
|
}
|
|
315
365
|
this.emit({ op: "label", name: armLabel });
|
|
316
|
-
// Bind pattern variables
|
|
317
|
-
|
|
366
|
+
// Bind pattern variables (for non-guard case; guard case already bound above)
|
|
367
|
+
if (!arm.guard) {
|
|
368
|
+
this.bindPattern(arm.pattern, subject);
|
|
369
|
+
}
|
|
318
370
|
const val = this.lowerExpr(arm.body);
|
|
319
371
|
this.emit({ op: "store", name: resultName, src: val });
|
|
320
372
|
this.emit({ op: "jump", target: endLabel });
|
|
@@ -331,7 +383,12 @@ export class IRGenerator {
|
|
|
331
383
|
// Lower lambda to anonymous function
|
|
332
384
|
const fnName = `__lambda_${this.labelCount++}`;
|
|
333
385
|
const savedInstrs = this.currentInstrs;
|
|
386
|
+
const savedScope = this.scopeStack;
|
|
334
387
|
this.currentInstrs = [];
|
|
388
|
+
this.scopeStack = [new Map()];
|
|
389
|
+
for (const p of expr.params) {
|
|
390
|
+
this.defineVar(p);
|
|
391
|
+
}
|
|
335
392
|
const result = this.lowerExpr(expr.body);
|
|
336
393
|
this.emit({ op: "ret", value: result });
|
|
337
394
|
this.functions.push({
|
|
@@ -340,6 +397,7 @@ export class IRGenerator {
|
|
|
340
397
|
blocks: [{ label: "entry", instrs: this.currentInstrs }],
|
|
341
398
|
});
|
|
342
399
|
this.currentInstrs = savedInstrs;
|
|
400
|
+
this.scopeStack = savedScope;
|
|
343
401
|
const dest = this.temp();
|
|
344
402
|
this.emit({ op: "load", dest, name: `@fn:${fnName}` });
|
|
345
403
|
return dest;
|
|
@@ -442,6 +500,7 @@ export class IRGenerator {
|
|
|
442
500
|
return dest;
|
|
443
501
|
}
|
|
444
502
|
case "BlockExpr": {
|
|
503
|
+
this.pushScope();
|
|
445
504
|
let last = this.temp();
|
|
446
505
|
this.emit({ op: "const", dest: last, value: null });
|
|
447
506
|
for (const s of expr.stmts) {
|
|
@@ -452,6 +511,7 @@ export class IRGenerator {
|
|
|
452
511
|
this.lowerStmt(s);
|
|
453
512
|
}
|
|
454
513
|
}
|
|
514
|
+
this.popScope();
|
|
455
515
|
return last;
|
|
456
516
|
}
|
|
457
517
|
case "AsyncExpr": {
|
package/dist/lexer.js
CHANGED
|
@@ -135,6 +135,10 @@ export function lex(source) {
|
|
|
135
135
|
const parts = [];
|
|
136
136
|
let hasInterp = false;
|
|
137
137
|
while (i < source.length && peek() !== '"') {
|
|
138
|
+
if (peek() === "\n") {
|
|
139
|
+
// Unterminated string - newline before closing quote
|
|
140
|
+
throw new Error(`Unterminated string literal at line ${sl}, col ${sc}`);
|
|
141
|
+
}
|
|
138
142
|
if (peek() === "{") {
|
|
139
143
|
hasInterp = true;
|
|
140
144
|
if (str.length > 0 || parts.length === 0) {
|
|
@@ -145,7 +149,21 @@ export function lex(source) {
|
|
|
145
149
|
// Lex the expression inside {} as tokens - just grab until matching }
|
|
146
150
|
let depth = 1;
|
|
147
151
|
let interpExpr = "";
|
|
152
|
+
const interpLine = line, interpCol = col;
|
|
148
153
|
while (i < source.length && depth > 0) {
|
|
154
|
+
if (peek() === '"') {
|
|
155
|
+
// Skip over string literals inside interpolation to avoid miscounting braces
|
|
156
|
+
interpExpr += advance(); // opening quote
|
|
157
|
+
while (i < source.length && peek() !== '"') {
|
|
158
|
+
if (peek() === '\\') {
|
|
159
|
+
interpExpr += advance();
|
|
160
|
+
} // escape char
|
|
161
|
+
interpExpr += advance();
|
|
162
|
+
}
|
|
163
|
+
if (i < source.length)
|
|
164
|
+
interpExpr += advance(); // closing quote
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
149
167
|
if (peek() === "{")
|
|
150
168
|
depth++;
|
|
151
169
|
if (peek() === "}") {
|
|
@@ -157,26 +175,84 @@ export function lex(source) {
|
|
|
157
175
|
}
|
|
158
176
|
if (peek() === "}")
|
|
159
177
|
advance(); // skip }
|
|
160
|
-
|
|
178
|
+
// Skip empty interpolation
|
|
179
|
+
if (interpExpr.trim().length > 0) {
|
|
180
|
+
parts.push(tok(TokenType.Ident, interpExpr, interpLine, interpCol));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Empty interpolation {} - treat as empty string part
|
|
184
|
+
parts.push(tok(TokenType.String, "", interpLine, interpCol));
|
|
185
|
+
}
|
|
161
186
|
continue;
|
|
162
187
|
}
|
|
163
188
|
if (peek() === "\\") {
|
|
164
189
|
advance();
|
|
190
|
+
if (i >= source.length) {
|
|
191
|
+
throw new Error(`Unterminated string literal (escape at end of file) at line ${sl}, col ${sc}`);
|
|
192
|
+
}
|
|
165
193
|
const esc = advance();
|
|
166
194
|
if (esc === "n")
|
|
167
195
|
str += "\n";
|
|
168
196
|
else if (esc === "t")
|
|
169
197
|
str += "\t";
|
|
198
|
+
else if (esc === "r")
|
|
199
|
+
str += "\r";
|
|
200
|
+
else if (esc === "0")
|
|
201
|
+
str += "\0";
|
|
170
202
|
else if (esc === "\\")
|
|
171
203
|
str += "\\";
|
|
172
204
|
else if (esc === '"')
|
|
173
205
|
str += '"';
|
|
206
|
+
else if (esc === "{")
|
|
207
|
+
str += "{";
|
|
208
|
+
else if (esc === "x") {
|
|
209
|
+
// \xNN - hex escape (2 digits)
|
|
210
|
+
let hex = "";
|
|
211
|
+
for (let h = 0; h < 2 && i < source.length; h++) {
|
|
212
|
+
const hc = peek();
|
|
213
|
+
if (/[0-9a-fA-F]/.test(hc)) {
|
|
214
|
+
hex += advance();
|
|
215
|
+
}
|
|
216
|
+
else
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
str += hex.length > 0 ? String.fromCharCode(parseInt(hex, 16)) : "x";
|
|
220
|
+
}
|
|
221
|
+
else if (esc === "u") {
|
|
222
|
+
// \u{NNNN} or \uNNNN - unicode escape
|
|
223
|
+
if (peek() === "{") {
|
|
224
|
+
advance(); // skip {
|
|
225
|
+
let hex = "";
|
|
226
|
+
while (i < source.length && peek() !== "}") {
|
|
227
|
+
hex += advance();
|
|
228
|
+
}
|
|
229
|
+
if (peek() === "}")
|
|
230
|
+
advance();
|
|
231
|
+
str += hex.length > 0 ? String.fromCodePoint(parseInt(hex, 16)) : "";
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// \uNNNN - 4 hex digits
|
|
235
|
+
let hex = "";
|
|
236
|
+
for (let h = 0; h < 4 && i < source.length; h++) {
|
|
237
|
+
const hc = peek();
|
|
238
|
+
if (/[0-9a-fA-F]/.test(hc)) {
|
|
239
|
+
hex += advance();
|
|
240
|
+
}
|
|
241
|
+
else
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
str += hex.length > 0 ? String.fromCharCode(parseInt(hex, 16)) : "u";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
174
247
|
else
|
|
175
248
|
str += esc;
|
|
176
249
|
continue;
|
|
177
250
|
}
|
|
178
251
|
str += advance();
|
|
179
252
|
}
|
|
253
|
+
if (i >= source.length) {
|
|
254
|
+
throw new Error(`Unterminated string literal at line ${sl}, col ${sc}`);
|
|
255
|
+
}
|
|
180
256
|
if (peek() === '"')
|
|
181
257
|
advance(); // skip closing quote
|
|
182
258
|
if (hasInterp) {
|
package/dist/modules.js
CHANGED
|
@@ -8,8 +8,10 @@ import { createEnv, runStmt } from "./interpreter.js";
|
|
|
8
8
|
const __filename2 = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname2 = dirname(__filename2);
|
|
10
10
|
const moduleCache = new Map();
|
|
11
|
+
const modulesInProgress = new Set();
|
|
11
12
|
export function clearModuleCache() {
|
|
12
13
|
moduleCache.clear();
|
|
14
|
+
modulesInProgress.clear();
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Resolve a module path to a file path.
|
|
@@ -44,10 +46,15 @@ export function resolveModule(path, basePath) {
|
|
|
44
46
|
*/
|
|
45
47
|
export function loadModule(filePath) {
|
|
46
48
|
const absPath = resolve(filePath);
|
|
49
|
+
// Detect circular imports — check before cache since cache is pre-populated with {}
|
|
50
|
+
if (modulesInProgress.has(absPath)) {
|
|
51
|
+
throw new Error(`Circular import detected: ${absPath}`);
|
|
52
|
+
}
|
|
47
53
|
if (moduleCache.has(absPath)) {
|
|
48
54
|
return moduleCache.get(absPath);
|
|
49
55
|
}
|
|
50
|
-
|
|
56
|
+
modulesInProgress.add(absPath);
|
|
57
|
+
// Pre-populate cache to handle any remaining edge cases
|
|
51
58
|
moduleCache.set(absPath, {});
|
|
52
59
|
const source = readFileSync(absPath, "utf-8");
|
|
53
60
|
const tokens = lex(source);
|
|
@@ -79,6 +86,7 @@ export function loadModule(filePath) {
|
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
moduleCache.set(absPath, exports);
|
|
89
|
+
modulesInProgress.delete(absPath);
|
|
82
90
|
return exports;
|
|
83
91
|
}
|
|
84
92
|
/**
|
package/dist/parser.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export declare class Parser {
|
|
|
24
24
|
private parseDo;
|
|
25
25
|
private parseUse;
|
|
26
26
|
private parseType;
|
|
27
|
+
private parseRet;
|
|
27
28
|
private parseTypeExpr;
|
|
28
29
|
private parseTypeAtom;
|
|
29
30
|
private parseBlock;
|
|
@@ -37,6 +38,7 @@ export declare class Parser {
|
|
|
37
38
|
private parseIf;
|
|
38
39
|
private parseMatch;
|
|
39
40
|
private parsePattern;
|
|
41
|
+
private parsePatternAtom;
|
|
40
42
|
private parseToolCall;
|
|
41
43
|
}
|
|
42
44
|
export declare function parse(tokens: Token[]): AST.Program;
|
package/dist/parser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Arc Language Parser - Recursive Descent with Pratt Parsing
|
|
2
|
-
import { TokenType } from "./lexer.js";
|
|
2
|
+
import { TokenType, lex } from "./lexer.js";
|
|
3
3
|
export class ParseError extends Error {
|
|
4
4
|
loc;
|
|
5
5
|
constructor(msg, loc) {
|
|
@@ -55,6 +55,7 @@ export class Parser {
|
|
|
55
55
|
case TokenType.Do: return this.parseDo();
|
|
56
56
|
case TokenType.Use: return this.parseUse();
|
|
57
57
|
case TokenType.Type: return this.parseType();
|
|
58
|
+
case TokenType.Ret: return this.parseRet();
|
|
58
59
|
default: {
|
|
59
60
|
const exprLoc = this.loc();
|
|
60
61
|
const expr = this.parseExpr();
|
|
@@ -226,6 +227,16 @@ export class Parser {
|
|
|
226
227
|
const def = this.parseTypeExpr();
|
|
227
228
|
return { kind: "TypeStmt", name, pub, def, loc };
|
|
228
229
|
}
|
|
230
|
+
parseRet() {
|
|
231
|
+
const loc = this.loc();
|
|
232
|
+
this.expect(TokenType.Ret);
|
|
233
|
+
let value;
|
|
234
|
+
// If the next token could start an expression, parse it
|
|
235
|
+
if (!this.at(TokenType.EOF) && !this.at(TokenType.RBrace) && !this.at(TokenType.Semicolon)) {
|
|
236
|
+
value = this.parseExpr();
|
|
237
|
+
}
|
|
238
|
+
return { kind: "RetStmt", value, loc };
|
|
239
|
+
}
|
|
229
240
|
parseTypeExpr() {
|
|
230
241
|
let typeExpr = this.parseTypeAtom();
|
|
231
242
|
// Check for enum/union: Type | Type
|
|
@@ -405,7 +416,9 @@ export class Parser {
|
|
|
405
416
|
const op = this.binaryOp();
|
|
406
417
|
if (op) {
|
|
407
418
|
this.advance();
|
|
408
|
-
|
|
419
|
+
// Right-associative for ** (power operator)
|
|
420
|
+
const nextPrec = op === "**" ? prec : prec + 1;
|
|
421
|
+
const right = this.parseExpr(nextPrec);
|
|
409
422
|
left = { kind: "BinaryExpr", op, left, right, loc: left.loc };
|
|
410
423
|
continue;
|
|
411
424
|
}
|
|
@@ -456,7 +469,8 @@ export class Parser {
|
|
|
456
469
|
// Unary operators
|
|
457
470
|
if (t.type === TokenType.Minus) {
|
|
458
471
|
this.advance();
|
|
459
|
-
|
|
472
|
+
// Precedence 7: binds looser than ** so -x ** 2 parses as -(x ** 2)
|
|
473
|
+
return { kind: "UnaryExpr", op: "-", operand: this.parseExpr(7), loc };
|
|
460
474
|
}
|
|
461
475
|
if (t.type === TokenType.Not) {
|
|
462
476
|
this.advance();
|
|
@@ -600,10 +614,13 @@ export class Parser {
|
|
|
600
614
|
parts.push(this.advance().value);
|
|
601
615
|
}
|
|
602
616
|
else if (this.at(TokenType.Ident)) {
|
|
603
|
-
//
|
|
617
|
+
// The lexer captures the raw text inside {} — re-lex and parse as expression
|
|
604
618
|
const identToken = this.advance();
|
|
605
|
-
|
|
606
|
-
|
|
619
|
+
const exprSource = identToken.value;
|
|
620
|
+
const exprTokens = lex(exprSource);
|
|
621
|
+
const exprParser = new Parser(exprTokens);
|
|
622
|
+
const expr = exprParser.parseExpr();
|
|
623
|
+
parts.push(expr);
|
|
607
624
|
}
|
|
608
625
|
else {
|
|
609
626
|
this.advance(); // skip unexpected
|
|
@@ -715,12 +732,35 @@ export class Parser {
|
|
|
715
732
|
return { kind: "MatchExpr", subject, arms, loc };
|
|
716
733
|
}
|
|
717
734
|
parsePattern() {
|
|
735
|
+
let pattern = this.parsePatternAtom();
|
|
736
|
+
// Check for or-pattern: pat | pat | ...
|
|
737
|
+
if (this.at(TokenType.Bar)) {
|
|
738
|
+
const patterns = [pattern];
|
|
739
|
+
while (this.at(TokenType.Bar)) {
|
|
740
|
+
this.advance();
|
|
741
|
+
patterns.push(this.parsePatternAtom());
|
|
742
|
+
}
|
|
743
|
+
return { kind: "OrPattern", patterns, loc: pattern.loc };
|
|
744
|
+
}
|
|
745
|
+
return pattern;
|
|
746
|
+
}
|
|
747
|
+
parsePatternAtom() {
|
|
718
748
|
const loc = this.loc();
|
|
719
749
|
const t = this.peek();
|
|
720
750
|
if (t.type === TokenType.Ident && t.value === "_") {
|
|
721
751
|
this.advance();
|
|
722
752
|
return { kind: "WildcardPattern", loc };
|
|
723
753
|
}
|
|
754
|
+
// Negative numeric literals
|
|
755
|
+
if (t.type === TokenType.Minus) {
|
|
756
|
+
this.advance();
|
|
757
|
+
const numTok = this.peek();
|
|
758
|
+
if (numTok.type === TokenType.Int || numTok.type === TokenType.Float) {
|
|
759
|
+
this.advance();
|
|
760
|
+
return { kind: "LiteralPattern", value: -parseFloat(numTok.value), loc };
|
|
761
|
+
}
|
|
762
|
+
throw new ParseError(`Expected number after - in pattern`, loc);
|
|
763
|
+
}
|
|
724
764
|
if (t.type === TokenType.Int || t.type === TokenType.Float) {
|
|
725
765
|
this.advance();
|
|
726
766
|
return { kind: "LiteralPattern", value: parseFloat(t.value), loc };
|
|
@@ -741,6 +781,18 @@ export class Parser {
|
|
|
741
781
|
this.advance();
|
|
742
782
|
return { kind: "LiteralPattern", value: null, loc };
|
|
743
783
|
}
|
|
784
|
+
// Array pattern: [pat, pat, ...]
|
|
785
|
+
if (t.type === TokenType.LBracket) {
|
|
786
|
+
this.advance();
|
|
787
|
+
const elements = [];
|
|
788
|
+
while (!this.at(TokenType.RBracket) && !this.at(TokenType.EOF)) {
|
|
789
|
+
elements.push(this.parsePattern());
|
|
790
|
+
if (this.at(TokenType.Comma))
|
|
791
|
+
this.advance();
|
|
792
|
+
}
|
|
793
|
+
this.expect(TokenType.RBracket);
|
|
794
|
+
return { kind: "ArrayPattern", elements, loc };
|
|
795
|
+
}
|
|
744
796
|
if (t.type === TokenType.Ident) {
|
|
745
797
|
this.advance();
|
|
746
798
|
return { kind: "BindingPattern", name: t.value, loc };
|
package/dist/repl.js
CHANGED
|
@@ -93,12 +93,31 @@ function main() {
|
|
|
93
93
|
}
|
|
94
94
|
// Multi-line support
|
|
95
95
|
buffer += (buffer ? "\n" : "") + line;
|
|
96
|
-
// Count braces
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
// Count braces (skip braces inside strings and comments)
|
|
97
|
+
let inString = false;
|
|
98
|
+
let escaped = false;
|
|
99
|
+
for (let ci = 0; ci < line.length; ci++) {
|
|
100
|
+
const ch = line[ci];
|
|
101
|
+
if (escaped) {
|
|
102
|
+
escaped = false;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (ch === "\\") {
|
|
106
|
+
escaped = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (ch === '"') {
|
|
110
|
+
inString = !inString;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (ch === "#" && !inString)
|
|
114
|
+
break; // rest is comment
|
|
115
|
+
if (!inString) {
|
|
116
|
+
if (ch === "{")
|
|
117
|
+
braceDepth++;
|
|
118
|
+
if (ch === "}")
|
|
119
|
+
braceDepth--;
|
|
120
|
+
}
|
|
102
121
|
}
|
|
103
122
|
if (braceDepth > 0) {
|
|
104
123
|
rl.setPrompt("... ");
|
package/dist/security.js
CHANGED
|
@@ -157,7 +157,7 @@ export class SafeInterpreter {
|
|
|
157
157
|
ctx.tick();
|
|
158
158
|
if (stmt.kind === "UseStmt") {
|
|
159
159
|
const useStmt = stmt;
|
|
160
|
-
validateImport(useStmt.
|
|
160
|
+
validateImport(useStmt.path.join("/"), this.config);
|
|
161
161
|
if (!this.config.disableImports) {
|
|
162
162
|
// No file context in sandbox, skip actual import
|
|
163
163
|
}
|
package/dist/version.js
CHANGED
|
@@ -11,14 +11,27 @@ export function printVersion() {
|
|
|
11
11
|
}
|
|
12
12
|
/** Semver comparison: returns -1, 0, or 1 */
|
|
13
13
|
export function compareSemver(a, b) {
|
|
14
|
-
|
|
15
|
-
const
|
|
14
|
+
// Strip pre-release suffixes for numeric comparison
|
|
15
|
+
const stripPre = (s) => s.replace(/-.*$/, "");
|
|
16
|
+
const pa = stripPre(a).split(".").map(Number);
|
|
17
|
+
const pb = stripPre(b).split(".").map(Number);
|
|
16
18
|
for (let i = 0; i < 3; i++) {
|
|
17
|
-
|
|
19
|
+
const va = pa[i] ?? 0;
|
|
20
|
+
const vb = pb[i] ?? 0;
|
|
21
|
+
if (isNaN(va) || isNaN(vb))
|
|
22
|
+
continue;
|
|
23
|
+
if (va < vb)
|
|
18
24
|
return -1;
|
|
19
|
-
if (
|
|
25
|
+
if (va > vb)
|
|
20
26
|
return 1;
|
|
21
27
|
}
|
|
28
|
+
// If numeric parts are equal, pre-release < release
|
|
29
|
+
const aHasPre = a.includes("-");
|
|
30
|
+
const bHasPre = b.includes("-");
|
|
31
|
+
if (aHasPre && !bHasPre)
|
|
32
|
+
return -1;
|
|
33
|
+
if (!aHasPre && bHasPre)
|
|
34
|
+
return 1;
|
|
22
35
|
return 0;
|
|
23
36
|
}
|
|
24
37
|
/** Check if a manifest's arc version requirement is compatible */
|
|
@@ -32,9 +45,17 @@ export function checkVersionCompatibility(required) {
|
|
|
32
45
|
}
|
|
33
46
|
if (required.startsWith("^")) {
|
|
34
47
|
const base = required.slice(1);
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
48
|
+
const parts = base.split(".").map(Number);
|
|
49
|
+
const curParts = current.split(".").map(Number);
|
|
50
|
+
let ok;
|
|
51
|
+
if (parts[0] === 0) {
|
|
52
|
+
// ^0.x.y means >=0.x.y, <0.(x+1).0 — constrain on minor when major is 0
|
|
53
|
+
ok = curParts[0] === 0 && curParts[1] === parts[1] && compareSemver(current, base) >= 0;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// ^x.y.z means >=x.y.z, <(x+1).0.0
|
|
57
|
+
ok = curParts[0] === parts[0] && compareSemver(current, base) >= 0;
|
|
58
|
+
}
|
|
38
59
|
return { compatible: ok, message: ok ? "Compatible" : `Requires Arc ${required}, current is ${current}` };
|
|
39
60
|
}
|
|
40
61
|
if (required.startsWith("~")) {
|
|
@@ -45,7 +66,7 @@ export function checkVersionCompatibility(required) {
|
|
|
45
66
|
return { compatible: ok, message: ok ? "Compatible" : `Requires Arc ${required}, current is ${current}` };
|
|
46
67
|
}
|
|
47
68
|
// Exact match
|
|
48
|
-
const ok = compareSemver(current, required)
|
|
69
|
+
const ok = compareSemver(current, required) === 0;
|
|
49
70
|
return { compatible: ok, message: ok ? "Compatible" : `Requires Arc ${required}, current is ${current}` };
|
|
50
71
|
}
|
|
51
72
|
const deprecations = [];
|