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
1 change: 1 addition & 0 deletions .claude/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
]
},
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/theme/ComparisonCards.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<span class="card-badge badge-go">vs</span>
<span class="card-title">Go</span>
</div>
<p class="card-body">TypeScript syntax instead of Go's idiosyncratic type system. Classes, generics, interfaces, and async/await work the way you expect.</p>
<p class="card-body">TypeScript syntax instead of Go's idiosyncratic type system. Classes, interfaces, closures, and async/await work the way you expect.</p>
</div>
</div>
<div class="comparison-callout">
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/theme/IRShowcase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ onUnmounted(() => {
<p class="cta-tagline">Congratulations, you wrote your first ChadScript app!</p>
<div class="cta-buttons">
<a href="/ChadScript/getting-started/installation" class="cta-link">Get Started</a>
<a href="/ChadScript/language/limitations" class="cta-link secondary">Learn More</a>
<a href="/ChadScript/language/features" class="cta-link secondary">Learn More</a>
</div>
</div>

Expand Down
4 changes: 2 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand All @@ -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?

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,7 +43,7 @@ curl -fsSL https://raw.githubusercontent.com/cs01/ChadScript/main/install.sh | s

<div class="cta-buttons">
<a href="/ChadScript/getting-started/installation" class="cta-button primary">Get Started</a>
<a href="/ChadScript/language/limitations" class="cta-button secondary">Learn More</a>
<a href="/ChadScript/language/features" class="cta-button secondary">Learn More</a>
</div>

</div>
Expand Down
8 changes: 4 additions & 4 deletions docs/language/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand Down
33 changes: 14 additions & 19 deletions docs/language/limitations.md → docs/language/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,15 @@
| `Map<K, V>`, `Set<T>` | 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 (`<T>`) | Not supported (built-in generics like `Map<K,V>` work) |
| User-defined generics (`<T>`) | 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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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.
7 changes: 6 additions & 1 deletion src/codegen/infrastructure/function-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
75 changes: 72 additions & 3 deletions src/codegen/infrastructure/integer-analysis.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions src/codegen/infrastructure/variable-allocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}`);
Expand Down
23 changes: 20 additions & 3 deletions src/codegen/llvm-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
AssignmentGenerator,
AssignmentGeneratorContext,
} from "./infrastructure/assignment-generator.js";
import { findI64EligibleVariables } from "./infrastructure/integer-analysis.js";
import {
getLLVMDeclarations,
getSafeStringHelper,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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"}). ` +
Expand Down
Loading
Loading