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
6 changes: 4 additions & 2 deletions .claude/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ Existing bridges: `regex-bridge.c`, `yyjson-bridge.c`, `os-bridge.c`, `child-pro
5. **Type cast field order must match FULL struct layout** — when the type extends a parent interface, the struct includes ALL parent fields. `as { name, closureInfo }` on a `LiftedFunction extends FunctionNode` (10 fields) reads index 1 instead of index 9. Include every field.
6. **`ret void` not `unreachable`** at end of void functions
7. **Class structs: boolean is `i1`; Interface structs: boolean is `double`**
8. **Set feature flags when emitting gated extern calls** — runtime declarations for C bridges (yyjson, curl, etc.) are conditionally emitted behind flags like `usesJson`, `usesCurl`. Any code path that emits `call @csyyjson_*` must call `ctx.setUsesJson(true)`, etc. Missing this causes "undefined value" errors from `opt` because the `declare` is never emitted.
8. **Propagate declared type before generating RHS for collection fields** — when a class field is typed `Set<string>`, `Map<K,V>`, etc., call `setCurrentDeclaredSetType` / `setCurrentDeclaredMapType` before `generateExpression` on the RHS so that `new Set()` / `new Map()` without explicit type args picks the right generator. See `handleClassFieldAssignment` in `assignment-generator.ts`.
9. **Set feature flags when emitting gated extern calls** — runtime declarations for C bridges (yyjson, curl, etc.) are conditionally emitted behind flags like `usesJson`, `usesCurl`. Any code path that emits `call @csyyjson_*` must call `ctx.setUsesJson(true)`, etc. Missing this causes "undefined value" errors from `opt` because the `declare` is never emitted.

## Interface Field Iteration

Expand All @@ -228,7 +229,8 @@ any interface with inheritance. `allocateDeclaredInterface` does this correctly;
1. **`new` in class field initializers** — codegen handles simple `new X()` in field initializers (both explicit and default constructors), but complex nested class instantiation may have edge cases. Prefer initializing in constructors for safety.
2. **Type assertions must match real struct field order AND count** — `as { type, left, right }` on a struct that's `{ type, op, left, right }` causes GEP to read wrong fields. Fields must be a PREFIX of the real struct in EXACT order. **Watch out for `extends`**: if `Child extends Parent`, the struct has ALL of Parent's fields first, then Child's. A type assertion on a Child must include Parent's fields too — even optional ones the object literal doesn't set (the compiler allocates slots for them anyway, filled with null/0).
3. **Never insert new optional fields in the MIDDLE of an interface** — The native compiler determines struct layouts from object literal creation sites. If an interface has multiple creation sites (e.g., `MethodCallNode` is created in parser-ts, parser-native, and codegen), inserting a new field before existing ones shifts GEP indices and breaks creation sites that don't include the new field. **Always add new optional fields at the END of interfaces.** Root cause: the native compiler doesn't unify struct layouts from interface definitions — it uses object literal field order, and different creation sites may have different subsets of fields.
4. **`||` fallback makes member access opaque** — `const x = foo.bar || { field: [] }` stores the result as `i8*` (opaque pointer) because the `||` merges two different types. Subsequent `.field` access on `x` does NOT generate a GEP — it just returns `x` itself. Fix: use a ternary that preserves the typed path: `const y = foo.bar ? foo.bar.field : []`. This applies to any `||` or `??` where the fallback is an inline object literal.
4. **`alloca` for collection structs stored in class fields** — `%Set`, `%StringSet`, `%Map`, and similar structs must be heap-allocated via `GC_malloc`, not `alloca`. Stack-allocated structs become dangling pointers when stored in a class field after the constructor returns. Use `emitCall("i8*", "@GC_malloc", "i64 N") + emitBitcast(...)` instead of `emit("... = alloca %Foo")`.
5. **`||` fallback makes member access opaque** — `const x = foo.bar || { field: [] }` stores the result as `i8*` (opaque pointer) because the `||` merges two different types. Subsequent `.field` access on `x` does NOT generate a GEP — it just returns `x` itself. Fix: use a ternary that preserves the typed path: `const y = foo.bar ? foo.bar.field : []`. This applies to any `||` or `??` where the fallback is an inline object literal.

## Stage 0 Compatibility

Expand Down
37 changes: 29 additions & 8 deletions scripts/run-examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ echo ""

# --- 1. hello.ts (simple print) ---

echo "[1/8] hello.ts"
echo "[1/9] hello.ts"
if compile examples/hello.ts "$BUILD_DIR/hello"; then
OUTPUT=$("$BUILD_DIR/hello" 2>&1) || true
if echo "$OUTPUT" | grep -q "Hello from ChadScript"; then
Expand All @@ -88,7 +88,7 @@ fi

# --- 2. timers.ts (event loop, self-terminating) ---

echo "[2/8] timers.ts"
echo "[2/9] timers.ts"
if compile examples/timers.ts "$BUILD_DIR/timers"; then
# No `timeout` on macOS — use background process + wait with a deadline
"$BUILD_DIR/timers" > "$BUILD_DIR/timers.out" 2>&1 &
Expand All @@ -110,7 +110,7 @@ fi

# --- 3. cli-parser-demo.ts (argparse) ---

echo "[3/8] cli-parser-demo.ts"
echo "[3/9] cli-parser-demo.ts"
if compile examples/cli-parser-demo.ts "$BUILD_DIR/cli-parser-demo"; then
OUTPUT=$("$BUILD_DIR/cli-parser-demo" -v -o result.txt myfile.txt 2>&1) || true
if echo "$OUTPUT" | grep -q "verbose"; then
Expand All @@ -124,7 +124,7 @@ fi

# --- 4. query.ts (sqlite in-memory) ---

echo "[4/8] query.ts"
echo "[4/9] query.ts"
if compile examples/query.ts "$BUILD_DIR/query"; then
OUTPUT=$("$BUILD_DIR/query" 2>&1) || true
if echo "$OUTPUT" | grep -q "Found"; then
Expand All @@ -138,7 +138,7 @@ fi

# --- 5. word-count.ts (file I/O) ---

echo "[5/8] word-count.ts"
echo "[5/9] word-count.ts"
if compile examples/word-count.ts "$BUILD_DIR/word-count"; then
# Create a test file to count
echo "hello world foo bar" > "$BUILD_DIR/test-input.txt"
Expand All @@ -154,7 +154,7 @@ fi

# --- 6. string-search.ts (grep-like) ---

echo "[6/8] string-search.ts"
echo "[6/9] string-search.ts"
if compile examples/string-search.ts "$BUILD_DIR/string-search"; then
# Create a test file to search
printf "line one\nfind me here\nline three\n" > "$BUILD_DIR/search-input.txt"
Expand All @@ -170,7 +170,7 @@ fi

# --- 7. http-server.ts (server + curl) ---

echo "[7/8] http-server.ts"
echo "[7/9] http-server.ts"
if compile examples/http-server.ts "$BUILD_DIR/http-server"; then
PORT=18080
"$BUILD_DIR/http-server" -p "$PORT" &
Expand Down Expand Up @@ -208,7 +208,7 @@ fi

# --- 8. hackernews/app.ts (full-stack server + curl) ---

echo "[8/8] hackernews/app.ts"
echo "[8/9] hackernews/app.ts"
if compile examples/hackernews/app.ts "$BUILD_DIR/hackernews"; then
PORT=18081
"$BUILD_DIR/hackernews" -p "$PORT" &
Expand Down Expand Up @@ -238,6 +238,27 @@ else
fail "hackernews/app.ts" "compile failed"
fi

# --- 9. parallel.ts (parallel HTTP fetches with Promise.all) ---

echo "[9/9] parallel.ts"
if compile examples/parallel.ts "$BUILD_DIR/parallel"; then
"$BUILD_DIR/parallel" > "$BUILD_DIR/parallel.out" 2>&1 &
PARALLEL_PID=$!
( sleep 30; kill "$PARALLEL_PID" 2>/dev/null ) &
WATCHDOG_PID=$!
wait "$PARALLEL_PID" 2>/dev/null || true
kill "$WATCHDOG_PID" 2>/dev/null || true
wait "$WATCHDOG_PID" 2>/dev/null || true
OUTPUT=$(cat "$BUILD_DIR/parallel.out")
if echo "$OUTPUT" | grep -q "stars"; then
pass "parallel.ts"
else
fail "parallel.ts" "unexpected output: $OUTPUT"
fi
else
fail "parallel.ts" "compile failed"
fi

# --- Summary ---

echo ""
Expand Down
7 changes: 6 additions & 1 deletion src/codegen/expressions/literals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,12 @@ export class LiteralExpressionGenerator {
return this.generateNewRegExp(args, params);
}
if (className === "Set") {
if (typeArgs && typeArgs.length > 0 && typeArgs[0] === "string") {
if (!typeArgs || typeArgs.length === 0) {
throw new Error(
"new Set() requires an explicit type argument, e.g. new Set<string>() or new Set<number>()",
);
}
if (typeArgs[0] === "string") {
return this.ctx.stringSetGen.generateEmptyStringSet();
}
return this.ctx.setGen.generateSetLiteral({ type: "set", values: [] }, params);
Expand Down
7 changes: 7 additions & 0 deletions src/codegen/infrastructure/assignment-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export interface AssignmentGeneratorContext {
setExpectedArrayElementType(type: "string" | "number" | "boolean" | "pointer" | null): void;
currentDeclaredMapType: string | undefined;
setCurrentDeclaredMapType(type: string | undefined): void;
currentDeclaredSetType: string | undefined;
setCurrentDeclaredSetType(type: string | undefined): void;
getThisPointer(): string | null;
getCurrentClassName(): string | null;
readonly symbolTable: SymbolTable;
Expand Down Expand Up @@ -249,9 +251,14 @@ export class AssignmentGenerator {
this.ctx.setCurrentDeclaredMapType(fieldTsType);
}

if (fieldTsType && fieldTsType.startsWith("Set<")) {
this.ctx.setCurrentDeclaredSetType(fieldTsType);
}

const value = this.ctx.generateExpression(memberAccessValue.value, params);
this.ctx.setExpectedArrayElementType(null);
this.ctx.setCurrentDeclaredMapType(undefined);
this.ctx.setCurrentDeclaredSetType(undefined);

let instancePtr: string | null = null;
const objType = object.type;
Expand Down
11 changes: 6 additions & 5 deletions src/codegen/types/collections/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export class SetGenerator {
throw new Error("Expected set literal");
}

// Allocate Set struct on stack
const setPtr = this.nextTemp();
this.emit(`${setPtr} = alloca %Set`);
// Allocate Set struct on heap so it's safe to store in class fields
const setMem = this.ctx.emitCall("i8*", "@GC_malloc", `i64 16`);
const setPtr = this.ctx.emitBitcast(setMem, "i8*", "%Set*");

// Initialize with empty array
const initialCapacity = setExpr.values.length > 4 ? setExpr.values.length : 4;
Expand Down Expand Up @@ -390,8 +390,9 @@ export class StringSetGenerator {
}

generateEmptyStringSet(): string {
const setPtr = this.nextTemp();
this.emit(`${setPtr} = alloca %StringSet`);
// Allocate StringSet struct on heap so it's safe to store in class fields
const setMem = this.ctx.emitCall("i8*", "@GC_malloc", `i64 16`);
const setPtr = this.ctx.emitBitcast(setMem, "i8*", "%StringSet*");

const initialCapacity = 4;
const ptrSize = this.getPtrSize();
Expand Down
Loading