Skip to content

fcapolini/goodscript

Repository files navigation

GoodScript

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 any type, no eval, no dynamic runtime types)
    • No type coercion, no var, no truthiness, no this surprises
    • Strict equality operators only (===, !==)
    • Reference counting with ownership tracking (no GC, no borrow checker)
    • Static cycle detection prevents memory leaks
  • A TypeScript to Rust transpiler
    • Compiles to Rust source code for native performance
    • Leverages Rust's Rc/Weak types 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.

🚀 Getting Started

Installation

# Install globally
npm install -g goodscript

# Or as a dev dependency
npm install --save-dev goodscript

Quick Start

// 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)

CLI Tools

GoodScript provides two command-line tools:

  • gsc - GoodScript Compiler (drop-in replacement for tsc)

    • Focused on compilation only
    • Compatible with existing TypeScript workflows
    • Use for gradual migration or TypeScript integration
  • gs - Modern Unified Toolchain (like go or cargo)

    • gs compile - Compile files (delegates to gsc)
    • 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.

Learn More

New to GoodScript? Start here:

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.

Target Use Cases

  • 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

Configuration

tsconfig.json GoodScript Property

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 use
  • verbose (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.

Drop-in Replacement for tsc

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.ts

See: TSC Replacement Guide for migration strategies.

What We're Excluding (The "Not Good Parts")

  • 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 - only let and const
  • No this binding surprises - lexical this only (arrow function semantics)
  • No undefined - only null for absence of value
  • No truthiness/falsiness - conditions must be actual booleans
  • No automatic semicolon insertion - semicolons required or explicitly forbidden
  • No with statement
  • No eval or Function() constructor
  • No implicit globals - all variables must be declared
  • No arguments object - use rest parameters instead
  • No prototypal inheritance - class-based only
  • No hoisting - temporal dead zone semantics everywhere
  • No for-in - use for-of or explicit iteration

Memory Management

GoodScript uses reference counting instead of garbage collection, with a novel ownership system that prevents cycles while maintaining ergonomic syntax.

Core Concepts

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):

  1. 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
      }
    }
  2. 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
  3. Local variables with new are owned (NOT nullable):

    let owner = new User();  // owns the User instance
    owner.greet();           // No null check needed
  4. 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
  5. 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)
    }
  6. Assigning to owns<T> fields requires owns<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>
      }
    }

Preventing Cycles

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)
    }

Null-Checking Patterns

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)

Performance Characteristics

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.

Performance and Optimizations

Reference Counting 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.

Null-Check Overhead for Usages

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:

  1. 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
    }
  2. Scope-based guarantees: If compiler proves the owner lives for an entire scope, usages within that scope don't need runtime checks

  3. 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
    }
  4. Profile-guided optimization: Mark hot paths where null checks can be optimized based on runtime profiling data

Comparison with Alternatives

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)

Expected Performance Profile

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.

Performance Comparison

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.

Exception Handling

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.

Target Audience

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

Why GoodScript?

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.

Implementation Strategy

Compiler Architecture

Frontend (TypeScript):

  1. Parse GoodScript source using TypeScript compiler API
  2. Type checking and ownership analysis
  3. Cycle detection in ownership graph
  4. 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

Compilation Phases

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 | null requirement 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.)

Rust Backend

GoodScript compiles to Rust for perfect semantic alignment:

Type mapping:

  • let owner: Tlet owner = Rc::new(T)
  • let ref?: Tlet ref = Weak::new(T)
  • owns<T> parameter → Rc<T>
  • Regular parameter → &T
  • Automatic cleanup → Drop trait

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/Weak types match ownership model exactly
  • ✅ Excellent toolchain with cross-compilation
  • ✅ Safety guarantees catch codegen bugs
  • ✅ Native executables for all platforms
  • ✅ Can target WASM via wasm32 target
  • ✅ Zero-cost abstractions and optimizations

Codegen Strategy: Phased Approach

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 Rc is 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:

  1. Single-owner detection: Use Box<T> instead of Rc<T> when ownership is never shared
  2. Escape analysis: Use stack allocation when objects don't escape scope
  3. Usage optimization: Minimize Rc::clone() calls when usage references are sufficient
  4. 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 parametersRc<T> for owned, usage references for borrowed semantics
  • Single ownersBox<T> = zero cost
  • Shared ownershipRc<T> only when necessary
  • Usage referencesWeak<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.

WASM Target

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 runtime

How it works:

  1. GoodScript → Rust source code
  2. Rust compiler (rustc) → WASM binary
  3. 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

Toolchain Distribution

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 optimization

Distribution 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 | sh

Best Practices & Design Patterns

When to Use Arena Pattern

Use 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 deallocation

Arena Pattern Examples

Doubly-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;
}

When Ownership Chains Work Well

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
}

Rule of Thumb

"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).

Examples

Basic Ownership

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)

Usage References

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);
}

Doubly-Linked List (Arena Pattern - RECOMMENDED)

⚠️ Important: For data structures that need reorganization (insert, remove, reorder), use the arena pattern to avoid accidentally breaking ownership chains and deallocating elements prematurely.

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.

Graph with Arena Pattern

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!

No Type Coercion

// 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) { }

📚 Documentation

All detailed documentation is in the docs/ folder:


GoodScript - Rust performance for the rest of us! 🚀

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published