diff --git a/.claude/rules.md b/.claude/rules.md index 13b373bb..7ffee29a 100644 --- a/.claude/rules.md +++ b/.claude/rules.md @@ -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`, `Map`, 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 @@ -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 diff --git a/scripts/run-examples.sh b/scripts/run-examples.sh index 6321920e..1a801486 100755 --- a/scripts/run-examples.sh +++ b/scripts/run-examples.sh @@ -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 @@ -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 & @@ -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 @@ -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 @@ -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" @@ -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" @@ -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" & @@ -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" & @@ -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 "" diff --git a/src/codegen/expressions/literals.ts b/src/codegen/expressions/literals.ts index ab0851cc..bc78a3de 100644 --- a/src/codegen/expressions/literals.ts +++ b/src/codegen/expressions/literals.ts @@ -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() or new Set()", + ); + } + if (typeArgs[0] === "string") { return this.ctx.stringSetGen.generateEmptyStringSet(); } return this.ctx.setGen.generateSetLiteral({ type: "set", values: [] }, params); diff --git a/src/codegen/infrastructure/assignment-generator.ts b/src/codegen/infrastructure/assignment-generator.ts index 8c01c3a1..679f87b8 100644 --- a/src/codegen/infrastructure/assignment-generator.ts +++ b/src/codegen/infrastructure/assignment-generator.ts @@ -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; @@ -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; diff --git a/src/codegen/types/collections/set.ts b/src/codegen/types/collections/set.ts index 01bf4696..bf84fa77 100644 --- a/src/codegen/types/collections/set.ts +++ b/src/codegen/types/collections/set.ts @@ -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; @@ -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();