Home Technical Decisions
On this page 24
Track architectural and design decisions to maintain consistency and provide historical context.
Format: Each decision includes context, options considered, chosen approach, and rationale.
Language Design
D001: Borrow Annotations - Implicit vs Explicit
Status: ✅Decided: Hybrid (Option C)
Priority: HIGH
Decided: 2025-10-21
Context: Home aims for "safety without ceremony." Need to decide how much borrowing syntax users must write.
Options:
A) Fully Explicit (Rust-style):
fn process(data: &string): usize { // & required
return data.len()
}
fn modify(data: &mut string) { // &mut required
data.push("!")
}
Pros: Clear intent, familiar to Rust devs
Cons: Verbose, ceremony
B) Fully Implicit (Inferred):
fn process(data: string): usize { // compiler infers &string
return data.len()
}
fn modify(data: mut string) { // compiler infers &mut string
data.push("!")
}
Pros: Clean syntax, less ceremony
Cons: Less obvious, harder to reason about ownership
C) Hybrid (Default implicit, explicit override):
// Implicit by default
fn process(data: string) { // inferred: &string
print(data)
}
// Explicit when taking ownership
fn consume(data: own string) { // explicit ownership
// data is moved
}
// Explicit mutable borrow
fn modify(data: mut string) { // inferred: &mut string
data.push("!")
}
Pros: Best of both worlds
Cons: Need to learn inference rules
Chosen:Option C (Hybrid)
Rationale: Reduces ceremony for common cases while allowing explicit control. Most functions don't need ownership, so default to borrowing.
Implementation:
- Compiler infers
&Twhen function doesn't move own Tkeyword for explicit ownership transfermutimplies&mut Tfor parameters- Escape analysis determines stack vs heap
D002: Generic Syntax
Status: ✅Decided: Parentheses (Option B)
Priority: HIGH
Decided: 2025-10-21
Context: Need consistent syntax for generic types that's familiar but distinct.
Options:
A) Angle Brackets (Rust/TypeScript/C++):
fn identity<T>(value: T): T { value }
let list: Vec<int> = Vec.new()
let map: Map<string, User> = Map.new()
Pros: Familiar to most developers
Cons: Parsing complexity (>> vs >), visual noise
B) Parentheses (Zig-style):
fn identity(comptime T: type)(value: T): T { value }
let list: Vec(int) = Vec.new()
let map: Map(string, User) = Map.new()
Pros: Easier parsing, consistent with function calls
Cons: Less familiar, function syntax verbose
C) Square Brackets:
fn identity[T](value: T): T { value }
let list: Vec[int] = Vec.new()
let map: Map[string, User] = Map.new()
Pros: Easy parsing, distinct from expressions
Cons: Conflicts with array syntax
D) Hybrid (Square for types, parens for bounds):
fn identity[T](value: T): T { value }
fn bounded[T: Trait](value: T): T { value }
let list: Vec[int] = Vec.new()
Pros: Clear separation, no parsing ambiguity
Cons: Two syntaxes to learn
Chosen:Option B (Parentheses)
Rationale: Easier parsing, no >> issues, consistent with function calls. Zig-style syntax familiar to systems programmers.
Implementation:
fn identity(comptime T: type)(value: T): T { value }
let list: Vec(int) = Vec.new()
let map: Map(string, User) = Map.new()
D003: Error Handling - Result Type Signature
Status: ✅Decided: Default Error Type (Option B)
Priority: MEDIUM
Decided: 2025-10-21
Context: Need to decide Result type API and default error type.
Options:
A) Explicit Error Type (Rust-style):
fn read*file(path: string): Result<string, IOError> {
// ...
}
Pros: Type-safe, explicit
Cons: Verbose for simple cases
B) Default Error Type:
fn read*file(path: string): Result<string> { // Error type implicit
// returns Result<string, Error>
}
Pros: Cleaner for common case
Cons: Less type safety, hidden type
C) Multiple Return Values (Go-style):
fn read*file(path: string): (string, error) {
// ...
}
Pros: Simple, familiar to Go devs
Cons: No ? operator, must check manually
Chosen:Option B (Default with override)
Rationale: Reduce ceremony for typical errors while allowing specificity when needed. Global Error trait that custom errors implement.
Implementation:
// Default error type
fn read*file(path: string): Result(string) { }
// Explicit when needed
fn parse(input: string): Result(Config, ParseError) { }
D004: Module System - File-based vs Explicit
Status: ✅Decided: Hybrid (Option C)
Priority: HIGH
Decided: 2025-10-21
Context: How do modules map to files and how are they imported?
Options:
A) File-based (Rust/TypeScript):
src/
main.home
parser/
mod.home # or parser.home
lexer.home
import parser/lexer { Lexer, Token }
Pros: Simple, predictable
Cons: File structure dictates API
B) Explicit Modules:
// lexer.home
module parser.lexer
export struct Lexer { }
// main.home
import parser.lexer { Lexer }
Pros: Flexible, decoupled from filesystem
Cons: More ceremony, can be confusing
C) Hybrid (File-based with re-exports):
src/
main.home
parser.home # Re-exports lexer + parser
parser/
lexer.home
parser.home
// parser.home
export * from parser/lexer
export * from parser/parser
// main.home
import parser { Lexer, Parser }
Pros: Control over public API, familiar
Cons: Need to maintain re-exports
Chosen:Option C (Hybrid)
Rationale: File-based for simplicity with explicit exports for API control. Matches TypeScript/Rust patterns.
Implementation:
// parser.home - controls public API
export * from parser/lexer
export * from parser/parser
// main.home
import parser { Lexer, Parser }
D005: String Type - UTF-8 or Byte Slice
Status: ✅Decided: UTF-8 with Unsafe Escape (Option D)
Priority: HIGH
Decided: 2025-10-21
Context: Balance between safety, performance, and ergonomics.
Options:
A) UTF-8 Validated:
let s: string = "Hello" // Always valid UTF-8
// Invalid UTF-8 rejected at compile time or runtime
Pros: Safe, prevents encoding bugs
Cons: Performance overhead, validation cost
B) Byte Slice:
let s: string = "Hello" // Just []u8
// No validation, raw bytes
Pros: Zero-cost, maximum performance
Cons: Unsafe, can have invalid UTF-8
C) Two Types (Swift/Rust-style):
let s: String = "Hello" // UTF-8 validated, heap allocated
let b: &str = "Hello" // UTF-8 slice, borrowed
Pros: Flexibility, performance when needed
Cons: Complexity, two types to learn
D) UTF-8 with unsafe escape:
let s: string = "Hello" // Validated by default
let raw = unsafe { string.from*bytes(bytes) } // Skip validation
Pros: Safe by default, fast path available
Cons: Unsafe blocks for performance
Chosen:Option D (UTF-8 + unsafe)
Rationale: Default to safety and correctness. Provide unsafe path for performance-critical code. Most code doesn't need raw bytes.
Implementation:
let s: string = "Hello" // UTF-8 validated
let raw = unsafe { string.from*bytes(bytes) } // Skip validation
D006: Integer Overflow Behavior
Status: ✅Decided: Debug Panic + Explicit (Option A+D)
Priority: MEDIUM
Decided: 2025-10-21
Context: What happens when integer arithmetic overflows?
Options:
A) Panic in Debug, Wrap in Release (Rust):
let x: u8 = 255
let y = x + 1 // Panics in debug, wraps to 0 in release
B) Always Panic:
let x: u8 = 255
let y = x + 1 // Always panics
C) Always Wrap:
let x: u8 = 255
let y = x + 1 // Always wraps to 0
D) Explicit Methods:
let y = x.wrapping*add(1) // Explicit wrapping
let y = x.saturating*add(1) // Saturates at max
let y = x.checked*add(1) // Returns Result
Chosen:Option A + D (Debug panic, release wrap, explicit methods)
Rationale: Catch bugs in development while maintaining performance in production. Provide explicit methods for different semantics.
Implementation:
let y = x + 1 // Panic in debug, wrap in release
let y = x.wrapping*add(1) // Explicit wrapping
let y = x.saturating*add(1) // Saturating
let y = x.checked*add(1)? // Returns Result
Compiler Architecture
D007: IR Format - SSA or Register-based
Status: ✅Decided: SSA
Priority: HIGH
Decided: 2025-10-21
Chosen: SSA (Static Single Assignment)
Rationale:
- Easier optimization (def-use chains)
- Better for incremental compilation
- Cranelift expects SSA
- Standard in modern compilers
Implementation:
%0 = load @var*x
%1 = load @var*y
%2 = add i32 %0, %1
ret %2
D008: Backend - Cranelift vs LLVM vs Custom
Status: ✅Decided: Cranelift primary, LLVM optional
Priority: HIGH
Decided: 2025-10-21
Chosen: Cranelift for development builds, optional LLVM for release
Rationale:
- Cranelift compiles faster (critical for DX)
- LLVM optimizes better (important for production)
- Backend abstraction allows both
- Match Rust's approach
Implementation Plan:
ion build # Uses Cranelift (fast)
ion build --opt # Uses LLVM (slow but optimized)
D009: Cache Storage - Filesystem vs Database
Status: ✅Decided: Filesystem (content-addressable)
Priority: MEDIUM
Decided: 2025-10-21
Chosen: Content-addressable filesystem cache
Rationale:
- Simple, no external dependencies
- Easy to inspect/debug
- Git-like model (familiar)
- Fast enough for typical projects
Structure:
.home/cache/
ir/
ab/cd/abcdef123... # IR files by hash
obj/
12/34/123456... # Object files
meta/
modules.db # SQLite for metadata
D010: Borrow Checker Algorithm
Status: ✅Decided: Hybrid (Option D)
Priority: HIGH
Decided: 2025-10-21
Context: Which algorithm to use for borrow checking?
Options:
A) NLL (Non-Lexical Lifetimes) - Rust 2018:
- Flow-sensitive analysis
- Precise but complex
B) Polonius - Next-gen Rust:
- Datalog-based
- More precise than NLL
- Still experimental
C) Conservative - Simple Scopes:
- Lexical scopes only
- Simple but restrictive
- Fast to implement
D) Hybrid - Conservative with NLL hints:
- Start conservative
- Add NLL for common patterns
- Progressive enhancement
Chosen:Option D (Hybrid)
Rationale: Ship conservative checker quickly (Month 5), iterate to NLL precision (Month 8). Avoid premature complexity.
Implementation Plan:
- Phase 1 (Month 5): Conservative lexical scopes
- Phase 2 (Month 8): Add NLL for common patterns
- Phase 3 (Month 12): Full NLL precision
Tooling
D011: LSP - Standalone vs Embedded
Status: ✅Decided: Both (Option C)
Priority: MEDIUM
Decided: 2025-10-21
Options:
A) Standalone Process:
ion lsp # Separate server process
Pros: Standard LSP, works with all editors
Cons: Extra process, startup time
B) Embedded in Daemon:
ion daemon start # Includes LSP
Pros: Shared cache, zero startup
Cons: Tightly coupled
C) Both:
ion lsp # Quick standalone
ion daemon lsp # Via daemon
Pros: Flexibility
Cons: Maintenance burden
Chosen:Option C (Both)
Rationale: Maximum flexibility for different editor setups. Daemon provides shared cache, standalone for simplicity.
Implementation:
ion lsp # Standalone process
ion daemon lsp # Via daemon (shared cache)
Standard Library
D012: Async Runtime - Bundled vs Optional
Status: ✅Decided: Implicit Tree-Shakeable (Option C)
Priority: HIGH
Decided: 2025-10-21
Options:
A) Bundled (Go/JavaScript):
// Runtime always available
async fn handler() { }
Pros: Zero setup, consistent
Cons: Binary size, not needed for all programs
B) Optional (Rust):
import std/runtime { Runtime }
fn main() {
let rt = Runtime.new()
rt.block*on(async*main())
}
Pros: Minimal binaries when not needed
Cons: Setup boilerplate
C) Implicit but Tree-Shakeable:
// Runtime linked only if async used
async fn handler() { } // Pulls in runtime automatically
Pros: Zero boilerplate, minimal when unused
Cons: Magic linking
Chosen:Option C (Implicit + tree-shakeable)
Rationale: Best DX. Linker can eliminate runtime if no async functions. Compiler warns if runtime size is large.
Implementation:
// Runtime auto-linked only if async used
async fn handler() { } // Pulls in runtime
fn main() { } // No async = no runtime overhead
D013: Collections - Interface-based vs Concrete
Status: ✅Decided: Hybrid (Option C)
Priority: MEDIUM
Decided: 2025-10-21
Options:
A) Concrete Types (Zig/Go):
let v: Vec<int> = Vec.new()
let m: Map<string, int> = Map.new()
B) Interface-based (Java):
let v: List<int> = Vec.new()
let m: Dict<string, int> = HashMap.new()
C) Hybrid (Rust):
// Concrete by default
let v: Vec<int> = Vec.new()
// Trait when needed
fn process(items: impl Iterator<int>) { }
Chosen:Option C (Hybrid)
Rationale: Concrete for simplicity, traits for abstraction. Most code doesn't need interface indirection.
Implementation:
// Concrete by default
let v: Vec(int) = Vec.new()
// Traits when needed
fn process(items: impl Iterator(int)) { }
Open Questions
Questions that need research before deciding:
Q001: Can we achieve zero-cost async
Research Needed:
- Measure overhead vs hand-rolled state machines
- Compare with Rust's async
- Profile typical async workloads
Q002: How to handle C interop with borrow checker
Research Needed:
- Study Rust's FFI model
- Consider automatic
unsafeboundaries - Test with real C libraries
Q003: WASM target performance expectations
Research Needed:
- Benchmark Cranelift WASM output
- Compare with Rust/C WASM
- Identify optimization opportunities
Q004: Can comptime replace macros entirely
Research Needed:
- List all macro use cases (Rust, C++)
- Prototype in comptime
- Identify gaps
Decision Process
How to make decisions:
- Research: Gather data, build prototypes, study prior art
- Document: Write options in this file with pros/cons
- Discuss: Get feedback (Discord, GitHub issues)
- Prototype: Build smallest test to validate
- Decide: Choose based on goals (speed, safety, DX)
- Ship: Implement and iterate
- Review: Revisit if problems emerge
Decision Criteria Priority:
- Compile time performance (beat Zig)
- Runtime performance (match Zig)
- Developer experience (beat Rust)
- Safety (beat C/Zig, approach Rust)
- Simplicity (implementation and mental model)
Versioning Strategy
How decisions evolve:
- Phase 0-1 (Months 1-8): High flexibility, iterate rapidly
- Phase 2-3 (Months 9-14): Stabilizing, breaking changes allowed with notice
- Phase 4-5 (Months 15-20): Limited breaking changes
- Phase 6 (Months 21-24): No breaking changes, 1.0 prep
After 1.0:
- Breaking changes only in major versions
- Deprecation warnings for 2+ minor versions
- Edition system (like Rust) if needed
Last Updated: 2025-10-21
Next Review: When starting each phase
Decision Template: See D001 format above