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 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, 11 stdlib modules, LSP, VS Code extension, package manager, build system, formatter, linter, security sandbox, rich error reporting, benchmarking framework, and migration tools. 504+ tests passing.
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() do\n let msg = "Hello from ${name}!"\nend\n`);
131
- writeFileSync(resolve(projectDir, "tests", "main.test.arc"), `fn test_main() do\n let x = 1 + 1\nend\n`);
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`);
@@ -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(` print(v) { console.log(typeof v === "object" && v !== null ? JSON.stringify(v) : v); },`);
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
- for (const instr of block.instrs) {
78
- lines.push(indent + emitInstr(instr));
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
- return `var ${S(instr.name)} = ${S(instr.src.slice(4))};`;
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)} && ${S(instr.obj)}.__map) ? ${S(instr.obj)}.entries.get(${JSON.stringify(instr.prop)}) : ${S(instr.obj)}[${JSON.stringify(instr.prop)}];`;
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 then = formatBlockExpr(expr.then, depth);
144
+ const thenInline = formatBlockExpr(expr.then, depth);
145
145
  if (expr.else_) {
146
+ let elInline;
146
147
  if (expr.else_.kind === "IfExpr") {
147
- return `if ${cond} ${then} el ${formatExpr(expr.else_, depth)}`;
148
+ elInline = formatExpr(expr.else_, depth);
148
149
  }
149
- const el = formatBlockExpr(expr.else_, depth);
150
- return `if ${cond} ${then} el ${el}`;
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
- return `if ${cond} ${then}`;
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
@@ -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 "+": return left + right;
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 "/": return left / right;
321
- case "%": return left % right;
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
- tailLoop: while (true) {
357
- const fnEnv = new Env(fn.closure);
358
- fn.params.forEach((p, i) => fnEnv.set(p, args[i] ?? null));
359
- const bodyResult = evalExprTCO(fn.body, fnEnv, fn.name);
360
- if (bodyResult && typeof bodyResult === "object" && "__tco" in bodyResult) {
361
- const tco = bodyResult;
362
- args = tco.args;
363
- // fn stays the same — it's a self-recursive tail call
364
- continue tailLoop;
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.emit({ op: "store", name: stmt.name, src: val });
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.emit({ op: "store", name: dt.names[i], src: dest });
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
- this.emit({ op: "store", name: stmt.name, src: `@fn:${stmt.name}` });
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.emit({ op: "store", name: stmt.variable, src: elem });
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
- // Pattern match AND guard
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
- const combined = this.temp();
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
- this.bindPattern(arm.pattern, subject);
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
- parts.push(tok(TokenType.Ident, interpExpr, line, col));
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
- // Prevent circular imports — set empty first
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
- const right = this.parseExpr(prec + 1);
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
- return { kind: "UnaryExpr", op: "-", operand: this.parseExpr(8), loc };
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
- // This is an interpolated expression identifier
617
+ // The lexer captures the raw text inside {} — re-lex and parse as expression
604
618
  const identToken = this.advance();
605
- // Try to parse a more complex expression from the token value
606
- parts.push({ kind: "Identifier", name: identToken.value, loc: { line: identToken.line, col: identToken.col } });
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
- for (const ch of line) {
98
- if (ch === "{")
99
- braceDepth++;
100
- if (ch === "}")
101
- braceDepth--;
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.module, this.config);
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
- const pa = a.split(".").map(Number);
15
- const pb = b.split(".").map(Number);
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
- if (pa[i] < pb[i])
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 (pa[i] > pb[i])
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 [major] = base.split(".").map(Number);
36
- const [curMajor] = current.split(".").map(Number);
37
- const ok = curMajor === major && compareSemver(current, base) >= 0;
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) >= 0;
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 = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arc-lang",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Arc ⚡ — A programming language designed by AI agents, for AI agents. 27-63% fewer tokens than JavaScript.",
5
5
  "type": "module",
6
6
  "bin": {