Rust performance for the rest of us
Write clean TypeScript. Get native performance. No borrow checker required.
⚠️ BETA STAGE: GoodScript is under active development. Phase 1 (parsing, validation, ownership analysis) and Phase 2 (language refinement with implicit nullability) are complete. Phase 3 (Rust code generation) is next. APIs and language features may change.
GoodScript is a TypeScript variant designed for high-performance computation in WASM modules and systems programming thanks to its ability to generate single native binaries:
- TypeScript syntax - Familiar and productive for millions of developers
- Native performance - Compiles to WASM or native executables (1.05-1.15x of C/Rust performance)
- No garbage collection - Reference counting with ownership tracking for predictable, deterministic cleanup
- No borrow checker - Simpler memory model than Rust, more powerful than language using garbage collection
- Systems programming ready - CLI tools, automation, servers, embedded systems
Conceptually it's two things:
- A TypeScript variant with the "Good Parts" only
- Fully statically typed (no
anytype, noeval, no dynamic runtime types) - No type coercion, no
var, no truthiness, nothissurprises - Strict equality operators only (
===,!==) - Reference counting with ownership tracking (no GC, no borrow checker)
- Static cycle detection prevents memory leaks
- Fully statically typed (no
- A TypeScript to Rust transpiler
- Compiles to Rust source code for native performance
- Leverages Rust's
Rc/Weaktypes for ownership semantics - Targets native executables and WASM via Rust toolchain
- 1.05-1.15x overhead vs C/Rust, deterministic performance
The first part gets rid of JS baggage and results in a more robust, cleaner language overall. It can serve as a stricter replacement for TypeScript, offering better maintainability.
Crucially, this stricter variant is also compilable, enabling the second part which will allow 1) huge performance gains compared to JavaScript runtimes and 2) compilation to self-contained binary executables.
# Install globally
npm install -g goodscript
# Or as a dev dependency
npm install --save-dev goodscript// hello.gs.ts
class Greeter {
name: string;
constructor(name: string) { this.name = name; }
greet() { console.log(`Hello, ${this.name}!`); }
}
let greeter = new Greeter("World");
greeter.greet();# Compile it
gs compile hello.gs.ts
# or use gsc directly
gsc hello.gs.ts
# Success!
✓ Compilation successful (1 GoodScript file)GoodScript provides two command-line tools:
-
gsc- GoodScript Compiler (drop-in replacement fortsc)- Focused on compilation only
- Compatible with existing TypeScript workflows
- Use for gradual migration or TypeScript integration
-
gs- Modern Unified Toolchain (likegoorcargo)gs compile- Compile files (delegates togsc)- Future:
gs run,gs build,gs test,gs fmt, etc. - Use for full GoodScript project development
📖 See: Modern CLI Documentation for the complete roadmap and planned commands.
New to GoodScript? Start here:
- 📖 Quick Start Guide - Get up and running in 5 minutes
- 🔧 Modern CLI Documentation -
gstoolchain and roadmap - 🎯 Phase 2 Complete - Language refinement with implicit nullability
- 🎯 Project Summary - Current implementation status
- 💻 Compiler Documentation - Technical details
- ⚙️ tsconfig.json Configuration - Project-level GoodScript settings
Current Status: Phase 2 complete - Implicit nullability and clean syntax implemented. 165 tests passing.
File Extension: GoodScript files use .gs.ts extension for better IDE support.
- CLI tools and automation - Predictable performance, instant resource cleanup
- System utilities - File I/O, networking, OS interactions with RAII-style management
- Performance-critical applications - No GC pauses, consistent latency
- WASM modules - Compile to WebAssembly for portable, high-performance code
- Embedded systems - Small runtime footprint, deterministic behavior
GoodScript supports a goodscript property in your tsconfig.json for project-level configuration:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"outDir": "./dist"
},
"goodscript": {
"skipOwnership": false,
"skipNullChecks": false,
"verbose": false
},
"include": ["src/**/*.gs.ts"]
}Configuration options:
skipOwnership(default:false) - Skip ownership analysis and cycle detection- Use for "Clean TypeScript" mode - enforce language restrictions without ownership checks
- Useful for gradual migration from TypeScript to GoodScript
skipNullChecks(default:false) - Reserved for future useverbose(default:false) - Reserved for future use
Priority: CLI flags override tsconfig.json, which overrides defaults.
See: tsconfig.json Configuration Guide for complete documentation and examples.
GoodScript compiler (gsc) can be used as a direct replacement for tsc:
# Compile with tsconfig.json (like tsc)
gsc
# Or explicitly specify tsconfig
gsc -p tsconfig.json
# Compile .gs.ts files with GoodScript validation
gsc src/**/*.gs.ts
# Compile .ts files as regular TypeScript
gsc src/**/*.ts
# Mixed projects - both .gs.ts and .ts files
gsc src/**/*.{ts,gs.ts}Modern CLI:
# Same as gsc (uses tsconfig.json)
gs compile
# Or with explicit files
gs compile src/**/*.gs.tsSee: TSC Replacement Guide for migration strategies.
- No type coercion - all conversions must be explicit
- No
==or!=- only===and!==exist (strict equality only) - No dynamic types - everything must be statically typed
- No
var- onlyletandconst - No
thisbinding surprises - lexicalthisonly (arrow function semantics) - No
undefined- onlynullfor absence of value - No truthiness/falsiness - conditions must be actual booleans
- No automatic semicolon insertion - semicolons required or explicitly forbidden
- No
withstatement - No
evalorFunction()constructor - No implicit globals - all variables must be declared
- No
argumentsobject - use rest parameters instead - No prototypal inheritance - class-based only
- No hoisting - temporal dead zone semantics everywhere
- No
for-in- usefor-ofor explicit iteration
GoodScript uses reference counting instead of garbage collection, with a novel ownership system that prevents cycles while maintaining ergonomic syntax.
Ownership vs Usage:
- Ownership = Strong reference, contributes to reference count
- Usage = Weak reference, does NOT contribute to reference count
- Only ownership relationships are counted, preventing reference cycles
- Usages are always nullable (can become null if owner is deallocated)
Syntax Rules (Phase 2 - Implicit Nullability):
-
Function parameters default to usage (implicitly nullable, require null checks):
function render(user: User, config: Config) { if (user !== null && config !== null) { // Safe to use here } }
-
Shared ownership uses
owns<T>(NOT nullable):function consume(data: owns<Data>) { // function shares ownership of data (ref count incremented) data.process(); // No null check needed } // ref count decremented here
-
Local variables with
neware owned (NOT nullable):let owner = new User(); // owns the User instance owner.greet(); // No null check needed
-
Assignment creates usage references (implicitly nullable):
let owner = new User(); let ref = owner; // usage reference (implicitly nullable) if (ref !== null) { // null check required ref.greet(); } // Alternative: short-circuit && idiom ref !== null && ref.greet(); // Concise null check
-
Optional parameters work as expected:
function process(callback?: owns<Callback>) { // callback can be omitted; if provided, takes ownership } function useCallback(callback?: Callback) { // callback can be omitted; if provided, it's a usage reference (| null | undefined) }
-
Assigning to
owns<T>fields requiresowns<T>parameters:class Container { item: owns<Item>; constructor(item: owns<Item>) { // ✅ Explicit ownership required this.item = item; // OK - shares ownership via RC } } // ❌ ERROR: Cannot assign usage reference to owns<T> field class BadContainer { item: owns<Item>; constructor(item: Item) { // Usage reference (nullable) this.item = item; // Type error: cannot assign Item to owns<Item> } }
The compiler statically analyzes ownership chains and rejects any cyclical ownership patterns. This makes reference counting safe and predictable.
Handling "cyclic" structures:
-
Trees/Lists: Forward links own, backward links are usages
class Node { next: owns<Node> | null; // owns next node prev: Node | null; // usage reference to previous }
-
Graphs: Use arena pattern - central owner with usage-based edges
class Graph { nodes: owns<Node>[]; // arena owns all nodes // Or use Set for O(1) lookup: nodes: Set<owns<Node>> } class Node { edges: Node[]; // usage references (no ownership cycles) }
Since usage references are implicitly nullable, GoodScript provides several ergonomic patterns for null checking:
1. Traditional if statement:
const greet = (user: User) => {
if (user !== null) {
console.log(user.name);
}
};2. Optional chaining (?.):
const greet = (user: User) => {
console.log(user?.name); // Returns undefined if user is null
};3. Short-circuit && idiom (NEW!):
const greet = (user: User) => {
// Concise: only calls method if user is non-null
user !== null && user.greet();
};
const processAll = (logger: Logger, items: Item[]) => {
items.forEach(item => {
// Chain multiple checks
logger !== null && item !== null && logger.log(item.toString());
});
};4. Early return pattern:
const process = (data: Data) => {
if (data === null) return;
// data is safe to use for rest of function
data.transform();
data.validate();
};The short-circuit && idiom is particularly useful for:
- Optional callbacks:
callback !== null && callback() - Conditional logging:
logger !== null && logger.debug("message") - Chaining operations:
a !== null && b !== null && combine(a, b)
Zero-cost usages: Only ownership operations trigger reference count modifications. Since most code uses usage references, RC overhead is minimal:
- Passing parameters: 0 RC operations (usage references)
- Local usage references: 0 RC operations
- Reading fields: 0 RC operations
- Only adding/removing ownership: RC increment/decrement
This results in predictable, deterministic cleanup with minimal runtime overhead.
Minimal RC operations: Because only ownership relationships trigger reference counting, the overhead is concentrated in rare operations:
- 0% overhead: Function calls (parameters are usage references)
- 0% overhead: Local usage references
- 0% overhead: Field reads
- RC cost only: Storing in collections, adding/removing ownership, returning owned values
In typical code, 90%+ of references are usages, meaning RC overhead affects only ~10% of reference operations.
Since usages are weak references that can become null if the owner is deallocated, every dereference requires a null check:
let ref?: User = owner;
if (ref) {
ref.doSomething(); // null check before access
}
// Or: ref?.doSomething();Performance impact:
-
Best case (typical): Null checks are highly predictable. Modern CPUs' branch predictors handle this with ~99% accuracy when usages rarely become null. Overhead: ~5%
-
Worst case: If usages frequently become null in unpredictable patterns, branch mispredictions are costly (10-20 CPU cycles each). Overhead: 15-25%
Compiler optimizations to mitigate:
-
Flow-sensitive analysis: Once a usage is checked, compiler knows it's non-null for subsequent accesses in the same scope:
if (ref) { ref.method1(); // check required ref.method2(); // compiler elides check ref.method3(); // compiler elides check }
-
Scope-based guarantees: If compiler proves the owner lives for an entire scope, usages within that scope don't need runtime checks
-
Lifetime annotations (future): Optional lifetime hints could allow compiler to eliminate checks in hot paths:
function process(owner: User) { let ref?: User = owner; // compiler knows owner lives for function // no null checks needed - compiler proves ref is valid }
-
Profile-guided optimization: Mark hot paths where null checks can be optimized based on runtime profiling data
| Approach | Memory Overhead | Runtime Checks | Predictability | Complexity |
|---|---|---|---|---|
| GoodScript RC | Low (RC metadata only) | Null checks on usage access | High (deterministic) | Medium |
| GC (Go, AssemblyScript) | Medium (GC metadata) | None | Low (unpredictable pauses) | Low |
| Rust borrow checker | Zero | None | High | High (lifetimes) |
| C/C++ raw pointers | Zero | None (UB if wrong) | High | High (manual) |
For the target use cases (CLI tools, system programming, automation):
- Typical overhead: 5-10% compared to manual memory management
- vs GC languages: Competitive or better for most workloads due to no GC pauses
- Predictability: Deterministic performance, no tail latencies from GC
- Resource cleanup: Immediate (RAII-style), vs delayed in GC languages
The tradeoff favors predictability over peak throughput, which aligns with systems programming requirements.
Expected performance bracket (relative to C baseline = 1.0x):
| Language/Approach | Typical Performance | Notes |
|---|---|---|
| C (manual memory) | 1.0x | Baseline, maximum performance |
| Rust (borrow checker) | 1.0-1.05x | Zero-cost abstractions |
| C++ (shared_ptr) | 1.05-1.10x | RC overhead similar to GoodScript |
| GoodScript | 1.05-1.15x | RC + null checks, deterministic |
| Go | 1.2-1.5x | GC overhead, pauses |
| AssemblyScript | 1.3-2.0x | WASM + GC overhead |
| Java/C# (JIT) | 1.5-3.0x | GC + JIT warmup |
| JavaScript (V8) | 3-10x | Dynamic typing + GC |
| Python | 10-100x | Interpreted |
Performance breakdown by workload:
- Compute-heavy (minimal allocation): 1.02-1.05x overhead (mostly null checks)
- Balanced (typical apps): 1.08-1.12x overhead (RC + null checks)
- Allocation-heavy: 1.10-1.15x overhead (RC on ownership operations)
- Latency-sensitive (p99): 1.05x overhead, but highly predictable (no GC pauses)
Key advantage over GC languages: Deterministic performance with no tail latencies from garbage collection pauses.
GoodScript supports zero-cost exceptions (like C++ and Rust):
- Happy path cost: 0% overhead - no runtime checks when exceptions aren't thrown
- Exception path: Expensive (microseconds to milliseconds) but exceptions should be rare
- Implementation: Uses Rust's panic unwinding or WASM exception handling
- Cleanup: Automatic via exception unwinding - owned objects are properly deallocated
Best practice: Use exceptions for truly exceptional cases, use Result<T, E> types for expected errors.
TypeScript developers who want to target WASM runtime and write system programs in TypeScript rather than Go, Rust, or C. Ideal for:
- CLI tools and automation - Predictable performance, deterministic resource cleanup
- System programming - File I/O, networking, OS interactions with RAII-style resource management
- Performance-critical applications - No GC pauses, predictable latency
- Embedded systems - Smaller runtime footprint, no garbage collector overhead
For TypeScript developers:
- Write in familiar syntax, compile to native performance
- No need to learn Rust's borrow checker or C++'s manual memory management
- Keep your TypeScript skills, gain systems programming capabilities
vs AssemblyScript:
- ✅ Deterministic, immediate cleanup (no GC pauses)
- ✅ Better resource management (files/sockets auto-close when owner drops)
- ✅ Predictable performance for real-time/systems code
- ✅ Smaller footprint
vs Rust:
- ✅ Familiar TypeScript syntax and tooling
- ✅ Simpler memory model (no borrow checker/lifetimes)
- ✅ Easier learning curve for TS developers
⚠️ Slightly slower (1.05-1.15x vs Rust's 1.0x, but still fast!)
vs Go:
- ✅ Compiles to WASM for true portability
- ✅ No GC pauses (deterministic performance)
- ✅ More control over memory layout and performance
- ✅ Faster in latency-sensitive scenarios (no GC tail latencies)
- ✅ Smaller footprint
The sweet spot: 90-95% of Rust/C++ performance with TypeScript ergonomics.
Frontend (TypeScript):
- Parse GoodScript source using TypeScript compiler API
- Type checking and ownership analysis
- Cycle detection in ownership graph
- Emit intermediate representation (JSON/Protocol Buffers)
Backend:
- Rust codegen - leverages Rust's Rc/Weak for perfect semantic match
- Targets native executables and WASM via Rust's compilation targets
Phase 1: TypeScript Analysis (COMPLETE ✅)
- Parse GoodScript using TypeScript compiler API
- Language restriction validation
- Ownership tracking and cycle detection
- Null-check enforcement
- Validate language design with real code
Phase 2: Language Refinement (COMPLETE ✅)
- Implicit nullability (bare types = nullable usage refs)
- Remove
| nullrequirement for cleaner syntax - Array element implicit nullability
- Enhanced flow-sensitive analysis
- Short-circuit
&&idiom for null checking
Phase 3: Rust Code Generation (NEXT)
- Generate Rust source code (perfect semantic match with ownership model)
- Simple Rc/Weak mapping (Phase 3.1)
- Optimized codegen with Box/& (Phase 3.2)
- Shell out to system rustc compiler
Phase 4: Self-Contained Toolchain (FUTURE)
- Bundle rustc compiler toolchain
- No external Rust installation required
- Cross-compilation support built-in
- Standard library implementation
- Ecosystem tooling (package manager, LSP server with diagnostics and quick-fixes, etc.)
GoodScript compiles to Rust for perfect semantic alignment:
Type mapping:
let owner: T→let owner = Rc::new(T)let ref?: T→let ref = Weak::new(T)owns<T>parameter →Rc<T>- Regular parameter →
&T - Automatic cleanup →
Droptrait
Example:
// GoodScript
let owner: User = new User();
let ref?: User = owner;
// Compiles to Rust
let owner = Rc::new(User::new());
let ref: Weak<User> = Rc::downgrade(&owner);Why Rust backend:
- ✅ Built-in
Rc/Weaktypes match ownership model exactly - ✅ Excellent toolchain with cross-compilation
- ✅ Safety guarantees catch codegen bugs
- ✅ Native executables for all platforms
- ✅ Can target WASM via
wasm32target - ✅ Zero-cost abstractions and optimizations
Phase 3.1: Simple and Correct (MVP)
Generate straightforward Rust using Rc/Weak for all ownership:
// GoodScript
class User { name: string; }
let owner: User = new User();
let ref?: User = owner;
function greet(user: User) {
console.log(user.name);
}Generates:
use std::rc::{Rc, Weak};
struct User { name: String }
let owner = Rc::new(User { name: String::from("") });
let ref: Weak<User> = Rc::downgrade(&owner);
fn greet(user: &User) {
println!("{}", user.name);
}Why this works:
- ✅ GoodScript's compiler guarantees no ownership cycles (static analysis)
- ✅ Therefore
Rcis always safe to use (no memory leaks) - ✅ Simple 1:1 mapping, easy to implement
- ✅ Correct semantics from day one
Performance: 1.05-1.15x overhead vs C/Rust (due to reference counting operations)
Phase 3.2: Optimized Codegen (Future)
Analyze ownership patterns to eliminate unnecessary reference counting and achieve near-Rust performance (1.0-1.05x):
// GoodScript - single owner, no sharing
function createAndProcess() {
let user: User = new User(); // Only owner
process(user);
// user dies here
}Optimized output:
fn create_and_process() {
let user = Box::new(User::new()); // Unique ownership, no RC!
process(&user);
// Drop called, no refcount overhead
}Optimization opportunities:
- Single-owner detection: Use
Box<T>instead ofRc<T>when ownership is never shared - Escape analysis: Use stack allocation when objects don't escape scope
- Usage optimization: Minimize
Rc::clone()calls when usage references are sufficient - Inline small objects: Eliminate heap allocation for simple types
Performance breakdown with optimizations:
- Function parameters with owns: 1.05x - RC clone on call
- Unique ownership (
Box<T>when single owner): 1.0x - same as Rust - Shared ownership (
Rc<T>when truly needed): 1.05-1.10x - minor RC overhead - Compute-heavy code with minimal allocation: 1.0-1.05x
The key insight: GoodScript's ownership analysis already provides the information needed for these optimizations. The compiler knows which variables have unique vs shared ownership, enabling intelligent code generation:
- Function parameters →
Rc<T>for owned, usage references for borrowed semantics - Single owners →
Box<T>= zero cost - Shared ownership →
Rc<T>only when necessary - Usage references →
Weak<T>for nullable weak references
The guarantees (no cycles, explicit ownership) make the generated Rust code safe by construction, and optimization is purely about performance, not correctness. This means GoodScript can achieve basically full Rust performance for many common patterns while maintaining simpler ergonomics.
Compile to WebAssembly via Rust's wasm32 target:
# Compile to WASM
goodscript build --target wasm32 app.gs
# Or
goodscript build --target wasm32-wasi app.gs # For WASI runtimeHow it works:
- GoodScript → Rust source code
- Rust compiler (
rustc) → WASM binary - Leverages Rust's mature WASM toolchain
Advantages:
- ✅ Single compilation artifact runs everywhere
- ✅ Web browsers, Node.js, Deno, edge workers, WASI runtimes
- ✅ Sandboxed execution environment
- ✅ Same ownership semantics and performance as native
- ✅ No separate WASM backend to maintain
Following the Go/Zig model for zero-friction developer experience:
# Single command, no external dependencies
goodscript build app.gs
# Cross-compilation built-in
goodscript build --target linux-x64 app.gs
goodscript build --target macos-arm64 app.gs
goodscript build --target windows-x64 app.gs
goodscript build --target wasm32 app.gs
# Optimization levels
goodscript build --optimize app.gs # -O3 optimizations
goodscript build --optimize --lto app.gs # Link-time optimizationDistribution includes:
- GoodScript compiler (TypeScript frontend)
- Bundled rustc compiler
- Rust standard library (std)
- Cross-compilation targets for all major platforms
- Total size: ~200-300MB (can be optimized with minimal builds)
Zero external dependencies required - installs and works out of the box, just like Go.
Installation:
# Via npm (compiles TS developers' preferred distribution channel)
npm install -g goodscript
# Or standalone installer
curl -sSf https://goodscript.dev/install.sh | shUse arena pattern when:
- ✅ Elements need to be reorganized (insert, remove, reorder)
- ✅ Multiple pointers to same element (e.g., doubly-linked lists)
- ✅ Complex graph structures
- ✅ Elements have lifetimes tied to container lifetime
Why? Ownership chains can break during reorganization, causing premature deallocation:
// ❌ PROBLEMATIC: Direct ownership in mutable structure
class ListNode {
next: owns<ListNode> | null; // Owns next node
prev: ListNode | null; // Reference to previous
}
// When removing or reordering nodes, you risk breaking the ownership chain!
// Example: removing node B from A → B → C
// If you set A.next = C, B gets deallocated even though you might still need it!
// ✅ RECOMMENDED: Arena pattern keeps everything alive
class LinkedList {
nodes: owns<ListNode>[]; // Arena owns ALL nodes
head: ListNode | null; // Just a reference
tail: ListNode | null; // Just a reference
}
// Now you can safely reorganize without worrying about deallocationDoubly-linked list:
class LinkedList {
nodes: owns<Node>[]; // Arena owns all
head: Node | null; // Usage references
tail: Node | null;
}Graph structures:
class Graph {
nodes: owns<GraphNode>[]; // Arena owns all nodes
}
class GraphNode {
edges: GraphNode[]; // Usage references to other nodes
}DOM-like tree:
class Document {
elements: owns<Element>[]; // Arena owns all elements
root: Element | null; // Root reference
}
class Element {
children: Element[]; // Usage references
parent: Element | null;
}Use direct ownership when:
- ✅ Clear parent-child hierarchy (tree with no reorganization)
- ✅ Unidirectional relationships
- ✅ Immutable or append-only structures
- ✅ Single ownership path is obvious
Examples:
// ✅ Simple tree (never reorganize)
class TreeNode {
children: owns<TreeNode>[]; // Owns children, they live as long as parent
}
// ✅ Singly-linked list (append-only)
class LinkedNode {
next: owns<LinkedNode> | null; // OK if you only append, never remove
}
// ✅ Builder pattern (consumed at end)
class RequestBuilder {
body: owns<RequestBody>;
headers: owns<Headers>;
// Built once, consumed once
}"If you're restructuring it, arena it."
When in doubt, use the arena pattern. It's safer, clearer, and the performance overhead is minimal (arena is just a vector of Rc pointers).
class User {
name: string;
email: string;
}
// Function uses reference (no ownership)
function displayUser(user: User | null) {
if (user !== null) {
console.log(user.name);
}
}
// Function takes ownership (shares ownership via ref counting)
function consumeUser(user: owns<User>) {
// user's ref count incremented on entry
// user's ref count decremented on exit
// object only deallocated when ref count reaches 0
}
let user: User = new User(); // ref count = 1
displayUser(user); // OK - passing usage reference (ref count unchanged)
consumeUser(user); // OK - shares ownership temporarily (ref count = 2, then back to 1)
// user deallocated here when it goes out of scope (ref count reaches 0)let owner: User = new User();
let ref1?: User = owner; // weak reference
let ref2?: User = owner; // another weak reference
// Must check for null since usages can become null
if (ref1) {
console.log(ref1.name);
}class ListNode {
value: number;
next: ListNode | null; // usage reference
prev: ListNode | null; // usage reference
constructor(value: number) {
this.value = value;
this.next = null;
this.prev = null;
}
}
class LinkedList {
nodes: owns<ListNode>[]; // Arena: owns all nodes
head: ListNode | null;
tail: ListNode | null;
constructor() {
this.nodes = [];
this.head = null;
this.tail = null;
}
append(value: number) {
const node = new ListNode(value);
this.nodes.push(node); // Arena owns the node
if (!this.tail) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node; // usage reference
node.prev = this.tail; // usage reference
this.tail = node;
}
}
remove(node: ListNode) {
// Safe! Can reorganize without worrying about ownership
if (node.prev) {
node.prev.next = node.next;
} else {
this.head = node.next;
}
if (node.next) {
node.next.prev = node.prev;
} else {
this.tail = node.prev;
}
// Node stays alive until removed from arena
}
}Why arena pattern? When you need to reorganize elements (insert, remove, reorder), ownership chains can break and cause premature deallocation. The arena keeps everything alive, and internal pointers are just references.
class Node {
id: string;
edges?: Node[]; // usage references only
constructor(id: string) {
this.id = id;
this.edges = [];
}
}
class Graph {
nodes: owns<Node>[]; // owns all nodes
constructor() {
this.nodes = [];
}
addNode(id: string): Node {
const node = new Node(id);
this.nodes.push(node);
return node; // returns usage reference
}
addEdge(from: Node, to: Node) {
if (!from.edges) from.edges = [];
from.edges.push(to); // usage reference - no cycle!
}
}
// Usage
const graph = new Graph();
const a = graph.addNode("A");
const b = graph.addNode("B");
const c = graph.addNode("C");
graph.addEdge(a, b);
graph.addEdge(b, c);
graph.addEdge(c, a); // cycle in references is fine!// ERROR: Cannot mix types with +
// let result = "sum: " + 1 + 2;
// Correct: explicit conversion
let result = "sum: " + (1 + 2).toString(); // "sum: 3"
// Or use template literals
let result2 = `sum: ${1 + 2}`; // "sum: 3"
// ERROR: string can't be used as boolean
let str = "hello";
// if (str) { }
// Correct: explicit check
if (str !== null) { }
if (str.length > 0) { }All detailed documentation is in the docs/ folder:
- Quick Start Guide
- Phase 2 Complete
- Phase 2 Migration Guide
- Compiler Documentation
- Project Summary
- Known Issues
GoodScript - Rust performance for the rest of us! 🚀