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
32 changes: 32 additions & 0 deletions .claude/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ TypeScript-to-native compiler using LLVM IR. Compiles .ts/.js files to native bi

| Dir | Purpose |
| ----------------------------------------- | --------------------------------------------------------------------------------- |
| `src/semantic/` | Semantic analysis passes run before codegen (closure mutation, union types) |
| `src/codegen/` | LLVM IR code generation (the core) |
| `src/codegen/expressions/method-calls.ts` | Central dispatcher for all `object.method()` calls |
| `src/codegen/types/collections/string/` | String method IR generators (manipulation.ts, search.ts, split.ts, etc.) |
Expand Down Expand Up @@ -205,6 +206,7 @@ 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.

## Interface Field Iteration

Expand Down Expand Up @@ -257,3 +259,33 @@ Parser preserves string values in `EnumMember.stringValue` and marks `EnumDeclar
## Async/Await Type Tracking

`allocateAwaitResult` in `variable-allocator.ts` must inspect the awaited expression to determine the correct SymbolKind. Default is `i8*`/string, but `Promise.all()` resolves to `%ObjectArray*`. For each new async API that resolves to a specific type, add a detection case to `allocateAwaitResult`.

## Semantic Analysis Passes

Semantic passes live in `src/semantic/` and run before codegen (called from `LLVMGenerator.generateParts()`).
They catch errors that would produce silently wrong native code — the native compiler can't throw exceptions
at runtime, so these must be compile-time errors.

Current passes:

- **`closure-mutation-checker.ts`** — ChadScript closures capture by value. Mutating a variable after capture
produces silently wrong results. This pass detects post-capture assignments and emits a compile error.
- **`union-type-checker.ts`** — Type alias unions like `type Mixed = string | number` bypass the inline union
check. This pass resolves aliases and rejects unions whose members map to different LLVM representations.

To add a new semantic pass: create `src/semantic/your-check.ts`, export a `checkX(ast: AST): void` function,
and call it from `generateParts()` in `llvm-generator.ts`.

## LLVMGenerator.reset()

`LLVMGenerator.reset()` calls `super.reset()` to reset all `BaseGenerator` fields, then clears its own
additional fields. If you add new per-function state to either class, add the reset in the right place:
base fields in `BaseGenerator.reset()`, LLVMGenerator-only fields in the override after `super.reset()`.

## Expression Orchestrator — No Silent Nulls

`orchestrator.ts` must **never** silently generate null pointers (`inttoptr i64 0 to i8*`) for unrecognized
expressions. These nulls are UB that LLVM `-O2` can exploit to prune unrelated code paths. Both fallback
paths (empty type, unsupported type) now call `ctx.emitError()` which is `never`-typed — it exits the
compiler immediately. If a new expression type is added to the parser, add a handler in the orchestrator;
don't rely on a fallback.
9 changes: 5 additions & 4 deletions docs/language/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
| `async`/`await` | Supported |
| Default parameters | Supported |
| Rest parameters (`...args`) | Supported |
| Closures | Supported (capture by value, not by reference) |
| Closures | Supported (capture by value; post-capture mutation is a compile error) |
| `declare function` (FFI) | Supported (see [FFI](#foreign-function-interface-ffi)) |
| Async generators / `for await...of` | Not supported |

Expand Down Expand Up @@ -183,15 +183,16 @@ Strings are null-terminated C strings, not JavaScript's UTF-16 strings. They wor

## Closures

Arrow functions and nested functions can capture outer variables, but captures are **by value, not by reference**. If you mutate a variable after a closure captures it, the closure won't see the change:
Arrow functions and nested functions can capture outer variables, but captures are **by value, not by reference**. Mutating a variable after a closure captures it is a **compile error**:

```typescript
let x = 1;
const f = () => console.log(x);
x = 2;
f(); // prints 1, not 2
x = 2; // error: variable 'x' is reassigned after being captured by a closure
```

This is enforced at compile time because the closure would silently see the old value — a common source of bugs in native code where there's no runtime to help.

Inline lambdas with captures work in array methods:

```typescript
Expand Down
2 changes: 2 additions & 0 deletions src/codegen/expressions/access/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface IndexAccessGeneratorContext {
isStringExpression(expr: Expression): boolean;
readonly stringGen: IStringGenerator;
ensureDouble(value: string): string;
setUsesJson(value: boolean): void;
}

/**
Expand Down Expand Up @@ -92,6 +93,7 @@ export class IndexAccessGenerator {
exprObjBase.type === "variable" &&
this.ctx.symbolTable.isJSON((expr.object as VariableNode).name)
) {
this.ctx.setUsesJson(true);
return this.generateJSONArrayIndex(expr, params);
}

Expand Down
23 changes: 9 additions & 14 deletions src/codegen/expressions/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface ExpressionOrchestratorContext {
setLastInlineLambdaEnvPtr(ptr: string | null): void;
setLastTypeAssertionSourceVar(name: string | null): void;
emitWarning(message: string, loc?: { line: number; column: number }, suggestion?: string): void;
emitError(message: string, loc?: { line: number; column: number }, suggestion?: string): never;
}

/**
Expand Down Expand Up @@ -119,16 +120,13 @@ export class ExpressionGenerator {
generate(expr: Expression, params: string[]): string {
const exprTyped = expr as { type: string };
if (!exprTyped.type || exprTyped.type.length === 0) {
// Strict: expressions must have a type. An empty type indicates a parser
// or AST construction bug — surface it instead of silently returning null.
this.ctx.emitWarning(
"expression has empty type — this likely indicates a parser bug, treating as null",
// Hard error: expressions must have a type. An empty type indicates a parser
// or AST construction bug. Previously this silently generated a null pointer,
// which LLVM -O2 could exploit as UB to prune unrelated code paths.
this.ctx.emitError(
"expression has empty type — this likely indicates a parser bug",
(expr as { loc?: { line: number; column: number } }).loc,
);
const temp = this.ctx.nextTemp();
this.ctx.emit(`${temp} = inttoptr i64 0 to i8*`);
this.ctx.setVariableType(temp, "i8*");
return temp;
}
// Literals
if (exprTyped.type === "number") {
Expand Down Expand Up @@ -323,15 +321,12 @@ export class ExpressionGenerator {
return this.indexAccessGen.generateAssignment(expr as IndexAccessAssignmentNode, params);
}

this.ctx.emitWarning(
// Hard error: unsupported expression types must not silently produce null pointers.
// A null here would be UB that LLVM -O2 can exploit to prune unrelated code.
this.ctx.emitError(
"unsupported expression type: " + exprTyped.type,
(expr as { loc?: { line: number; column: number } }).loc,
"this expression will evaluate to null",
);
const temp = this.ctx.nextTemp();
this.ctx.emit(`${temp} = inttoptr i64 0 to i8*`);
this.ctx.setVariableType(temp, "i8*");
return temp;
}

/**
Expand Down
19 changes: 7 additions & 12 deletions src/codegen/llvm-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ import type { TypeChecker } from "../typescript/type-checker.js";
import { InterfaceStructGenerator } from "./types/interface-struct-generator.js";
import { JsonObjectMeta } from "./expressions/access/member.js";
import type { TargetInfo } from "../target-types.js";
import { checkClosureMutations } from "../semantic/closure-mutation-checker.js";
import { checkUnionTypes } from "../semantic/union-type-checker.js";

export interface SemaSymbolData {
names: string[];
Expand Down Expand Up @@ -1717,18 +1719,8 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
}

reset(): void {
this.tempCounter = 0;
this.labelCounter = 0;
this.currentLabel = "entry";
this.output.length = 0;
this.outputIsTerminator.length = 0;
this.outputCount = 0;
this.thisPointer = null;
this.currentClassName = null;
this.currentFunctionReturnType = "double";
this.symbolTable.clearLocals();
this.variableTypes.clear();
this.expressionTypes.clear();
super.reset();
// LLVMGenerator-specific fields not in BaseGenerator
this.stringBuilderSlen.clear();
this.stringBuilderScap.clear();
}
Expand Down Expand Up @@ -2357,6 +2349,9 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
}

generateParts(): string[] {
checkClosureMutations(this.ast);
checkUnionTypes(this.ast);

const irParts: string[] = [];

const interfaceStructDefs = this.interfaceStructGen.generateStructTypeDefinitions();
Expand Down
Loading
Loading