Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ cd .worktrees/<name>
# do work, commit, then open a PR
```

## Autonomous PR Workflow

Agents can work autonomously end-to-end: create worktrees, make changes, push branches, create PRs,
monitor CI, and merge when green. You have push access to feature branches and merge access to PRs.

1. Create a worktree and branch
2. Make changes, run `npm run verify:quick`, commit
3. `git push origin <branch>` — push to remote
4. `gh pr create` — open a PR
5. `gh pr checks <number>` — monitor CI
6. When CI is green: `gh pr merge <number> --squash` — merge to main
7. Pull main and continue with next task

**Never push to main directly.** Always go through PRs.

## Testing & Commit Workflow

After completing each todo:
Expand Down
72 changes: 31 additions & 41 deletions src/codegen/expressions/access/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,34 @@ export class IndexAccessGenerator {
return arg;
}

private generateStringArrayIndex(expr: IndexAccessNode, params: string[]): string {
const stringArrayPtr = this.ctx.generateExpression(expr.object, params);
const indexDouble = this.ctx.generateExpression(expr.index, params);

// Convert double index to i32 for getelementptr
const indexType = this.ctx.getVariableType(indexDouble);
let index = indexDouble;
private toI32Index(indexValue: string): string {
const indexType = this.ctx.getVariableType(indexValue);
if (indexType === "double") {
index = this.ctx.nextTemp();
this.ctx.emit(`${index} = fptosi double ${indexDouble} to i32`);
const temp = this.ctx.nextTemp();
this.ctx.emit(`${temp} = fptosi double ${indexValue} to i32`);
return temp;
} else if (indexType === "i64") {
index = this.ctx.nextTemp();
this.ctx.emit(`${index} = trunc i64 ${indexDouble} to i32`);
const temp = this.ctx.nextTemp();
this.ctx.emit(`${temp} = trunc i64 ${indexValue} to i32`);
return temp;
}
return indexValue;
}

private emitBoundsCheck(arrayPtr: string, arrayType: string, index: string): void {
const lenPtr = this.ctx.nextTemp();
this.ctx.emit(
`${lenPtr} = getelementptr inbounds ${arrayType}, ${arrayType}* ${arrayPtr}, i32 0, i32 1`,
);
const len = this.ctx.nextTemp();
this.ctx.emit(`${len} = load i32, i32* ${lenPtr}`);
this.ctx.emit(`call void @__cs_bounds_check(i32 ${index}, i32 ${len})`);
}

private generateStringArrayIndex(expr: IndexAccessNode, params: string[]): string {
const stringArrayPtr = this.ctx.generateExpression(expr.object, params);
const indexDouble = this.ctx.generateExpression(expr.index, params);
const index = this.toI32Index(indexDouble);

const dataPtr = this.ctx.nextTemp();
this.ctx.emit(
Expand All @@ -193,25 +207,16 @@ export class IndexAccessGenerator {

const elem = this.ctx.nextTemp();
this.ctx.emit(`${elem} = load i8*, i8** ${elemPtr}`);
// Track that this loaded value is a string
this.ctx.setVariableType(elem, "i8*");
return elem;
}

private generateNumericArrayIndex(expr: IndexAccessNode, params: string[]): string {
const arrayPtr = this.ctx.generateExpression(expr.object, params);
const indexDouble = this.ctx.generateExpression(expr.index, params);
const index = this.toI32Index(indexDouble);

// Convert double index to i32 for getelementptr
const indexType = this.ctx.getVariableType(indexDouble);
let index = indexDouble;
if (indexType === "double") {
index = this.ctx.nextTemp();
this.ctx.emit(`${index} = fptosi double ${indexDouble} to i32`);
} else if (indexType === "i64") {
index = this.ctx.nextTemp();
this.ctx.emit(`${index} = trunc i64 ${indexDouble} to i32`);
}
this.emitBoundsCheck(arrayPtr, "%Array", index);

const dataPtr = this.ctx.nextTemp();
this.ctx.emit(`${dataPtr} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`);
Expand All @@ -222,7 +227,6 @@ export class IndexAccessGenerator {
const elemPtr = this.ctx.nextTemp();
this.ctx.emit(`${elemPtr} = getelementptr inbounds double, double* ${data}, i32 ${index}`);

// Load double element
const elem = this.ctx.nextTemp();
this.ctx.emit(`${elem} = load double, double* ${elemPtr}`);
this.ctx.setVariableType(elem, "double");
Expand All @@ -233,15 +237,7 @@ export class IndexAccessGenerator {
const arrayPtr = this.ctx.generateExpression(expr.object, params);
const indexDouble = this.ctx.generateExpression(expr.index, params);

const indexType = this.ctx.getVariableType(indexDouble);
let index = indexDouble;
if (indexType === "double") {
index = this.ctx.nextTemp();
this.ctx.emit(`${index} = fptosi double ${indexDouble} to i32`);
} else if (indexType === "i64") {
index = this.ctx.nextTemp();
this.ctx.emit(`${index} = trunc i64 ${indexDouble} to i32`);
}
const index = this.toI32Index(indexDouble);

const arrayType = this.ctx.getVariableType(arrayPtr);
if (arrayType === "%ObjectArray*") {
Expand Down Expand Up @@ -313,15 +309,9 @@ export class IndexAccessGenerator {
const arrayPtr = this.ctx.generateExpression(expr.object, params);
const indexDouble = this.ctx.generateExpression(expr.index, params);

const indexType = this.ctx.getVariableType(indexDouble);
let index = indexDouble;
if (indexType === "double") {
index = this.ctx.nextTemp();
this.ctx.emit(`${index} = fptosi double ${indexDouble} to i32`);
} else if (indexType === "i64") {
index = this.ctx.nextTemp();
this.ctx.emit(`${index} = trunc i64 ${indexDouble} to i32`);
}
const index = this.toI32Index(indexDouble);

this.emitBoundsCheck(arrayPtr, "%Uint8Array", index);

const dataFieldPtr = this.ctx.nextTemp();
this.ctx.emit(
Expand Down
42 changes: 42 additions & 0 deletions src/codegen/infrastructure/llvm-declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,48 @@ export function getSafeStringHelper(): string {
return ir;
}

export function getBoundsCheckHelper(): string {
let ir = "";
ir +=
'@.str.oob_fmt = private unnamed_addr constant [49 x i8] c"Error: array index %d out of bounds (length %d)\\0A\\00", align 1\n';
ir += "define void @__cs_bounds_check(i32 %index, i32 %length) {\n";
ir += "entry:\n";
ir += " %too_high = icmp sge i32 %index, %length\n";
ir += " %too_low = icmp slt i32 %index, 0\n";
ir += " %oob = or i1 %too_high, %too_low\n";
ir += " br i1 %oob, label %fail, label %ok\n";
ir += "fail:\n";
ir += " %stderr = load i8*, i8** @stderr\n";
ir +=
" call i32 (i8*, i8*, ...) @fprintf(i8* %stderr, i8* getelementptr([49 x i8], [49 x i8]* @.str.oob_fmt, i32 0, i32 0), i32 %index, i32 %length)\n";
ir += " call void @exit(i32 1)\n";
ir += " unreachable\n";
ir += "ok:\n";
ir += " ret void\n";
ir += "}\n\n";
return ir;
}

export function getNullCheckHelper(): string {
let ir = "";
ir +=
'@.str.null_fmt = private unnamed_addr constant [39 x i8] c"Error: cannot access property of null\\0A\\00", align 1\n';
ir += "define void @__cs_null_check(i8* %ptr) {\n";
ir += "entry:\n";
ir += " %is_null = icmp eq i8* %ptr, null\n";
ir += " br i1 %is_null, label %fail, label %ok\n";
ir += "fail:\n";
ir += " %stderr = load i8*, i8** @stderr\n";
ir +=
" call i32 (i8*, i8*, ...) @fprintf(i8* %stderr, i8* getelementptr([39 x i8], [39 x i8]* @.str.null_fmt, i32 0, i32 0))\n";
ir += " call void @exit(i32 1)\n";
ir += " unreachable\n";
ir += "ok:\n";
ir += " ret void\n";
ir += "}\n\n";
return ir;
}

export function getStringHashHelper(): string {
let ir = "";
ir += "; DJB2 hash function for strings\n";
Expand Down
4 changes: 4 additions & 0 deletions src/codegen/llvm-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ import {
getSafeStringHelper,
getDoubleToStringHelper,
getStringHashHelper,
getBoundsCheckHelper,
getNullCheckHelper,
getGlobalVariables,
} from "./infrastructure/llvm-declarations.js";
import {
Expand Down Expand Up @@ -2558,6 +2560,8 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
if (strHash) {
irParts.push(strHash);
}
irParts.push(getBoundsCheckHelper());
irParts.push(getNullCheckHelper());

irParts.push(this.fsGen.generateReaddirSyncHelper());
irParts.push(this.fsGen.generateStatSyncHelper());
Expand Down
4 changes: 2 additions & 2 deletions src/codegen/types/collections/array/combine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export function generateArrayJoin(
params: string[],
): string {
if (expr.args.length > 1) {
throw new Error("join() accepts 0 or 1 arguments (separator)");
return gen.emitError("join() accepts 0 or 1 arguments (separator)", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand Down Expand Up @@ -812,7 +812,7 @@ export function generateArrayConcat(
}

if (expr.args.length !== 1) {
throw new Error("concat() requires exactly 1 argument");
return gen.emitError("concat() requires exactly 1 argument", expr.loc);
}

const otherArrayPtr = gen.generateExpression(expr.args[0], params);
Expand Down
23 changes: 13 additions & 10 deletions src/codegen/types/collections/array/iteration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function generateArrayFilter(
params: string[],
): string {
if (expr.args.length !== 1) {
throw new Error("filter() requires exactly 1 argument (predicate function)");
return gen.emitError("filter() requires exactly 1 argument (predicate function)", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand All @@ -45,7 +45,7 @@ export function generateArrayFilter(
predicateFn = gen.generateExpression(callbackArg, params);
gen.setExpectedCallbackParamType(null);
} else {
throw new Error("filter() argument must be a function name or inline function");
return gen.emitError("filter() argument must be a function name or inline function", expr.loc);
}

let result: string;
Expand Down Expand Up @@ -271,7 +271,7 @@ export function generateArrayForEach(
params: string[],
): string {
if (expr.args.length !== 1) {
throw new Error("forEach() requires exactly 1 argument (callback function)");
return gen.emitError("forEach() requires exactly 1 argument (callback function)", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand All @@ -288,7 +288,7 @@ export function generateArrayForEach(
callbackFn = gen.generateExpression(callbackArg, params);
gen.setExpectedCallbackParamType(null);
} else {
throw new Error("forEach() argument must be a function name or inline function");
return gen.emitError("forEach() argument must be a function name or inline function", expr.loc);
}

let result: string;
Expand Down Expand Up @@ -402,7 +402,10 @@ export function generateArrayReduce(
params: string[],
): string {
if (expr.args.length < 1 || expr.args.length > 2) {
throw new Error("reduce() requires 1-2 arguments (callback, optional initialValue)");
return gen.emitError(
"reduce() requires 1-2 arguments (callback, optional initialValue)",
expr.loc,
);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand Down Expand Up @@ -430,7 +433,7 @@ export function generateArrayReduce(
callbackFn = gen.generateExpression(callbackArg, params);
gen.setExpectedCallbackParamType(null);
} else {
throw new Error("reduce() argument must be a function name or inline function");
return gen.emitError("reduce() argument must be a function name or inline function", expr.loc);
}

let initialValue: string | null = null;
Expand Down Expand Up @@ -595,7 +598,7 @@ export function generateArrayMap(
params: string[],
): string {
if (expr.args.length !== 1) {
throw new Error("map() requires exactly 1 argument (callback function)");
return gen.emitError("map() requires exactly 1 argument (callback function)", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand All @@ -616,7 +619,7 @@ export function generateArrayMap(
gen.setExpectedCallbackParamType(null);
gen.setExpectedCallbackReturnType(null);
} else {
throw new Error("map() argument must be a function name or inline function");
return gen.emitError("map() argument must be a function name or inline function", expr.loc);
}

let result: string;
Expand All @@ -635,7 +638,7 @@ export function generateStringArrayMap(
params: string[],
): string {
if (expr.args.length !== 1) {
throw new Error("map() requires exactly 1 argument (callback function)");
return gen.emitError("map() requires exactly 1 argument (callback function)", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand All @@ -651,7 +654,7 @@ export function generateStringArrayMap(
gen.setExpectedCallbackParamType(null);
gen.setExpectedCallbackReturnType(null);
} else {
throw new Error("map() argument must be a function name or inline function");
return gen.emitError("map() argument must be a function name or inline function", expr.loc);
}

const result = generateStringArrayMapImpl(gen, arrayPtr, callbackFn);
Expand Down
2 changes: 1 addition & 1 deletion src/codegen/types/collections/array/literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function generateArrayLiteral(
): string {
const e = expr as ExprBase;
if (e.type !== "array") {
throw new Error("Expected array literal");
return gen.emitError("Expected array literal");
}

const arrExpr = expr as ArrayExpr;
Expand Down
4 changes: 2 additions & 2 deletions src/codegen/types/collections/array/mutators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function generateArrayPush(
): string {
// arr.push(value) - adds value to array and returns new length
if (expr.args.length !== 1) {
throw new Error("push() requires exactly 1 argument");
return gen.emitError("push() requires exactly 1 argument", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand Down Expand Up @@ -69,7 +69,7 @@ export function generateArrayPop(
): string {
// arr.pop() - removes and returns last element
if (expr.args.length !== 0) {
throw new Error("pop() requires 0 arguments");
return gen.emitError("pop() requires 0 arguments", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand Down
6 changes: 3 additions & 3 deletions src/codegen/types/collections/array/reorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function generateArrayReverse(
params: string[],
): string {
if (expr.args.length !== 0) {
throw new Error("reverse() requires 0 arguments");
return gen.emitError("reverse() requires 0 arguments", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand Down Expand Up @@ -161,7 +161,7 @@ export function generateArrayShift(
params: string[],
): string {
if (expr.args.length !== 0) {
throw new Error("shift() requires 0 arguments");
return gen.emitError("shift() requires 0 arguments", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand Down Expand Up @@ -363,7 +363,7 @@ export function generateArrayUnshift(
params: string[],
): string {
if (expr.args.length !== 1) {
throw new Error("unshift() requires exactly 1 argument");
return gen.emitError("unshift() requires exactly 1 argument", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);
Expand Down
Loading
Loading