From c009774192e6e6844afc9e9950b88e3a24c8b02f Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Wed, 25 Feb 2026 22:13:33 -0800 Subject: [PATCH 1/6] docs cleanup and improvements --- docs/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.md b/docs/index.md index a0972da5..2ebf5dbc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,6 +21,8 @@ features: details: Everything you'd npm install — HTTP, SQLite, fetch, crypto, JSON — is built in. No dependencies. - title: Single-Binary Deploy details: Embed HTML, CSS, and assets at compile time. Ship one file. + - title: Watch Mode + details: "`chad watch` recompiles and restarts your app on every save. No config needed." --- From 6e42baf7c76348be2af5ba665f9cb4b0f3f2916f Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Wed, 25 Feb 2026 22:55:20 -0800 Subject: [PATCH 2/6] remove llvm ir animation from homepage and swap watch mode for single-binary deploy feature --- docs/index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 2ebf5dbc..a0972da5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,8 +21,6 @@ features: details: Everything you'd npm install — HTTP, SQLite, fetch, crypto, JSON — is built in. No dependencies. - title: Single-Binary Deploy details: Embed HTML, CSS, and assets at compile time. Ship one file. - - title: Watch Mode - details: "`chad watch` recompiles and restarts your app on every save. No config needed." --- From b12beaf8d155682f958e5887077ebc6f9a9c2c67 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Wed, 25 Feb 2026 23:34:05 -0800 Subject: [PATCH 3/6] rename limitations.md to features.md and simplify unsupported items --- docs/.vitepress/config.mts | 2 +- docs/.vitepress/theme/IRShowcase.vue | 2 +- docs/faq.md | 4 +-- docs/getting-started/quickstart.md | 2 +- docs/index.md | 2 +- docs/language/architecture.md | 2 +- docs/language/{limitations.md => features.md} | 29 ++++++++----------- 7 files changed, 19 insertions(+), 24 deletions(-) rename docs/language/{limitations.md => features.md} (89%) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 797473e1..48835055 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -37,7 +37,7 @@ export default defineConfig({ { text: 'Installation', link: '/getting-started/installation' }, { text: 'Examples', link: '/getting-started/quickstart' }, { text: 'CLI Reference', link: '/getting-started/cli' }, - { text: 'Supported Features', link: '/language/limitations' }, + { text: 'Supported Features', link: '/language/features' }, { text: 'Debugging', link: '/getting-started/debugging' } ] }, diff --git a/docs/.vitepress/theme/IRShowcase.vue b/docs/.vitepress/theme/IRShowcase.vue index 5c1056f4..ca4dacbb 100644 --- a/docs/.vitepress/theme/IRShowcase.vue +++ b/docs/.vitepress/theme/IRShowcase.vue @@ -242,7 +242,7 @@ onUnmounted(() => {

Congratulations, you wrote your first ChadScript app!

diff --git a/docs/faq.md b/docs/faq.md index 9c49cb26..c16d677b 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -6,7 +6,7 @@ ChadScript is a compiler that takes TypeScript source code and produces native E ## Is ChadScript a drop-in replacement for TypeScript? -No. ChadScript supports a practical subset of TypeScript. It compiles to native machine code, so all types must be known at compile time and dynamic features like `eval()` aren't available. See [Language Support](/language/limitations) for details. +No. ChadScript supports a practical subset of TypeScript. It compiles to native machine code, so all types must be known at compile time and dynamic features like `eval()` aren't available. See [Language Support](/language/features) for details. ## What TypeScript features are supported? @@ -22,7 +22,7 @@ Most of the core language: variables, functions, classes, interfaces, arrays, st - Decorators, symbols, `Proxy`, `Reflect` - `WeakMap`, `WeakSet` -See [Language Support](/language/limitations) for the complete list of what works and what doesn't. +See [Language Support](/language/features) for the complete list of what works and what doesn't. ## How fast is it? diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index eb02363e..257e9e61 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -45,4 +45,4 @@ chad run examples/hackernews/app.ts # http://localhost:3000 - Browse the [Standard Library](/stdlib/) for all available APIs - See [CLI Reference](/getting-started/cli) for all compiler options -- Check [Supported Features](/language/limitations) to understand the TypeScript subset +- Check [Supported Features](/language/features) to understand the TypeScript subset diff --git a/docs/index.md b/docs/index.md index a0972da5..18a1b24e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,7 +43,7 @@ curl -fsSL https://raw.githubusercontent.com/cs01/ChadScript/main/install.sh | s diff --git a/docs/language/architecture.md b/docs/language/architecture.md index 61728970..9d775fce 100644 --- a/docs/language/architecture.md +++ b/docs/language/architecture.md @@ -2,7 +2,7 @@ ChadScript is an ahead-of-time TypeScript compiler that produces standalone native binaries. Write standard TypeScript, run `chad build`, and get an ELF or Mach-O binary that starts in under 2ms — no Node.js, no JVM, no runtime VM. The compiler is self-hosting: it is written in TypeScript and compiles itself to a native binary. -It targets a practical subset of TypeScript where all types are known at compile time. This has a nice side effect: the supported language is simpler and easier to reason about. Like C++ has a "safe subset" that avoids its footguns, ChadScript is a safe subset of TypeScript — you get the familiar syntax without the dynamic corners that make large codebases hard to follow. See [Supported Features](/language/limitations) for the full feature list. +It targets a practical subset of TypeScript where all types are known at compile time. This has a nice side effect: the supported language is simpler and easier to reason about. Like C++ has a "safe subset" that avoids its footguns, ChadScript is a safe subset of TypeScript — you get the familiar syntax without the dynamic corners that make large codebases hard to follow. See [Supported Features](/language/features) for the full feature list. ## How It Compiles Your Code diff --git a/docs/language/limitations.md b/docs/language/features.md similarity index 89% rename from docs/language/limitations.md rename to docs/language/features.md index d010ddf8..4b5dbbf7 100644 --- a/docs/language/limitations.md +++ b/docs/language/features.md @@ -51,18 +51,15 @@ | `Map`, `Set` | Supported | | Enums (numeric and string) | Supported | | Type aliases | Supported | -| Union types (`string \| null`) | Supported (nullable unions only — unsafe unions like `string \| number` are rejected at compile time) | +| Union types (`string \| null`) | Supported (nullable unions only) | | `any`, `unknown`, `never` | Not supported | -| User-defined generics (``) | Not supported (built-in generics like `Map` work) | +| User-defined generics (``) | Not supported | | Intersection types (`A & B`) | Not supported | | Mapped / conditional / template literal types | Not supported | -| `satisfies` | Not supported | -| `instanceof` | Not supported (no runtime type tags) | -| `Symbol` | Not supported | +| `satisfies`, `instanceof`, `Symbol` | Not supported | | `WeakMap`, `WeakSet`, `WeakRef` | Not supported | | `SharedArrayBuffer`, `Atomics` | Not supported | -| `FinalizationRegistry` | Not supported | -| `Intl` | Not supported | +| `FinalizationRegistry`, `Intl` | Not supported | ## Classes & Interfaces @@ -96,9 +93,9 @@ | `import { foo as baz } from './bar'` | Supported | | Default imports | Supported | | Named exports | Supported | -| Dynamic `import()` | Not supported | | Re-exports (`export { foo } from './bar'`) | Supported | | `export default` | Supported | +| Dynamic `import()` | Not supported | ## Async @@ -169,12 +166,12 @@ Linker flags (`-lm`, `-lpthread`, etc.) are auto-detected from linked libraries. These require runtime code evaluation and are not possible in a native compiler: -| Feature | Why | -|---------|-----| -| `eval()` | No runtime code evaluation | -| `Function()` constructor | No runtime code evaluation | -| `Proxy` / `Reflect` | Require runtime interception | -| `globalThis` | Not available | +| Feature | Status | +|---------|--------| +| `eval()` | Not supported | +| `Function()` constructor | Not supported | +| `Proxy` / `Reflect` | Not supported | +| `globalThis` | Not supported | ## Numbers @@ -208,6 +205,4 @@ npm packages work as long as they only use supported TypeScript features. ## Standard Library -Everything is built in — no `npm install` needed: - -`ChadScript.embed` · `child_process` · `console` · `crypto` · `Date` · `fetch` · `fs` · `httpServe` · `JSON` · `Map` · `Math` · `os` · `path` · `process` · `RegExp` · `Set` · `sqlite` +Everything is built in — no `npm install` needed. See the [Standard Library](/stdlib/) for the full API reference. From d5f006fdb43b1ecf2355d26fd4efb0228214a274 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Wed, 25 Feb 2026 23:36:39 -0800 Subject: [PATCH 4/6] remove overclaims about generics, integer optimization, and npm compatibility --- docs/.vitepress/theme/ComparisonCards.vue | 2 +- docs/index.md | 2 +- docs/language/architecture.md | 6 +++--- docs/language/features.md | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/.vitepress/theme/ComparisonCards.vue b/docs/.vitepress/theme/ComparisonCards.vue index 1d09610c..5d0b25f7 100644 --- a/docs/.vitepress/theme/ComparisonCards.vue +++ b/docs/.vitepress/theme/ComparisonCards.vue @@ -21,7 +21,7 @@ vs Go -

TypeScript syntax instead of Go's idiosyncratic type system. Classes, generics, interfaces, and async/await work the way you expect.

+

TypeScript syntax instead of Go's idiosyncratic type system. Classes, interfaces, closures, and async/await work the way you expect.

diff --git a/docs/index.md b/docs/index.md index 18a1b24e..af004b68 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,7 +16,7 @@ features: - title: No Runtime details: Compiles to standalone ELF binaries that start in under 2ms. - title: Familiar Syntax - details: Standard TypeScript — classes, interfaces, async/await, generics. + details: Standard TypeScript syntax — classes, interfaces, async/await, closures. - title: Batteries Included details: Everything you'd npm install — HTTP, SQLite, fetch, crypto, JSON — is built in. No dependencies. - title: Single-Binary Deploy diff --git a/docs/language/architecture.md b/docs/language/architecture.md index 9d775fce..4f13b57b 100644 --- a/docs/language/architecture.md +++ b/docs/language/architecture.md @@ -37,11 +37,11 @@ The string literal is a global constant. `getelementptr` produces a pointer to i ## Types and Semantics -TypeScript (and JavaScript) has a single `number` type — a 64-bit IEEE 754 float. ChadScript preserves this at the source level, but the compiler is smart about it: integer literals (`42`, `0xFF`) compile to native 64-bit integer registers; fractional values (`3.14`) use 64-bit doubles. Integer arithmetic (`+`, `-`, `*`, `%`) between integers stays in integer registers — no floating-point overhead. Division always returns a float. This is automatic and invisible. +TypeScript (and JavaScript) has a single `number` type — a 64-bit IEEE 754 float. ChadScript preserves this at the source level, but the compiler is smart about it: integer literal expressions (`42 + 10`, `0xFF & mask`) use native 64-bit integer instructions. Fractional values (`3.14`) use 64-bit doubles. Division always returns a float. -This matters because LLVM distinguishes the two: `add i64 %a, %b` is an integer add, `fadd double %a, %b` is a floating-point add. Getting the right instruction means you pay for the operation you actually need. +This matters because LLVM distinguishes the two: `add i64 %a, %b` is an integer add, `fadd double %a, %b` is a floating-point add. The optimization currently applies to literal expressions and intermediate arithmetic — variables are stored as doubles. This is an area of active improvement. -**Null safety.** In C, `null` is just a zero-valued pointer. Dereferencing it is undefined behavior — the compiler trusts you, and if you're wrong you get a segfault or worse. In TypeScript (and ChadScript), `null` is a value that must be explicitly included in a type: `string | null`. If a variable is typed `string`, the type checker guarantees it is never null before it's used. The compiled output is still a pointer under the hood, but the type system prevents the class of bugs C can't catch. You get the safety of TypeScript's type model with the performance profile of C. +**Null safety.** In C, `null` is just a zero-valued pointer. Dereferencing it is undefined behavior — the compiler trusts you, and if you're wrong you get a segfault or worse. In TypeScript (and ChadScript), `null` is a value that must be explicitly included in a type: `string | null`. If a variable is typed `string`, the type checker guarantees it is never null before it's used. The compiled output is still a pointer under the hood, but the type system prevents the class of bugs C can't catch. **Why this works well for LLMs.** TypeScript is one of the most widely represented languages in public training data — models know it well and generate it fluently. A safe, statically-typed subset means LLM-generated code is more likely to be correct: the types act as inline documentation telling both the compiler and the model what each value is. Simpler semantics (no `eval`, no `Proxy`, no runtime type mutation) mean programs are shorter and more predictable — fewer edge cases to reason about, fewer tokens needed to express intent, better output. diff --git a/docs/language/features.md b/docs/language/features.md index 4b5dbbf7..ad268cd8 100644 --- a/docs/language/features.md +++ b/docs/language/features.md @@ -175,7 +175,7 @@ These require runtime code evaluation and are not possible in a native compiler: ## Numbers -All numbers are `number` (no separate integer type), but the compiler automatically uses native 64-bit integers for values initialized as integer literals. Integer arithmetic (`+`, `-`, `*`, `%`) between integer values stays in integer registers for better performance. Division always returns a float. The conversion is automatic. +All numbers are `number` (no separate integer type). Integer literal expressions use native 64-bit integer instructions — `42 + 10` compiles to `add i64` rather than `fadd double`. Division always returns a float. Variables are stored as doubles. ## Strings @@ -201,7 +201,7 @@ const result = [1, 2, 3].map(x => x + offset); // [11, 12, 13] ## npm Compatibility -npm packages work as long as they only use supported TypeScript features. +npm packages work if they only use supported TypeScript features. In practice most packages use generics, dynamic types, or runtime features that ChadScript doesn't support, so compatibility is limited. ## Standard Library From 3ce1ba461e70bb272f4fe717ae21782353c5f6cf Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 26 Feb 2026 00:32:18 -0800 Subject: [PATCH 5/6] implement i64 integer optimization for global variables --- .../infrastructure/function-generator.ts | 7 +- .../infrastructure/integer-analysis.ts | 75 ++++++++++++++++++- .../infrastructure/variable-allocator.ts | 4 + src/codegen/llvm-generator.ts | 23 +++++- tests/fixtures/globals/integer-globals.ts | 26 +++++++ 5 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/globals/integer-globals.ts diff --git a/src/codegen/infrastructure/function-generator.ts b/src/codegen/infrastructure/function-generator.ts index 0e204523..7b1c1873 100644 --- a/src/codegen/infrastructure/function-generator.ts +++ b/src/codegen/infrastructure/function-generator.ts @@ -558,7 +558,12 @@ export class FunctionGenerator { this.ctx.emit(`${resultPromise} = call %Promise* @__Promise_new()`); } - const eligible = findI64EligibleVariables(funcBody.statements); + // Access func.body.statements directly to preserve struct type info. + // funcBody (from `func.body || {...}`) is an opaque i8* in the native compiler, + // which means .statements access doesn't GEP into the struct. Using func.body + // directly preserves the FunctionNode → BlockStatement type chain. + const bodyStmts = func.body ? func.body.statements : []; + const eligible = findI64EligibleVariables(bodyStmts); this.ctx.setI64EligibleVars(eligible); const result = this.ctx.generateBlock(funcBody, funcParams); diff --git a/src/codegen/infrastructure/integer-analysis.ts b/src/codegen/infrastructure/integer-analysis.ts index 430bfbb2..194933fd 100644 --- a/src/codegen/infrastructure/integer-analysis.ts +++ b/src/codegen/infrastructure/integer-analysis.ts @@ -1,5 +1,74 @@ -import { Statement } from "../../ast/types.js"; +// Static analysis to find variables safe to keep as native i64 instead of double. +// Used by both function-level and global-level codegen to enable integer optimization. +// +// NOTE: The parameter uses `object[]` instead of `Statement[]` because `Statement` +// is a union type, and standalone functions with union-type parameters cause codegen +// issues in the native compiler (the type name gets emitted literally). Using `object[]` +// ensures the ObjectArray is passed through correctly. -export function findI64EligibleVariables(statements: Statement[]): string[] { - return []; +// Returns true if the expression is an integer literal. +function isIntegerLiteral(val: object): boolean { + const expr = val as { type: string; value?: number }; + if (!expr || !expr.type) return false; + if (expr.type !== "number") return false; + const v = expr.value; + if (v === null || v === undefined) return false; + return v % 1 === 0; +} + +export function findI64EligibleVariables(statements: object[]): string[] { + if (!statements || !statements.length) return []; + const len = statements.length; + + const candidates: string[] = []; + const isConst: boolean[] = []; + + // Pass 1: Collect variables initialized with integer literals + for (let i = 0; i < len; i++) { + const stmt = statements[i]; + if (!stmt) continue; + const stmtTyped = stmt as { type: string; kind?: string; name?: string; value?: unknown }; + if (!stmtTyped.type) continue; + if (stmtTyped.type !== "variable_declaration") continue; + if (!stmtTyped.value || !stmtTyped.name) continue; + if (isIntegerLiteral(stmtTyped.value)) { + candidates.push(stmtTyped.name); + isConst.push(stmtTyped.kind === "const"); + } + } + + if (candidates.length === 0) return []; + + // Pass 2: Scan assignments to demote let variables with non-integer RHS + const isDemoted: boolean[] = []; + for (let k = 0; k < candidates.length; k++) { + isDemoted.push(false); + } + + for (let i = 0; i < len; i++) { + const stmt = statements[i]; + if (!stmt) continue; + const stmtTyped = stmt as { type: string; name?: string; value?: unknown }; + if (!stmtTyped.type) continue; + if (stmtTyped.type !== "assignment") continue; + if (!stmtTyped.name || !stmtTyped.value) continue; + for (let j = 0; j < candidates.length; j++) { + if (candidates[j] === stmtTyped.name) { + if (isConst[j]) break; + if (!isIntegerLiteral(stmtTyped.value)) { + isDemoted[j] = true; + } + break; + } + } + } + + // Build result: candidates minus demoted + const result: string[] = []; + for (let i = 0; i < candidates.length; i++) { + if (!isDemoted[i]) { + result.push(candidates[i]); + } + } + return result; } diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index ee34c0b4..6922fc3b 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -222,6 +222,7 @@ export interface VariableAllocatorContext { readonly typeResolver?: TypeResolver; readonly arrowFunctionGen: ArrowFunctionGeneratorLike; ensureDouble(value: string): string; + ensureI64(value: string): string; getI64EligibleVars(): string[]; isUint8ArrayExpression(expr: Expression): boolean; setWantsBinaryReturn(value: boolean): void; @@ -470,6 +471,9 @@ export class VariableAllocator { storeValue = cast; } this.ctx.emit(`store ${llvmType} ${storeValue}, ${llvmType}* ${globalPtr}`); + } else if (llvmType === "i64") { + const coerced = this.ctx.ensureI64(value); + this.ctx.emit(`store i64 ${coerced}, i64* ${globalPtr}`); } else if (llvmType === "double") { const coerced = this.ctx.ensureDouble(value); this.ctx.emit(`store double ${coerced}, double* ${globalPtr}`); diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index b0cc8f6b..1a0da1eb 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -60,6 +60,7 @@ import { AssignmentGenerator, AssignmentGeneratorContext, } from "./infrastructure/assignment-generator.js"; +import { findI64EligibleVariables } from "./infrastructure/integer-analysis.js"; import { getLLVMDeclarations, getSafeStringHelper, @@ -1791,6 +1792,8 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { return ir; } const items = this.ast.topLevelStatements; + // Find which numeric globals can stay as native i64 instead of double + const i64Eligible = findI64EligibleVariables(items); for (let stmtIdx = 0; stmtIdx < totalCount; stmtIdx++) { const stmt = items[stmtIdx] as { type: string; @@ -2288,9 +2291,23 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { exprNodeType === "index_access" || exprNodeType === "member_access" ) { - llvmType = "double"; - kind = SymbolKind.Number; - defaultValue = "0.0"; + // Use i64 for integer-eligible globals to avoid double conversion overhead + let isI64 = false; + for (let ei = 0; ei < i64Eligible.length; ei++) { + if (i64Eligible[ei] === name) { + isI64 = true; + break; + } + } + if (isI64) { + llvmType = "i64"; + kind = SymbolKind.Number; + defaultValue = "0"; + } else { + llvmType = "double"; + kind = SymbolKind.Number; + defaultValue = "0.0"; + } } else { return this.emitError( `cannot determine type of module-scope variable '${name}' (expression type: ${exprNodeType || "unknown"}). ` + diff --git a/tests/fixtures/globals/integer-globals.ts b/tests/fixtures/globals/integer-globals.ts new file mode 100644 index 00000000..ee893ca7 --- /dev/null +++ b/tests/fixtures/globals/integer-globals.ts @@ -0,0 +1,26 @@ +// Tests that integer globals use i64 optimization instead of double +const LIMIT = 100; +const STEP = 10; +let counter = 0; + +// Arithmetic between integer globals +const total = LIMIT + STEP; +counter = LIMIT - STEP; + +// Integer comparison +if (counter === 90) { + console.log("comparison works"); +} else { + console.log("FAIL: comparison"); + process.exit(1); +} + +// Verify arithmetic result +if (total === 110) { + console.log("arithmetic works"); +} else { + console.log("FAIL: arithmetic"); + process.exit(1); +} + +console.log("TEST_PASSED"); From 2e5e8ccd4a3ff01ccdc401c35dd2573af990ff39 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 26 Feb 2026 00:33:32 -0800 Subject: [PATCH 6/6] add opaque pointer from || fallback to crash patterns in rules --- .claude/rules.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.claude/rules.md b/.claude/rules.md index 8c6cf68f..e478e7e7 100644 --- a/.claude/rules.md +++ b/.claude/rules.md @@ -226,6 +226,7 @@ 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. ## Stage 0 Compatibility