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
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/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/.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..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
@@ -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..4f13b57b 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
@@ -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/limitations.md b/docs/language/features.md
similarity index 84%
rename from docs/language/limitations.md
rename to docs/language/features.md
index d010ddf8..ad268cd8 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,16 +166,16 @@ 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
-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
@@ -204,10 +201,8 @@ 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
-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.
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");