arc-lang 0.5.2 → 0.5.3
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/dist/ast.d.ts +6 -1
- package/dist/build.js +2 -2
- package/dist/codegen-js.js +64 -2
- package/dist/formatter.js +37 -5
- package/dist/interpreter.js +42 -13
- package/dist/lexer.js +10 -0
- package/dist/parser.d.ts +1 -0
- package/dist/parser.js +11 -0
- package/package.json +1 -1
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
|
@@ -74,9 +74,71 @@ function emitFunction(fn, baseIndent = "") {
|
|
|
74
74
|
}
|
|
75
75
|
function emitBlock(block, indent) {
|
|
76
76
|
const lines = [];
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
const instrs = block.instrs;
|
|
78
|
+
const sections = [];
|
|
79
|
+
let currentSection = { label: "__start", instrs: [] };
|
|
80
|
+
sections.push(currentSection);
|
|
81
|
+
for (const instr of instrs) {
|
|
82
|
+
if (instr.op === "label") {
|
|
83
|
+
currentSection = { label: instr.name, instrs: [] };
|
|
84
|
+
sections.push(currentSection);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
currentSection.instrs.push(instr);
|
|
88
|
+
}
|
|
79
89
|
}
|
|
90
|
+
// If no labels exist, emit linearly (simple case)
|
|
91
|
+
if (sections.length === 1) {
|
|
92
|
+
for (const instr of instrs) {
|
|
93
|
+
lines.push(indent + emitInstr(instr));
|
|
94
|
+
}
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
// Use a state-machine approach with a __pc variable and a while/switch loop
|
|
98
|
+
// to properly handle branches and jumps
|
|
99
|
+
const labelToIdx = new Map();
|
|
100
|
+
for (let i = 0; i < sections.length; i++) {
|
|
101
|
+
labelToIdx.set(sections[i].label, i);
|
|
102
|
+
}
|
|
103
|
+
lines.push(`${indent}var __pc = 0;`);
|
|
104
|
+
lines.push(`${indent}__control: while (true) {`);
|
|
105
|
+
lines.push(`${indent} switch (__pc) {`);
|
|
106
|
+
for (let i = 0; i < sections.length; i++) {
|
|
107
|
+
const section = sections[i];
|
|
108
|
+
lines.push(`${indent} case ${i}: /* ${section.label} */`);
|
|
109
|
+
for (const instr of section.instrs) {
|
|
110
|
+
if (instr.op === "jump") {
|
|
111
|
+
const target = labelToIdx.get(instr.target);
|
|
112
|
+
if (target !== undefined) {
|
|
113
|
+
lines.push(`${indent} __pc = ${target}; continue __control;`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (instr.op === "branch") {
|
|
117
|
+
const ifTrue = labelToIdx.get(instr.ifTrue);
|
|
118
|
+
const ifFalse = labelToIdx.get(instr.ifFalse);
|
|
119
|
+
lines.push(`${indent} if (${S(instr.cond)}) { __pc = ${ifTrue}; } else { __pc = ${ifFalse}; } continue __control;`);
|
|
120
|
+
}
|
|
121
|
+
else if (instr.op === "ret") {
|
|
122
|
+
lines.push(`${indent} ` + emitInstr(instr));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
lines.push(`${indent} ` + emitInstr(instr));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Fall through to next section if no jump/branch/ret at end
|
|
129
|
+
const lastInstr = section.instrs[section.instrs.length - 1];
|
|
130
|
+
if (!lastInstr || (lastInstr.op !== "jump" && lastInstr.op !== "branch" && lastInstr.op !== "ret")) {
|
|
131
|
+
if (i + 1 < sections.length) {
|
|
132
|
+
lines.push(`${indent} __pc = ${i + 1}; continue __control;`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
lines.push(`${indent} break __control;`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
lines.push(`${indent} default: break __control;`);
|
|
140
|
+
lines.push(`${indent} }`);
|
|
141
|
+
lines.push(`${indent}}`);
|
|
80
142
|
return lines;
|
|
81
143
|
}
|
|
82
144
|
function emitInstr(instr) {
|
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
|
@@ -243,6 +243,13 @@ function makePrelude(env) {
|
|
|
243
243
|
env.set(name, fn);
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
+
// Return signal — thrown to unwind the stack on `ret`
|
|
247
|
+
class ReturnSignal {
|
|
248
|
+
value;
|
|
249
|
+
constructor(value) {
|
|
250
|
+
this.value = value;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
246
253
|
// Evaluate expression in tail position — returns TCOSignal for self-recursive tail calls
|
|
247
254
|
function evalExprTCO(expr, env, fnName) {
|
|
248
255
|
// Only handle tail-position expressions specially
|
|
@@ -317,8 +324,16 @@ function evalExpr(expr, env) {
|
|
|
317
324
|
case "+": return left + right;
|
|
318
325
|
case "-": return left - right;
|
|
319
326
|
case "*": return left * right;
|
|
320
|
-
case "/":
|
|
321
|
-
|
|
327
|
+
case "/": {
|
|
328
|
+
if (right === 0)
|
|
329
|
+
throw new Error(`Division by zero at line ${expr.loc.line}`);
|
|
330
|
+
return left / right;
|
|
331
|
+
}
|
|
332
|
+
case "%": {
|
|
333
|
+
if (right === 0)
|
|
334
|
+
throw new Error(`Modulo by zero at line ${expr.loc.line}`);
|
|
335
|
+
return left % right;
|
|
336
|
+
}
|
|
322
337
|
case "**": return Math.pow(left, right);
|
|
323
338
|
case "==": return left === right;
|
|
324
339
|
case "!=": return left !== right;
|
|
@@ -353,18 +368,28 @@ function evalExpr(expr, env) {
|
|
|
353
368
|
let fn = callee;
|
|
354
369
|
// Tail call optimization loop: if the function body resolves to
|
|
355
370
|
// a tail call back to itself, reuse the frame instead of recursing
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
371
|
+
try {
|
|
372
|
+
tailLoop: while (true) {
|
|
373
|
+
const fnEnv = new Env(fn.closure);
|
|
374
|
+
fn.params.forEach((p, i) => fnEnv.set(p, args[i] ?? null));
|
|
375
|
+
const bodyResult = evalExprTCO(fn.body, fnEnv, fn.name);
|
|
376
|
+
if (bodyResult && typeof bodyResult === "object" && "__tco" in bodyResult) {
|
|
377
|
+
const tco = bodyResult;
|
|
378
|
+
args = tco.args;
|
|
379
|
+
// fn stays the same — it's a self-recursive tail call
|
|
380
|
+
continue tailLoop;
|
|
381
|
+
}
|
|
382
|
+
result = bodyResult;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
if (e instanceof ReturnSignal) {
|
|
388
|
+
result = e.value;
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
throw e;
|
|
365
392
|
}
|
|
366
|
-
result = bodyResult;
|
|
367
|
-
break;
|
|
368
393
|
}
|
|
369
394
|
}
|
|
370
395
|
else {
|
|
@@ -615,6 +640,10 @@ function evalStmt(stmt, env) {
|
|
|
615
640
|
}
|
|
616
641
|
throw new Error(`Cannot assign index on ${toStr(obj)}`);
|
|
617
642
|
}
|
|
643
|
+
case "RetStmt": {
|
|
644
|
+
const value = stmt.value ? evalExpr(stmt.value, env) : null;
|
|
645
|
+
throw new ReturnSignal(value);
|
|
646
|
+
}
|
|
618
647
|
case "ExprStmt": return evalExpr(stmt.expr, env);
|
|
619
648
|
case "UseStmt": {
|
|
620
649
|
// Module imports handled by interpretWithFile; no-op if no file context
|
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) {
|
|
@@ -162,6 +166,9 @@ export function lex(source) {
|
|
|
162
166
|
}
|
|
163
167
|
if (peek() === "\\") {
|
|
164
168
|
advance();
|
|
169
|
+
if (i >= source.length) {
|
|
170
|
+
throw new Error(`Unterminated string literal (escape at end of file) at line ${sl}, col ${sc}`);
|
|
171
|
+
}
|
|
165
172
|
const esc = advance();
|
|
166
173
|
if (esc === "n")
|
|
167
174
|
str += "\n";
|
|
@@ -177,6 +184,9 @@ export function lex(source) {
|
|
|
177
184
|
}
|
|
178
185
|
str += advance();
|
|
179
186
|
}
|
|
187
|
+
if (i >= source.length) {
|
|
188
|
+
throw new Error(`Unterminated string literal at line ${sl}, col ${sc}`);
|
|
189
|
+
}
|
|
180
190
|
if (peek() === '"')
|
|
181
191
|
advance(); // skip closing quote
|
|
182
192
|
if (hasInterp) {
|
package/dist/parser.d.ts
CHANGED
package/dist/parser.js
CHANGED
|
@@ -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
|