diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..90d7701 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +testdata/core/* -text +testdata/core/lib/** -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..92f6dc4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + pull_request: + +jobs: + linux: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Install qemu and the arm64 and riscv64 C libraries + run: sudo apt-get update && sudo apt-get install -y qemu-user libc6-arm64-cross libc6-riscv64-cross + - run: go vet ./... + - run: go test ./... + + runtime: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install clang and lld + run: sudo apt-get update && sudo apt-get install -y clang lld llvm + - name: Test the runtime natively + working-directory: runtime + run: | + clang -O1 -fno-builtin -fno-math-errno -w test/harness.c -o /tmp/harness && /tmp/harness + clang -O1 -fno-builtin -fno-math-errno -w test/gc.c -o /tmp/gc && /tmp/gc + - name: Build the runtime blobs + run: sh runtime/build.sh + + native: + strategy: + fail-fast: false + matrix: + os: [macos-14, windows-2022, windows-11-arm] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: go build ./... + - name: Core programs + run: go test -count=1 -run TestCorePrograms -v . diff --git a/FUTURE.md b/FUTURE.md deleted file mode 100644 index 4ad8f32..0000000 --- a/FUTURE.md +++ /dev/null @@ -1,1300 +0,0 @@ -# Tim — Deferred & Future Features - -This file holds language features and implementation notes that were removed from -the authoritative specs (`LANGUAGESPEC.md`, `GRAMMAR.md`) to keep the core small -and fully implementable. Nothing here is guaranteed to be implemented or stable. -Content is preserved verbatim from earlier revisions of the specs. - -Status legend: -- **Deferred** — designed but not implemented; may be added later. -- **Implementation note** — describes one compiler's internals, not language semantics. - ---- - -# Deferred: Classes and Object-Oriented Programming - -> **Status: Deferred.** Not implemented (tests skip with "OOP features not yet -> fully implemented"). Removed from `LANGUAGESPEC.md` / `GRAMMAR.md`. The grammar -> productions below are kept for reference if OOP is revived. - -### Grammar productions (removed from GRAMMAR.md) - -```ebnf -declaration = ... | class_decl | ... ; -class_decl = "class" identifier [ extend_clause ] "{" { class_member } "}" ; -extend_clause = { "<>" identifier } ; -class_member = class_field_decl - | method_decl ; -class_field_decl = identifier "." identifier "=" expression ; -method_decl = identifier "=" lambda_expr ; -``` - - -### Specification prose (removed from LANGUAGESPEC.md) - -## Classes and Object-Oriented Programming - -Tim supports classes as syntactic sugar over maps and closures, following the philosophy that everything is `map[uint64]float64`. - -### Design Philosophy - -- **Syntactic sugar:** Classes compile to regular maps and lambdas -- **No new types:** Objects are still `map[uint64]float64` -- **Composition:** Use `<>` to extend with behavior maps (no inheritance) -- **Minimal syntax:** Only adds the `class` keyword -- **Transparent:** You can always see what the class desugars to - -### Class Declaration - -Classes group data and methods together: - -```tim -class Point { - init = (x, y) -> { - .x = x - .y = y - } - - distance = other -> { - dx := other.x - .x - dy := other.y - .y - sqrt(dx * dx + dy * dy) - } - - move = (dx, dy) -> { - .x <- .x + dx - .y <- .y + dy - } -} - -// Create instance -p1 := Point(10, 20) -p2 := Point(30, 40) - -// Call methods -dist := p1.distance(p2) -p1.move(5, 5) -``` - -### How Classes Work - -A class declaration creates a constructor function: - -```tim -// This class: -class Counter { - init = start -> { - .count = start - } - - increment = () -> { - .count <- .count + 1 - } -} - -// Desugars to this: -Counter := start -> { - instance := {} - instance["count"] = start - instance["increment"] = () -> { - instance["count"] <- instance["count"] + 1 - } - ret instance -} -``` - -### Instance Fields and "this" - -Inside methods, `.field` accesses instance fields. The `. ` expression (dot followed by space or newline) means "this": - -```tim -class List { - init = () => { - .items = [] - } - - add = item -> { - .items <- .items :: item - ret . // Return this (self) for chaining - } - - size = () => .items.length -} - -list = List().add(1).add(2).add(3) // Method chaining via `. ` -println(list.size()) // 3 -``` - -**Key points:** -- `.field` accesses instance field inside methods -- `. ` (dot space or dot newline) means "this" (the current instance) -- Return `. ` for method chaining -- Outside methods, use `instance.field` explicitly -- No `this` keyword - use `. ` instead - -```tim -class Account { - init = balance -> { - .balance = balance - } - - withdraw = amount -> { - amount > .balance { - ret -1 // Insufficient funds - } - .balance <- .balance - amount - ret 0 - } - - deposit = amount -> { - .balance <- .balance + amount - } - - get_balance = () => .balance -} - -acc = Account(100) -acc.deposit(50) -println(acc.get_balance()) // 150 -``` - -### Class Fields (Static) - -Class fields are shared across all instances: - -```tim -class Entity { - Entity.count = 0 - Entity.all = [] - - init = name -> { - .name = name - .id = Entity.count - Entity.count <- Entity.count + 1 - Entity.all <- Entity.all :: instance - } - - get_total = () -> Entity.count -} - -e1 := Entity("Alice") -e2 := Entity("Bob") -println(e1.get_total()) // 2 -println(Entity.count) // 2 -``` - -### Composition with `<>` - -Extend classes with behavior maps using the `<>` composition operator: - -```tim -// Define behavior map -Serializable := { - to_json: () -> { - // Serialize instance to JSON string - keys := this.keys() - @ i in 0.. { - // Parse JSON and populate instance - } -} - -// Extend class with behavior using <> -class User <> Serializable { - init = (name, email) -> { - .name = name - .email = email - } -} - -user := User("Alice", "alice@example.com") -json := user.to_json() -``` - -**Multiple composition** - chain `<>` operators: - -```tim -class Product <> Serializable <> Validatable <> Timestamped { - init = (name, price) -> { - .name = name - .price = price - .created_at = now() - } -} -``` - -**How `<>` works:** The `<>` operator merges behavior maps into the class. At runtime, all methods from the behavior maps are copied into the instance during construction, with later maps overriding earlier ones if there are conflicts. - -### Method Semantics - -**Instance methods** close over the instance: - -```tim -class Box { - init = value -> { - .value = value - } - - get = () -> .value - set = v -> { .value <- v } -} - -b := Box(42) -getter := b.get // Captures b -println(getter()) // 42 -``` - -**Class methods** don't capture instances: - -```tim -class Math { - Math.PI = 3.14159 - - // Note: no init, Math is never instantiated - Math.circle_area = radius -> Math.PI * radius * radius -} - -area := Math.circle_area(10) -``` - -### Private Methods (Convention) - -Use underscore prefix for "private" methods: - -```tim -class Parser { - init = input -> { - .input = input - .pos = 0 - } - - _peek = () -> { - .pos < .input.length { - ret .input[.pos] - } - ret -1 - } - - _advance = () -> { - .pos <- .pos + 1 - } - - parse_number = () -> { - result := 0 - @ ._peek() >= 48 and ._peek() <= 57 { - result <- result * 10 + (._peek() - 48) - ._advance() - } - ret result - } -} -``` - -### Method Chaining - -Return `. ` (this) to enable chaining: - -```tim -class StringBuilder { - init = () => { - .parts = [] - } - - append = str -> { - .parts <- .parts :: str - ret . // Return this (self) - } - - build = () => { - result := "" - @ part in .parts { - result <- result + part - } - ret result - } -} - -str = StringBuilder() - .append("Hello") - .append(" ") - .append("World") - .build() - -println(str) // "Hello World" -``` - -### Integration with CStruct - -Combine classes and CStruct for high performance: - -```tim -cstruct Vec3Data { - x as float64, - y as float64, - z as float64 -} - -class Vec3 { - init = (x, y, z) -> { - .data = c.malloc(Vec3Data.size as uint64) - - unsafe float64 { - rax <- .data as ptr - [rax] <- x - [rax + 8] <- y - [rax + 16] <- z - } - } - - dot = other -> { - unsafe float64 { - rax <- .data as ptr - rbx <- other.data as ptr - xmm0 <- [rax] - xmm0 <- xmm0 * [rbx] - xmm1 <- [rax + 8] - xmm1 <- xmm1 * [rbx + 8] - xmm0 <- xmm0 + xmm1 - xmm1 <- [rax + 16] - xmm1 <- xmm1 * [rbx + 16] - xmm0 <- xmm0 + xmm1 - } - } - - free = () -> c.free(.data as ptr) -} - -v1 := Vec3(1, 2, 3) -v2 := Vec3(4, 5, 6) -println(v1.dot(v2)) // 32.0 -v1.free() -v2.free() -``` - -### No Inheritance - -Tim does not support classical inheritance. Use composition: - -```tim -// Instead of: -// class Dog extends Animal { ... } - -// Do this: -Animal := { - eat: () -> println("Eating..."), - sleep: () -> println("Sleeping...") -} - -class Dog <> Animal { - init = name -> { - .name = name - } - - bark = () -> println("Woof!") -} - -dog := Dog("Rex") -dog.eat() // From Animal -dog.bark() // From Dog -``` - -### When to Use Classes - -**Use classes when:** -- You have related data and behavior -- You want familiar OOP syntax -- You need encapsulation (via naming conventions) -- You're building objects with state - -**Don't use classes when:** -- Simple data structures (use maps) -- Stateless functions (use plain functions) -- Performance-critical code (use CStruct + functions) - -### Examples - -**Stack data structure:** - -```tim -class Stack { - init = () => { - .items = [] - } - - push = item -> { - .items <- .items :: item - } - - pop = () => { - .items.length == 0 { - ret ?? // Empty - } - last := .items[.items.length - 1] - .items <- .items[0..<(.items.length - 1)] - ret last - } - - is_empty = () => .items.length == 0 -} - -s = Stack() -s.push(1) -s.push(2) -s.push(3) -println(s.pop()) // 3 -``` - -**Simple ORM-like class:** - -```tim -class Model { - Model.table = "" - - init = data -> { - .data = data - } - - save = () -> { - query := f"INSERT INTO {Model.table} VALUES (...)" - // Execute query... - } - - delete = () -> { - id := .data["id"] - query := f"DELETE FROM {Model.table} WHERE id = {id}" - // Execute query... - } -} - -class User <> Model { - Model.table = "users" - - init = (name, email) -> { - .data = { name: name, email: email } - } -} - -user := User("Alice", "alice@example.com") -user.save() -``` - - ---- - -# Implementation notes (removed from LANGUAGESPEC.md) - -> **Status: Implementation note.** These sections describe the current Go -> compiler's internals (arena machine code, optimization passes), not language -> semantics a reimplementation must match. - -# Arena Allocator System - -## Overview - -Tim uses an arena-based memory allocator for all runtime string, list, and map operations. This provides fast, predictable memory allocation with automatic cleanup at scope boundaries. - -## High-Level Design - -### Memory Allocation Strategy - -**Static Data (Read-Only):** -- String literals: stored in `.rodata` section -- Constant lists: `[1, 2, 3]` stored in `.rodata` -- Constant maps: `{x: 10}` stored in `.rodata` -- Never allocated at runtime, zero overhead - -**Dynamic Data (Arena-Allocated):** -- String concatenation: `"hello" + "world"` -- Dynamic lists: `[x, y, z]` where x/y/z are variables -- Runtime map construction -- Function closures (future) -- Variadic argument lists - -**User-Controlled Allocation:** -- `c.malloc()` - C malloc, user must call `c.free()` -- `c.realloc()` - C realloc -- `c.free()` - C free -- `alloc()` - Tim builtin (uses malloc internally) - -### Arena Lifecycle - -``` -Program Start: - └─> Initialize global arena (1MB) - └─> All runtime operations use this arena - └─> Arena grows automatically via realloc -Program End: - └─> Free global arena - -Future: arena { ... } blocks: - Block Start: - └─> Create scoped arena - └─> All allocations inside use this arena - Block End: - └─> Free scoped arena (all allocations freed at once) -``` - -### Benefits - -1. **Speed**: O(1) bump allocation (just increment a pointer) -2. **Predictability**: Deterministic memory usage -3. **No Fragmentation**: Memory is contiguous within an arena -4. **Batch Deallocation**: Free entire arena at once -5. **Efficient**: Perfect for frame-based allocation patterns - -## Machine Code Level - -### Data Structures - -#### Meta-Arena Structure -```c -// _tim_arena_meta: Global variable holding pointer to arena array -uint64_t _tim_arena_meta; // Pointer to arena_ptr[] - -// Meta-arena array (dynamically allocated) -void* arena_ptrs[]; // Array of pointers to arena structs - -// Meta-arena metadata -uint64_t _tim_arena_meta_cap; // Capacity of arena_ptrs array -uint64_t _tim_arena_meta_len; // Number of arenas currently allocated -``` - -#### Arena Structure -```c -// Individual arena (32 bytes) -struct Arena { - void* base; // [offset 0] Base pointer to arena buffer - uint64_t capacity; // [offset 8] Total arena size in bytes - uint64_t used; // [offset 16] Bytes currently used - uint64_t alignment; // [offset 24] Allocation alignment (typically 8) -}; -``` - -### Initialization Sequence - -At program start, `initializeMetaArenaAndGlobalArena()` executes: - -```assembly -# 1. Allocate meta-arena array (8 bytes for 1 pointer) -mov rdi, 8 -call malloc -# rax = pointer to meta-arena array (P1) - -# 2. Store meta-arena pointer in global variable -lea rbx, [_tim_arena_meta] -mov [rbx], rax # _tim_arena_meta = P1 - -# 3. Allocate arena buffer (1MB) -mov rdi, 1048576 -call malloc -# rax = arena buffer (P2) -mov r12, rax - -# 4. Allocate arena struct (32 bytes) -mov rdi, 32 -call malloc -# rax = arena struct (P3) - -# 5. Initialize arena struct -mov [rax + 0], r12 # base = P2 -mov rcx, 1048576 -mov [rax + 8], rcx # capacity = 1MB -xor rcx, rcx -mov [rax + 16], rcx # used = 0 -mov rcx, 8 -mov [rax + 24], rcx # alignment = 8 - -# 6. Store arena struct pointer in meta-arena[0] -lea rbx, [_tim_arena_meta] -mov rbx, [rbx] # rbx = P1 (meta-arena array) -mov [rbx], rax # P1[0] = P3 (arena struct) -``` - -**Memory Layout After Initialization:** -``` -_tim_arena_meta --> P1 --> [P3, NULL, NULL, ...] - | - v - Arena Struct { - base: P2 --> [1MB buffer] - capacity: 1048576 - used: 0 - alignment: 8 - } -``` - -### Allocation Sequence - -When allocating N bytes, `tim_arena_alloc(arena_ptr, size)` executes: - -```assembly -# Input: rdi = arena_ptr (P3), rsi = size (N) -# Output: rax = allocated pointer - -# 1. Load arena fields -mov r8, [rdi + 0] # r8 = base (P2) -mov r9, [rdi + 8] # r9 = capacity -mov r10, [rdi + 16] # r10 = used -mov r11, [rdi + 24] # r11 = alignment - -# 2. Align offset -mov rax, r10 # rax = used -add rax, r11 # rax += alignment -sub rax, 1 # rax += alignment - 1 -mov rcx, r11 -sub rcx, 1 # rcx = alignment - 1 -not rcx # rcx = ~(alignment - 1) -and rax, rcx # rax = aligned_offset - -# 3. Check capacity -mov rdx, rax -add rdx, rsi # rdx = aligned_offset + size -cmp rdx, r9 # if (rdx > capacity) -jg arena_grow # goto grow path - -# 4. Fast path: allocate -arena_fast: -mov rax, r8 # rax = base -add rax, r13 # rax = base + aligned_offset -mov rdx, r13 -add rdx, r12 # rdx = aligned_offset + size -mov [rbx + 16], rdx # arena->used = new_offset -jmp arena_done - -# 5. Grow path: realloc buffer -arena_grow: -mov rdi, r9 -add rdi, r9 # rdi = capacity * 2 -# ... (grow logic, realloc arena buffer) - -arena_done: -# rax = allocated pointer -ret -``` - -### String Concatenation Example - -Concatenating `"hello" + "world"`: - -```assembly -# Strings in rodata: -str_1: [5.0][0][104.0][1][101.0][2][108.0][3][108.0][4][111.0] # "hello" -str_2: [5.0][0][119.0][1][111.0][2][114.0][3][108.0][4][100.0] # "world" - -# Concatenation code: -lea r12, [str_1] # r12 = left string -lea r13, [str_2] # r13 = right string - -# Load lengths -movsd xmm0, [r12] # xmm0 = 5.0 -cvttsd2si r14, xmm0 # r14 = 5 (left length) -movsd xmm0, [r13] -cvttsd2si r15, xmm0 # r15 = 5 (right length) - -# Calculate size: 8 + (left_len + right_len) * 16 -mov rbx, r14 -add rbx, r15 # rbx = 10 (total length) -mov rax, rbx -shl rax, 4 # rax = 10 * 16 = 160 -add rax, 8 # rax = 168 (total size) - -# Allocate from arena -mov rdi, rax # rdi = 168 (size) -call callArenaAlloc # Arena allocation -# rax = pointer to new string - -# Copy data (omitted for brevity) -# Result: [10.0][0][104.0][1][101.0]...[9][100.0] # "helloworld" -``` - -### Dynamic List Creation Example - -Creating `[x, y]` where x=10, y=20: - -```assembly -# Calculate size: 8 + (2 * 16) = 40 bytes -mov rdi, 40 # size = 40 - -# Allocate from arena -lea r11, [_tim_arena_meta] # Step 1: Get meta-arena variable address -mov r11, [r11] # Step 2: Load meta-arena pointer (P1) -mov r11, [r11] # Step 3: Load arena[0] pointer (P3) -mov rsi, rdi # rsi = size -mov rdi, r11 # rdi = arena_ptr -call tim_arena_alloc # Allocate -# rax = list pointer - -# Store count -mov rcx, 2 -cvtsi2sd xmm0, rcx -movsd [rax], xmm0 # list[0] = 2.0 (count) - -# Store element 0: key=0, value=10 -xor rcx, rcx -mov [rax + 8], rcx # list[8] = 0 (key) -movsd xmm0, [rbp - 16] # xmm0 = x = 10.0 -movsd [rax + 16], xmm0 # list[16] = 10.0 (value) - -# Store element 1: key=1, value=20 -mov rcx, 1 -mov [rax + 24], rcx # list[24] = 1 (key) -movsd xmm0, [rbp - 24] # xmm0 = y = 20.0 -movsd [rax + 32], xmm0 # list[32] = 20.0 (value) -``` - -## Implementation Details - -### callArenaAlloc() Function - -The `callArenaAlloc()` function in `arena.go` is a helper that: - -1. Takes size in `rdi` -2. Loads the global arena pointer -3. Calls `tim_arena_alloc` with proper arguments -4. Returns allocated pointer in `rax` - -**Critical Pattern (must load twice):** -```go -// Step 1: Load address of _tim_arena_meta variable -fc.out.LeaSymbolToReg("r11", "_tim_arena_meta") // r11 = &_tim_arena_meta - -// Step 2: Load meta-arena pointer from variable -fc.out.MovMemToReg("r11", "r11", 0) // r11 = *_tim_arena_meta = P1 - -// Step 3: Load arena struct pointer from meta-arena[0] -fc.out.MovMemToReg("r11", "r11", 0) // r11 = P1[0] = P3 - -// Now r11 = arena struct pointer, ready to pass to tim_arena_alloc -``` - -**Why TWO loads?** -- First load: dereference `_tim_arena_meta` to get meta-arena array pointer -- Second load: dereference `meta_arena[0]` to get arena struct pointer - -### Integration Points - -**String Concatenation (`_tim_string_concat`):** -```go -// OLD: fc.eb.GenerateCallInstruction("malloc") -// NEW: -fc.callArenaAlloc() -``` - -**Dynamic List Creation:** -```go -// OLD: fc.trackFunctionCall("malloc") -// fc.eb.GenerateCallInstruction("malloc") -// NEW: -fc.callArenaAlloc() -``` - -**Dynamic Map Creation:** -```go -// Maps with constant keys/values use .rodata -// Maps with runtime keys/values use callArenaAlloc() -``` - -## Cleanup - -At program end, `cleanupAllArenas()` frees all arenas: - -```assembly -# Load meta-arena pointer -lea rbx, [_tim_arena_meta] -mov rbx, [rbx] # rbx = P1 - -# Loop through arenas -xor r8, r8 # r8 = index = 0 -cleanup_loop: -cmp r8, rcx # if (index >= len) -jge cleanup_done # exit loop - -# Load arena pointer -mov rax, r8 -shl rax, 3 # offset = index * 8 -add rax, rbx # rax = &meta_arena[index] -mov rdi, [rax] # rdi = arena_ptr -call free # Free arena struct (also frees buffer) - -inc r8 # index++ -jmp cleanup_loop - -cleanup_done: -# Free meta-arena array -mov rdi, rbx -call free -``` - -## Future: Arena Blocks - -Planned syntax for scoped arenas: - -```tim -# Global arena (default) -global_data := "stays alive" - -arena { - # Scoped arena - all allocations freed at block exit - frame_data := "temporary" - temp_list := [1, 2, 3] - - do_work(temp_list) - -} # <-- All arena allocations freed here - -# global_data still valid, frame_data freed -``` - -**Implementation:** -- Push new arena on arena stack -- Update `currentArena` index -- All allocations use new arena -- Pop arena and free at block exit - -## Debugging - -**Common Issues:** - -1. **Segfault in callArenaAlloc:** - - Check that meta-arena is initialized before first use - - Verify TWO loads are performed to get arena struct pointer - - Ensure `tim_arena_alloc` is being called, not generated inline - -2. **Null pointer from allocation:** - - Arena may be full and realloc failed - - Check error handling in `tim_arena_alloc` - -3. **Corrupted data:** - - Check alignment is respected - - Verify arena->used is updated correctly - - Ensure no double-free scenarios - -**Verification:** -```tim -# Test arena allocation -s1 := "hello" -s2 := " world" -s3 := s1 + s2 # Should allocate from arena -printf("%s\n", s3) # Should print "hello world" - -list := [1, 2, 3] # Should allocate from arena -printf("%f\n", list[0]) # Should print "1.000000" -``` - -## Performance Characteristics - -**Arena Allocation:** -- Time: O(1) - just pointer arithmetic -- Space: Minimal overhead (32 bytes per arena struct) -- Growth: O(n) when realloc needed (rare) - -**vs. Malloc:** -- ~10x faster for typical frame-based allocations -- No fragmentation -- Better cache locality -- Batch deallocation is instant - -**Best Use Cases:** -- Frame-based event loops (allocate per frame, free at frame end) -- Level loading (allocate for level, free when done) -- String building (concatenate many strings, use once) -- Temporary data structures - -## Summary - -The arena allocator provides: -- ✅ Fast O(1) allocation -- ✅ Automatic cleanup at scope boundaries -- ✅ Zero fragmentation -- ✅ Predictable memory usage -- ✅ Integration with existing Tim runtime -- ✅ Compatibility with C malloc/free when needed - -All Tim runtime operations (string concat, list creation, etc.) now use arena allocation by default, while user code can still use `c.malloc()` for manual memory management when needed. -# Tim Compiler Optimizations - -## Overview - -The Tim compiler includes several modern CPU instruction optimizations that provide significant performance improvements for numerical and bit manipulation code. These optimizations use runtime CPU feature detection to ensure compatibility across different processors. - -## Implemented Optimizations - -### 1. FMA (Fused Multiply-Add) 🚀 - -**Status:** ✅ Core Implementation Complete (2025-12-10) -**CPU Requirements:** Intel Haswell (2013+), AMD Piledriver (2012+) or newer -**CPU Coverage:** ~98% of modern x86-64 CPUs -**Performance Impact:** 1.5-2.0x speedup on numerical code -**Architecture Support:** x86-64 (FMA3/AVX-512), ARM64 (NEON/SVE), RISC-V (RVV) - -#### What is FMA? - -FMA (Fused Multiply-Add) combines multiplication and addition into a single instruction with higher precision and better performance: -- **Single instruction** instead of two separate operations -- **One rounding** instead of two (better numerical accuracy) -- **Lower latency** - typically 3-4 cycles vs 7-8 cycles for separate mul+add - -#### Automatic Pattern Detection - -The compiler automatically detects and optimizes these patterns: - -```tim -// Pattern 1: (a * b) + c -result := x * y + z // Optimized to VFMADD132SD - -// Pattern 2: c + (a * b) -result := z + x * y // Optimized to VFMADD132SD - -// Pattern 3: Polynomial evaluation -result := a * x * x + b * x + c // Multiple FMA instructions -``` - -#### Runtime Behavior - -```asm -; CPU Feature Check (done once at startup) -cpuid ; Query CPU features -bt ecx, 12 ; Test FMA bit -setc [cpu_has_fma] ; Store result - -; Code Generation (for a * b + c) -test [cpu_has_fma] ; Check if FMA available -jz fallback ; Jump to fallback if not - -; FMA path (modern CPUs): -vfmadd132sd xmm0, xmm2, xmm1 ; xmm0 = xmm0*xmm1 + xmm2 (3 cycles) -jmp done - -fallback: ; Fallback path (older CPUs): -mulsd xmm0, xmm1 ; xmm0 = xmm0 * xmm1 (4 cycles) -addsd xmm0, xmm2 ; xmm0 = xmm0 + xmm2 (3 cycles) - -done: -``` - -#### Benefits - -1. **Performance:** 30-80% faster for numerical computations -2. **Accuracy:** Single rounding provides IEEE 754 compliant results -3. **Compatibility:** Graceful fallback ensures code runs on older CPUs -4. **Zero configuration:** Works automatically, no compiler flags needed - -#### Use Cases - -- **Polynomial evaluation:** `a*x² + b*x + c` -- **Dot products:** Sum of element-wise products -- **Matrix operations:** Accumulating products -- **Physics simulations:** Force calculations, numerical integration -- **Graphics:** Vertex transformations, ray tracing - -#### Implementation Details - -The FMA optimization is implemented in three stages: - -1. **Pattern Detection (optimizer.go):** The `foldConstantExpr` function detects `(a * b) + c` and `c + (a * b)` patterns during constant folding and creates `FMAExpr` AST nodes. - -2. **AST Representation (ast.go):** The `FMAExpr` type captures the three operands (A, B, C) and operation type (add/subtract): - ```go - type FMAExpr struct { - A, B, C Expression // a * b ± c - IsSub bool // true for FMSUB (subtract) - IsNegMul bool // true for FNMADD (negate multiply) - } - ``` - -3. **Code Generation (codegen.go):** The compiler emits architecture-specific FMA instructions: - - **x86-64:** `VFMADD231PD` (AVX2 256-bit) or `VFMADD231PD` with EVEX (AVX-512 512-bit) - - **ARM64:** `FMLA` (NEON 128-bit) or `FMLA` (SVE 512-bit scalable) - - **RISC-V:** `vfmadd.vv` (RVV scalable vector) - -The instruction encoders in `vfmadd.go` handle all three architectures with complete VEX/EVEX/SVE/RVV encoding. - -**Current Limitations:** -- No runtime CPU feature detection yet (assumes FMA available) -- FMSUB (subtract variant) AST support exists but needs instruction encoder -- Only vector width variants implemented (no scalar VFMADD213SD yet) - -### 2. Bit Manipulation Instructions ⚡ - -**Status:** ✅ Fully Implemented -**CPU Requirements:** Intel Nehalem (2008+), AMD K10 (2007+) or newer -**CPU Coverage:** ~95% of x86-64 CPUs -**Performance Impact:** 10-50x speedup for bit counting operations - -#### POPCNT - Population Count - -Counts the number of set bits in a 64-bit integer. - -```tim -count := popcount(255.0) // Returns 8.0 (0b11111111 has 8 bits set) -``` - -**Performance:** -- **With POPCNT:** 3 cycles (single instruction) -- **Without POPCNT:** 25+ cycles (loop implementation) -- **Speedup:** ~8x - -**Machine Code:** -```asm -; Optimized path: -popcnt rax, rax ; Count bits (3 cycles) - -; Fallback path (loop): -xor rcx, rcx ; count = 0 -.loop: - test rdx, rdx ; while (x != 0) - jz .done - mov rax, rdx - and rax, 1 ; count += x & 1 - add rcx, rax - shr rdx, 1 ; x >>= 1 - jmp .loop -.done: -``` - -#### LZCNT - Leading Zero Count - -Counts the number of leading zero bits (useful for finding highest set bit). - -```tim -zeros := clz(8.0) // Returns 60.0 (0b1000 in 64-bit has 60 leading zeros) -``` - -**Performance:** -- **With LZCNT:** 3 cycles -- **Without LZCNT:** 10-15 cycles (BSR + adjustment) -- **Speedup:** ~4x - -#### TZCNT - Trailing Zero Count - -Counts the number of trailing zero bits (useful for finding lowest set bit). - -```tim -zeros := ctz(8.0) // Returns 3.0 (0b1000 has 3 trailing zeros) -``` - -**Performance:** -- **With TZCNT:** 3 cycles -- **Without TZCNT:** 10-15 cycles (BSF) -- **Speedup:** ~4x - -#### Use Cases - -- **Bit manipulation:** Fast bit counting and scanning -- **Data structures:** Bloom filters, bit sets, hash tables -- **Compression:** Huffman coding, entropy encoding -- **Cryptography:** Bit-level operations -- **Game development:** Collision detection, spatial hashing - -### 3. CPU Feature Detection - -All optimizations use runtime CPU feature detection to ensure compatibility: - -```tim -// This code automatically: -// 1. Detects CPU features at startup (CPUID) -// 2. Uses optimal instructions if available -// 3. Falls back to compatible code on older CPUs - -x := 2.0 -y := 3.0 -z := 4.0 -result := x * y + z // Uses FMA if available, mul+add otherwise -``` - -**Features Detected:** -- `cpu_has_fma` - FMA3 support (Haswell 2013+) -- `cpu_has_avx2` - AVX2 support (Haswell 2013+) [Reserved for future use] -- `cpu_has_popcnt` - POPCNT/LZCNT/TZCNT support (Nehalem 2008+) -- `cpu_has_avx512` - AVX-512 support (Skylake-X 2017+) [Used for hashmap operations] - -## Performance Benchmarks - -### FMA Optimization - -```tim -// Polynomial evaluation benchmark -polynomial := fn(x) { a * x * x + b * x + c } - -// Results (1 million iterations): -Without FMA: 142 ms (baseline) -With FMA: 78 ms (1.82x faster) -``` - -### Bit Operations - -```tim -// Bit counting benchmark -sum := 0.0 -for i in 0..1000000 { - sum += popcount(i) -} - -// Results: -Loop implementation: 850 ms (baseline) -POPCNT instruction: 98 ms (8.7x faster) -``` - -## AVX-512 for Hashmaps 🔥 - -**Status:** ✅ Implemented -**CPU Requirements:** Intel Skylake-X (2017+), AMD Zen 4 (2022+) or newer -**CPU Coverage:** ~30% (high on servers, growing on desktop) -**Performance Impact:** 4-8x speedup for hashmap lookups - -### What is AVX-512? - -AVX-512 processes 8 double-precision values simultaneously (vs 2 for SSE2, 4 for AVX2) and includes advanced features: -- **512-bit ZMM registers:** 8x float64 operations per instruction -- **Mask registers (k0-k7):** Predicated execution without branches -- **Gather/Scatter:** Load/store non-contiguous memory in single instruction - -### Hashmap Optimization - -Tim uses AVX-512 `vgatherqpd` to search 8 hashmap entries at once: - -```tim -// This automatically uses AVX-512 if available: -my_map := {"key1": 10.0, "key2": 20.0, "key3": 30.0} -value := my_map["key2"] // Searches 8 keys per iteration with AVX-512 -``` - -**Performance:** -- **With AVX-512:** Process 8 keys per iteration (~12 cycles) -- **With SSE2:** Process 2 keys per iteration (~8 cycles each) -- **Speedup:** 4-5x for large hashmaps - -**Machine Code (simplified):** -```asm -; Broadcast search key to all 8 lanes -vbroadcastsd zmm3, xmm2 ; zmm3 = [key, key, key, ...] - -; Gather 8 keys from hashmap -vmovdqu64 zmm4, [indices] ; Load indices: 0, 16, 32, ... -vgatherqpd zmm0{k1}, [rbx + zmm4*1] ; Gather 8 keys in one instruction - -; Compare all 8 keys simultaneously -vcmppd k2{k1}, zmm0, zmm3, 0 ; Compare, result in mask k2 - -; Extract which key matched -kmovb eax, k2 ; Move mask to GPR -test eax, eax ; Check if any matched -bsf edx, eax ; Find first match index -``` - -### Graceful Degradation - -The compiler generates both AVX-512 and SSE2 paths: -- **AVX-512 CPUs:** Use gather instructions (8 keys/iteration) -- **Older CPUs:** Use SSE2 scalar path (2 keys/iteration) -- **Detection:** Single CPUID check at startup (~100 cycles) - -## Future Optimizations (Planned) - -### AVX2 Loop Vectorization - -**Effort:** 2-4 weeks -**Impact:** 3-4x for array operations, 7x for matrices -**Status:** Infrastructure exists (20+ SIMD instruction files), needs wiring - -Process 4 float64 values simultaneously: -```tim -// Future optimization: -for i in 0..length { - c[i] = a[i] + b[i] // Could use VADDPD ymm (4 doubles per instruction) -} -``` - -### General AVX-512 Vectorization - -**Effort:** 3-4 weeks -**Impact:** 6-8x for vectorizable loops -**Status:** Infrastructure ready, needs loop analysis and transformation - -Process 8 float64 values simultaneously: -```tim -// Future optimization: -for i in 0..length { - c[i] = a[i] + b[i] // Could use VADDPD zmm (8 doubles per instruction) -} -``` - -## Compiler Implementation Details - -### Code Organization - -- **Feature Detection:** `codegen.go` lines 553-595 (CPU feature detection at startup) -- **FMA Pattern Detection:** `codegen.go` lines 10114-10216 (AST pattern matcher) -- **FMA Code Generation:** `codegen.go` lines 10118-10169 (runtime dispatch) -- **Bit Operations:** `codegen.go` lines 14784-15002 (POPCNT/LZCNT/TZCNT) -- **SIMD Instructions:** `v*.go` files (20+ files, ~3000 LOC ready for future use) - -### Testing - -Comprehensive test suite in `optimization_test.go`: -- FMA pattern detection tests -- Bit manipulation correctness tests -- CPU feature detection tests -- Precision tests for FMA - -Run tests: -```bash -go test -v -run "TestFMA|TestBit" -``` - -## Compatibility - -### Minimum Requirements - -- **x86-64:** Any 64-bit Intel/AMD processor (2003+) -- **FMA Optimization:** Intel Haswell (2013+), AMD Piledriver (2012+) -- **Bit Optimizations:** Intel Nehalem (2008+), AMD K10 (2007+) - -### Graceful Degradation - -All optimizations include fallback code paths: -- Older CPUs use traditional instructions (mul+add, bit loops) -- No performance penalty for CPU feature detection (~100 cycles at startup) -- Binary works on any x86-64 CPU, optimizes automatically - -## References - -### CPU Instructions - -- [Intel FMA Reference](https://www.intel.com/content/www/us/en/docs/intrinsics-guide/) -- [AMD FMA3 Support](https://www.amd.com/en/technologies/fma4) -- [POPCNT Instruction](https://www.felixcloutier.com/x86/popcnt) -- [LZCNT Instruction](https://www.felixcloutier.com/x86/lzcnt) - -### Performance Analysis - -- [Agner Fog's Instruction Tables](https://www.agner.org/optimize/) -- [Intel Optimization Manual](https://www.intel.com/content/www/us/en/develop/documentation/cpp-compiler-developer-guide-and-reference/top/optimization-and-programming-guide.html) - -## Summary - -Tim now includes production-ready optimizations that provide: -- ✅ **1.5-1.8x faster** numerical code (FMA) -- ✅ **8-50x faster** bit operations (POPCNT/LZCNT/TZCNT) -- ✅ **4-8x faster** hashmap lookups (AVX-512 gather) -- ✅ **Better precision** for floating-point math (single rounding) -- ✅ **Zero configuration** (automatic detection and optimization) -- ✅ **Full compatibility** (works on all x86-64 CPUs) - -The compiler is positioned to add general loop vectorization (AVX2/AVX-512) in the future, with all infrastructure already implemented (~3000 LOC in v*.go files). - -**Tim can now compete with C/Rust for numerical computing and data structure performance!** 🚀 - ---- - -# Deferred: Variadic functions - -> **Status: Not working.** Variadic functions (`(first, rest...) -> ...`, the -> `#rest` length operator on the collected args, and spread `f(a, xs..., b)`) -> parse and compile but segfault at runtime — the variadic argument-list codegen -> is incomplete. Documented in `LANGUAGESPEC.md` with a NOT-YET-WORKING banner. -> Fix requires correcting the runtime arg-list construction in `codegen.go` -> (search `VariadicParam` / `IsVariadic`). diff --git a/GRAMMAR.md b/GRAMMAR.md index 5beab31..f19f2ae 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -1,1872 +1,371 @@ -# Tim Grammar Specification +# Tim Grammar -**Version:** 1.5.0 -**Date:** 2025-12-03 -**Status:** Canonical Grammar Reference for Tim 1.5.0 Release +**Version:** 2.0 -This document defines the complete formal grammar of the Tim programming language using Extended Backus-Naur Form (EBNF). +This is the canonical grammar of Tim. `lexer.go` and `parser.go` implement it +exactly; anything not described here is not Tim. -## ⚠️ CRITICAL: The Universal Type +Tim is small on purpose. Each idea has one spelling, borrowed from wherever it +reads best: -Tim has exactly ONE runtime type: `map[uint64]float64`, an ordered map. +| From | Tim takes | +|-------------|----------------------------------------------------------------------------| +| Mathematics | exact numbers, `f(x) = x**2` definitions, chained `0 <= i < n`, `..<` `..=`| +| Python | `and or not in`, truthiness, negative indices, slices, comprehensions | +| Scheme | first-class closures, guaranteed tail calls, every block is a value | +| Haskell | guard matches `\| x > 0 =>`, `\|>` pipelines, terse `x -> x + 1` lambdas | +| Rust | `->` and `=>`, `as` casts, `if` as an expression, immutable by default | +| Go | one way to do things, no classes, `defer`, explicit mutability | +| C | the operator set, `cstruct`, direct FFI, `printf` | +| Assembly | 64-bit bit operations, `popcount`, `read_*`/`write_*` on raw memory, `ret` | -Not "represented as" or "backed by" — every value IS this map: +## 1. Notation -```tim -42 // {0: 42.0} -"Hello" // {0: 72.0, 1: 101.0, 2: 108.0, 3: 108.0, 4: 111.0} -[1, 2, 3] // {0: 1.0, 1: 2.0, 2: 3.0} -{x: 10} // {hash("x"): 10.0} -[] // {} -{} // {} -``` - -**Even C foreign types are stored as maps:** - -```tim -// C pointer (0x7fff1234) stored as float64 bits -ptr: cptr = sdl.SDL_CreateWindow(...) // {0: } - -// C string pointer -err: cstring = sdl.SDL_GetError() // {0: } - -// C int -result: cint = sdl.SDL_Init(...) // {0: 1.0} or {0: 0.0} -``` - -There are NO special types, NO primitives, NO exceptions. -Everything is a map from uint64 to float64. - -This is not an implementation detail — this IS Tim. - -## Type Annotations - -Type annotations are **metadata** that specify: -1. **Semantic intent** - what does this map represent? -2. **FFI conversions** - how to marshal at C boundaries -3. **Optimization hints** - compiler optimizations - -They do NOT change the runtime representation (always `map[uint64]float64`). - -### Native Tim Types -- `num` - number (default type) -- `str` - string (map of char codes) -- `list` - list (map with integer keys) -- `map` - explicit map -- `bool` - boolean (yes/no values) - -### Foreign C Types -- `cstring` - C `char*` (pointer stored as `{0: }`) -- `cptr` - C pointer (e.g., `SDL_Window*`) -- `cint` - C `int`/`int32_t` -- `clong` - C `int64_t`/`long` -- `cfloat` - C `float` -- `cdouble` - C `double` -- `cbool` - C `bool`/`_Bool` -- `cvoid` - C `void` (return type only) - -Foreign types are used at FFI boundaries to guide marshalling. - -## Table of Contents - -- [Grammar Notation](#grammar-notation) -- [Block Disambiguation Rules](#block-disambiguation-rules) -- [Shadow Keyword](#shadow-keyword) -- [Complete Grammar](#complete-grammar) -- [Lexical Elements](#lexical-elements) -- [Keywords](#keywords) -- [Operators](#operators) -- [Operator Precedence](#operator-precedence) - -## Grammar Notation - -The grammar uses Extended Backus-Naur Form (EBNF): - -| Notation | Meaning | -|-------------------|---------------------------| -| `=` | Definition | -| `;` | Termination | -| `\` | Alternation | -| `[ ... ]` | Optional (zero or one) | -| `{ ... }` | Repetition (zero or more) | -| `( ... )` | Grouping | -| `"..."` | Terminal string | -| `letter`, `digit` | Character classes | - -## Block Disambiguation Rules - -When the parser encounters `{`, it determines the block type by examining contents: - -### Rule 1: Map Literal -**Condition:** First element contains `:` (before any `=>` or `~>`) - -```tim -config = { port: 8080, host: "localhost" } -settings = { "key": value, "other": 42 } -``` - -### Rule 2: Match Block -**Condition:** Contains `=>` or `~>` in the block's scope - -There are TWO forms: - -#### Form A: Value Match (with expression before `{`) -Evaluates expression, then matches its result against patterns: - -```tim -// Match on literal values -x { - 0 => "zero" - 5 => "five" - ~> "other" -} - -// Boolean match -x > 0 { - 1 => "positive" // true = 1 - 0 => "zero" // false = 0 -} -``` - -#### Form B: Guard Match (no expression, uses `|` at line start) -Each branch evaluates its own condition independently: - -```tim -// Guard branches with | at line start -{ - | x == 0 => "zero" - | x > 0 => "positive" - | x < 0 => "negative" - ~> "unknown" // optional default -} -``` - -**Important:** The `|` is only a guard marker when at the start of a line/clause. -Otherwise `|` is the pipe operator: `data | transform | filter` - -### Rule 3: Statement Block -**Condition:** No `=>` or `~>` in scope, not a map - -```tim -compute = x -> { - temp = x * 2 - result = temp + 10 - result // Last expression returned -} -``` - -**Disambiguation order:** -1. Check for `:` → Map literal -2. Check for `=>` or `~>` → Match block -3. Otherwise → Statement block - -**Match block type:** -- Has expression before `{` → Value match -- No expression, has `|` at line start → Guard match - -## Shadow Keyword - -The `shadow` keyword is used to explicitly declare that a variable shadows (hides) an existing variable from an outer scope. This prevents accidental shadowing bugs while allowing intentional shadowing when needed. - -### Syntax - -```tim -shadow identifier [: type] = expression -shadow identifier [: type] := expression -``` - -### Rules - -1. **Shadow is required** when declaring a variable that would shadow: - - A module-level constant or variable - - A variable from an outer function scope - - A parameter from an outer lambda - -2. **Shadow is forbidden** for: - - Module-level declarations (nothing to shadow at top level) - - First declaration of a name in a scope (nothing being shadowed) - -3. **Without shadow**: Attempting to declare a variable with a name that exists in an outer scope is a compilation error - -### Examples - -```tim -// Module level -PORT = 8080 -config = { host: "localhost" } - -// Function that needs to use same name -main = { - shadow PORT = 9000 // ✓ OK: explicitly shadows module PORT - shadow config = {} // ✓ OK: explicitly shadows module config - println(PORT) // Prints 9000 -} - -// Nested scopes -process = x -> { - shadow x = x * 2 // ✓ OK: shadows parameter x - inner = y -> { - shadow x = x + y // ✓ OK: shadows outer x - x - } - inner(10) -} - -// Error cases -main = { - x = 42 // ✓ OK: first declaration - x = 100 // ✗ ERROR: immutable, can't reassign - shadow x = 100 // ✗ ERROR: shadow not needed, x is local -} - -X = 100 // Module level -test = { - x = 42 // ✗ ERROR: would shadow X (case-insensitive check) - shadow x = 42 // ✓ OK: explicitly shadows -} -``` - -### Rationale - -**Why require explicit shadowing?** -1. **Prevents bugs**: Accidental shadowing is a common source of errors -2. **Makes intent clear**: Reader knows shadowing is intentional -3. **Helps refactoring**: Renaming outer variables won't silently break inner scopes -4. **No ALL_UPPERCASE rule needed**: Variables can use natural naming in all scopes - -**Case sensitivity:** -- Variable names are case-sensitive for lookup -- Shadow checking is case-insensitive to catch `x` shadowing `X` - -## Import and Export System - -Tim's import system provides a unified way to import libraries, git repositories, and local directories. The export system controls which functions are available to importers and whether they require namespace prefixes. - -### Export Statements - -The `export` statement controls which functions are available to importers: - -**Three export modes:** - -1. **`export *`** - Export all functions into global namespace (no prefix required) - ```tim - export * - - hello = { println("Hello from this module!") } - goodbye = { println("Goodbye!") } - ``` - When imported: - ```tim - import "github.com/user/greetings" as greet - hello() // Works - no prefix needed - goodbye() // Works - no prefix needed - ``` - -2. **`export func1 func2 ...`** - Export only listed functions (prefix required) - ```tim - export hello goodbye - - hello = { println("Hello!") } - goodbye = { println("Goodbye!") } - internal_helper = { println("Internal") } // Not exported - ``` - When imported: - ```tim - import "github.com/user/greetings" as greet - greet.hello() // Works - prefix required - greet.goodbye() // Works - prefix required - greet.internal_helper() // Error - not exported - ``` - -3. **No export statement** - All functions available (prefix required) - ```tim - // No export statement - - hello = { println("Hello!") } - goodbye = { println("Goodbye!") } - ``` - When imported: - ```tim - import "github.com/user/greetings" as greet - greet.hello() // Works - prefix required - greet.goodbye() // Works - prefix required - ``` - -**Design rationale:** -- `export *` is for beginner-friendly libraries (e.g., frameworks that provide a simplified API) -- `export func1 func2` is for controlled APIs with selective exposure -- No export is for general libraries where namespace pollution is a concern - -**Example: Beginner-friendly library** -```tim -// simplelib/main.tim -export * - -// Library initialization -init_window = (width, height, title) -> { ... } - -// Drawing functions -draw_rect = (x, y, w, h, color) -> { ... } -draw_circle = (x, y, radius, color) -> { ... } -clear_screen = color -> { ... } - -// Usage: -import "github.com/user/simplelib" as lib - -// No prefixes needed - feels like built-in functions! -init_window(800, 600, "My App") -@ { - clear_screen(0) - draw_rect(100, 100, 50, 50, 0xFF0000) - draw_circle(400, 300, 30, 0x00FF00) -} -``` - -### Import Resolution Priority - -1. **Libraries** (highest priority) - - System libraries via pkg-config (Linux/macOS) - - .dll files in current directory or system paths (Windows) - - Headers in standard include paths - -2. **Git Repositories** - - GitHub, GitLab, Bitbucket - - SSH or HTTPS URLs - - Optional version specifiers - -3. **Local Directories** (lowest priority) - - Relative or absolute paths - - Current directory with `.` - -### Import Syntax - -```tim -// Library import (uses pkg-config or finds .dll) -import sdl3 as sdl -import raylib as rl - -// Git repository import -import github.com/example/tim-math as math -import github.com/example/tim-math@v1.0.0 as math -import github.com/example/tim-math@latest as math -import github.com/example/tim-math@main as math -import git@github.com:example/tim-math.git as math - -// Directory import -import . as local // Current directory -import ./subdir as sub // Relative path -import /absolute/path as abs // Absolute path - -// C library file import -import /path/to/libmylib.so as mylib -import SDL3.dll as sdl -``` - -### Import Behavior - -- **Libraries**: Searches for library files and headers, parses C headers for FFI -- **Git Repos**: Clones to `~/.cache/tim/` (respects `XDG_CACHE_HOME`), imports all top-level `.tim` files -- **Directories**: Imports all top-level `.tim` files from the directory -- **Version Specifiers**: - - `@v1.0.0` - Specific tag - - `@main` or `@master` - Specific branch - - `@latest` - Latest tag (or default branch if no tags) - - No `@` - Uses default branch - -### Namespace Rules - -When importing a Tim module: - -1. **If module has `export *`**: Functions available without prefix - ```tim - import "github.com/user/simplelib" as lib - init_window() // No prefix needed - ``` - -2. **If module has `export func1 func2`**: Only listed functions available, prefix required - ```tim - import "github.com/user/api" as api - api.exported_func() // Prefix required - api.internal_func() // Error - not exported - ``` - -3. **If module has no export**: All functions available, prefix required - ```tim - import "github.com/user/utils" as utils - utils.helper() // Prefix required - ``` - -## Program Execution Model - -Tim programs can be structured in three ways: - -### 1. Main Function -When a `main` function is defined, it becomes the program entry point: - -```tim -main = { println("Hello!") } // A lambda that returns the value returned from println (true/1.0) -main = 42 // A Tim number {0: 42.0} -main = () -> { 100 } // A lambda that returns 100 -main = { 100 } // A lambda that returns 100 -``` - -**Return value rules:** -- If `main` is set to a number, it is converted to int32 for the exit code -- If `main` returns an empty map `{}` or empty list `[]` or true: exit code 0 -- If `main` is callable (function): called, result becomes exit code -- Return values are implicitly cast to int32 for `_start` - -### 2. Main Variable -When a `main` variable (not a function) is defined without top-level code: - -```tim -main = 42 // Exit with code 42 -main = {} // Exit with code 0 (empty map) -main = [] // Exit with code 0 (empty list) -``` - -**Evaluation:** -- The value of `main` becomes the program's exit code -- Non-callable values are used directly - -### 3. Top-Level Code -When there's no `main` function or variable, top-level code executes: - -```tim -println("Hello!") -x := 42 -println(x) -// Last expression or ret determines exit code -``` - -**Exit code:** -- Last expression value becomes exit code -- `ret` keyword sets explicit exit code -- No explicit return: returns true (1.0), exit code 0 - -### Mixed Cases - -**Top-level code + main function:** -- Top-level code executes first -- It's the responsibility of top-level code to call `main()` -- If top-level doesn't call `main()`, `main()` is never executed -- Last expression in top-level code provides exit code - -```tim -// Top-level setup -x := 100 - -main = { println(x); 42 } - -// main is defined but not called - exit code is 0 -// To call: main() must appear in top-level code -``` - -**Top-level code + main variable:** -- Top-level code executes -- `main` variable is accessible but not special -- Last top-level expression provides exit code - -```tim -main = 99 - -println("Setup") -42 // Exit code is 42, not 99 -``` - -## Complete Grammar - -```ebnf -program = { statement { newline } } ; - -statement = assignment - | if_statement - | guard_statement - | expression_statement - | loop_statement - | unsafe_statement - | arena_statement - | parallel_statement - | cstruct_decl - | return_statement - | error_statement - | defer_statement - | import_statement - | export_statement ; - -return_statement = "ret" [ "@" [ integer ] ] [ expression ] ; - -(* err msg returns a NaN-boxed error: desugars to `ret error(msg)`. - A bare err returns the generic "err" code. *) -error_statement = "err" [ expression ] ; - -(* A guard-match clause as a statement: sugar for `if expression { statement }`. - Canonical early-exit precondition: | b == 0 => err "division by zero" - A block whose | clause lines are followed by ordinary statement lines is a - statement block of guard statements; all-clause blocks stay guard matches. *) -guard_statement = "|" expression "=>" statement ; - -defer_statement = "defer" expression ; - -if_statement = "if" expression block - { "elif" expression block } - [ "else" block ] ; - -import_statement = "import" import_source [ "as" identifier ] ; - -export_statement = "export" ( "*" | identifier { identifier } ) ; - -import_source = string_literal (* library name, file path, or directory, unquoted string *) - | git_url [ "@" version_spec ] ; (* git repository with optional version *) - -git_url = identifier { "." identifier } { "/" identifier } (* github.com/user/repo *) - | "git@" identifier ":" identifier "/" identifier ".git" ; (* git@github.com:user/repo.git *) - -version_spec = identifier (* tag, branch, "latest", or semver like "v1.0.0" *) - | "latest" ; - -cstruct_decl = "cstruct" identifier "{" { field_decl } "}" ; - -field_decl = identifier "as" c_type [ "," ] ; - -c_type = "int8" | "int16" | "int32" | "int64" - | "uint8" | "uint16" | "uint32" | "uint64" - | "float32" | "float64" - | "cptr" | "cstring" ; - -arena_statement = "arena" block ; - -loop_statement = "@" block - | "@" identifier "in" expression [ "!" expression ] block - | "@" expression [ "!" expression ] block - | "foreach" identifier "in" expression [ "!" expression ] block - | "break" [ "@" [ integer ] ] - | "continue" [ "@" [ integer ] ] ; - -parallel_statement = "||" identifier "in" expression block ; - -unsafe_statement = "unsafe" type_cast block [ block ] [ block ] ; - -type_cast = "int8" | "int16" | "int32" | "int64" - | "uint8" | "uint16" | "uint32" | "uint64" - | "float32" | "float64" - | "number" | "string" | "list" | "address" - | "packed" | "aligned" ; - -assignment = [ "fun" ] identifier [ ":" type_annotation ] ("=" | ":=" | "<-") expression - | identifier ("+=" | "-=" | "*=" | "/=" | "%=" | "**=") expression - | indexed_expr "<-" expression - | identifier_list ("=" | ":=" | "<-") expression ; // Multiple assignment - -identifier_list = identifier { "," identifier } ; - -type_annotation = native_type | foreign_type ; - -native_type = "num" | "str" | "list" | "map" | "bool" ; - -foreign_type = "cstring" | "cptr" | "cint" | "clong" - | "cfloat" | "cdouble" | "cbool" | "cvoid" ; - -indexed_expr = identifier "[" expression "]" ; - -expression_statement = expression [ match_block ] ; - -match_block = "{" ( default_arm - | match_clause { match_clause } [ default_arm ] - | guard_clause { guard_clause } [ default_arm ] ) "}" ; - -match_clause = expression [ "=>" match_target ] ; - -guard_clause = "|" expression "=>" match_target ; // | must be at start of line - -default_arm = ( "~>" | "_" "=>" ) match_target ; - -match_target = jump_target | expression ; - -jump_target = integer ; - -block = "{" { statement { newline } } [ expression ] "}" ; - -expression = pipe_expr ; - -pipe_expr = reduce_expr { ( "|" | "||" ) reduce_expr } ; - -reduce_expr = receive_expr ; - -receive_expr = "<=" pipe_expr | or_bang_expr ; - -or_bang_expr = send_expr { ( "or!" | "¤" ) send_expr } ; // ¤ (U+00A4) is a one-character alias for or! - -send_expr = or_expr { "<-" or_expr } ; - -or_expr = and_expr { "or" and_expr } ; - -xor_expr = and_expr { "xor" and_expr } ; - -and_expr = comparison_expr { "and" comparison_expr } ; - -comparison_expr = cons_expr { comparison_op cons_expr } ; - -comparison_op = "==" | "!=" | "<" | "<=" | ">" | ">=" ; - -(* List cons, right-associative: `1 :: 2 :: xs` prepends, so the new list has - the element at index 0 and old elements shifted up one index. Binds tighter - than comparison and looser than ranges/arithmetic (like Haskell's `:`). *) -cons_expr = bitwise_or_expr [ "::" cons_expr ] ; - -bitwise_or_expr = bitwise_xor_expr { "|b" bitwise_xor_expr } ; - -bitwise_xor_expr = bitwise_and_expr { "^b" bitwise_and_expr } ; - -bitwise_and_expr = shift_expr { "&b" shift_expr } ; - -shift_expr = additive_expr { shift_op additive_expr } ; - -shift_op = "<>b" | "<<>>b" ; - -additive_expr = multiplicative_expr { ("+" | "-") multiplicative_expr } ; - -multiplicative_expr = power_expr { ("*" | "/" | "%") power_expr } ; - -power_expr = unary_expr { ( "**" | "^" ) unary_expr } ; - -unary_expr = ( "-" | "not" | "!b" | "~b" | "#" | "µ" ) unary_expr - | postfix_expr ; - -postfix_expr = primary_expr { postfix_op } ; - -postfix_op = "[" expression "]" - | "." ( identifier | integer ) - | "(" [ argument_list ] ")" - | "#" - | match_block ; - -primary_expr = identifier - | number - | boolean - | string - | fstring - | list_literal - | map_literal - | lambda_expr - | enet_address - | address_value - | "(" expression ")" - | "??" - | unsafe_expr - | arena_expr ; - -boolean = "yes" | "no" ; - -enet_address = "&" port_or_host_port ; - -port_or_host_port = port | [ hostname ":" ] port ; - -address_value = "$" expression ; - -port = digit { digit } ; - -hostname = identifier | ip_address ; - -ip_address = digit { digit } "." digit { digit } "." digit { digit } "." digit { digit } ; - -arena_expr = "arena" "{" { statement { newline } } [ expression ] "}" ; - -unsafe_expr = "unsafe" "{" { statement { newline } } [ expression ] "}" - [ "{" { statement { newline } } [ expression ] "}" ] - [ "{" { statement { newline } } [ expression ] "}" ] - [ "as" type_cast ] ; - -lambda_expr = [ parameter_list ] "->" lambda_body - | parameter_list block // Arrow optional with parenthesized params + block body - | block ; // Inferred lambda with no parameters in assignment context - -parameter_list = variadic_params - | identifier { "," identifier } - | "(" [ param_decl_list ] ")" ; - -param_decl_list = param_decl { "," param_decl } ; - -param_decl = identifier [ ":" type_annotation ] [ "..." ] ; - -variadic_params = "(" identifier [ ":" type_annotation ] { "," identifier [ ":" type_annotation ] } "," identifier [ ":" type_annotation ] "..." ")" ; - -lambda_body = [ "->" type_annotation ] ( block | expression [ match_block ] ) ; - -// Lambda Syntax Rules: -// -// Explicit lambda syntax (always works): -// x -> x * 2 // One parameter, expression body -// (x, y) -> x + y // Multiple parameters (parens required) -// (x, y, rest...) -> sum(rest) // Variadic parameters (last param with ...) -// -> println("hi") // No parameters (explicit ->) -// x -> { temp = x * 2; temp } // Single param with block body -// (n) -> { n * n } // Parenthesized param with block body -// (n) { n * n } // Arrow optional when params in parens + block -// (a, b) { a + b } // Multiple params, arrow optional with block -// -// With type annotations: -// (x: num, y: num) -> num { x + y } // Parameter and return types -// (name: str) -> str { upper(name) } // String function -// (ptr: cptr) -> cint { sdl.SDL_DoSomething(ptr) } // C types -// greet(name: str) -> str { f"Hello, {name}!" } // Function definition with types -// -// Inferred lambda syntax (works ONLY in assignment context): -// main = { println("hello") } // Inferred: main = -> { println("hello") } -// handler = { | x > 0 => "pos" } // Inferred: handler = -> { | x > 0 => "pos" } -// -// When `->` can be omitted: -// 1. Parameters in parentheses with block body: `(n) { n * 2 }` or `(a, b) { a + b }` -// 2. In assignment context with just block: `name = { ... }` -// 3. Block contains statements or guard match (| at line start) -// -// When `->` is REQUIRED: -// 1. Single parameter without parens: `x -> x * 2` (arrow distinguishes from identifier) -// 2. Lambda body is an expression, not block: `-> 42` or `n -> n * 2` -// 3. Lambda is NOT being assigned: `[1, 2, 3] | x -> x * 2` -// 4. Lambda is a function argument: `map(data, x -> x * 2)` -// -// Parentheses rules: -// - Single parameter: `x -> x * 2` (no parens) OR `(x) -> x * 2` (with parens) -// - Single parameter + block: `x -> { ... }` OR `(x) { ... }` (arrow optional with parens) -// - Multiple parameters: `(x, y) -> x + y` (parens required) -// - Multiple parameters + block: `(x, y) { x + y }` (arrow optional) -// - Type annotations: `(x: num) -> num { x * 2 }` (parens required) -// - No parameters with explicit ->: `-> println("hi")` (no parens needed) -// - No parameters inferred from block: `main = { ... }` (no parens needed) -// -// Block type determination: -// { x: 10 } // Map literal (has `:` before any `=>`) -// { | x > 0 => "pos" } // Guard match block (has `|` at line start) -// { temp = x * 2; temp } // Statement block (no `:`, no `=>` or `~>`) -// { stmt1; stmt2; | guard => result } // Mixed block (statements + guards) -// x { 0 => "zero" ~> "other" } // Value match (expression before `{`) -// -// Examples: -// // Function definitions (inferred lambda) -// main = { println("Hello!") } -// process = { | x > 0 => "pos" | x < 0 => "neg" } -// -// // Lambdas with parameters (explicit) -// square = x -> x * x -// add = (x, y) -> x + y -// map_fn = f -> data | f - -argument_list = expression { "," expression } ; - -list_literal = "[" [ expression { "," expression } ] "]" ; - -map_literal = "{" [ map_entry { "," map_entry } ] "}" ; - -map_entry = ( identifier | string ) ":" expression ; - -identifier = letter { letter | digit | "_" } ; - -number = [ "-" ] digit { digit } [ "." digit { digit } ] ; - -string = '"' { character } '"' ; - -fstring = 'f"' { character | "{" expression "}" } '"' ; -``` - -## Lexical Elements - -### Identifiers - -Identifiers start with a letter and contain letters, digits, or underscores: - -```ebnf -identifier = letter { letter | digit | "_" } ; -letter = "a" | "b" | ... | "z" | "A" | "B" | ... | "Z" ; -digit = "0" | "1" | ... | "9" ; -``` - -**Rules:** -- Case-sensitive -- Can start with letter only (not digit or underscore) -- No length limit -- Can include Unicode letters - -**Valid examples:** -```tim -x, count, user_name, myVar, value2, Temperature, λ -``` - -**Invalid:** -```tim -2count // starts with digit -_private // starts with underscore -my-var // contains hyphen -``` - -### Booleans - -Booleans are `map[uint64]float64` with a special marker value: - -```ebnf -boolean = "yes" | "no" ; -``` - -**Examples:** -```tim -yes // {0: 1.0, 1: 1.0} (marker: key 1 exists with value 1.0) -no // {0: 0.0, 1: 0.0} (marker: key 1 exists with value 0.0) -``` - -**Conversions:** -- `yes as cstr` → `"true"` (C string) -- `no as cstr` → `"false"` (C string) -- `yes as cbool` → `true` (C bool) -- `no as cbool` → `false` (C bool) -- `yes as num` → `1.0` -- `no as num` → `0.0` - -**Comparison with numbers:** -Booleans are NOT the same as `1.0` or `0.0`. They have a distinct internal representation: -```tim -yes == 1.0 // no (different internal structure) -no == 0.0 // no (different internal structure) -yes == yes // yes (same boolean value) -no == no // yes (same boolean value) -``` - -**Default return value:** -Functions that don't explicitly return a value return `1.0` (number). To return a boolean success indicator, explicitly use `yes` or `no`. - -### Numbers - -Numbers are `map[uint64]float64` with a single entry at key 0: - -```ebnf -number = [ "-" ] digit { digit } [ "." digit { digit } ] ; -``` - -**Examples:** -```tim -42 // {0: 42.0} -3.14159 // {0: 3.14159} --17 // {0: -17.0} -0.001 // {0: 0.001} -1000000 // {0: 1000000.0} --273.15 // {0: -273.15} -``` - -**Special values:** -- `??` - cryptographically secure random number [0, 1) → `{0: random_value}` -- Result of `0/0` - NaN (used for error encoding) → `{0: NaN}` - -**Note:** While the values stored happen to be IEEE 754 doubles, this is an implementation detail. Numbers ARE maps, not primitives. - -### Strings - -Strings are `map[uint64]float64` where keys are indices and values are character codes: - -```ebnf -string = '"' { character } '"' ; -``` - -**Examples:** -```tim -"Hello" // {0: 72.0, 1: 101.0, 2: 108.0, 3: 108.0, 4: 111.0} -"A" // {0: 65.0} -"" // {} (empty map) -``` - -**Escape sequences:** -- `\n` - newline (character code 10) -- `\t` - tab (character code 9) -- `\r` - carriage return (character code 13) -- `\\` - backslash -- `\"` - quote -- `\xHH` - hex byte -- `\uHHHH` - Unicode code point - -**String operations:** -- `.bytes` - get byte array -- `.runes` - get Unicode code point array -- `+` - concatenation -- `[n]` - access byte at index - -### F-Strings (Interpolated Strings) - -F-strings allow embedded expressions: - -```ebnf -fstring = 'f"' { character | "{" expression "}" } '"' ; -``` - -**Examples:** -```tim -name = "World" -greeting = f"Hello, {name}!" -result = f"2 + 2 = {2 + 2}" -``` - -### Comments - -```tim -// Single-line comment (C++ style) -``` - -No multi-line comments. - -## Keywords - -### Reserved Keywords - -``` -ret arena unsafe cstruct as defer spawn import shadow yes no -fun if elif else break continue foreach malloc free -``` - -**Note:** In Tim, lambda definitions use `->` (thin arrow) and match arms use `=>` (fat arrow), similar to Rust syntax, except that `~>` is used for the default case. - -**No-argument lambdas** can be written as `-> expr` or inferred from context in assignments: `name = { ... }` - -The `shadow` keyword is required when declaring a variable that would shadow an outer scope variable (see Shadow Keyword section above). +EBNF: `=` defines, `|` separates alternatives, `[ ]` is optional, `{ }` repeats +zero or more times, `( )` groups, `"x"` is a terminal. Rules in `UPPER` case are +tokens produced by the lexer. -### Type Keywords +## 2. Lexical structure -Type annotations use these keywords (context-dependent): +Source is UTF-8. -**Native Tim types:** -``` -num str list map bool -``` - -**Foreign C types:** -``` -cstring cptr cint clong cfloat cdouble cbool cvoid -``` - -**Legacy type cast keywords (for `unsafe` blocks and `cstruct`):** -``` -int8 int16 int32 int64 uint8 uint16 uint32 uint64 float32 float64 -cptr cstring number string address packed aligned -``` - -**Usage:** -```tim -// Type annotations (preferred) -x: num = 42 -name: str = "Alice" -ptr: cptr = sdl.SDL_CreateWindow(...) - -// Type casts in unsafe blocks (legacy) -value = unsafe int32 { ... } -``` - -Type keywords are contextual - you can still use them as variable names in most contexts: - -```tim -num = 100 // OK - variable named num -x: num = num * 2 // OK - type annotation vs variable -``` - -## Memory Management and Builtins - -**CRITICAL DESIGN PRINCIPLE:** Tim keeps builtin functions to an ABSOLUTE MINIMUM. - -**Memory allocation syntax sugar:** -- `malloc(size)` - allocates using arena allocator (syntax sugar for allocate within arena) -- `free(ptr)` - no-op (arena cleanup happens automatically) -- For explicit C memory: use `c.malloc`, `c.free`, `c.realloc`, `c.calloc` - -```tim -// Arena allocator (recommended) - explicit syntax -result = arena { - data = allocate(1024) - process(data) -} - -// Arena allocator - convenient sugar -data := malloc(1024) // Uses arena allocator automatically -// free(data) is no-op, arena cleans up - -// Alternative: explicit C FFI -ptr := c.malloc(1024) -defer c.free(ptr) -``` - -**List operations:** -- Use builtin functions: `head(xs)` for first element, `tail(xs)` for remaining elements -- Use `#` length operator (prefix or postfix) - -**Why minimal builtins?** -1. **Simplicity:** Less to learn and remember -2. **Orthogonality:** One concept, one way -3. **Extensibility:** Users can define their own functions -4. **Predictability:** No hidden magic - -**What IS builtin:** -- Operators: `#`, arithmetic, logic, bitwise -- Control flow: `@`, match blocks, `ret` -- Core I/O: `print`, `println`, `printf` (and error/exit variants) -- List operations: `head()`, `tail()` -- Keywords: `arena`, `unsafe`, `cstruct`, `defer`, etc. - -**Everything else via:** -1. **Operators** for common operations (`#xs` for length) -2. **Builtin functions** for core operations (`head(xs)`, `tail(xs)`) -3. **C FFI** for system functionality (`c.sin`, `c.malloc`, etc.) -4. **User-defined functions** for application logic - -## Operators - -### Arithmetic Operators - -``` -+ Addition -- Subtraction (binary) or negation (unary) -* Multiplication -/ Division -% Modulo -** Exponentiation -^ Exponentiation (alias for **) -``` - -### Comparison Operators - -``` -== Equal -!= Not equal -< Less than -<= Less than or equal -> Greater than ->= Greater than or equal -``` - -### Logical Operators - -``` -and Logical AND (short-circuit) -or Logical OR (short-circuit) -xor Logical XOR -not Logical NOT -``` - -### Bitwise Operators - -All bitwise operators use `b` suffix: - -``` -&b Bitwise AND -|b Bitwise OR -^b Bitwise XOR -!b Bitwise NOT (unary) -~b Bitwise NOT (alias for !b) -<>b Arithmetic right shift -<<>>b Rotate right -?b Bit test (tests if bit at position is set, returns 1 or 0) -``` - -Example of bit test: -```tim -x = 0b10110 // Binary 22 -bit2 = x ?b 2 // Returns 1 (bit 2 is set) -bit3 = x ?b 3 // Returns 0 (bit 3 is not set) -``` - -### Assignment Operators - -``` -= Immutable assignment (cannot reassign variable or modify value) -:= Mutable assignment (can reassign variable and modify value) -<- Update/reassignment (for mutable vars) - -+= Add and assign (for lists: append element) --= Subtract and assign -*= Multiply and assign -/= Divide and assign -%= Modulo and assign -**= Exponentiate and assign -``` - -**Arrow Operator Summary:** - -| Operator | Context | Meaning | Example | -|----------|-------------------|------------------------------------|------------------------------------| -| `->` | Lambda definition | Lambda arrow | `x -> x * 2` or `-> println("hi")` | -| `=>` | Match block | Match arm | `x { 0 => "zero" ~> "other" }` | -| `~>` | Match block | Default match arm | `x { 0 => "zero" ~> "other" }` | -| `_ =>` | Match block | Default match arm (alias for ~>) | `x { 0 => "zero" _ => "other" }` | -| `=` | Variable binding | Immutable assignment | `x = 42` (standard for functions) | -| `:=` | Variable binding | Mutable assignment | `x := 42` (can reassign later) | -| `<-` | Update/Send | Update mutable var OR send to ENet | `x <- 99` or `&8080 <- msg` | -| `<=` | Comparison | Less than or equal | `x <= 10` | -| `>=` | Comparison | Greater than or equal | `x >= 10` | - -**Important Conventions:** -- **Functions/methods** should use `=` (immutable), not `:=`, since they rarely need reassignment -- **Lambda syntax**: `->` always defines a lambda, `=>` always defines a match arm -- **Update operator** `<-` is for updating existing mutable variables or sending to ENet channels -- **Comparison** operators `<=` and `>=` are for comparisons, not assignment or arrows - -**Multiple Assignment (Tuple Unpacking):** - -```tim -// Functions can return multiple values as a list -a, b = some_function() // Unpack first two elements -x, y, z := [1, 2, 3] // Unpack list literal - -// Practical example with pop() -new_list, popped_value = pop(old_list) -``` - -When a function returns a list, multiple assignment unpacks the elements: -- Right side must evaluate to a list/map -- Left side specifies variable names separated by commas -- Variables are assigned elements at indices 0, 1, 2, etc. -- If list has fewer elements than variables, remaining variables get 0 -- If list has more elements, extra elements are ignored - -### Collection Operators - -``` -# Length operator (prefix or postfix) -``` - -### Other Operators - -``` --> Lambda arrow (can be omitted in assignment context with blocks) -=> Match arm -~> Default match arm -| Pipe operator -|| Parallel map -<> Function composition (f <> g creates a new function that applies g then f) -<- Update/Send (update mutable var OR send to ENet) -<= Receive (ENet, prefix) OR less-than-or-equal comparison -µ Memory ownership/movement operator (prefix) -. Field access -[] Indexing -() Function call (parentheses optional for zero or one argument in some contexts) -@ Loop -& ENet address (network endpoints) -$ Address value (memory addresses) -?? Random number (cryptographically safe) -or! Error/null handler (executes right side if left is error or null pointer) -``` - -## Operator Precedence - -From highest to lowest precedence: - -1. **Primary**: `()` `[]` `.` function call, postfix `#` -2. **Unary**: `-` `not` `!b` `#` `µ` -3. **Power**: `**` -4. **Multiplicative**: `*` `/` `%` -5. **Additive**: `+` `-` -6. **Shift**: `<>b` `<<>>b` -7. **Bitwise AND**: `&b` -8. **Bitwise XOR**: `^b` -9. **Bitwise OR**: `|b` -10. **Comparison**: `==` `!=` `<` `<=` `>` `>=` -11. **Logical AND**: `and` -12. **Logical OR**: `or` -13. **Or-bang**: `or!` -14. **Function Composition**: `<>` -15. **Send**: `<-` -16. **Receive**: `<=` -17. **Pipe**: `|` `||` -18. **Match**: `{ }` (postfix) -19. **Assignment**: `=` `:=` `<-` `+=` `-=` `*=` `/=` `%=` `**=` - -**Associativity:** -- Left-associative: All binary operators except `**` and assignments -- Right-associative: `**`, all assignments -- Non-associative: Comparison operators (can't chain) - -## Parsing Rules - -### Minimal Parentheses Philosophy - -Tim minimizes parenthesis usage. Use parentheses only when: - -1. **Precedence override needed:** - ```tim - (x + y) * z // Override precedence - ``` - -2. **Complex condition grouping:** - ```tim - (x > 0 && y < 10) { ... } // Group condition - ``` - -3. **Multiple lambda parameters:** - ```tim - (x, y) -> x + y // Multiple params - ``` - -**Not needed:** -```tim -// Good: no unnecessary parens -x > 0 { => "positive" ~> "negative" } -result = x + y * z -classify = x -> x { 0 => "zero" ~> "other" } - -// Bad: unnecessary parens -result = x > 0 { => ("positive") ~> ("negative") } -compute = (x) -> (x * 2) -``` - -### Statement Termination - -Statements are terminated by newlines: - -```tim -x = 10 -y = 20 -z = x + y -``` - -Multiple statements on one line require explicit semicolons: - -```tim -x = 10; y = 20; z = x + y -``` - -### Whitespace Rules - -- **Significant newlines**: End statements -- **Insignificant whitespace**: Spaces, tabs (except in strings) -- **Indentation**: Not significant (unlike Python) - -### Edge Cases - -#### Pipe vs Guard - -The `|` character is context-dependent: - -```tim -// Pipe operator (| not at line start) -result = data | transform | filter - -// Guard marker (| at line start) -classify = x -> { - | x > 0 => "positive" - | x < 0 => "negative" - ~> "zero" -} -``` - -**Rule:** `|` at the start of a line/clause (after `{` or newline) is a guard marker. Otherwise it's the pipe operator. - -#### Arrow Disambiguation - -```tim -=> Match arm result -~> Default match arm --> Lambda or receive -``` - -Context determines meaning: - -```tim -f = x -> x + 1 // Lambda with one arg -msg <= &8080 // Receive from channel -x { 0 => "zero" } // Match arm -x { ~> "default" } // Default arm -greet = { println("Hi") } // No-arg lambda +```ebnf +IDENT = ( letter | "_" ) { letter | digit | "_" } ; (* letter: any Unicode letter *) +NUMBER = DECIMAL | "0x" HEX { [ "_" ] HEX } | "0o" OCT { [ "_" ] OCT } | "0b" BIN { [ "_" ] BIN } ; +DECIMAL = DIGITS [ "." DIGITS ] [ ( "e" | "E" ) [ "+" | "-" ] DIGITS ] ; +DIGITS = digit { [ "_" ] digit } ; +STRING = '"' { char | escape } '"' | "`" { char } "`" ; (* raw: no escapes, may span lines *) +FSTRING = 'f"' { char | escape | "{{" | "}}" | "{" expr "}" } '"' ; +escape = "\n" | "\t" | "\r" | "\0" | "\\" | '\"' | "\{" | "\x" HEX HEX | "\u{" HEX { HEX } "}" ; ``` -#### No-Argument Lambdas - -```tim -// Inferred lambda (in assignment context): -greet = { println("Hello!") } // Inferred: greet = -> { println("Hello!") } -worker = { @ { process_forever() } } // Inferred: worker = -> { @ { process_forever() } } - -// Explicit no-argument lambda: -greet = -> println("Hello!") // Explicit -> -handler = -> process_events() // Explicit -> +- **Comments:** `// to end of line` and `/* block */` (block comments do not nest). +- **Numbers** are exact: `0.1` is the rational 1/10, `1e-9` is 1/10⁹. `_` separates + digit groups: `1_000_000`. +- **Strings** are UTF-8 byte strings; `\u{1F600}` encodes a code point. A raw + string in backquotes has no escapes and may span lines. +- **Newlines** end statements, except (1) inside `( )` and `[ ]`, (2) after a token + that cannot end an expression (a binary operator, `,`, `(`, `[`, `{`, `->`, `=>`, + `~>`, `=`, `:=`, `<-`, `|>`), and (3) before a line that starts with `|>`, + `.`, `or!`, `and` or `or`. `;` also ends a statement. -// With block body: -worker = { // Inferred lambda - @ { process_forever() } -} - -// Common use cases: -init = { setup_resources() } // Inferred (assignment context) -cleanup = { release_all() } // Inferred (assignment context) -background = { @ { poll_events() } } // Inferred (assignment context) +**Keywords:** -// When explicit -> is needed: -callbacks = [-> print("A"), -> print("B")] // Not in assignment, need explicit -> -process(-> get_data()) // Function argument, need explicit -> ``` - -#### Loop Forms - -The `@` symbol introduces loops (one of three forms): - -```tim -@ { ... } // Infinite loop -@ i in collection { ... } // For-each loop -@ condition { ... } // While loop +and as break continue cstruct defer elif else err export if import +in inf no not or ret yes arena ``` -**Loop Control with `ret @` and Numbered Labels:** - -Instead of `break`/`continue` keywords, Tim uses `ret @` with automatically numbered loop labels. - -**Loop Numbering:** Loops are numbered from outermost to innermost: -- `@1` = outermost loop -- `@2` = second level (nested inside @1) -- `@3` = third level (nested inside @2) -- `@` = current/innermost loop +**Operators and punctuation:** -```tim -// Exit current loop -@ i in 0..<100 { - i > 50 { ret @ } // Exit current loop (same as ret @1 here) - i == 42 { ret @ 42 } // Exit loop with value 42 - println(i) -} - -// Nested loops with numbered labels -@ i in 0..<10 { // Loop @1 (outermost) - @ j in 0..<10 { // Loop @2 (inner) - j == 5 { ret @ } // Exit loop @2 (innermost) - i == 5 { ret @1 } // Exit loop @1 (outer) - i == 3 and j == 7 { ret @1 42 } // Exit loop @1 with value - println(i, j) - } -} - -// ret without @ returns from function (not loop) -compute = n -> { - @ i in 0..<100 { - i == n { ret i } // Return from function - i == 50 { ret @ } // Exit loop only, continue function - } - ret 0 -} ``` - -**Loop `!` Keyword:** - -Loops with unknown bounds or modified counters require `!`: - -```tim -// Counter modified, needs ! -@ i in 0..<10 ! 20 { - i++ // Modified counter -} - -// Unknown iterations, needs ! -@ msg in read_channel() ! inf { - process(msg) -} ++ - * / % ** arithmetic +== != < <= > >= comparison (chainable) +& | ^ ~ << >> bitwise, on 64-bit two's complement integers += := <- += -= *= /= %= binding and update +-> => ~> |> or! ..< ..= ... # . , : ; ! @ ( ) [ ] { } ``` -#### Defer Statement - -The `defer` keyword schedules an expression to execute when the current scope exits (function return, block exit, or error). Deferred expressions execute in **LIFO (Last In, First Out)** order. +## 3. Programs and statements -**Syntax:** ```ebnf -defer_statement = "defer" expression ; -``` - -**Examples:** -```tim -// Resource cleanup with defer -init_resources = () -> { - file := open("data.txt") or! { - println("Failed to open file") - ret 0 - } - defer close(file) // Always closes when function returns - - buffer := c_malloc(1024) or! { - println("Out of memory") - ret 0 - } - defer c_free(buffer) // Frees before file closes (LIFO) - - process(file, buffer) - ret 1 -} - -// C FFI with defer (SDL3 example) -sdl.SDL_Init(sdl.SDL_INIT_VIDEO) or! { - println("SDL init failed") - ret 1 -} -defer sdl.SDL_Quit() // Always called on return - -window := sdl.SDL_CreateWindow("Title", 640, 480, 0) or! { - println("Window creation failed") - ret 1 // SDL_Quit still called via defer -} -defer sdl.SDL_DestroyWindow(window) // Executes before SDL_Quit - -// More resources... -``` +program = { stmt END } ; +END = NEWLINE | ";" ; +block = "{" { stmt END } [ guardtail ] "}" ; (* value: the last statement's value *) +guardtail = { "|" expr "=>" result END } [ "~>" result END ] ; -**Execution Order:** -Deferred calls execute in reverse order of declaration (LIFO): -```tim -defer println("1") // Executes third -defer println("2") // Executes second -defer println("3") // Executes first -// Output: 3, 2, 1 -``` - -**When Defer Executes:** -- On function return (`ret`) -- On block exit (normal completion) -- On early return from error handling -- On loop exit with `ret @` - -**Best Practices:** -1. Use `defer` immediately after resource acquisition -2. Combine with `or!` for railway-oriented error handling -3. Rely on LIFO order for proper cleanup sequence -4. Use `defer` for C FFI resources (files, sockets, SDL objects) -5. Return from error blocks instead of `exit()` - defer ensures cleanup +stmt = import | export | cstruct + | fundef | binding | update + | loop | if | ret | err | break | continue | defer | arena + | expr ; -**Common Pattern:** -```tim -// Railway-oriented with defer -resource := acquire() or! { - println("Acquisition failed") - ret error("acq") -} -defer cleanup(resource) - -// Work with resource... -// cleanup always happens, even on error -``` +import = "import" ( IDENT | STRING ) [ "as" IDENT ] ; +export = "export" ( "*" | IDENT { [ "," ] IDENT } ) ; -#### Address Operator +cstruct = "cstruct" IDENT [ "packed" ] [ "aligned" "(" NUMBER ")" ] "{" { field [ "," | END ] } "}" ; +field = IDENT { "," IDENT } ":" IDENT ; (* int8..uint64, float32, float64, ptr, cstr, or a cstruct *) -The `&` symbol creates ENet addresses (network endpoints): +fundef = [ IDENT "." ] IDENT "(" [ params ] ")" [ ":" type ] "=" expr ; +binding = target [ ":" type ] ( "=" | ":=" ) expr ; +target = IDENT { "," IDENT } ; +update = place ( "<-" | "+=" | "-=" | "*=" | "/=" | "%=" ) expr ; +place = IDENT { "[" expr "]" | "." IDENT } ; -```tim -&8080 // Port only: & followed by digits -&localhost:8080 // Host:port: & followed by identifier/IP + : -&192.168.1.1:3000 // IP:port -``` +loop = "@" [ loopspec ] [ "!" expr ] block ; +loopspec = IDENT [ ":" IDENT ] "in" expr (* for each element *) + | expr ; (* while the condition holds *) +break = "break" [ "@" NUMBER ] ; +continue = "continue" [ "@" NUMBER ] ; -**Examples:** -```tim -// Loops (statement context) -@ { println("Forever") } // Infinite loop -@ i in [1, 2, 3] { println(i) } // For-each loop -@ x < 10 ! 100 { x = x + 1 } // While loop (condition loops require `!`) - -// Addresses (expression context) -server = @8080 // Address literal -client = &localhost:9000 // Address with hostname -remote = &192.168.1.100:3000 // Address with IP - -// Unambiguous in context -listen(&8080) // Function call with address -@ x > 0 ! 100 { send(&8080, data) } // Loop with address inside +ret = "ret" [ expr ] ; +err = "err" [ expr ] ; +defer = "defer" expr ; +arena = "arena" block ; ``` -#### Block vs Map vs Match - -Disambiguated by contents (see Block Disambiguation Rules above): +### Bindings ```tim -{ x: 10 } // Map: contains : -x { 0 -> "zero" } // Match: contains -> -{ temp = x * 2; temp } // Statement block: no : or -> -``` - -## Error Handling and Result Types - -Tim uses a **Result type** for operations that can fail. A Result is still `map[uint64]float64`, but with special semantic meaning tracked by the compiler. - -### Result Type Design - -A Result is encoded as follows: - -**Byte Layout:** -``` -[type_byte][length][key][value][key][value]...[0x00] -``` - -**Type Bytes:** -``` -0x01 - Tim Number (success) -0x02 - Tim String (success) -0x03 - Tim List (success) -0x04 - Tim Map (success) -0x05 - Tim Address (success) -0xE0 - Error (failure, followed by 4-char error code) -0x10 - C int8 -0x11 - C int16 -0x12 - C int32 -0x13 - C int64 -0x14 - C uint8 -0x15 - C uint16 -0x16 - C uint32 -0x17 - C uint64 -0x18 - C float32 -0x19 - C float64 -0x1A - C pointer -0x1B - C string pointer -``` - -**Success case:** -- Type byte indicates the Tim or C type -- Length field (uint64) indicates number of key-value pairs -- Key-value pairs follow (each pair is uint64 key, float64 value) -- Terminated with 0x00 byte - -**Error case:** -- Type byte is 0xE0 -- Followed by 4-byte error code (ASCII, space-padded) -- Terminated with 0x00 byte - -### Standard Error Codes - -``` -"dv0 " - Division by zero -"idx " - Index out of bounds -"key " - Key not found -"typ " - Type mismatch -"nil " - Null pointer -"mem " - Out of memory -"arg " - Invalid argument -"io " - I/O error -"net " - Network error -"prs " - Parse error -"ovf " - Overflow -"udf " - Undefined +x = 42 // immutable binding +n := 0 // mutable binding +n <- n + 1 // update the nearest binding named n (it must be mutable) +n += 1 // same as n <- n + 1 +xs[0] <- 7 // update an element of a mutable list or map +p.x <- 1.5 // update a field of a cstruct or a map +a, b = pair // destructure a list: a = pair[0], b = pair[1] +count: num = 0 // optional type annotation ``` -**Note:** Error codes are 4 bytes, space-padded if shorter. The `.error` accessor strips trailing spaces on access. +A binding introduces a new name in the current block; binding a name that already +exists in an *outer* block shadows it. Binding the same name twice in one block is +an error. -### The `.error` Accessor +### Functions -Every value has a `.error` accessor that: -- Returns `""` (empty string) for success values -- Returns the error code string (spaces stripped) for error values +A function is a value. There are two ways to write one: ```tim -x = 10 / 2 // Success: returns 5.0 -x.error // Returns "" (empty) - -y = 10 / 0 // Error: division by zero -y.error // Returns "dv0" (spaces stripped) - -// Typical usage -result.error { - "" => proceed(result) - ~> handle_error(result.error) -} +square(x) = x * x // definition: name(params) = body +add = (a, b) -> a + b // lambda bound to a name +inc = x -> x + 1 // one parameter needs no parentheses +greet = { println("hi") } // a block on the right of `=` is a function of no arguments +sum(xs...) = fold(xs, 0, (a, b) -> a + b) // the last parameter may be variadic +Point.norm(self) = sqrt(self.x**2 + self.y**2) // method on a cstruct: p.norm() ``` -### The `or!` Operator +`name(params) = body` at statement level is sugar for `name = (params) -> body`, and +top-level functions may call each other in any order. Every call in tail position +is a jump, so tail recursion runs in constant stack space. Closures capture +variables by reference. -The `or!` operator provides a default value or executes a block when the left side is an error or null: +### Loops ```tim -// Handle errors -x = 10 / 0 // Error result -safe = x or! 99 // Returns 99 (error case) - -y = 10 / 2 // Success result (value 5) -safe2 = y or! 99 // Returns 5 (success case) - -// Handle null pointers from C FFI -window := sdl.SDL_CreateWindow("Title", 640, 480, 0) or! { - println("Failed to create window!") - sdl.SDL_Quit() - exit(1) -} - -// Inline null check with default -ptr := c_malloc(1024) or! 0 // Returns 0 if allocation failed +@ { ... } // forever +@ i in 0..<10 { ... } // i = 0, 1, ..., 9 +@ x in xs { ... } // each element of a list, each byte of a string, each key of a map +@ n > 1 { ... } // while n > 1 +@ x in xs ! 1000 { ... } // at most 1000 iterations ``` -**Semantics:** -1. Evaluate left operand -2. Check if NaN (error value) OR if value equals 0.0 (null pointer) -3. If NaN or null: - - And right side is a block: execute block, result in xmm0 - - And right side is an expression: evaluate right side, result in xmm0 -4. Otherwise (value is valid): keep left operand value in xmm0 -5. Right side is NOT evaluated unless left is NaN/null (lazy/short-circuit evaluation) - -**Error Checking:** -- **NaN check**: Compares value with itself using UCOMISD (NaN != NaN) -- **Null check**: Compares value with 0.0 using UCOMISD - -**When checking for null (C FFI pointers):** -- All values are float64, so pointer 0 is encoded as 0.0 -- `or!` treats 0.0 (null pointer) as a failure case -- Enables railway-oriented programming for C interop -- Works with any C function that returns pointers - -**Precedence:** Lower than logical OR, higher than send operator - -### Error Propagation Patterns - -```tim -// Check and early return -process = input -> { - step1 = validate(input) - step1.error { != "" => step1 } // Return error - - step2 = transform(step1) - step2.error { != "" => step2 } +`break` and `continue` act on the innermost loop; `break @1` names loops by depth, +`@1` being the outermost. A loop's value is 0. - finalize(step2) -} - -// Default values with or! -compute = input -> { - x = parse(input) or! 0 - y = divide(100, x) or! -1 - y * 2 -} - -// Match on error code -result = risky() -result.error { - "" => println("Success:", result) - "dv0" => println("Division by zero") - "mem" => println("Out of memory") - ~> println("Unknown error:", result.error) -} -``` +## 4. Expressions -### Creating Custom Errors +From lowest to highest precedence: -Use the `error` function to create error Results: - -```tim -// Create error with code -err = error("arg") // Type byte 0xE0 + "arg " +```ebnf +expr = lambda | pipe { matchblock } ; (* a match applies to the whole expression *) +lambda = params "->" ( expr | block ) ; +params = IDENT | "(" [ param { "," param } ] ")" ; +param = IDENT [ ":" type ] [ "..." ] ; + +pipe = orbang { "|>" orbang } ; (* x |> f is f(x); x |> f(y) is f(x, y) *) +orbang = or { "or!" or } ; (* a or! b is a unless a is an error or 0 *) +or = and { "or" and } ; +and = not { "and" not } ; +not = "not" not | compare ; +compare = range { cmpop range } ; (* a < b < c means a < b and b < c *) +cmpop = "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not" "in" ; +range = bitor [ ( "..<" | "..=" ) bitor ] ; +bitor = bitxor { "|" bitxor } ; +bitxor = bitand { "^" bitand } ; +bitand = shift { "&" shift } ; +shift = sum { ( "<<" | ">>" ) sum } ; +sum = product { ( "+" | "-" ) product } ; +product = cast { ( "*" | "/" | "%" ) cast } ; +cast = unary { "as" type } ; +unary = ( "-" | "~" | "#" ) unary | power ; +power = postfix [ "**" unary ] ; (* right-associative; -2**2 is -4 *) +postfix = primary { "(" [ args ] ")" | "[" index "]" | "." IDENT } ; +args = expr { "," expr } ; +index = expr | [ expr ] ":" [ expr ] ; (* xs[i], xs[a:b], xs[:b], xs[a:] *) + +primary = NUMBER | STRING | FSTRING | "yes" | "no" | "inf" | IDENT + | "(" expr ")" | list | map | block | guards | if ; +list = "[" [ expr ( { "," expr } | "@" IDENT "in" expr [ "if" expr ] ) ] "]" ; +map = "{" "}" | "{" key ":" expr { "," key ":" expr } "}" ; +key = IDENT | STRING | NUMBER ; +if = "if" expr block { "elif" expr block } [ "else" block ] ; + +type = "num" | "str" | "bool" | "list" | "map" | "fn" | IDENT ; (* IDENT: a cstruct or C type *) +``` + +### Matching + +A `{ ... }` directly after an expression, on the same line, matches on its value: -// Or use division by zero for runtime errors -fail = 0 / 0 // Returns error "dv0" +```ebnf +matchblock = "{" ( arms | { stmt END } ) "}" ; +arms = { arm END } [ "~>" result END ] ; +arm = expr "=>" result (* value equals expr, or lies in a range *) + | "=>" result ; (* value is truthy *) +guards = "{" { "|" expr "=>" result END } [ "~>" result END ] "}" ; +result = expr | block | ret | err | break | continue ; ``` -### Compiler Type Tracking - -The compiler tracks whether a value is a Result type: - ```tim -// Compiler knows this returns Result -divide = (a, b) -> { - b == 0 { ret error("dv0") } - a / b -} - -// Compiler propagates Result type -compute = x -> { - y = divide(100, x) // y has Result type - y or! 0 // Handles potential error +kind = n { + 0 => "zero" + 1..<10 => "small" + ~> "large" } -``` - -See [TYPE_TRACKING.md](TYPE_TRACKING.md) for implementation details. - -### Result Type Memory Layout - -**Success value (number 42):** -``` -Bytes: 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 45 00 00 00 00 00 00 00 00 - ↑ ↑----- length=1 ----↑ ↑------- key=0 -------↑ ↑------- value=42.0 ------↑ ↑ term - type=01 (number) -``` - -**Error value (division by zero):** -``` -Bytes: E0 64 76 30 20 00 - ↑ ↑----- error code "dv0 " -----↑ ↑ term - type=E0 (error) -``` - -### `.error` Implementation - -The `.error` accessor: -1. Checks type byte (first byte) -2. If 0xE0: extract next 4 bytes as error code string -3. Strip trailing spaces -4. Return error code string -5. Otherwise: return empty string "" - -### `or!` Implementation - -The `or!` operator: -1. Evaluates left operand -2. Checks type byte -3. If 0xE0: returns right operand -4. Otherwise: returns left operand value (strips type metadata) - -## Parsing Algorithm - -### High-Level Flow - -``` -1. Tokenize (lexer.go) - Source → Tokens - -2. Parse (parser.go) - Tokens → AST - -3. Type Inference (optional, see TYPE_TRACKING.md) - AST → AST with type annotations - -4. Code Generation (x86_64_codegen.go, arm64_codegen.go, riscv64_codegen.go) - AST → Machine code - -5. Linking (elf.go, macho.go) - Machine code → Executable -``` - -### Parser Implementation Notes - -**Recursive Descent:** -- Hand-written recursive descent parser -- Operator precedence climbing for expressions -- Look-ahead for block disambiguation - -**Error Recovery:** -- Continue parsing after errors when possible -- Collect multiple errors per pass -- Provide helpful error messages with line numbers - -**Performance:** -- Single-pass parsing (no separate AST transformation) -- Minimal memory allocation -- Fast compilation (typically <100ms for small programs) - -## Implementation Guidelines - -**Memory Management:** -- **ALWAYS use arena allocation** instead of malloc/free when possible -- The arena allocator (`tim_arena_alloc`) provides fast bump allocation with automatic growth -- Arena memory is freed in bulk, avoiding fragmentation -- Only use malloc for external C library compatibility - -**Register Management:** -- The compiler has a sophisticated register allocator (`RegisterAllocator` in register_allocator.go) -- Real-time register tracking via `RegisterTracker` (register_tracker.go) -- Register spilling when needed via `RegisterSpiller` (register_allocator.go) -- Use these systems instead of ad-hoc register assignment - -**Code Generation:** -- Target-independent IR through `Out` abstraction layer -- Backend-specific optimizations in arm64_backend.go, riscv64_backend.go, x86_64_codegen.go -- SIMD operations for parallel loops (AVX-512 on x86_64) - -## Type Annotations - -Type annotations are **optional metadata** that specify semantic intent and guide FFI marshalling. They do NOT change the runtime representation (always `map[uint64]float64`). - -### Syntax - -**Variable declarations:** -```tim -x: num = 42 // Number annotation -name: str = "Alice" // String annotation -items: list = [1, 2, 3] // List annotation -config: map = {port: 8080} // Map annotation - -// C types for FFI -ptr: cptr = sdl.SDL_CreateWindow("Hi", 640, 480, 0) -err: cstring = sdl.SDL_GetError() -result: cint = sdl.SDL_Init(sdl.SDL_INIT_VIDEO) -value: cdouble = 3.14159 -``` - -**Function signatures:** -```tim -// Parameter and return types -add(x: num, y: num) -> num { x + y } - -// String functions -greet(name: str) -> str { f"Hello, {name}!" } - -// C FFI functions -create_window(title: str, w: cint, h: cint) -> cptr { - sdl.SDL_CreateWindow(title, w, h, 0) +sign = { + | x > 0 => 1 + | x < 0 => -1 + ~> 0 } - -// Mixed types -format_error(code: cint) -> str { - f"Error {code}: {sdl.SDL_GetError()}" +clamp(x, lo, hi) = { + | x < lo => ret lo // guard lines followed by statements are early exits + | x > hi => ret hi + x } -``` - -### Type Semantics - -| Type | Runtime Repr | Purpose | Example | -|-----------|-----------------------|---------------|-----------------------| -| `num` | `{0: 42.0}` | Number intent | `x: num = 42` | -| `str` | `{0: 72.0, 1: 105.0}` | String intent | `name: str = "Hi"` | -| `list` | `{0: 1.0, 1: 2.0}` | List intent | `xs: list = [1, 2]` | -| `map` | `{hash("x"): 10.0}` | Map intent | `m: map = {x: 10}` | -| `cstring` | `{0: }` | C `char*` | `s: cstring = c.fn()` | -| `cptr` | `{0: }` | C pointer | `p: cptr = sdl.fn()` | -| `cint` | `{0: 42.0}` | C `int` | `n: cint = sdl.fn()` | -| `clong` | `{0: 42.0}` | C `int64_t` | `l: clong = c.time()` | -| `cfloat` | `{0: 3.14}` | C `float` | `f: cfloat = 3.14` | -| `cdouble` | `{0: 3.14}` | C `double` | `d: cdouble = c.fn()` | -| `cbool` | `{0: 1.0}` | C `bool` | `ok: cbool = c.fn()` | - -### FFI Marshalling - -Type annotations guide automatic conversions at C FFI boundaries: - -**Tim → C conversions:** -```tim -// Tim string → C string (calls tim_string_to_cstr) -title: str = "Window" -window = sdl.SDL_CreateWindow(title, 640, 480, 0) // title converted to char* - -// Tim number → C int (extracts {0: value}) -result: cint = sdl.SDL_Init(0x00000020) // Tim num → C int -``` - -**C → Tim conversions:** -```tim -// C char* → cstring (stored as pointer in {0: }) -err: cstring = sdl.SDL_GetError() // char* stored as-is - -// When needed, convert cstring → str manually -err_str: str = str(err) // Convert C string to Tim string -``` - -### Type Inference - -When annotations are omitted, the compiler infers types: - -```tim -x = 42 // Inferred: num -name = "Alice" // Inferred: str -items = [1, 2, 3] // Inferred: list -ptr = sdl.SDL_CreateWindow(...) // Inferred: cptr (from FFI signature) -``` - -### When to Use Type Annotations - -**Use annotations when:** -1. Clarifying intent (documentation) -2. Working with C FFI (marshalling guidance) -3. Catching type errors early -4. Enabling future optimizations - -**Omit annotations when:** -1. Type is obvious from context -2. Writing quick scripts -3. Type doesn't matter for correctness - ---- - -**Note:** This grammar is the canonical reference for Tim 3.0. The compiler implementation (lexer.go, parser.go) must match this specification exactly. - -**See also:** -- [LANGUAGESPEC.md](LANGUAGESPEC.md) - Complete language semantics -- [LIBERTIES.md](LIBERTIES.md) - Documentation accuracy guidelines +n % 15 == 0 { println("FizzBuzz") } // no arms: run the block when the value is truthy +ok = x > 0 { => "positive" ~> "not positive" } +``` + +Guard lines may also appear inside any block: when they end the block they form +a guard match that is the block's value; followed by more statements, each +`| c => r` means `if c { r }`. + +The value `v` is evaluated once. Arms are tried in order; a pattern arm matches when +`v == pattern`, or `v in pattern` for a range pattern. The first match wins; with no +match the value is the `~>` result, or 0. + +### Blocks + +`{` starts a map when it is followed by `}` or by `key :` (but not `IDENT : type =`, +which is an annotated binding); after an expression on the same line it starts a +match; `{ |` starts a guard match; otherwise it is a block. A block is an expression +whose value is its last statement's value. A block that is the entire right-hand +side of a *binding* (`name = { ... }`) is a function of no arguments; the block of a +definition (`f(x) = { ... }`) or a lambda (`x -> { ... }`) is its body. + +### Literals and collections + +```tim +xs = [1, 2, 3] +squares = [x * x @ x in xs if x > 1] // comprehension: [4, 9] +xs[0] xs[-1] xs[1:] #xs // 1, 3, [2, 3], 3 +m = {name: "Tim", "two words": 2, 7: yes} +m.name m["two words"] m[7] "name" in m +s = "héllo" +#s s[0] s[1:3] // 6 bytes, 104, "é" +f"{s} has {#s} bytes" +``` + +## 5. Values + +| Type | Values | +|--------|-------------------------------------------------------------------------------| +| `num` | exact integers and rationals of any size, and inexact float64 | +| `bool` | `yes` and `no`, which are the numbers 1 and 0 | +| `str` | immutable UTF-8 byte strings | +| `list` | ordered, indexed from 0; negative indices count from the end | +| `map` | hash maps from numbers or strings to values | +| `fn` | functions and closures | +| error | a value carrying a short error code, produced by failing operations or `err` | + +- **Numbers.** `+ - * / % **` are exact on exact operands: `7 / 2` is `3.5`, + `2 ** 100` is exact, `0.1 + 0.2 == 0.3`. `%` is floored: the remainder has the + divisor's sign, so `-7 % 3` is `2`. `sqrt`, `sin`, `log` and friends, C + `float`/`double`, and `x as float64` give inexact float64; any arithmetic with an + inexact operand is inexact. +- **Truthiness.** `0`, `no`, `""`, `[]`, `{}` and errors are false; everything else is true. +- **Equality** compares numbers by value, strings and lists by content, maps and + functions by identity. +- **Errors.** `10 / 0`, `xs[99]` and `m["missing"]` evaluate to errors, and + arithmetic on an error propagates it. `v.error` is the error's code as a string + (`""` for a non-error), `v or! d` substitutes `d`, and `err "code"` returns an + error from the current function. Printing an error prints `error: `. +- **Memory.** Values live on a garbage-collected heap. Inside `arena { ... }`, + allocation is a pointer bump in a region of its own, and leaving the block + frees the region at once, so a game frame or a server request that + allocates freely costs nothing to clean up. It is safe: a value that + outlives the block, because the block returns it or stores it somewhere + older, is kept. Arenas nest. + +## 6. C interop + +```tim +import sdl3 as sdl // a C library; its headers are found with pkg-config +import "libfoo.so" as foo // a library by file name +sdl.SDL_Init(sdl.SDL_INIT_VIDEO) // functions and constants from the headers +c.puts("hello") // `c`, the C library, needs no import +``` + +Arguments convert by their C parameter types: numbers to integers (truncated) or to +floating point, strings to `const char *` (a Tim string ends in a NUL byte; C must not +write to it), `ptr` values and cstructs to pointers, and `0` to `NULL`. Results +convert back: integers and floating point to numbers, pointers to `ptr` values (`0` for +`NULL`), `void` to `0`. When a function has no known signature, an exact integer +passes as an integer, any other number as a `double`, and the result is an integer. + +A call to a function whose signature is known, and whose arguments fit in the +platform's argument registers, is a direct machine call: the conversions are a few +inline instructions. Variadic functions such as `printf` go through a general path. +On Linux, `syscall(n, ...)` is a system call without any library. + +A **ptr** is a C address. `p + n` is n bytes further, `p == 0` tests for `NULL`, +`cstr(p)` copies a C string into a Tim string, and `read_u8(p, i)` ... +`read_f64(p, i)`, `write_u8(p, i, v)` ... `write_f64(p, i, v)` access memory at +byte offset `i`. + +### cstruct + +```tim +cstruct Vec { x: float64, y: float64 } +v := Vec(3, 4) // a new struct; fields in order, missing ones are 0 +v.x <- 6 // fields read and write through their C types +p = c.malloc(Vec.size) as Vec // treat a ptr as a Vec +Vec.len(self) = sqrt(self.x ** 2 + self.y ** 2) // a method: v.len() +Vec.add(self, o: Vec) = Vec(self.x + o.x, self.y + o.y) +println(v.len(), (v + v).x, Vec.size, Vec.y.offset) // 7.211102550927978 12 16 8 +``` + +Fields are `int8` ... `uint64`, `float32`, `float64`, `ptr`, `cstr` (a `char *` read as +a string) or another cstruct, held as a pointer to it. Layout follows C unless the +cstruct is `packed` or `aligned(n)`. A cstruct value is a pointer to its bytes, so it +passes to C as is; constructed ones live on the garbage-collected heap. Writing a +field needs a variable bound with `:=`, as for list elements. + +The compiler knows which cstruct a value is from constructors, `as`, parameter +annotations `(v: Vec)`, `self`, typed loop variables `@ v: Vec in vs`, lists of +cstructs, and functions that always return one. Reading a field of a value of +unknown cstruct type is a compile error. On cstruct values, `a + b`, `a - b` and +`a * b` call the methods `add`, `sub` and `mul`; `a * s`, `s * a` and `a / s` call +`scale(a, s)` and `scale(a, 1 / s)`. + +## 7. Program execution + +Top-level statements run in order. If the program defines a function `main` and +never calls it at top level, `main()` runs after the top level. The exit code is 0, +unless the program calls `exit(n)`, returns `ret n` at top level, or `main` gives a +number, which is truncated to an integer; an error exits with 1. Deferred calls run +when the program ends, last first, as they do when a function returns. + +## 8. What changed from Tim 1 + +| Tim 1 | Tim 2 | +|------------------------------------------|----------------------------------------------| +| `&b \|b ^b ~b <>b` | `& \| ^ ~ << >>` | +| `?b`, `<<>>b`, `!b` | `bit(x, n)`, `rotl(x, n)`, `rotr(x, n)`, `~` | +| `^` as a power operator | `**` only | +| `x \| f` pipe, `\|\|` parallel map | `x \|> f`, `map(xs, f)` | +| `f <> g` composition, `::` cons | lambdas, `[x] + xs` | +| `yes`/`no` as heap maps | the numbers 1 and 0 | +| `field as type` in `cstruct`, `(p as T)` | `field: type`, `(p: T)` | +| `fun f(x) { }` | `f(x) = { }` | +| `ret @N`, `@N`, `foreach` | `break @N`, `continue @N`, `@` | +| `\| cond => stmt` guard statements | `cond { stmt }` or `if` | +| `_ =>` default arm | `~>` | +| `??`, `µ`, `$`, `¤`, `x++`, postfix `#` | `random()`, removed, `+= 1`, `#x` | +| `call()!`, `! N` recursion bounds | removed: FFI signatures carry the types | +| ENet `&8080`, `<-` send, `<=` receive | removed from the language | +| `class`, `with`, `alias`, `spawn` | removed: use functions, maps and cstructs | +| `unsafe` register blocks, `vec2`/`vec4` | removed: C functions, `read_*`/`write_*` | +| `malloc`, `free`, `arena_create` | `arena { }`, `alloc(n)`, or `c.malloc` | +| `shadow` | inner blocks shadow freely | +| `@first` `@last` `@counter` `@i` | removed | +| condition loops require `! N` | `! N` is optional everywhere | diff --git a/LANGUAGESPEC.md b/LANGUAGESPEC.md deleted file mode 100644 index 3010b97..0000000 --- a/LANGUAGESPEC.md +++ /dev/null @@ -1,3132 +0,0 @@ -# Tim Language Specification - -**Version:** 1.5.0 -**Date:** 2025-12-03 -**Status:** Canonical Language Reference for Tim 1.5.0 Release - -This document describes the complete semantics, behavior, and design philosophy of the Tim programming language. For the formal grammar, see [GRAMMAR.md](GRAMMAR.md). - -## ⚠️ CRITICAL: The Universal Type - -Tim has exactly ONE type: `map[uint64]float64` - -Not "represented as" or "backed by" — every value IS this map: - -```tim -42 // {0: 42.0} -"Hello" // {0: 72.0, 1: 101.0, 2: 108.0, 3: 108.0, 4: 111.0} -[1, 2, 3] // {0: 1.0, 1: 2.0, 2: 3.0} -{x: 10} // {hash("x"): 10.0} -[] // {} -``` - -There are NO special types, NO primitives, NO exceptions. -Everything is a map from uint64 to float64. - -This is not an implementation detail — this IS Tim. - -## Table of Contents - -- [What Makes Tim Unique](#what-makes-tim-unique) -- [Design Philosophy](#design-philosophy) -- [Type System](#type-system) -- [Variables and Assignment](#variables-and-assignment) -- [Control Flow](#control-flow) -- [Functions and Lambdas](#functions-and-lambdas) -- [Loops](#loops) -- [Parallel Programming](#parallel-programming) -- [ENet Channels](#enet-channels) -- [C FFI](#c-ffi) -- [CStruct](#cstruct) -- [Memory Management](#memory-management) -- [Unsafe Blocks](#unsafe-blocks) -- [Built-in Functions](#built-in-functions) -- [Error Handling](#error-handling) -- [Examples](#examples) - -## What Makes Tim Unique - -Tim brings together several novel or rare features that distinguish it from other systems programming languages: - -### 1. Universal Map Type System - -The entire language is built on a single type: `map[uint64]float64`. Every value—numbers, strings, lists, functions—IS this map. This radical simplification enables: -- No type system complexity -- Uniform memory representation -- Natural duck typing -- Simple FFI (cast to native types only at boundaries) - -### 2. Direct Machine Code Generation - -The compiler emits x86_64, ARM64, and RISCV64 machine code directly from the AST: -- **No intermediate representation** - AST → machine code in one pass -- **No dependencies** - completely self-contained compiler -- **Fast compilation** - no IR translation overhead -- **Small compiler** - ~30k lines of Go -- **Deterministic output** - same code every time - -### 3. Blocks: Maps, Matches, and Statements - -Blocks `{ ... }` are disambiguated by their contents: - -```tim -// Map literal: contains key: value -config = { port: 8080, host: "localhost" } - -// Statement block: no => or ~> arrows -compute = x -> { - temp = x * 2 - result = temp + 10 - result // last value returned -} - -// Value match: expression before {, patterns with => -classify = x -> x { - 0 => "zero" - 5 => "five" - ~> "other" -} - -// Guard match: no expression before {, branches with | at line start -classify = x -> { - | x == 0 => "zero" - | x > 0 => "positive" - ~> "negative" -} -``` - -**Block disambiguation rules:** -1. Contains `:` (before arrows) → Map literal -2. Contains `=>` or `~>` → Match block (value or guard) or Mixed block -3. Otherwise → Statement block - -**Mixed blocks** contain both statements and guard clauses: -```tim -process = x -> { - println("Processing:", x) // Statement - | x == 0 => "zero" // Guard clause - | x > 0 => "positive" - ~> "negative" -} -``` -Statements execute first, then the guard match is evaluated and returned. - -This unifies maps, pattern matching, guards, and function bodies into one syntax. - -**Blocks as Expressions:** - -All blocks that return a value are valid expressions. The value of a block is the value of its last expression: - -```tim -// Block as expression in assignment -result = { - temp = compute() - temp * 2 + 10 -} - -// Block as error handler with or! -window := sdl.SDL_CreateWindow("Title", 640, 480, 0) or! { - println("Window creation failed") - ret 1 // Block returns 1 as error code -} - -// Block in conditional -value = condition { - 1 -> { - println("Processing true case") - process_true() - 42 // Block returns 42 - } - 0 -> { - println("Processing false case") - process_false() - 99 // Block returns 99 - } -} - -// Nested blocks -result = { - x = { - compute_inner() - inner_value - } - x * 2 -} -``` - -This design enables: -- Clean error handling without explicit returns -- Railway-oriented programming with `or!` -- Composable computation blocks -- Consistent semantics across all contexts - -### 4. Unified Lambda Syntax - -All functions use `->` for lambda definitions and `=>` for match arms. Define with `=` (immutable) not `:=` unless reassignment needed: - -```tim -// Use = for functions (standard) with -> for lambdas -square = x -> x * 2 -add = (x, y) -> x + y -compute = x -> { temp = x * 2; temp + 10 } -classify = x -> x { 0 => "zero" ~> "other" } // -> for lambda, => for match arm -hello = -> println("Hello!") // No params: explicit -> - -// Shorthand: () and -> can be omitted when context is clear (assignment + block) -main = { println("Hello!") } // Inferred as: main = () -> { println("Hello!") } - -// Only use := if function will be reassigned -handler := x -> println(x) -handler <- x -> println("DEBUG:", x) // reassignment with <- -``` - -**Convention:** -- Functions are immutable by default (`=`), only use `:=` when needed -- `->` is always for lambda definitions -- `=>` is always for match arms - -### 5. Minimal Parentheses - -Avoid parentheses unless needed for precedence or grouping: - -```tim -// Good: no unnecessary parens -x > 0 { 1 => "positive" ~> "negative" } -result = x + y * z -classify = x -> x { 0 => "zero" ~> "other" } - -// () and -> can be omitted in assignment context with blocks -main = { println("Running") } // Inferred: main = () -> { ... } - -// Only use when needed -result = (x + y) * z // precedence -cond = (x > 0 and y < 10) { ... } // complex condition grouping -add = (x, y) -> x + y // multiple lambda parameters -``` - -### 6. Bitwise Operators with `b` Suffix - -All bitwise operations are suffixed with `b` to eliminate ambiguity: - -```tim -<>b <<>>b // Shifts and rotates -&b |b ^b !b ~b // Bitwise logic -?b // Bit test (tests if bit at position is set) -``` - -### 7. Explicit String Encoding - -```tim -text = "Hello" -bytes = text.bytes // Map of byte values {0: byte0, 1: byte1, ...} -runes = text.runes // Map of Unicode code points {0: rune0, 1: rune1, ...} -``` - -### 8. ENet for All Concurrency - -Network-style message passing for concurrency: - -```tim -&8080 <- "Hello" // Send to channel -msg <= &8080 // Receive from channel -``` - -### 9. Fork-Based Process Model - -Parallel loops use `fork()` for true isolation: - -```tim -|| i in 0..10 { // Each iteration in separate process - compute(i) -} -``` - -### 10. Pipe Operators for Data Flow - -```tim -| Pipe (transform) -|| Parallel map -``` - -### 11. List Access Functions - -```tim -head(xs) // Get first element -tail(xs) // Get all but first element -#xs // Length (prefix or postfix) -``` - -**Semantics:** -```tim -// Lists and maps -xs = [1, 2, 3] -head(xs) // 1.0 (first element) -tail(xs) // [2, 3] (remaining elements) -#xs // 3.0 (length) - -// Numbers (single-element maps) -n = 42 -head(n) // 42.0 (the number itself) -tail(n) // [] (empty - no remaining elements) -#n // 1.0 (map has one entry) - -// Empty collections -head([]) // [] (no head) -tail([]) // [] (no tail) -#[] // 0.0 (empty) -``` - -### 12. C FFI via DWARF - -Parse C headers automatically using DWARF debug info: - -```tim -result = c_function(arg1, arg2) // Direct C calls -``` - -### 13. CStruct with Direct Memory Access - -```tim -cstruct Point { - x as float64, - y as float64 -} -p = Point(1.0, 2.0) -p.x // Direct memory offset access -``` - -### 14. Tail-Call Optimization Always On - -```tim -factorial = (n, acc) -> (n == 0) { - 1 => acc - ~> factorial(n - 1, acc * n) // Optimized to loop -} -``` - -### 15. Cryptographically Secure Random - -```tim -x = ?? // Uses OS CSPRNG -``` - -### 16. Memory Ownership Operator `µ` (Prefix) - -```tim -new_owner = µold_owner // Transfer ownership -``` - -### 17. Result Type with NaN Error Encoding - -```tim -result = risky_operation() -result.error { != "" -> println("Error:", result.error) } -``` - -### 18. Immutable-by-Default - -```tim -x = 42 // Immutable -y := 100 // Mutable (explicit) -``` - -## Design Philosophy - -### Core Principles - -1. **Simplicity over complexity** - - One universal type (map) - - Minimal syntax - - Direct code generation - -2. **Explicit over implicit** - - Mutability must be declared (`:=`) - - String encoding is explicit (`.bytes`, `.runes`) - - Bitwise ops marked with `b` suffix - -3. **Performance without compromise** - - Direct machine code generation - - Tail-call optimization - - Zero-cost abstractions - - No garbage collection overhead - -4. **Safety where it matters** - - Immutable by default - - Explicit unsafe blocks - - Arena allocators - - Move semantics - -5. **Minimal conventions** - - Functions use `=` not `:=` - - Avoid unnecessary parentheses - - Match blocks require explicit condition or guards - -## Type System - -Tim uses a **universal map type**: `map[uint64]float64` - -Every value in Tim IS `map[uint64]float64`: - -- **Numbers**: `{0: number_value}` -- **Strings**: `{0: char0, 1: char1, 2: char2, ...}` -- **Lists**: `{0: elem0, 1: elem1, 2: elem2, ...}` -- **Objects**: `{key_hash: value, ...}` -- **Functions**: `{0: code_pointer, 1: closure_data, ...}` - -There are no special cases. No "single entry maps", no "byte indices", no "field hashes" — just uint64 keys and float64 values in every case. - -### Type Annotations - -Type annotations are **optional metadata** that specify semantic intent and guide FFI conversions. They do NOT change the runtime representation (always `map[uint64]float64`). - -**Native Tim types:** -```tim -num // number (default type) -str // string (map of character codes) -list // list (map with sequential keys) -map // explicit map -bool // boolean (yes/no values) -``` - -**Foreign C types:** -```tim -cstring // C char* (null-terminated string pointer) -cptr // C pointer (e.g., SDL_Window*, void*) -cint // C int/int32_t -clong // C int64_t/long -cfloat // C float -cdouble // C double -cbool // C bool/_Bool -cvoid // C void (return type only) -``` - -**Usage:** - -```tim -// Variable declarations -x: num = 42 -name: str = "Alice" -values: list = [1, 2, 3] - -// Function parameters and return types -add = (x: num, y: num) -> num { x + y } -greet = (name: str) -> str { f"Hello, {name}!" } - -// FFI functions (type annotations required for correct marshalling) -window: cptr = sdl.SDL_CreateWindow("Game", 800, 600, 0) -error: cstring = sdl.SDL_GetError() -result: cint = sdl.SDL_Init(0x00000020) - -// Without annotations, Tim uses heuristics (may be imprecise) -window := sdl.SDL_CreateWindow("Game", 800, 600, 0) // Inferred as map -``` - -**When to use type annotations:** -- **Optional** for pure Tim code (types inferred from usage) -- **Recommended** for FFI code (enables precise marshalling) -- **Required** for complex FFI (pointer types, structs, proper error handling) - -Without annotations, Tim uses heuristics at FFI boundaries. With annotations, Tim marshalls precisely. - -### Type Conversions - -Use `as` for explicit type casts at FFI boundaries: - -```tim -x as int32 // Cast to C int32 -ptr as cstr // Cast to C string pointer -val as float64 // Cast to C double -``` - -**Supported cast types (legacy, for `unsafe` blocks):** -``` -int8 int16 int32 int64 -uint8 uint16 uint32 uint64 -float32 float64 -ptr cstr -``` - -### Duck Typing - -Since everything is a map, Tim has structural typing: - -```tim -point = { x: 10, y: 20 } -point.x // Works - map has "x" key - -person = { name: "Alice", x: 5 } -person.x // Also works - different map, same key -``` - -Type annotations are contextual keywords - you can use them as identifiers: - -```tim -num = 100 // OK - variable named num -x: num = num * 2 // OK - type annotation vs variable -``` - -### Boolean Type - -Tim has a dedicated boolean type with two values: `yes` and `no`. - -**Representation:** -Booleans are `map[uint64]float64` with a special marker to distinguish them from numbers: -```tim -yes // {0: 1.0, 1: 1.0} (marker: key 1 exists with value 1.0) -no // {0: 0.0, 1: 0.0} (marker: key 1 exists with value 0.0) -``` - -**Key distinction:** Booleans are NOT the same as `1.0` and `0.0`: -```tim -yes == 1.0 // no (different internal structure) -no == 0.0 // no (different internal structure) -yes == yes // yes -no == no // yes -``` - -**Boolean operations:** -```tim -result: bool = yes -flag: bool = no - -// Logical operations -yes and yes // yes -yes and no // no -yes or no // yes -not yes // no -not no // yes -``` - -**Type conversions:** -```tim -// To C strings -yes as cstr // "true" -no as cstr // "false" - -// To C bool -yes as cbool // true -no as cbool // false - -// To numbers -yes as num // 1.0 -no as num // 0.0 -``` - -**Default return value:** -Functions that don't explicitly return a value return `1.0` (number). To explicitly return success/failure as a boolean, use `yes` or `no`: -```tim -process = { - println("Processing...") - yes // Explicitly return yes -} - -validate = input -> { - | input > 0 => yes - ~> no -} -``` - -**Comparison operations return booleans:** -```tim -x = 10 -result = x > 5 // yes -result = x < 5 // no - -name = "Alice" -same = name == "Alice" // yes -``` - -**Usage in control flow:** -```tim -// Match on booleans -check = value > 100 -check { - yes => println("Large value") - no => println("Small value") -} - -// Guard matches -result = { - | value > 100 => yes - | value > 50 => no - ~> no -} -``` - -## Variables and Assignment - -### Shadowing Rules - -**Shadow Keyword Requirement:** -When declaring a variable that would shadow (hide) a variable from an outer scope, the `shadow` keyword is **required**. This prevents accidental shadowing bugs while allowing intentional shadowing. - -```tim -// Module level -port = 8080 -config = { host: "localhost" } - -// Function that intentionally shadows -main = { - shadow port = 9000 // ✓ OK: explicitly shadows module port - shadow config = {} // ✓ OK: explicitly shadows module config - println(port) // Prints 9000 -} - -// ERROR: Forgot shadow keyword -test = { - port = 3000 // ✗ ERROR: would shadow module 'port' without 'shadow' keyword -} - -// Nested scopes -process = x -> { - shadow x = x * 2 // ✓ OK: shadows parameter x - inner = y -> { - shadow x = x + y // ✓ OK: shadows outer x - x - } - inner(10) -} - -// First declaration doesn't need shadow -compute = { - value = 42 // ✓ OK: first declaration in this scope - result = value * 2 // ✓ OK: no shadowing -} -``` - -**Rationale:** -- **Prevents bugs**: Accidental shadowing is a common source of errors -- **Makes intent clear**: Reader knows shadowing is intentional -- **Helps refactoring**: Renaming outer variables won't silently break inner scopes -- **Natural naming**: Variables can use natural names in all scopes (no ALL_UPPERCASE requirement) - -### Immutable Assignment (`=`) - -Creates immutable binding (cannot reassign variable or modify contents): - -```tim -x = 42 -x = 100 // ERROR: cannot reassign immutable variable - -nums = [1, 2, 3] -nums[0] = 99 // ERROR: cannot modify immutable value -``` - -**Use for:** -- Constants -- Function definitions (standard practice) -- Values that won't change - -### Mutable Assignment (`:=`) - -Creates mutable binding (can reassign variable and modify contents): - -```tim -x := 42 -x := 100 // OK: reassign mutable variable -x <- 200 // OK: update with <- - -nums := [1, 2, 3] -nums[0] <- 99 // OK: modify mutable value -``` - -**Use for:** -- Loop counters -- Accumulators -- Values that will change -- Functions that need reassignment (rare) - -### Update Operator (`<-`) - -Updates mutable variables or map elements: - -```tim -x := 10 -x <- 20 // Update variable - -nums := [1, 2, 3] -nums[0] <- 99 // Update list element - -config := { port: 8080 } -config.port <- 9000 // Update map field -``` - -### Multiple Assignment (Tuple Unpacking) - -Multiple variables can be assigned from a list in a single statement: - -```tim -// Unpack function return (list) -new_list, popped_value = pop([1, 2, 3]) -println(new_list) // [1, 2] -println(popped_value) // 3 - -// Unpack list literal -x, y, z := [10, 20, 30] - -// Works with any list expression -first, second = some_function() -a, b, c = [1, 2, 3, 4, 5] // a=1, b=2, c=3 (extras ignored) -``` - -**Rules:** -- Right side must evaluate to a list/map -- Variables are assigned elements at indices 0, 1, 2, etc. -- If list has fewer elements, remaining variables get `0` -- If list has more elements, extras are ignored -- Can use `=`, `:=`, or `<-` (with mutable vars) - -**Common patterns:** -```tim -// Swap values -a, b := 1, 2 -a, b <- [b, a] // Swap using list literal - -// Split list -first, rest = head(xs), tail(xs) // First element and remaining - -// Function with multiple returns -quotient, remainder = divmod(17, 5) -``` - -### Function Assignment Convention - -**Always use `=` for functions** unless the function variable needs reassignment: - -```tim -// Standard (use =) -add = (x, y) -> x + y -factorial = n -> n { 0 => 1 ~> n * factorial(n-1) } - -// Only use := if reassigning -handler := x -> println(x) -handler <- x -> println("DEBUG:", x) // reassign -``` - -### Mutability Semantics - -The assignment operator determines both **variable mutability** and **value mutability**: - -| Operator | Variable Mutability | Value Mutability | -|----------|---------------------|------------------| -| `=` | Immutable (can't reassign) | Immutable (can't modify contents) | -| `:=` | Mutable (can reassign) | Mutable (can modify contents) | - -**Examples:** - -```tim -// Immutable binding, immutable value -nums = [1, 2, 3] -nums <- [4, 5, 6] // ERROR: can't reassign -nums[0] <- 99 // ERROR: can't modify - -// Mutable binding, mutable value -vals := [1, 2, 3] -vals <- [4, 5, 6] // OK: reassign -vals[0] <- 99 // OK: modify -``` - -## Control Flow - -### If / Elif / Else - -Tim supports statement-level conditional branching with `if`, `elif`, and `else`: - -```tim -x := 10 - -if x > 20 { - println("large") -} elif x > 5 { - println("medium") -} else { - println("small") -} -``` - -- Conditions use standard truthiness (`0` is false, non-zero is true) -- `elif` is optional and can be repeated -- `else` is optional - -### Match Expressions - -Match blocks have two forms: **value match** and **guard match**. - -#### Value Match (with expression before `{`) - -Evaluates expression, then matches its result against patterns: - -```tim -// Match on literal values -x = 5 -result = x { - 0 => "zero" - 5 => "five" - 10 => "ten" - ~> "other" -} - -// Match on boolean (1 = true, 0 = false) -result = (x > 0) { - 1 => "positive" - 0 => "not positive" -} - -// Shorthand with default -result = (x > 10) { - 1 => "large" - ~> "small" -} -``` - -#### Guard Match (no expression, branches with `|` at line start) - -Each branch evaluates its own condition: - -```tim -// Guard branches with | at line start -classify = x -> { - | x == 0 => "zero" - | x > 0 => "positive" - | x < 0 => "negative" - ~> "unknown" // optional default -} - -// Multiple conditions -category = age -> { - | age < 13 => "child" - | age < 18 => "teen" - | age < 65 => "adult" - ~> "senior" -} -``` - -**Important:** The `|` is only a guard marker when at the start of a line/clause. -Otherwise `|` is the pipe operator: - -```tim -// This is a guard (| at start) -x -> { | x > 0 => "positive" } - -// This is a pipe operator (| not at start) -result = data | transform | filter -``` - -**Key difference:** -- **Value match:** One expression evaluated once, result matched against patterns -- **Guard match:** Each `|` branch (at line start) evaluates independently (short-circuits on first true) - -**Default case:** `~>` works in both forms - -### Tail Calls - -The compiler automatically optimizes tail calls to loops: - -```tim -// Explicit tail call with => in match arms -factorial = (n, acc) -> (n == 0) { - 1 => acc - ~> factorial(n - 1, acc * n) -} - -// Tail call in default case -sum_list = (list, acc) -> (list.length) { - 0 => acc - ~> sum_list(list[1:], acc + list[0]) -} -``` - -**Tail position rules:** -- Last expression in function body -- After `=>` or `~>` in match arm -- In final expression of block - -## Functions and Lambdas - -### Function Definition - -Functions can be defined using `=` with lambdas, or with optional `fun` keyword for clarity: - -```tim -// Traditional lambda syntax -square = x -> x * x -add = (x, y) -> x + y - -// With optional 'fun' keyword for clarity -fun factorial = n -> { - result := 1 - @ i in 1..n { - result *= i - } - result -} - -// fun keyword doesn't change semantics, just visual clarity -fun process = data -> data * 2 -// Same as: process = data -> data * 2 - -// No-argument lambda: () and -> can be omitted when assigning blocks -greet = { println("Hello!") } -fun init = { setup() } -// Both equivalent to: name = () -> { ... } -``` - -### Lambda Expressions - -Lambdas can be defined with or without the `->` arrow, depending on syntax: - -```tim -// Inline lambda with arrow (required for expressions) -[1, 2, 3] | x -> x * 2 - -// Single parameter with block -process = n -> { n * 2 + 1 } - -// Parenthesized parameters - arrow optional with block body -square = (n) { n * n } // Arrow omitted -add = (a, b) { a + b } // Arrow omitted -mult = (x, y) -> { x * y } // Arrow included (both work) - -// Multi-line lambda -process = data -> { - cleaned = data | x -> x.trim() - cleaned | x -> x.length > 0 -} - -// Zero-argument lambda (inferred in assignment) -greet = { println("Hello!") } - -// Explicit zero-argument lambda -greet = () { println("Hello!") } // Arrow optional -greet = () -> { println("Hello!") } // Arrow explicit - -// Lambdas can have local variables -compute = x -> { - temp = x * 2 - result = temp + 10 - result // Returns 2*x + 10 -} - -// Implicit true (1.0) return for statement-only blocks -init = { - config := load_config() - cache <- init_cache() - // Implicitly returns true (1.0) - no explicit return needed -} -``` - -**Arrow Rules:** -- **Required:** Expression body: `x -> x * 2`, non-parenthesized params: `x -> { ... }`, lambda as argument: `map(xs, x -> x * 2)` -- **Optional:** Parenthesized params with block: `(n) { n * 2 }` or `(a, b) { a + b }` -- **Inferred:** Zero params in assignment: `main = { ... }` becomes `main = () { ... }` - -### Closures - -Lambdas capture their environment: - -```tim -make_counter = start -> { - count := start - () -> { - count <- count + 1 - count - } -} - -counter = make_counter(0) -counter() // 1 -counter() // 2 -``` - -### Higher-Order Functions - -Functions can take and return functions, including calling a function passed as a parameter: - -```tim -apply_twice = (f, x) -> f(f(x)) - -increment = x -> x + 1 -result = apply_twice(increment, 10) // 12 -``` - -### Function Composition - -The `<>` operator composes two functions, creating a new function that applies the right operand first, then the left: - -```tim -// Basic composition -double = x -> x * 2 -add_ten = x -> x + 10 - -// compose creates: x -> double(add_ten(x)) -compose = double <> add_ten -result = compose(5) // (5 + 10) * 2 = 30 - -// Multiple compositions -triple = x -> x * 3 -add_five = x -> x + 5 -transform = triple <> double <> add_five -value = transform(10) // ((10 + 5) * 2) * 3 = 90 - -// Composition is right-associative, so: -// f <> g <> h means f <> (g <> h) -// And evaluates as: x -> f(g(h(x))) -``` - -The composition operator provides a concise way to build complex transformations from simple functions. - -### Variadic Functions - -> **Status: NOT YET WORKING.** Variadic functions parse and compile, but calling -> one currently segfaults at runtime (the variadic argument-list machinery is -> incomplete). The design below is the intended behavior; do not rely on it yet. - -Functions can accept a variable number of arguments using the `...` suffix on the last parameter: - -```tim -// Simple variadic function -sum = (first, rest...) -> { - total := first - @ item in rest { - total <- total + item - } - total -} - -result = sum(1, 2, 3, 4, 5) // 15 - -// Variadic with multiple fixed parameters -printf = (format, args...) -> { - // format is required, args... collects remaining arguments - c.printf(format, args...) -} - -// All arguments variadic -log = (messages...) -> { - @ msg in messages { - println(msg) - } -} - -log("Error:", "File not found:", filename) -``` - -**Variadic Rules:** -- Only the last parameter can be variadic (have `...` suffix) -- The variadic parameter receives a list of all remaining arguments -- If no extra arguments are passed, the variadic parameter is an empty list `[]` -- Variadic parameters require parentheses: `(args...)` not `args...` -- Can be used with fixed parameters: `(x, y, rest...)` is valid - -**Variadic Parameter Passing:** - -When calling a variadic function, you can: -1. Pass arguments individually: `sum(1, 2, 3, 4)` -2. Spread a list with `...`: `sum(values...)` -3. Mix both: `sum(1, 2, values...)` - -```tim -// Define variadic function -max = (nums...) -> { - result := nums[0] - @ n in nums { - ? n > result { result <- n } - } - result -} - -// Call with individual args -max(5, 10, 3, 8) // 10 - -// Call with spread operator -values = [5, 10, 3, 8] -max(values...) // 10 - -// Mix individual and spread -max(1, 2, values..., 99) // 99 -``` - -## Loops - -### Infinite Loop - -```tim -@ { - println("Forever") -} -``` - -### Counted Loop - -```tim -@ 10 { - println("Hello") -} -``` - -### Range Loop - -```tim -@ i in 0..10 { - println(i) -} - -// With step -@ i in 0..100..10 { // 0, 10, 20, ... - println(i) -} -``` - -### Collection Loop - -```tim -nums = [1, 2, 3, 4, 5] -@ n in nums { - println(n) -} -``` - -### Loop Control - -Tim supports both traditional and modern loop control syntax: - -**Traditional syntax** (using `ret @`): -```tim -// Exit current loop -@ i in 0..<100 { - i > 50 { ret @ } // Exit current loop - i == 42 { ret @ 42 } // Exit loop with value 42 - println(i) -} - -// Nested loops with explicit labels -@ i in 0..<10 { // Loop @1 (outer) - @ j in 0..<10 { // Loop @2 (inner) - j == 5 { ret @ } // Exit inner loop (@2) - i == 5 { ret @1 } // Exit outer loop (@1) - i == 3 and j == 7 { ret @1 42 } // Exit outer loop with value - println(i, j) - } -} -``` - -**Modern syntax sugar** (keywords `break`, `continue`, `foreach`): -```tim -// break/continue syntax (aliases for ret @) -foreach i in 0..<100 { - i > 50 { break } // Same as: ret @ - i % 2 == 0 { continue } // Same as: ret @ [] (continue) - println(i) -} - -// Nested with explicit labels -foreach i in 0..<10 { // Loop @1 - foreach j in 0..<10 { // Loop @2 - j == 5 { break } // Exit inner loop - i == 5 { break @1 } // Exit outer loop - println(i, j) - } -} - -// foreach is syntax sugar for @ ... in -foreach x in items { process(x) } // Same as: @ x in items { process(x) } -``` - -**Recommendation:** Use `foreach/break/continue` for familiar C-style loops, use `@ / ret @` for advanced control flow. - -**Loop Label Numbering:** - -Loops are automatically numbered from **outermost to innermost**: -- `@1` = outermost loop -- `@2` = second level (nested inside @1) -- `@3` = third level (nested inside @2) -- `@` = current/innermost loop (same as highest number) - -**Loop Control Syntax:** -- `ret @` or `break` or `break @1` - Exit innermost loop -- `ret @2` or `break @2` - Exit second loop level (jump out to @1) -- `ret @N value` - Exit loop N with return value -- `continue` or `continue @1` - Continue to next iteration -- `ret value` - Return from function (not loop) - -### Loop `!` Keyword - -Loops with unknown bounds or modified counters require `!`: - -```tim -// Counter modified in loop -@ i in 0..<10 ! 20 { - i++ // Modified counter, needs ! -} - -// Unknown iteration count -@ msg in read_channel() ! inf { - process(msg) -} - -// Condition-based loop -@ x < threshold ! 1000 { - x = compute_next(x) -} -``` - -## Parallel Programming - -### Parallel Loops - -Use `||` for parallel iteration (each iteration in separate process): - -```tim -|| i in 0..10 { - // Runs in separate forked process - expensive_computation(i) -} -``` - -**Implementation:** Uses `fork()` for true OS-level parallelism. - -### Parallel Map - -```tim -// Sequential map -results = [1, 2, 3] | x => x * 2 - -// Parallel map -results = [1, 2, 3] || x => expensive(x) -``` - - - -## ENet Channels - -Tim uses **ENet-style message passing** for concurrency: - -### Send Messages - -```tim -&8080 <- "Hello" // Send to port 8080 -&"host:9000" <- data // Send to remote host -``` - -### Receive Messages - -```tim -msg <= &8080 // Receive from port 8080 -data <= &"server:9000" // Receive from remote -``` - -### Channel Patterns - -```tim -// Worker pattern -worker = -> { - @ { - task <= &8080 - result = process(task) - &8081 <- result - } -} - -// Pipeline pattern -stage1 = -> @ { &8080 <- generate_data() } -stage2 = -> @ { data <= &8080; &8081 <- transform(data) } -stage3 = -> @ { result <= &8081; save(result) } -``` - -**Note:** ENet channels are compiled directly into machine code that uses ENet library calls. - -## C FFI - -Tim can call C functions directly using DWARF debug information and automatic header parsing: - -### Calling C Functions - -```tim -// Standard C library functions (automatically available) -result = c.malloc(1024) -c.free(result) - -// C math functions -x := c.sin(0.0) // Returns 0 -y := c.cos(0.0) // Returns 1 -z := c.sqrt(16.0) // Returns 4 - -// With type casts -size = buffer_size as int32 -ptr = c.malloc(size) - -// Import C library namespaces -import "sdl3" as sdl - -// Access constants from C headers -flags := sdl.SDL_INIT_VIDEO -window := sdl.SDL_CreateWindow("Title", 640, 480, flags) -``` - -## Import and Export System - -Tim provides a unified import system for libraries, git repositories, and local files. The export system controls function visibility and namespace requirements. - -### Export Control - -The `export` statement at the top of a Tim file controls which functions are available to importers: - -**Three export modes:** - -1. **`export *`** - All functions exported to global namespace (no prefix needed) - ```tim - // gamelib.tim - export * - - init = (w, h) -> { ... } - draw_rect = (x, y, w, h) -> { ... } - ``` - Usage: - ```tim - import "gamelib" as game - init(800, 600) // No prefix - draw_rect(10, 20, 50, 50) - ``` - -2. **`export func1 func2`** - Only listed functions exported (prefix required) - ```tim - // api.tim - export public_func another_func - - public_func = { ... } // Exported - another_func = { ... } // Exported - internal_helper = { ... } // Not exported - ``` - Usage: - ```tim - import "api" as api - api.public_func() // OK - api.another_func() // OK - api.internal_helper() // Error - not exported - ``` - -3. **No export** - All functions available (prefix required) - ```tim - // utils.tim - // No export statement - - helper1 = { ... } - helper2 = { ... } - ``` - Usage: - ```tim - import "utils" as utils - utils.helper1() // Prefix required - utils.helper2() // Prefix required - ``` - -**Use cases:** -- `export *`: Beginner-friendly libraries and frameworks -- `export list`: Controlled public APIs with internal implementation details -- No export: General libraries where namespace pollution matters - -### Import Priority - -1. **Libraries** (highest priority) - system libraries, .dll/.so files -2. **Git Repositories** - GitHub, GitLab, etc. with version control -3. **Local Directories** (lowest priority) - relative/absolute paths - -### Library Imports - -```tim -// System library (uses pkg-config on Linux/macOS) -import "sdl3" as sdl -import "raylib" as rl - -// Windows DLL (searches current dir, then system paths) -import "SDL3.dll" as sdl -import "kernel32.dll" as kernel - -// Direct library file path -import "/usr/lib/libmylib.so" as mylib -import "C:\\Windows\\System32\\user32.dll" as user32 -``` - -**Library resolution:** -- Linux/macOS: Uses `pkg-config` to find headers and libraries -- Windows: Searches for .dll in current directory, then system paths -- Parses C headers automatically for function signatures and constants - -### Git Repository Imports - -```tim -// HTTPS format (recommended) -import "github.com/example/tim-math" as math - -// With version specifier -import "github.com/example/tim-math@v1.0.0" as math -import "github.com/example/tim-math@v2.1.3" as math -import "github.com/example/tim-math@latest" as math -import "github.com/example/tim-math@main" as math - -// SSH format -import "git@github.com:example/tim-math.git" as math - -// GitLab and other providers -import "gitlab.com/user/project" as proj -import "bitbucket.org/user/repo" as repo -``` - -**Git repository behavior:** -- Clones to `~/.cache/tim/` (respects `XDG_CACHE_HOME`) -- Imports all top-level `.tim` files from the repository -- Version specifiers: - - `@v1.0.0` - Specific tag - - `@main` / `@master` - Specific branch - - `@latest` - Latest tag, or default branch if no tags - - No `@` - Uses default branch - -### Directory Imports - -```tim -// Current directory -import "." as local - -// Relative paths -import "./lib" as lib -import "../shared" as shared - -// Absolute paths -import "/opt/timlib" as timlib -import "C:\\Projects\\mylib" as mylib -``` - -**Directory behavior:** -- Imports all top-level `.tim` files from the directory -- Relative paths resolved from current working directory -- Allows organizing code into modules - -### Import Examples - -```tim -// Complete application setup -import "sdl3" as sdl // System library -import "github.com/example/tim-math" as math // Git repo -import "./game_logic" as game // Local directory - -main = { - sdl.SDL_Init(sdl.SDL_INIT_VIDEO) - angle := math.radians(45) - game.start_level(1) -} -``` - -### Header Parsing and Constants - -Tim automatically parses C header files using pkg-config and library introspection: - -```tim -import sdl3 as sdl // Parses SDL3 headers, extracts constants and function signatures - -// Constants are available with the namespace prefix -init_flags := sdl.SDL_INIT_VIDEO | sdl.SDL_INIT_AUDIO -window_flags := sdl.SDL_WINDOW_RESIZABLE | sdl.SDL_WINDOW_FULLSCREEN - -// Function signatures are type-checked at compile time -window := sdl.SDL_CreateWindow("Title", 640, 480, window_flags) -``` - -**How it works:** -1. `import sdl3 as sdl` triggers header parsing -2. Compiler uses `pkg-config --cflags sdl3` to find header paths -3. Parses main header file for `#define` constants and function signatures -4. Constants become available as `sdl.CONSTANT_NAME` -5. Functions are linked and type-checked - -### Type Mapping - -| Tim | C | -|------|---| -| `x as int32` | `int32_t` | -| `x as float64` | `double` | -| `ptr as cstr` | `char*` | -| `ptr as ptr` | `void*` | - -### Null Pointer Literals - -When calling C functions, you can use any of these as null pointer (0): - -```tim -// All of these represent null pointer when used in C FFI context -c.some_function(0) // Number literal 0 -c.some_function([]) // Empty list -c.some_function({}) // Empty map -c.some_function(0 as ptr) // Explicit cast -c.some_function([] as ptr) // Empty list cast -c.some_function({} as ptr) // Empty map cast -``` - -This makes Tim's null pointer representation flexible and intuitive: -- `0` is the traditional null pointer value -- `[]` and `{}` represent "empty" or "nothing", which conceptually maps to null -- Explicit casts make the intent clear in code - -### Null Pointer Handling with `or!` - -C functions that return pointers return 0 (null) on failure. Use `or!` for clean error handling: - -```tim -// Old style: manual null check -window := sdl.SDL_CreateWindow("Title", 640, 480, 0) -window == 0 { - println("Failed to create window!") - sdl.SDL_Quit() - exit(1) -} - -// New style: or! with block -window := sdl.SDL_CreateWindow("Title", 640, 480, 0) or! { - println("Failed to create window!") - sdl.SDL_Quit() - exit(1) -} - -// Or with default value -ptr := c.malloc(1024) or! 0 -``` - -**Semantics:** -- `or!` checks for both NaN (error values) and 0 (null pointers) -- If the left side is NaN or 0: - - If right side is a block: executes the block (lazy evaluation) - - If right side is an expression: evaluates and returns it -- If the left side is valid (not NaN, not 0): returns the left value -- Right side is NOT evaluated unless left side is error/null (short-circuit evaluation) - -### Railway-Oriented C Interop - -Chain multiple C calls with `or!` for clean error handling: - -```tim -init_graphics = () => { - // Each call handles its own error with or! - sdl.SDL_Init(sdl.SDL_INIT_VIDEO) or! { - println("SDL_Init failed!") - exit(1) - } - - window := sdl.SDL_CreateWindow("Title", 640, 480, 0) or! { - println("Create window failed!") - sdl.SDL_Quit() - exit(1) - } - - renderer := sdl.SDL_CreateRenderer(window, 0) or! { - println("Create renderer failed!") - sdl.SDL_DestroyWindow(window) - sdl.SDL_Quit() - exit(1) - } - - ret [window, renderer] -} -``` - -### C Library Linking - -The compiler links with `-lc` by default. Additional libraries: - -```bash -tim program.tim -o program -L/path/to/libs -lmylib - -# SDL3 example -tim sdl_demo.tim -o sdl_demo $(pkg-config --libs sdl3) -``` - -## CStruct - -Define C-compatible structures with explicit memory layout: - -### Declaration - -```tim -cstruct Point { - x as float64, - y as float64 -} - -cstruct Rect { - top_left as Point, - width as float64, - height as float64 -} -``` - -### Usage - -```tim -// Create struct -p = Point(3.0, 4.0) - -// Access fields (direct memory offset, no overhead) -println(p.x) // 3.0 -p.x <- 10.0 // Update field - -// Nested structs -r = Rect(Point(0.0, 0.0), 100.0, 50.0) -println(r.top_left.x) -``` - -### Memory Layout - -CStructs have C-compatible memory layout: -- Fields stored sequentially in memory -- No hidden metadata -- Can be passed to C functions directly -- Access via direct pointer arithmetic - -## Memory Management - -### Stack vs Heap - -- **Stack**: Function local variables, temporaries -- **Heap**: Dynamically allocated data (lists, maps, large objects) - -### Arena Allocators and Minimal Builtins - -**CRITICAL DESIGN PRINCIPLE:** Tim keeps builtin functions to an ABSOLUTE MINIMUM. - -**Memory management with syntax sugar:** -- `malloc(size)` - Arena allocator (syntax sugar for allocate within arena) -- `free(ptr)` - No-op (arena cleanup happens automatically) -- For explicit C memory: use `c.malloc`, `c.free`, `c.realloc`, `c.calloc` -- For zero-initialized memory: use `c.calloc(count, size)` - -```tim -// Explicit arena syntax (recommended for large scopes) -result = arena { - data = allocate(1024) - process(data) - final_value -} -// All arena memory freed here - -// Convenient syntax sugar - uses arena automatically -buffer := malloc(1024) -// free(buffer) is no-op, arena cleanup happens automatically - -// Explicit C FFI (when you need manual control) -ptr := c.malloc(1024) -defer c.free(ptr) - -// For C structs that need zero-initialization: -event := c.calloc(1, 128)! as SDL_Event // Allocates and zeros 128 bytes -``` - -**Note:** `malloc()`/`free()` keywords use arena allocator behind the scenes. This provides familiar syntax while maintaining arena safety. Only use `c.malloc`/`c.free` when you need explicit manual memory management. - -**List operations:** -- Use builtin functions: `head(xs)` for first element, `tail(xs)` for remaining elements -- Use `#` length operator (prefix or postfix) - -**Why minimal builtins?** -1. **Simplicity:** Less to learn, fewer concepts -2. **Orthogonality:** One way to do each thing -3. **Extensibility:** Users define their own functions -4. **Predictability:** No hidden behavior -5. **Transparency:** Everything explicit - -**What IS builtin:** -- **Operators:** `#`, arithmetic, logic, bitwise, etc. -- **Control flow:** `@` loops, match blocks, `ret`, `defer` -- **Core I/O:** `print`, `println`, `printf`, `eprint`, `eprintln`, `eprintf`, `exitln`, `exitf` -- **List operations:** `head()`, `tail()` -- **Keywords:** `arena`, `unsafe`, `cstruct`, `import`, etc. - -**Everything else via:** -1. **Operators** for common operations (`#xs` for length) -2. **Builtin functions** for core operations (`head(xs)`, `tail(xs)`) -3. **C FFI** for system functions (`c.sin` not `sin`, `c.malloc` not `malloc`) -4. **User-defined functions** for application logic - -This keeps the language core minimal and forces clarity at call sites. - -### Defer for Resource Management - -The `defer` statement schedules cleanup code to execute when the current scope exits, enabling automatic resource management similar to Go's defer or C++'s RAII. - -**Syntax:** -```tim -defer expression -``` - -**Execution Guarantees:** -- Deferred expressions execute when scope exits (return, block end, error) -- Execution order is LIFO (Last In, First Out) -- Always executes, even on early returns or errors -- Multiple defers in same scope form a cleanup stack - -**Basic Example:** -```tim -open_file = filename -> { - file := c.fopen(filename, "r") or! { - println("Failed to open:", filename) - ret 0 - } - defer c.fclose(file) // Always closes, even on error - - // Read and process file... - data := read_all(file) - - ret data - // c.fclose(file) executes here automatically -} -``` - -**LIFO Execution Order:** -```tim -process = () => { - defer println("First") // Executes last (3rd) - defer println("Second") // Executes second (2nd) - defer println("Third") // Executes first (1st) -} -// Output: Third, Second, First -``` - -**With C FFI (SDL3 Example):** -```tim -init_sdl = () => { - sdl.SDL_Init(sdl.SDL_INIT_VIDEO) or! { - println("SDL init failed") - ret 0 - } - defer sdl.SDL_Quit() // Cleanup registered immediately - - window := sdl.SDL_CreateWindow("App", 640, 480, 0) or! { - println("Window creation failed") - ret 0 // SDL_Quit still called via defer - } - defer sdl.SDL_DestroyWindow(window) // Will execute before SDL_Quit - - renderer := sdl.SDL_CreateRenderer(window, 0) or! { - println("Renderer creation failed") - ret 0 // Both SDL_DestroyWindow and SDL_Quit called - } - defer sdl.SDL_DestroyRenderer(renderer) // Will execute first - - // Use SDL resources... - render_frame(renderer) - - ret 1 - // Cleanup order: renderer, window, SDL_Quit -} -``` - -**Railway-Oriented Pattern:** -```tim -// Combine defer with or! for clean error handling -acquire_resources = () => { - db := connect_db() or! { - println("DB connection failed") - ret error("db") - } - defer disconnect_db(db) - - cache := init_cache() or! { - println("Cache init failed") - ret error("cache") - } - defer cleanup_cache(cache) - - // Work with resources... - // All cleanup happens automatically -} -``` - -**Best Practices:** -1. **Register immediately after acquisition:** `resource := acquire(); defer cleanup(resource)` -2. **Use with or! operator:** Error blocks can return early, defer ensures cleanup -3. **Avoid exit():** Use `ret` instead so defers execute -4. **LIFO cleanup order:** Register defers in acquisition order, they'll clean up in reverse -5. **C FFI resources:** Perfect for file handles, sockets, SDL objects, etc. - -**When NOT to use defer:** -- For Tim data structures (use arena allocators instead) -- When cleanup must happen immediately, not at scope exit -- When cleanup order must be explicit (just call cleanup functions directly) - -### Move Semantics - -Transfer ownership with postfix `!`: - -```tim -large_data := [1, 2, 3, /* ... */, 1000000] -new_owner = large_data! // Move, don't copy -// large_data now invalid -``` - -### Manual Memory (C FFI) - -Tim does NOT provide `malloc`/`free` as builtins. Use C FFI: - -```tim -unsafe ptr { - ptr = c.malloc(1024) - // Use ptr - c.free(ptr) -} -``` - -## Unsafe Blocks - -For direct machine code access, Tim provides architecture-specific unsafe blocks. The compiler selects the appropriate block based on the target ISA (instruction set architecture). - -### Architecture Model - -Tim's platform support separates **ISA** from **OS**: -- **ISA** (x86_64, arm64, riscv64) → determines registers and instructions -- **OS** (Linux, Windows, macOS) → determines binary format and calling conventions -- **Target** = ISA + OS (e.g., arm64-darwin, x86_64-windows) - -**Unsafe blocks are ISA-based, not target-based.** This means: -- Write assembly for the CPU architecture (3 blocks) -- Compiler handles OS differences automatically -- Same arm64 block works on Linux, macOS, and Windows - -See [PLATFORM_ARCHITECTURE.md](PLATFORM_ARCHITECTURE.md) for the full design rationale. - -### Supported Targets - -| Target | ISA | OS | Status | -|--------|-----|-------|--------| -| x86_64-linux | x86_64 | Linux | ✅ Complete | -| x86_64-windows | x86_64 | Windows | ✅ Complete | -| arm64-linux | arm64 | Linux | 🚧 90% (needs defer, dynamic linking) | -| arm64-darwin | arm64 | macOS | ❌ Not started | -| arm64-windows | arm64 | Windows | ❌ Not started | -| riscv64-linux | riscv64 | Linux | 🚧 80% (needs testing) | - -### Syntax - -```tim -unsafe { - # x86_64 block -} { - # arm64 block -} { - # riscv64 block -} as return_type - -// Or the legacy syntax (type before blocks): -unsafe return_type { - # x86_64 block -} { - # arm64 block -} { - # riscv64 block -} -``` - -### Examples - -```tim -// Direct memory access (3 ISA variants) - new syntax -value = unsafe { - rax <- ptr # x86_64 - rax <- [rax + offset] -} { - x0 <- ptr # arm64 - x0 <- [x0 + offset] -} { - a0 <- ptr # riscv64 - a0 <- [a0 + offset] -} as float64 - -// Or legacy syntax: -value = unsafe float64 { - rax <- ptr - rax <- [rax + offset] -} { - x0 <- ptr - x0 <- [x0 + offset] -} { - a0 <- ptr - a0 <- [a0 + offset] -} - -// Syscall (x86_64 only example, expand to 3 blocks in real code) -unsafe { - rax <- 1 // sys_write (x86_64) - rdi <- 1 // stdout - rsi <- msg_ptr - rdx <- msg_len - syscall -} { - x8 <- 64 // sys_write (arm64) - x0 <- 1 // stdout - x1 <- msg_ptr - x2 <- msg_len - svc -} { - a7 <- 64 // sys_write (riscv64) - a0 <- 1 // stdout - a1 <- msg_ptr - a2 <- msg_len - ecall -} -``` - -### Platform Differences (Compiler Handles Automatically) - -While you write ISA-specific code, the compiler handles these OS differences: - -**Binary Format:** -- Linux/BSD → ELF -- Windows → PE -- macOS → Mach-O - -**Calling Conventions:** -- x86_64 Linux/macOS → System V ABI (rdi, rsi, rdx, rcx, r8, r9) -- x86_64 Windows → Microsoft x64 (rcx, rdx, r8, r9) -- arm64 Linux/macOS → AAPCS64 (x0-x7) -- arm64 Windows → ARM64 Windows (x0-x7, different struct passing) - -**Syscalls:** -- Linux x86_64 → `syscall` instruction -- Linux arm64 → `svc #0` instruction -- Windows (any arch) → C FFI to kernel32.dll (no direct syscall) - -**Note:** Unsafe blocks break portability and safety guarantees. Use only when absolutely necessary (e.g., custom syscalls, direct hardware access, performance-critical assembly). - -## Built-in Functions - -### I/O - -```tim -// Standard output (stdout) -println(x) // Print with newline to stdout -print(x) // Print without newline to stdout -printa(x) // Atomic print (thread-safe) -printf(fmt, ...) // Formatted print to stdout - -// Standard error (stderr) - Returns Result with error code "out" -eprintln(x) // Print with newline to stderr, returns Result -eprint(x) // Print without newline to stderr, returns Result -eprintf(fmt, ...) // Formatted print to stderr, returns Result - -// Quick exit error printing - Print to stderr and exit(1) -exitln(x) // Print with newline to stderr and exit(1) -exitf(fmt, ...) // Formatted print to stderr and exit(1) -``` - -**Error Print Functions (`eprint`, `eprintln`, `eprintf`):** -- Print to stderr instead of stdout -- Return a Result type with error code "out" -- Useful for logging and error messages -- Can be chained with `.error` accessor or `or!` operator - -**Quick Exit Functions (`exitln`, `exitf`):** -- Print to stderr and immediately exit with code 1 -- Never return (equivalent to eprint + exit(1)) -- Useful for fatal error messages and early termination -- Simpler than using eprint followed by manual exit() - -**Usage examples:** - -```tim -// Basic error printing -eprintln("Warning: low memory") -eprintf("Error: invalid value %v\n", x) - -// Check result from error print -result := eprintln("This is an error message") -result.error { - "out" -> println("Successfully wrote to stderr") - ~> println("Unexpected error") -} - -// Quick exit on fatal error -x < 0 { - exitln("Fatal: negative value not allowed") - // Never reaches here - program exits -} - -// Formatted quick exit -age < 0 { - exitf("Fatal: invalid age %v\n", age) -} - -// Equivalent to: -x < 0 { - eprintln("Fatal: negative value not allowed") - exit(1) -} -``` - -### String Operations - -```tim -s = "Hello" -s.length // 5 (number of entries in the map) -s.bytes // Map of byte values {0: 72.0, 1: 101.0, ...} -s.runes // Map of Unicode code points -s + " World" // Concatenation (merges maps) -``` - -#### F-String Interpolation - -F-strings provide powerful string interpolation with full expression support: - -```tim -// Basic interpolation -name = "Alice" -greeting = f"Hello, {name}!" // "Hello, Alice!" - -// Arithmetic expressions -x = 10 -y = 20 -result = f"{x} + {y} = {x + y}" // "10 + 20 = 30" - -// Function calls in interpolation -double = (n) { n * 2 } -triple = (n) { n * 3 } -msg = f"double(5) = {double(5)}" // "double(5) = 10" - -// Nested function calls -msg = f"triple(double(4)) = {triple(double(4))}" // "triple(double(4)) = 24" - -// Multiple interpolations in one string -a = 3 -b = 4 -result = f"{a} + {b} = {a + b}, {a} * {b} = {a * b}" -// "3 + 4 = 7, 3 * 4 = 12" - -// Complex expressions -items = [1, 2, 3] -msg = f"List has {#items} items, first is {items[0]}" -// "List has 3 items, first is 1" - -// Nested f-strings work too -inner = "world" -outer = f"Hello, {f"{inner}"}!" // "Hello, world!" -``` - -**Implementation Note:** F-strings are evaluated at runtime, allowing dynamic expressions within `{}`. Any valid Tim expression can be used, including function calls, arithmetic, map access, and nested interpolations. - -### List Operations - -```tim -nums = [1, 2, 3] -nums.length // 3 -nums[0] // 1 -nums[1:] // [2, 3] -nums + [4, 5] // [1, 2, 3, 4, 5] -0 :: nums // [0, 1, 2, 3] — cons (prepend), right-associative -1 :: 2 :: [3] // [1, 2, 3] — 1 :: (2 :: [3]) -``` - -### Map Operations - -```tim -m = { x: 10, y: 20 } -m.x // 10 -m.z <- 30 // Add field -keys = m.keys() // Get all keys -``` - -### Math Functions - -All standard math via C FFI: - -```tim -sin(x) -cos(x) -sqrt(x) -pow(x, y) -abs(x) -``` - -## Error Handling - -### Result Type Design - -Tim uses **NaN-boxing** to encode errors within float64 values. This elegant approach, inspired by ENet's use of bit patterns for encoding flags and types, keeps everything as `map[uint64]float64` while enabling robust error handling. - -**Encoding Scheme:** -- **Success values:** Regular float64 (standard IEEE 754 representation) -- **Error values:** Quiet NaN with 32-bit error code encoded in the mantissa - -**Error NaN Format:** -``` -IEEE 754 Double (64 bits): -[Sign][Exponent (11)][Mantissa (52)] - -Error encoding: -[0][11111111111][1][000][0...0][cccccccccccccccccccccccccccccccc] - ^^^^^^^^^^^ ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - All 1s = NaN | 32-bit error code (4 ASCII chars) - Quiet NaN bit - -Hex representation: 0x7FF8_0000_[CODE]_[CODE] -Example ("dv0\0"): 0x7FF8_0000_6476_3000 -``` - -**Key Properties:** -- Errors are distinguishable from all valid floats (including ±Inf, regular NaN) -- Error checking is a single NaN test: `x != x` or `UCOMISD` -- Error codes are human-readable 4-char ASCII strings -- Zero runtime overhead for success cases -- Compatible with all IEEE 754 compliant hardware - -### Standard Error Codes - -Error codes are exactly 4 bytes (null-padded if shorter), encoded as 32-bit integers: - -``` -"dv0\0" (0x64763000) - Division by zero -"idx\0" (0x69647800) - Index out of bounds -"key\0" (0x6B657900) - Key not found -"typ\0" (0x74797000) - Type mismatch -"nil\0" (0x6E696C00) - Null pointer -"mem\0" (0x6D656D00) - Out of memory -"arg\0" (0x61726700) - Invalid argument -"io\0\0" (0x696F0000) - I/O error -"net\0" (0x6E657400) - Network error -"prs\0" (0x70727300) - Parse error -"ovf\0" (0x6F766600) - Overflow -"udf\0" (0x75646600) - Undefined -``` - -**Note:** The `.error` accessor extracts the code and converts it to a Tim string, stripping null bytes. - -### Operations That Return Results - -```tim -// Arithmetic errors -x = 10 / 0 // Error: "dv0 " (division by zero) -y = 2 ** 1000 // Error: "ovf " (overflow) - -// Index errors -xs = [1, 2, 3] -z = xs[10] // Error: "idx " (out of bounds) - -// Key errors -m = { x: 10 } -w = m.y // Error: "key " (key not found) - -// Custom errors -err = error("arg") // Create error with code "arg " -``` - -### The `.error` Accessor - -Every value has a `.error` accessor that: -- Returns `""` (empty string) for success values -- Returns the error code string (spaces stripped) for error values - -```tim -x = 10 / 2 // Success: returns 5.0 -x.error // Returns "" (empty) - -y = 10 / 0 // Error: division by zero -y.error // Returns "dv0" (spaces stripped) - -// Typical usage -result.error { - "" -> proceed(result) - ~> handle_error(result.error) -} - -// Match on specific errors -result.error { - "" -> println("Success:", result) - "dv0" -> println("Division by zero") - "mem" -> println("Out of memory") - ~> println("Unknown error:", result.error) -} -``` - -### The `or!` Operator - -The `or!` operator provides a default value or executes a block when the left side is an error or null: - -```tim -// Handle errors -x = 10 / 0 // Error result -safe = x or! 99 // Returns 99 (error case) - -y = 10 / 2 // Success result (value 5) -safe2 = y or! 99 // Returns 5 (success case) - -// Handle null pointers from C FFI -window := sdl.SDL_CreateWindow("Title", 640, 480, 0) or! { - println("Failed to create window!") - sdl.SDL_Quit() - exit(1) -} - -// Inline null check with default -ptr := c.malloc(1024) or! 0 // Returns 0 if allocation failed - -// Railway-oriented programming pattern -result := sdl.SDL_Init(sdl.SDL_INIT_VIDEO) or! { - println("SDL_Init failed!") - exit(1) -} -``` - -**Semantics:** -1. Evaluate left operand -2. Check if error (type byte 0xE0) or null (value is 0 for pointer types) -3. If error/null and right side is a block: execute block -4. If error/null and right side is an expression: return right operand -5. Otherwise: return left operand value - -**When checking for null (C FFI pointers):** -- The compiler recognizes pointer-returning C functions -- `or!` treats 0 (null pointer) as a failure case -- Enables railway-oriented programming for C interop -- Blocks can contain cleanup code and exits - -**Precedence:** Lower than logical OR, higher than send operator - -**The `¤` alias:** the currency sign `¤` (U+00A4, Shift+4 on Nordic keyboards) -is a one-character alias for `or!`: - -```tim -x = risky() ¤ 0 // same as: x = risky() or! 0 -w = open_window() ¤ { exit(1) } // same with a handler block -``` - -### Guard Statements - -A guard-match clause may stand alone as a statement — `| cond => stmt` is -sugar for `if cond { stmt }`. Guards read as preconditions and pair naturally -with `err`/`ret` early exits: - -```tim -divide = (a, b) -> { - | b == 0 => err "division by zero" - ret a / b -} - -clamp = (x, lo, hi) -> { - | x < lo => ret lo - | x > hi => ret hi - x -} -``` - -**Disambiguation:** a block whose `|` clause lines are followed by ordinary -statement lines is a statement block containing guard statements. A block -consisting only of clauses (optionally ending in `~>` default) remains a -guard-match expression, and `{ statements… | clauses ~> default }` remains a -mixed block whose trailing guard match is its value. - -### The `err` Keyword - -`err msg` returns a NaN-boxed error from the enclosing function: it desugars -to `ret error(msg)`, so the caller's `or!`/`err?` sees an error value, never -the message string itself. A bare `err` returns the generic `"err"` code. - -```tim -f = (b) -> { - | b == 0 => err "dv0" // caller's or! catches this - ret 7 -} -safe = f(0) or! -1 // -1 -``` - -### Error Propagation Patterns - -```tim -// Check and early return -process = input -> { - step1 = validate(input) - step1.error { != "" -> step1 } // Return error early - - step2 = transform(step1) - step2.error { != "" -> step2 } - - finalize(step2) -} - -// Default values with or! -compute = input -> { - x = parse(input) or! 0 // Use 0 if parse fails - y = divide(100, x) or! -1 // Use -1 if division fails - y * 2 -} - -// Chained operations with error handling -result = fetch_data() - | parse or! [] // Default to empty list - | transform - | validate or! error("typ") // Custom error - -// C FFI null pointer handling -init_sdl = () => { - // Initialize SDL - sdl.SDL_Init(sdl.SDL_INIT_VIDEO) or! { - println("Failed to initialize SDL!") - exit(1) - } - - // Create window with error handling - window := sdl.SDL_CreateWindow("Title", 640, 480, 0) or! { - println("Failed to create window!") - sdl.SDL_Quit() - exit(1) - } - - // Create renderer with error handling - renderer := sdl.SDL_CreateRenderer(window, 0) or! { - println("Failed to create renderer!") - sdl.SDL_DestroyWindow(window) - sdl.SDL_Quit() - exit(1) - } - - ret [window, renderer] -} - -// Simpler pattern with defaults -allocate_buffer = size -> { - ptr := c.malloc(size) or! 0 - ptr == 0 { - ret error("mem") // Out of memory - } - ret ptr -} -``` - -### Creating Custom Errors - -Use the `error` function to create error Results: - -```tim -// Create error with code -validate = x -> { - x < 0 { ret error("arg") } // Negative argument - x -} - -// Or detect errors from operations -divide = (a, b) => { - result = a / b - result.error { - != "" -> result // Propagate error - ~> result // Return success - } -} -``` - -### Error Type Tracking - -The compiler tracks whether a value is a Result type: - -```tim -// Compiler knows this returns Result -divide = (a, b) => { - b == 0 { ret error("dv0") } - a / b -} - -// Compiler propagates Result type -compute = x -> { - y = divide(100, x) // y has Result type - y or! 0 // Handles potential error -} - -// Compiler warns if Result not checked -risky = x -> { - y = divide(100, x) // Warning: unchecked Result - println(y) // May print error value -} -``` - -See [TYPE_TRACKING.md](TYPE_TRACKING.md) for implementation details. - -### Result Type Memory Layout - -**Success value (number 42.0):** -``` -IEEE 754: 0x4045000000000000 (standard float64 encoding) -Binary: [0][10000000100][0101000000000000...] (exp=1028, mantissa=5*2^48) -``` - -**Error value (division by zero "dv0"):** -``` -Hex: 0x7FF8000064763000 -Binary: [0][11111111111][1][000][0...0][01100100 01110110 00110000 00000000] - ^ ^^^^^^^^^^^ ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | NaN exponent | "dv0\0" = 0x64763000 - + Quiet bit + - Sign=0 Reserved bits (future use) -``` - -### `.error` Implementation - -The `.error` accessor: -1. Checks if value is NaN: `UCOMISD xmm0, xmm0` (sets parity flag if NaN) -2. If not NaN: returns empty string "" -3. If NaN: extracts low 32 bits of mantissa as error code -4. Converts 4-byte code to Tim string (strips null bytes) -5. Returns error code string -5. Otherwise: return empty string "" - -### `or!` Implementation - -The `or!` operator: -1. Evaluates left operand -2. Checks type byte (first byte) -3. If 0xE0: returns right operand -4. Otherwise: returns left operand value (strips type metadata) - -### Best Practices - -**Do:** -- Check `.error` for operations that can fail -- Use `or!` for simple default values -- Match on specific error codes for different handling -- Propagate errors up the call chain -- Create custom errors with descriptive codes - -**Don't:** -- Ignore error results -- Use empty error codes (use specific 4-char codes) -- Mix error codes with success values -- Assume all errors are the same - -**Examples:** - -```tim -// Good: check errors -result = fetch_data() -result.error { - "" -> process(result) - "net" -> retry() - ~> fail(result.error) -} - -// Good: use or! for defaults -data = fetch_data() or! [] -count = #data - -// Bad: ignore errors -result = fetch_data() // May be error -process(result) // May process error value - -// Bad: vague error code -validate = x -> x < 0 { ret error("bad") } // Use "arg" instead -``` - -## Compilation and Execution - -### Compiler Usage - -```bash -# Compile to executable -tim program.tim -o program - -# Compile with C library -tim program.tim -o program -lm - -# Specify target architecture -tim program.tim -o program -arch arm64 -tim program.tim -o program -arch riscv64 - -# Hot reload mode (Unix) -tim --hot program.tim - -# Show version -tim --version -``` - -### Supported Architectures - -- **x86_64** (AMD64) - Primary platform -- **ARM64** (AArch64) - Full support -- **RISCV64** - Full support - -### Compilation Process - -1. **Lexing**: Source code → tokens -2. **Parsing**: Tokens → AST -3. **Type Inference**: Track semantic types (see TYPE_TRACKING.md) -4. **Code Generation**: AST → machine code (direct, no IR) -5. **Linking**: Produce ELF (Linux), Mach-O (macOS), or PE (Windows) - -### Performance Characteristics - -- **Compilation**: Fast (no IR overhead) -- **Tail calls**: Always optimized to loops -- **Arithmetic**: SIMD for vectorizable operations -- **Memory**: Arena allocators for predictable patterns -- **Concurrency**: OS-level parallelism via fork() - -### Program Execution Model - -Tim supports three program structures: - -#### 1. Main Function Entry Point - -When a `main` function is defined, it serves as the entry point: - -```tim -main = { println("Hello, World!") } // main() called, prints and returns true (1.0) -main = { 42 } // Returns 42 as exit code -main = () -> { println("Starting"); 1 } // Returns 1 -``` - -**Behavior:** -- If `main` is callable (function/lambda): called automatically at program end UNLESS `main()` is explicitly called anywhere in top-level code -- Auto-call is skipped if any top-level statement (outside of lambda definitions) calls `main()`, regardless of context (direct call, inside match expression, etc.) -- Return value is implicitly cast to int32 for the OS exit code -- Empty blocks `{}` return true (1.0) -- Numeric values are converted to int32 - -**Examples:** -```tim -// Auto-called (no explicit main() call in top-level code) -main = { println("Hello!") } -// Output: Hello! - -// NOT auto-called (explicit main() call exists) -main = { println("Hello!") } -main() // Explicit call -// Output: Hello! (printed once, not twice) - -// NOT auto-called (main() called in match expression) -main = { println("Hello!") } -x = 42 { 0 => 0 ~> main() } // main() in match default case -// Output: Hello! (called once from match) - -// Auto-called (main() only called inside a lambda, not at top level) -main = { println("Hello!") } -wrapper = { main() } // main() inside lambda doesn't count -// Output: Hello! (auto-called, wrapper is not executed) -``` - -**Rule:** The compiler tracks whether `main()` appears as a call expression in any top-level statement. If it does, the automatic call at program end is suppressed. - -#### 2. Top-Level Code - -When there's no `main` defined, top-level statements execute sequentially: - -```tim -println("Starting") -result := compute_something() -println(f"Result: {result}") -0 // Last expression is exit code -``` - -**Exit code determination:** -- Last expression value becomes exit code -- `ret` statement sets explicit exit code -- No explicit return or value: defaults to true (1.0) - -#### Mixed Cases - -**Top-level code WITH main function:** - -When both exist, top-level code executes first and is responsible for calling `main()`: - -```tim -// Setup in top-level -config := load_config() - -main = { - println("In main") - process(config) - 0 -} - -// Top-level must call main explicitly -main() // Without this, main never runs -``` - -If top-level code doesn't call `main()`, the function is never executed, and the last top-level expression determines the exit code. - -## Examples - -### Hello World - -```tim -println("Hello, World!") -``` - -### Factorial - -```tim -// Iterative -factorial = n -> { - result := 1 - @ i in 1..n { - result *= i - } - result -} - -// Tail-recursive (optimized to loop) -factorial = (n, acc) => n == 0 { - -> acc - ~> factorial(n-1, n*acc) -} - -// Usage -println(factorial(5, 1)) // 120 -``` - -### FizzBuzz - -```tim -@ i in 1..100 { - result = i % 15 { - 0 -> "FizzBuzz" - ~> i % 3 { - 0 -> "Fizz" - ~> i % 5 { - 0 -> "Buzz" - ~> i - } - } - } - println(result) -} -``` - -### List Processing - -```tim -// Map, filter, reduce -numbers = [1, 2, 3, 4, 5] - -// Map: double each number -doubled = numbers | x -> x * 2 - -// Filter: only even numbers -evens = numbers | x -> x % 2 == 0 { 1 => x ~> [] } - -println(f"Evens: {evens}") -``` - -### Pattern Matching - -```tim -// Value match -classify_number = x -> x { - 0 -> "zero" - 1 -> "one" - 2 -> "two" - ~> "many" -} - -// Guard match -classify_age = age -> { - | age < 13 -> "child" - | age < 18 -> "teen" - | age < 65 -> "adult" - ~> "senior" -} - -// Nested match -check_value = x -> x { - 0 -> "zero" - ~> x > 0 { - 1 -> "positive" - 0 -> "negative" - } -} -``` - -### Error Handling - -```tim -// Division with error handling -safe_divide = (a, b) => { - result = a / b - result.error { - "" -> f"Result: {result}" - ~> f"Error: {result.error}" - } -} - -println(safe_divide(10, 2)) // "Result: 5" -println(safe_divide(10, 0)) // "Error: dv0" - -// With or! operator -compute = (a, b) => { - x = a / b or! 1.0 // Default to 1.0 on error - y = x * 2 - y -} -``` - -### Parallel Processing - -```tim -data = [1, 2, 3, 4, 5, 6, 7, 8] - -// Process in parallel -results = data || x -> expensive_computation(x) - -println(f"Results: {results}") -``` - -### Web Server (ENet) - -```tim -// Simple echo server -server =>> { - @ { - request <= &8080 - println(f"Received: {request}") - &8080 <- f"Echo: {request}" - } -} - -// HTTP-like handler -handle_request = req -> { - method = req.method - path = req.path - - method { - "GET" -> path { - "/" -> "Welcome!" - "/api" -> "{status: ok}" - ~> "Not found" - } - "POST" -> process_post(req) - ~> "Method not allowed" - } -} - -server() -``` - -### C Interop - -```tim -// Define C struct -cstruct Buffer { - data as ptr, - size as int32, - capacity as int32 -} - -// Use C functions with or! for clean error handling -create_buffer = size -> { - ptr := c.malloc(size) or! { - println("Memory allocation failed!") - ret Buffer(0, 0, 0) - } - Buffer(ptr, 0, size) -} - -// Simpler version with default -create_buffer_safe = size -> { - ptr := c.malloc(size) or! 0 - Buffer(ptr, 0, size) -} - -write_buffer = (buf, data) => { - buf.size + 1 > buf.capacity { - 1 -> buf // Buffer full - ~> { - c_memcpy(buf.data + buf.size, data, 1) - buf.size <- buf.size + 1 - buf - } - } -} - -free_buffer = buf -> { - buf.data != 0 { c.free(buf.data) } -} - -// Usage -buf := create_buffer(1024) -buf := write_buffer(buf, 65) // Write 'A' -free_buffer(buf) -``` - -### SDL3 Graphics Example - -```tim -import sdl3 as sdl - -// Initialize with railway-oriented error handling -init_sdl = () => { - sdl.SDL_Init(sdl.SDL_INIT_VIDEO) or! { - println("SDL_Init failed!") - exit(1) - } - - window := sdl.SDL_CreateWindow("Demo", 640, 480, 0) or! { - println("Failed to create window!") - sdl.SDL_Quit() - exit(1) - } - - renderer := sdl.SDL_CreateRenderer(window, 0) or! { - println("Failed to create renderer!") - sdl.SDL_DestroyWindow(window) - sdl.SDL_Quit() - exit(1) - } - - ret [window, renderer] -} - -// Main rendering loop -main = () => { - window, renderer = init_sdl() - - @ frame in 0..<100 ! 200 { - // Clear screen to black - sdl.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) - sdl.SDL_RenderClear(renderer) - - // Draw a red rectangle - sdl.SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255) - sdl.SDL_RenderFillRect(renderer, 100, 100, 200, 150) - - // Present - sdl.SDL_RenderPresent(renderer) - sdl.SDL_Delay(16) // ~60 FPS - } - - // Cleanup - sdl.SDL_DestroyRenderer(renderer) - sdl.SDL_DestroyWindow(window) - sdl.SDL_Quit() -} - -main() -``` - -### Advanced: Custom Allocator - -```tim -// Arena allocator pattern -process_requests = requests -> { - arena { - results := [] - @ req in requests { - result = handle_request(req) - results <- results + [result] - } - results - } - // All arena memory freed here -} -``` - -### Advanced: Unsafe Assembly - -```tim -// Direct syscall (Linux x86_64) -print_fast = msg -> { - len = msg.length - unsafe { - rax <- 1 // sys_write - rdi <- 1 // stdout - rsi <- msg // buffer - rdx <- len // length - syscall - } -} - -// Atomic compare-and-swap -cas = (ptr, old, new) => unsafe int32 { - rax <- old - lock cmpxchg [ptr], new -} { - 1 // Success -} { - 0 // Failed -} -``` - -## Design Rationale - -### Why One Universal Type? - -Traditional languages have complex type hierarchies: -- Primitive types (int, float, char, bool) -- Reference types (objects, arrays, strings) -- Special types (null, undefined, NaN) -- Type conversions and coercions -- Boxing/unboxing overhead - -**Tim's approach:** Everything is `map[uint64]float64` - -**Benefits:** -1. **Conceptual simplicity:** Learn one type, understand the entire language -2. **Implementation simplicity:** One memory layout, one set of operations -3. **No type coercion bugs:** No implicit conversions to reason about -4. **Uniform FFI:** Cast to C types only at boundaries -5. **Natural duck typing:** If it has the key, it works -6. **Optimization freedom:** Compiler can represent values efficiently while preserving semantics - -### Why Direct Machine Code Generation? - -Most compilers use intermediate representations (IR): -- LLVM IR (Rust, Swift, Clang) -- JVM bytecode (Java, Kotlin, Scala) -- WebAssembly (many languages) -- Custom IR (Go, V8) - -**Tim's approach:** AST → Machine code directly - -**Benefits:** -1. **Fast compilation:** No IR translation overhead -2. **Small compiler:** ~30k lines vs hundreds of thousands -3. **No dependencies:** Self-contained, no LLVM/GCC required -4. **Predictable output:** Same code every time -5. **Full control:** Optimize for Tim's semantics, not general-purpose IR - -**Trade-offs:** -- More code per architecture (x86_64, ARM64, RISCV64) -- Manual optimization (no LLVM optimization passes) -- More maintenance burden - -**Why it's worth it:** Tim's simplicity makes per-architecture code manageable. The universal type system means optimizations work uniformly across all values. - -### Why Fork-Based Parallelism? - -Many languages use threads or async/await: -- Shared memory (requires synchronization) -- Green threads (runtime complexity) -- Async/await (function coloring problem) - -**Tim's approach:** `fork()` + ENet channels - -**Benefits:** -1. **True isolation:** Separate address spaces -2. **No data races:** No shared memory to corrupt -3. **OS-level scheduling:** Leverage existing scheduler -4. **Simple mental model:** Process per task -5. **Fault isolation:** One process crash doesn't kill others - -**Trade-offs:** -- Higher memory overhead per task -- Process creation cost -- IPC overhead for communication - -**Why it's worth it:** Safety and simplicity trump performance for most use cases. For hot paths, use threads in unsafe blocks. - -### Why ENet for Concurrency? - -Traditional approaches: -- Channels (Go, Rust): Good but language-specific -- Actors (Erlang, Akka): Heavy runtime -- MPI: Complex API - -**Tim's approach:** ENet-style network channels - -**Benefits:** -1. **Familiar model:** Network programming concepts -2. **Local or remote:** Same API for both -3. **Simple implementation:** Thin wrapper over ENet library -4. **Battle-tested:** ENet proven in real-time networking -5. **Scales naturally:** From single machine to distributed - -**Design:** -```tim -&8080 <- msg // Send to local port -&"host:9000" <- msg // Send to remote host -data <= &8080 // Receive from port -``` - -Clean, minimal, network-inspired. - -### Why No Garbage Collection? - -GC languages (Java, Go, Python) have: -- Unpredictable pause times -- Memory overhead (GC metadata) -- Performance cliffs (GC pressure) -- Tuning complexity - -**Tim's approach:** Arena allocators + move semantics - -**Benefits:** -1. **Predictable performance:** No GC pauses -2. **Low overhead:** No GC metadata -3. **Simple reasoning:** Allocation/deallocation explicit -4. **Natural patterns:** Arena for requests, move for ownership - -**Trade-offs:** -- Manual memory management -- Potential for leaks (if not careful) -- More cognitive load - -**Why it's worth it:** Systems programming demands predictability. Arenas make manual management tractable. - -### Why Minimal Syntax? - -Many languages accumulate features: -- Multiple ways to do the same thing -- Special case syntax -- Keyword proliferation - -**Tim's approach:** Minimal, orthogonal features - -**Examples:** -- One loop construct: `@` -- One function syntax: `=>` -- One block syntax: `{ }` -- Disambiguate by contents, not syntax - -**Benefits:** -1. **Easy to learn:** Fewer concepts -2. **Easy to parse:** Simpler compiler -3. **Less bikeshedding:** Fewer style debates -4. **Uniform code:** Looks consistent - -**Philosophy:** "One obvious way to do it" - -### Why Bitwise Operators Need `b` Suffix? - -In C-like languages: -```c -if (x & FLAG) // Bitwise AND - easy to confuse with && -if (x | FLAG) // Bitwise OR - easy to confuse with || -if (~x) // Bitwise NOT - easy to confuse with logical ! -``` - -**Tim's approach:** Explicit `b` suffix for bitwise, word operators for logical - -```tim -x &b FLAG // Clearly bitwise AND -x and y // Clearly logical AND -x | transform // Clearly pipe -x |b mask // Clearly bitwise OR -not x // Clearly logical NOT -!b x // Clearly bitwise NOT -~b x // Also bitwise NOT (alternative syntax) -x ?b 5 // Bit test: is bit 5 set? -``` - -**Benefits:** -1. **No ambiguity:** Obvious at a glance -2. **No precedence confusion:** Different operators, different precedence -3. **Frees `|` for pipes:** Pipe operator feels natural -4. **Consistent:** All bitwise ops have `b` suffix -5. **Readable:** `not` is clearer than `!` for logical negation -6. **Efficient bit testing:** `?b` compiles to BT/TEST instructions - -### Design Principles Summary - -1. **Radical simplification:** One type, one way -2. **Explicit over implicit:** No hidden complexity -3. **Performance without compromise:** Direct code generation -4. **Safety where practical:** Arenas, move semantics, immutable-by-default -5. **Minimal syntax:** Orthogonal features, no redundancy -6. **Predictable behavior:** No GC, no hidden allocations -7. **Systems-level control:** Direct assembly when needed -8. **Familiar concepts:** Borrow from proven designs - -**Tim is not trying to be:** -- A replacement for application languages (Python, JavaScript) -- A replacement for safe languages (Rust, Ada) -- A general-purpose language for all domains - -**Tim is designed for:** -- Systems programming with radical simplicity -- Performance-critical code with predictable behavior -- Programmers who value minimalism over features -- Domains where direct machine control matters - ---- - -## Frequently Asked Questions - -### Is Tim practical for real projects? - -Yes, but in specific domains: -- Systems utilities -- Network services -- Embedded systems -- Performance-critical components - -Not ideal for: -- Large applications (no module system yet) -- GUI applications (no standard library) -- Rapid prototyping (manual memory management) - -### How fast is Tim? - -Comparable to C for: -- Arithmetic operations -- Memory operations -- System calls - -Slower than C for: -- String operations (map overhead) -- Complex data structures (map overhead) - -Faster than C for: -- Compilation (direct code generation) -- FFI (no marshalling overhead) - -### Is the universal type system really practical? - -Yes, with caveats: -- **Numbers:** Zero overhead (compiler optimizes to registers) -- **Small strings:** Some overhead (map allocation) -- **Large data:** Similar to C (heap allocation either way) -- **FFI:** Zero overhead (direct casts at boundaries) - -The compiler's type tracking (see TYPE_TRACKING.md) eliminates most overhead. - -### Why not use LLVM? - -LLVM would give: -- Better optimization -- More architectures -- Proven backend - -But cost: -- 500MB+ dependency -- Slow compilation -- Complex integration -- Loss of control - -For Tim's goals (fast compilation, small compiler, direct control), hand-written backends win. - -### What about memory safety? - -Tim is **not memory-safe by default** like Rust. - -However: -- Immutable-by-default reduces bugs -- Arena allocators prevent use-after-free -- Move semantics reduce double-free -- No GC means no GC bugs - -Trade-off: Less safe than Rust, simpler to use. - -### Can I use Tim in production? - -Tim 1.6.0 is ready for: -- Personal projects -- Internal tools -- Experiments -- Performance prototypes - -Not yet ready for: -- Mission-critical systems -- Large teams (no module system) -- Long-term maintenance (young language) - -Use your judgment. - ---- - -**For grammar details, see [GRAMMAR.md](GRAMMAR.md)** - -**For compiler type tracking, see [TYPE_TRACKING.md](TYPE_TRACKING.md)** - -**For documentation accuracy, see [LIBERTIES.md](LIBERTIES.md)** - -**For development info, see [DEVELOPMENT.md](DEVELOPMENT.md)** - -**For known issues, see [FAILURES.md](FAILURES.md)** diff --git a/Makefile b/Makefile index e612aa9..3a7b007 100644 --- a/Makefile +++ b/Makefile @@ -5,16 +5,12 @@ SOURCES := $(wildcard *.go) GO ?= go MODULE_FILES := go.mod $(wildcard go.sum) -GOFLAGS ?= -mod=vendor -v +GOFLAGS ?= -# macOS, FreeBSD and Linux detection UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) PREFIX ?= /usr/local MAKE ?= make -else ifeq ($(UNAME_S),FreeBSD) - PREFIX ?= /usr/local - MAKE ?= gmake else PREFIX ?= /usr MAKE ?= make @@ -35,10 +31,10 @@ install: $(PROGRAM) test: @echo "Running tests..." - $(GO) test -failfast -timeout 1m ./... + $(GO) test -timeout 10m ./... cover: - $(GO) test -mod=vendor -coverprofile=coverage.out -coverpkg=./... ./... + $(GO) test -coverprofile=coverage.out -coverpkg=./... ./... $(GO) tool cover -func=coverage.out clean: diff --git a/README.md b/README.md index 90641c5..bdf285b 100644 --- a/README.md +++ b/README.md @@ -1,206 +1,77 @@ # Tim -**Tim** is a fully vibecoded compiler for an experimental programming language. It compiles directly to machine code for Linux (x86_64) and Windows (x86_64). Linux (aarch64 and riscv64) and macOS support is experimental. This entire compiler is experimental. The Tim programming language is experimental too. - -## Features - -* **Fast Compilation**: Compiles directly to machine code. No LLVM, no intermediate steps, sub-second build times. -* **Highly Optimized**: - * **Tail Call Optimization (TCO)** - * **Pure Function Memoization** - * **SIMD & FMA** -* **Minimalist Syntax**: - * Unified syntax for functions, lambdas, and pattern matching. - * The `@` symbol handles all loops (range, while, infinite, for-each). -* **Universal Type System**: Everything is a `map[uint64]float64` at runtime. This eliminates type erasure issues and simplifies serialization, while compile-time annotations (`: num`, `: str`) preserve safety. -* **Compact & Standalone**: "Hello World" is ~21KB on Linux. No `libc` dependency on Linux (uses direct syscalls). -* **Manual Memory Management w/ Safety**: First-class **Arena** allocators for bulk deallocation and `defer` for resource cleanup. No Garbage Collector pauses. - -## Installation - -```bash -go install github.com/vmaxer/tim@latest -``` - -Ensure `~/go/bin` is in your PATH. - -## Quick Start - -Create `hello.tim`: - -```python -println("Hello, Tim!") -``` - -Compile and run: - -```bash -tim hello.tim -o hello -./hello -``` - -## Language Tour - -### 1. Everything is a Map -Tim has one runtime type. -```go -42 // {0: 42.0} -"Hi" // {0: 72.0, 1: 105.0} (ASCII codes) -[1, 2] // {0: 1.0, 1: 2.0} -{x: 10} // {hash("x"): 10.0} -``` - -### 2. Variables & Functions -```go -// Immutable (default) -x = 42 -add = (a, b) -> a + b - -// Mutable -count := 0 -count <- count + 1 - -// Implicit lambda in assignment -run = { println("Running...") } -``` - -### 3. Unified Control Flow -Pattern matching and lambdas share syntax. - -```go -// Value matching -sign = x { - | x > 0 => "positive" - | x < 0 => "negative" - ~> "zero" -} - -// Range Loop -@ i in 0..<10 { - println(i) -} - -// While Loop (condition loops require a `!` bound) -@ count > 0 ! 1000 { - count <- count - 1 -} -``` - -### Error Handling (`or!`) - -The `or!` operator handles errors (encoded as NaN) or nulls (0.0). - -```go -// Returns 42 if risky() fails -val = risky() or! 42 - -// Executes block on error -file = open("data.txt") or! { - println("Failed to open file") - ret 1 -} -``` - -### 5. Memory & Resources -Use `defer` for LIFO cleanup and `arena` for high-performance temporary allocations. +Tim is a small programming language and a compiler that writes machine code +directly: no LLVM, no assembler, no linker. It targets Linux on x86-64, arm64 +and riscv64, Windows on x86-64 and arm64, and macOS on arm64. ```tim -arena { - data = allocate(1024) - // ... use data ... -} // Automatically freed here +fact(n) = n <= 1 { => 1 ~> n * fact(n - 1) } +println(fact(30)) // 265252859812191058636308480000000 -ptr := c.malloc(64) -defer c.free(ptr) +words = split("the quick brown fox", " ") +println([upper(w) @ w in words if #w > 3]) // ["QUICK", "BROWN"] +println(0.1 + 0.2 == 0.3, 7 / 2) // 1 3.5 ``` -### 6. C Interop (FFI) -Call C libraries directly. Tim parses headers and links dynamically. - -```go -import sdl3 as sdl +## Install -sdl.SDL_Init(sdl.SDL_INIT_VIDEO) -defer sdl.SDL_Quit() +```sh +go install github.com/vmaxer/tim@latest +tim hello.tim && ./hello # build +tim run hello.tim a b # build and run with arguments +tim -c 'println(2 ** 100)' # run a one-liner +tim --target arm64-windows hello.tim # cross-compile: hello.exe +tim test # run the test functions of *_test.tim ``` -## Example: Displaying an image with SDL3 - -* Requires `img/grumpy-cat.bmp`. -* When compiling for Windows, this also requires `SDL3.dll` and the `include/SDL3` folder (with SDL3 header files). - -```go -// Import the SDL3 library (auto detect header files and library files with pkg-config on Linux, use SDL3.dll and include/* on Windows) -import sdl3 as sdl - -// Set the window dimentions -width = 620 -height = 387 - -// Initialize SDL with SDL_Init. Use the "or!" keyword to handle the case where SDL_Init returns nothing. -sdl.SDL_Init(sdl.SDL_INIT_VIDEO) or! { - // Exitf is like printf, but writes to stderr and also quits the program with error code 1 - exitf("SDL_Init failed: %s\n", sdl.SDL_GetError()) -} - -// Call SDL_Quit when the program ends -defer sdl.SDL_Quit() - -// Create window, or exit with an error -window = sdl.SDL_CreateWindow("Hello World!", width, height, sdl.SDL_WINDOW_RESIZABLE) or! { - exitf("Failed to create window: %s\n", sdl.SDL_GetError()) -} - -// Call SDL_DestroyWindow when the program ends (before SDL_Quit) -defer sdl.SDL_DestroyWindow(window) - -// Create renderer, or exit with an error -renderer = sdl.SDL_CreateRenderer(window, 0) or! { - exitf("Failed to create renderer: %s\n", sdl.SDL_GetError()) -} - -// Call SDL_DestroyRenderer when the program ends (before SDL_DestroyWindow and SDL_Quit) -defer sdl.SDL_DestroyRenderer(renderer) - -// Load BMP file, or exit with an error -file = sdl.SDL_IOFromFile("img/grumpy-cat.bmp", "rb") or! { - exitf("Error reading file: %s\n", sdl.SDL_GetError()) -} - -// Load surface from file, or exit with an error -bmp = sdl.SDL_LoadBMP_IO(file, 1) or! { - exitf("Error creating surface: %s\n", sdl.SDL_GetError()) -} - -// Clean up the surface when the program ends -defer sdl.SDL_DestroySurface(bmp) - -// Create texture from surface, or exit with an error -tex = sdl.SDL_CreateTextureFromSurface(renderer, bmp) or! { - exitf("Error creating texture: %s\n", sdl.SDL_GetError()) -} - -// Clean up the surface when the program ends -defer sdl.SDL_DestroyTexture(tex) - -// Main rendering loop. Run for approximately 2 seconds (20 frames * 100ms = 2s) -@ frame in 0..<20 { - - // Clear screen - sdl.SDL_RenderClear(renderer) - - // Render texture (fills entire window) - sdl.SDL_RenderTexture(renderer, tex, 0, 0) - - // Present the rendered frame - sdl.SDL_RenderPresent(renderer) - - // Delay to maintain framerate - sdl.SDL_Delay(100) -} +A file that starts with `#!/usr/bin/env tim` runs as a script. `tim help` +lists the targets and flags. + +## The language in a minute + +- **One number type.** Integers of any size and rationals are exact; `sqrt`, + `sin` and `as float64` give float64. `yes` and `no` are 1 and 0. +- **Bindings.** `x = 1` is immutable, `n := 0` is mutable and `n <- n + 1` + (or `n += 1`) updates it. +- **Functions** are values: `square(x) = x * x`, `inc = x -> x + 1`. + Closures capture by reference and every tail call is a jump. +- **Matching.** A block after an expression matches on its value: + `n { 0 => "zero" 1..<10 => "small" ~> "large" }`; `{ | x > 0 => 1 ~> -1 }` + is a guard match. +- **Loops** all start with `@`: `@ i in 0..<10 { }`, `@ x in xs { }`, + `@ n > 1 { }`, `@ { }`, with `break`, `continue` and optional `! N` bounds. +- **Errors are values.** `10 / 0` and `xs[99]` are errors that print as + `error: division by zero`; `x or! default` replaces an error, `v.error` + reads its code and `err "code"` returns one. +- **Memory** is garbage collected, and `arena { }` makes a region: allocation + inside is a pointer bump and leaving frees it at once, keeping only what + escapes. One arena per game frame or per request makes garbage free. +- **C interop.** `import sdl3 as sdl` makes C functions callable as `sdl.SDL_Init(...)`. + +The full grammar and semantics are in [GRAMMAR.md](GRAMMAR.md) and the +builtin functions in [STDLIB.md](STDLIB.md). + +## How it works + +The compiler lexes and parses (`lexer.go`, `parser.go`), then checks names, +mutability, arities and the types it can infer (`check.go`), reporting errors +with source context and suggestions. The core code generator (`core.go`) +compiles through a small interface that each instruction set implements +(`core_x86.go`, `core_a64.go`, `core_rv64.go`), and writes ELF, PE or Mach-O +files. Values are NaN-boxed 64-bit words: plain doubles get inline fast paths, +and everything else calls the runtime, `runtime/rt.c`, a freestanding C +library with exact arithmetic, UTF-8 strings, lists, maps and a garbage +collector, embedded in each executable as a position-independent blob. + +C functions are called through the platform ABI by the runtime (`rt_ffi`), +with signatures read from the library's headers (`cffi.go`, `core_ffi.go`). + +## Development + +```sh +go test ./... # includes cross-architecture tests when qemu is installed +TIM_UPDATE=1 go test -run TestCorePrograms # rewrite testdata/core/*.want +sh runtime/build.sh # rebuild the runtime blobs (clang, ld.lld) ``` -## General info - -* Version: 1.0.3 -* License: The Unlicense +License: The Unlicense. diff --git a/SDL3.dll b/SDL3.dll deleted file mode 100644 index 2051b08..0000000 Binary files a/SDL3.dll and /dev/null differ diff --git a/STDLIB.md b/STDLIB.md new file mode 100644 index 0000000..fa0cd31 --- /dev/null +++ b/STDLIB.md @@ -0,0 +1,96 @@ +# Tim builtin functions + +Every program can call these. A program may define a function with the same +name, which then takes precedence. Functions that fail return an error value. + +## Output and the process + +| Function | Description | +|---|---| +| `print(v...)` | print values separated by spaces | +| `println(v...)` | print values separated by spaces, then a newline | +| `printf(fmt, v...)` | print with a format: `%d %x %X %f %.2f %e %g %s %v %q %c %b %%`, with widths like `%5d` and `%-8s` | +| `eprint(v...)`, `eprintln(v...)`, `eprintf(fmt, v...)` | the same, to stderr | +| `exit(code)` | end the program; an error exits with 1 | +| `exitf(fmt, v...)` | `eprintf`, then exit with 1: `x or! { exitf("failed: %s\n", why) }` | +| `args()` | the command-line arguments as a list of strings | +| `env(name)` | an environment variable, or an error | +| `platform()` | the target, like `"amd64-linux"` or `"arm64-darwin"`, known when compiling | + +## Files and input + +| Function | Description | +|---|---| +| `readln()` | a line from stdin without its newline, or an error at the end of input | +| `read_file(path)` | a whole file as a string, or an error | +| `write_file(path, s)` | write a string, returning its length or an error | + +## Conversions + +| Function | Description | +|---|---| +| `str(v)` | a value as a string, as `println` shows it | +| `num(s)` | parse `42`, `-1_000`, `0.5`, `1e-9`, `0xff`, `0b101`; exact; an error otherwise | +| `float(x)` | a number as float64 | +| `type(v)` | `"num"`, `"str"`, `"list"`, `"map"`, `"fn"`, `"error"` or `"ptr"` | +| `error(v)` | an error value with the given code or message | +| `chr(n)`, `ord(s)` | a code point as UTF-8, and the first code point of a string | +| `bytes(s)`, `runes(s)` | a string's bytes, or its code points, as a list | + +Casts: `x as str`, `x as num`, `x as float64`, `x as int64` (truncates), `x as bool`. + +## Numbers + +| Function | Description | +|---|---| +| `abs floor ceil round trunc` | exact on exact numbers; `round` rounds halves away from zero | +| `sqrt exp log log10 sin cos tan asin acos atan` | float64 | +| `atan2(y, x)`, `pow(x, y)` | `pow` is `x ** y` | +| `min(v...)`, `max(v...)` | of the arguments, or of one list | +| `gcd(a, b)` | the greatest common divisor of two integers | +| `random()` | a float64 in [0, 1) | +| `bit(x, n)`, `rotl(x, n)`, `rotr(x, n)` | bit n of x, and 64-bit rotations | +| `popcount(x)`, `clz(x)`, `ctz(x)` | the 1 bits, leading zero bits and trailing zero bits of a 64-bit integer | + +## Strings + +| Function | Description | +|---|---| +| `upper(s)`, `lower(s)`, `trim(s)` | ASCII case and surrounding whitespace | +| `split(s, sep)`, `join(xs, sep)` | `split(s, "")` splits into characters | +| `replace(s, old, new)` | every occurrence | +| `starts_with(s, p)`, `ends_with(s, p)` | 1 or 0 | +| `find(s, sub)` | the byte index of `sub`, or -1; also finds an element in a list | +| `reverse(s)` | by code point | + +`#s` is the length in bytes, `s[i]` a byte, `s[a:b]` a substring, `+` joins +and `*` repeats. + +## Lists and maps + +| Function | Description | +|---|---| +| `push(xs, v)`, `pop(xs)` | append to, or remove and return the last element of, a mutable list | +| `keys(m)`, `values(m)` | in insertion order | +| `remove(c, k)` | remove and return a map entry or a list element | +| `sort(xs)`, `sort(xs, key)` | a stable sorted copy | +| `reverse(xs)`, `sum(xs)` | | +| `map(xs, f)`, `filter(xs, f)`, `fold(xs, init, f)` | `fold(xs, 0, (acc, x) -> acc + x)` | +| `any(xs, f)`, `all(xs, f)` | 1 or 0 | +| `zip(a, b)`, `enumerate(xs)` | lists of pairs | + +`#xs`, `xs[i]` (negative from the end), `xs[a:b]`, `x in xs`, `a + b`, +`xs * n`; `m[k]`, `m.name`, `k in m`. + +## Memory and C + +| Function | Description | +|---|---| +| `cstr(p)` | copy the C string at a `ptr` into a string | +| `alloc(n)` | `n` zeroed bytes on the garbage-collected heap, as a `ptr`, kept while the program references it | +| `read_i8 read_u8 read_i16 read_u16 read_i32 read_u32 read_i64 read_u64 read_f32 read_f64 read_ptr` | `read_u8(p, i)` reads a value of that C type at byte offset `i` | +| `write_i8 write_u8 write_i16 write_u16 write_i32 write_u32 write_i64 write_u64 write_f32 write_f64 write_ptr` | `write_u8(p, i, v)` writes one | +| `syscall(n, a...)` | Linux system call `n` with up to six arguments, one instruction: numbers pass as integers, strings and `ptr`s as addresses; the result is the kernel's, a negative errno on failure | + +`p + n` offsets a `ptr` by `n` bytes and `x as ptr` makes one from an address. +C functions are called through their library: `c.malloc(16)`, `sdl.SDL_Init(0)`. diff --git a/add.go b/add.go deleted file mode 100644 index b7295f8..0000000 --- a/add.go +++ /dev/null @@ -1,324 +0,0 @@ -// Completion: 100% - Instruction implementation complete -package main - -import ( - "fmt" - "os" -) - -// ADD instruction implementation for all architectures -// Essential for implementing Tim's arithmetic foundation (map[float64]float64): -// - Arithmetic expressions: n + 1, x + y -// - Array/pointer arithmetic: address + offset -// - Index calculations: me.entities + [i] -// - Increment operations: me.x := me.x + dx -// - List concatenation: smaller + [pivot] + larger - -// AddRegToReg generates ADD dst, src (dst = dst + src) -func (o *Out) AddRegToReg(dst, src string) { - if o.backend != nil { - o.backend.AddRegToReg(dst, src) - return - } - // Fallback for x86_64 - switch o.target.Arch() { - case ArchX86_64: - o.addX86RegToReg(dst, src) - case ArchARM64: - o.addARM64RegToReg(dst, src) - case ArchRiscv64: - o.addRISCVRegToReg(dst, src) - } -} - -// AddImmToReg generates ADD dst, imm (dst = dst + imm) -func (o *Out) AddImmToReg(dst string, imm int64) { - if o.backend != nil { - o.backend.AddImmToReg(dst, imm) - return - } - // Fallback for x86_64 - switch o.target.Arch() { - case ArchX86_64: - o.addX86ImmToReg(dst, imm) - case ArchARM64: - o.addARM64ImmToReg(dst, imm) - case ArchRiscv64: - o.addRISCVImmToReg(dst, imm) - } -} - -// AddRegToRegToReg generates ADD dst, src1, src2 (dst = src1 + src2) -// For ARM64 and RISC-V which have 3-operand form -func (o *Out) AddRegToRegToReg(dst, src1, src2 string) { - switch o.target.Arch() { - case ArchX86_64: - // x86 doesn't have 3-operand ADD, use MOV + ADD - // MOV dst, src1; ADD dst, src2 - o.MovRegToReg(dst, src1) - o.AddRegToReg(dst, src2) - case ArchARM64: - o.addARM64RegToRegToReg(dst, src1, src2) - case ArchRiscv64: - o.addRISCVRegToRegToReg(dst, src1, src2) - } -} - -// x86-64 ADD reg, reg -func (o *Out) addX86RegToReg(dst, src string) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - srcReg, srcOk := GetRegister(o.target.Arch(), src) - if !dstOk || !srcOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "add %s, %s:", dst, src) - } - - // REX prefix for 64-bit operation - rex := uint8(0x48) - if (dstReg.Encoding & 8) != 0 { - rex |= 0x01 // REX.B - } - if (srcReg.Encoding & 8) != 0 { - rex |= 0x04 // REX.R - } - o.Write(rex) - - // ADD opcode (0x01 for r/m64, r64) - o.Write(0x01) - - // ModR/M: 11 (register direct) | reg (src) | r/m (dst) - modrm := uint8(0xC0) | ((srcReg.Encoding & 7) << 3) | (dstReg.Encoding & 7) - o.Write(modrm) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// x86-64 ADD reg, imm -func (o *Out) addX86ImmToReg(dst string, imm int64) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - if !dstOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "add %s, %d:", dst, imm) - } - - // REX prefix for 64-bit operation - rex := uint8(0x48) - if (dstReg.Encoding & 8) != 0 { - rex |= 0x01 // REX.B - } - o.Write(rex) - - // Check if immediate fits in 8 bits - if imm >= -128 && imm <= 127 { - // ADD r/m64, imm8 (opcode 0x83 /0) - o.Write(0x83) - modrm := uint8(0xC0) | (dstReg.Encoding & 7) // ModR/M: 11 000 reg - o.Write(modrm) - o.Write(uint8(imm & 0xFF)) - } else { - // ADD r/m64, imm32 (opcode 0x81 /0) - o.Write(0x81) - modrm := uint8(0xC0) | (dstReg.Encoding & 7) - o.Write(modrm) - - // Write 32-bit immediate - imm32 := uint32(imm) - o.Write(uint8(imm32 & 0xFF)) - o.Write(uint8((imm32 >> 8) & 0xFF)) - o.Write(uint8((imm32 >> 16) & 0xFF)) - o.Write(uint8((imm32 >> 24) & 0xFF)) - } - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// ARM64 ADD Xd, Xn, Xm -func (o *Out) addARM64RegToReg(dst, src string) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - srcReg, srcOk := GetRegister(o.target.Arch(), src) - if !dstOk || !srcOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "add %s, %s, %s:", dst, dst, src) - } - - // ADD (shifted register): sf 0 0 01011 shift(2) 0 Rm imm6 Rn Rd - // sf=1 (64-bit), shift=00 - instr := uint32(0x8B000000) | - (uint32(srcReg.Encoding&31) << 16) | // Rm - (uint32(dstReg.Encoding&31) << 5) | // Rn (same as Rd for 2-operand) - uint32(dstReg.Encoding&31) // Rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// ARM64 ADD Xd, Xn, #imm -func (o *Out) addARM64ImmToReg(dst string, imm int64) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - if !dstOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "add %s, %s, #%d:", dst, dst, imm) - } - - if imm < 0 || imm > 4095 { - if VerboseMode { - fmt.Fprintf(os.Stderr, " (immediate out of range)") - } - imm = 0 - } - - // ADD (immediate): sf 0 0 10001 shift(2) imm12 Rn Rd - instr := uint32(0x91000000) | - (uint32(imm&0xFFF) << 10) | // imm12 - (uint32(dstReg.Encoding&31) << 5) | // Rn (same as Rd) - uint32(dstReg.Encoding&31) // Rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// ARM64 ADD Xd, Xn, Xm (3-operand form) -func (o *Out) addARM64RegToRegToReg(dst, src1, src2 string) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - src1Reg, src1Ok := GetRegister(o.target.Arch(), src1) - src2Reg, src2Ok := GetRegister(o.target.Arch(), src2) - if !dstOk || !src1Ok || !src2Ok { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "add %s, %s, %s:", dst, src1, src2) - } - - instr := uint32(0x8B000000) | - (uint32(src2Reg.Encoding&31) << 16) | // Rm - (uint32(src1Reg.Encoding&31) << 5) | // Rn - uint32(dstReg.Encoding&31) // Rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// RISC-V ADD rd, rs1, rs2 -func (o *Out) addRISCVRegToReg(dst, src string) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - srcReg, srcOk := GetRegister(o.target.Arch(), src) - if !dstOk || !srcOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "add %s, %s, %s:", dst, dst, src) - } - - // ADD: 0000000 rs2 rs1 000 rd 0110011 - instr := uint32(0x33) | - (uint32(srcReg.Encoding&31) << 20) | // rs2 - (uint32(dstReg.Encoding&31) << 15) | // rs1 (same as rd) - (uint32(dstReg.Encoding&31) << 7) // rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// RISC-V ADDI rd, rs1, imm -func (o *Out) addRISCVImmToReg(dst string, imm int64) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - if !dstOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "addi %s, %s, %d:", dst, dst, imm) - } - - if imm < -2048 || imm > 2047 { - if VerboseMode { - fmt.Fprintf(os.Stderr, " (immediate out of range)") - } - imm = 0 - } - - // ADDI: imm[11:0] rs1 000 rd 0010011 - instr := uint32(0x13) | - (uint32(imm&0xFFF) << 20) | // imm[11:0] - (uint32(dstReg.Encoding&31) << 15) | // rs1 (same as rd) - (uint32(dstReg.Encoding&31) << 7) // rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// RISC-V ADD rd, rs1, rs2 (3-operand form) -func (o *Out) addRISCVRegToRegToReg(dst, src1, src2 string) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - src1Reg, src1Ok := GetRegister(o.target.Arch(), src1) - src2Reg, src2Ok := GetRegister(o.target.Arch(), src2) - if !dstOk || !src1Ok || !src2Ok { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "add %s, %s, %s:", dst, src1, src2) - } - - instr := uint32(0x33) | - (uint32(src2Reg.Encoding&31) << 20) | // rs2 - (uint32(src1Reg.Encoding&31) << 15) | // rs1 - (uint32(dstReg.Encoding&31) << 7) // rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} diff --git a/address_types.go b/address_types.go deleted file mode 100644 index 9db8c2e..0000000 --- a/address_types.go +++ /dev/null @@ -1,64 +0,0 @@ -// address_types.go - Strongly typed addresses to prevent mixing file offsets, virtual addresses, and text offsets -package main - -import "fmt" - -// VirtualAddr represents an address in virtual memory (e.g., 0x403000) -type VirtualAddr uint64 - -// FileOffset represents an offset in the ELF file (e.g., 0x3000) -type FileOffset uint64 - -// TextOffset represents an offset within the .text buffer (e.g., 0x9b) -type TextOffset uint64 - -// RodataOffset represents an offset within the .rodata buffer -type RodataOffset uint64 - -// DataOffset represents an offset within the .data buffer -type DataOffset uint64 - -func (v VirtualAddr) String() string { - return fmt.Sprintf("0x%x", uint64(v)) -} - -func (f FileOffset) String() string { - return fmt.Sprintf("file:0x%x", uint64(f)) -} - -func (t TextOffset) String() string { - return fmt.Sprintf("text:0x%x", uint64(t)) -} - -// AddressSpace tracks the mapping between different address spaces -type AddressSpace struct { - baseAddr VirtualAddr // Base virtual address (e.g., 0x400000) - textFileOff FileOffset // Where .text starts in file - textVirtAddr VirtualAddr // Where .text is loaded in memory -} - -func NewAddressSpace(base VirtualAddr, textFile FileOffset, textVirt VirtualAddr) *AddressSpace { - return &AddressSpace{ - baseAddr: base, - textFileOff: textFile, - textVirtAddr: textVirt, - } -} - -// TextOffsetToVirtAddr converts a text buffer offset to virtual address -func (as *AddressSpace) TextOffsetToVirtAddr(offset TextOffset) VirtualAddr { - return as.textVirtAddr + VirtualAddr(offset) -} - -// FileOffsetToVirtAddr converts a file offset to virtual address -func (as *AddressSpace) FileOffsetToVirtAddr(offset FileOffset) VirtualAddr { - return as.baseAddr + VirtualAddr(offset) -} - -// VirtAddrToFileOffset converts a virtual address to file offset -func (as *AddressSpace) VirtAddrToFileOffset(addr VirtualAddr) FileOffset { - if addr < as.baseAddr { - panic(fmt.Sprintf("Virtual address %s is before base %s", addr, as.baseAddr)) - } - return FileOffset(addr - as.baseAddr) -} diff --git a/and.go b/and.go deleted file mode 100644 index 3d5b233..0000000 --- a/and.go +++ /dev/null @@ -1,317 +0,0 @@ -// Completion: 100% - Instruction implementation complete -package main - -import ( - "fmt" - "os" -) - -// AND instruction implementation for all architectures -// Used for bitwise operations in unsafe blocks: -// - Bitmask operations: flags & MASK -// - Bit testing: value & (1 << n) -// - Clearing bits: value & ~mask -// - Color channel extraction: color & 0xFF - -// AndRegWithReg generates AND dst, src (dst = dst & src) -func (o *Out) AndRegWithReg(dst, src string) { - if o.backend != nil { - o.backend.AndRegWithReg(dst, src) - return - } - // Fallback for x86_64 - switch o.target.Arch() { - case ArchX86_64: - o.andX86RegWithReg(dst, src) - } -} - -// AndRegWithImm generates AND dst, imm (dst = dst & imm) -func (o *Out) AndRegWithImm(dst string, imm int32) { - switch o.target.Arch() { - case ArchX86_64: - o.andX86RegWithImm(dst, imm) - case ArchARM64: - o.andARM64RegWithImm(dst, imm) - case ArchRiscv64: - o.andRISCVRegWithImm(dst, imm) - } -} - -// AndRegWithRegToReg generates AND dst, src1, src2 (dst = src1 & src2) -// 3-operand form for ARM64 and RISC-V -func (o *Out) AndRegWithRegToReg(dst, src1, src2 string) { - switch o.target.Arch() { - case ArchX86_64: - // x86-64: MOV dst, src1; AND dst, src2 - o.MovRegToReg(dst, src1) - o.AndRegWithReg(dst, src2) - case ArchARM64: - o.andARM64RegWithRegToReg(dst, src1, src2) - case ArchRiscv64: - o.andRISCVRegWithRegToReg(dst, src1, src2) - } -} - -// ============================================================================ -// x86-64 implementations -// ============================================================================ - -// x86-64 AND (register-register) -func (o *Out) andX86RegWithReg(dst, src string) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - srcReg, srcOk := GetRegister(o.target.Arch(), src) - if !dstOk || !srcOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "and %s, %s:", dst, src) - } - - // REX prefix for 64-bit operation - rex := uint8(0x48) - if (dstReg.Encoding & 8) != 0 { - rex |= 0x01 // REX.B - } - if (srcReg.Encoding & 8) != 0 { - rex |= 0x04 // REX.R - } - o.Write(rex) - - // AND opcode (0x21 for r/m64, r64) - o.Write(0x21) - - // ModR/M: 11 (register direct) | reg (src) | r/m (dst) - modrm := uint8(0xC0) | ((srcReg.Encoding & 7) << 3) | (dstReg.Encoding & 7) - o.Write(modrm) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// x86-64 AND with immediate -func (o *Out) andX86RegWithImm(dst string, imm int32) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - if !dstOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "and %s, %d:", dst, imm) - } - - // REX prefix for 64-bit operation - rex := uint8(0x48) - if (dstReg.Encoding & 8) != 0 { - rex |= 0x01 // REX.B - } - o.Write(rex) - - // Check if immediate fits in 8 bits - if imm >= -128 && imm <= 127 { - // AND r/m64, imm8 (opcode 0x83 /4) - o.Write(0x83) - modrm := uint8(0xE0) | (dstReg.Encoding & 7) // opcode extension /4 - o.Write(modrm) - o.Write(uint8(imm & 0xFF)) - } else { - // AND r/m64, imm32 (opcode 0x81 /4) - o.Write(0x81) - modrm := uint8(0xE0) | (dstReg.Encoding & 7) // opcode extension /4 - o.Write(modrm) - - // Write 32-bit immediate - o.Write(uint8(imm & 0xFF)) - o.Write(uint8((imm >> 8) & 0xFF)) - o.Write(uint8((imm >> 16) & 0xFF)) - o.Write(uint8((imm >> 24) & 0xFF)) - } - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// ============================================================================ -// ARM64 implementations -// ============================================================================ - -// ARM64 AND (register-register, 2-operand form) -func (o *Out) andARM64RegWithReg(dst, src string) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - srcReg, srcOk := GetRegister(o.target.Arch(), src) - if !dstOk || !srcOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "and %s, %s, %s:", dst, dst, src) - } - - // AND Xd, Xn, Xm (shifted register) - // Format: sf 0 01010 shift 0 Rm imm6 Rn Rd - // sf=1 (64-bit), shift=00 (LSL #0), Rm=src, Rn=dst (same as Rd), Rd=dst - instr := uint32(0x8A000000) | - (uint32(srcReg.Encoding&31) << 16) | // Rm - (uint32(dstReg.Encoding&31) << 5) | // Rn (same as Rd for 2-operand) - uint32(dstReg.Encoding&31) // Rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// ARM64 AND - 3 operand form -func (o *Out) andARM64RegWithRegToReg(dst, src1, src2 string) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - src1Reg, src1Ok := GetRegister(o.target.Arch(), src1) - src2Reg, src2Ok := GetRegister(o.target.Arch(), src2) - if !dstOk || !src1Ok || !src2Ok { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "and %s, %s, %s:", dst, src1, src2) - } - - instr := uint32(0x8A000000) | - (uint32(src2Reg.Encoding&31) << 16) | // Rm - (uint32(src1Reg.Encoding&31) << 5) | // Rn - uint32(dstReg.Encoding&31) // Rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// ARM64 AND with immediate -func (o *Out) andARM64RegWithImm(dst string, imm int32) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - if !dstOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "and %s, %s, #%d:", dst, dst, imm) - } - - // AND (immediate) uses a complex bitmask encoding - // For simplicity, we'll use a limited set of encodable immediates - // Format: sf 1 00100 1 0 immr imms Rn Rd - // This is a simplified version - full implementation would need bitmask encoding - instr := uint32(0x92000000) | - (uint32(dstReg.Encoding&31) << 5) | // Rn (same as Rd) - uint32(dstReg.Encoding&31) // Rd - // Note: immr and imms fields would need proper bitmask encoding - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// ============================================================================ -// RISC-V implementations -// ============================================================================ - -// RISC-V AND (register-register, 2-operand form) -func (o *Out) andRISCVRegWithReg(dst, src string) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - srcReg, srcOk := GetRegister(o.target.Arch(), src) - if !dstOk || !srcOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "and %s, %s, %s:", dst, dst, src) - } - - // AND: 0000000 rs2 rs1 111 rd 0110011 - instr := uint32(0x33) | - (7 << 12) | // funct3 = 111 (AND) - (uint32(srcReg.Encoding&31) << 20) | // rs2 - (uint32(dstReg.Encoding&31) << 15) | // rs1 (same as rd) - (uint32(dstReg.Encoding&31) << 7) // rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// RISC-V AND - 3 operand form -func (o *Out) andRISCVRegWithRegToReg(dst, src1, src2 string) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - src1Reg, src1Ok := GetRegister(o.target.Arch(), src1) - src2Reg, src2Ok := GetRegister(o.target.Arch(), src2) - if !dstOk || !src1Ok || !src2Ok { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "and %s, %s, %s:", dst, src1, src2) - } - - instr := uint32(0x33) | - (7 << 12) | // funct3 = 111 (AND) - (uint32(src2Reg.Encoding&31) << 20) | // rs2 - (uint32(src1Reg.Encoding&31) << 15) | // rs1 - (uint32(dstReg.Encoding&31) << 7) // rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} - -// RISC-V ANDI (AND immediate) -func (o *Out) andRISCVRegWithImm(dst string, imm int32) { - dstReg, dstOk := GetRegister(o.target.Arch(), dst) - if !dstOk { - return - } - - if VerboseMode { - fmt.Fprintf(os.Stderr, "andi %s, %s, %d:", dst, dst, imm) - } - - // ANDI: imm[11:0] rs1 111 rd 0010011 - instr := uint32(0x13) | - (7 << 12) | // funct3 = 111 (ANDI) - (uint32(imm&0xFFF) << 20) | // imm[11:0] - (uint32(dstReg.Encoding&31) << 15) | // rs1 (same as rd) - (uint32(dstReg.Encoding&31) << 7) // rd - - o.Write(uint8(instr & 0xFF)) - o.Write(uint8((instr >> 8) & 0xFF)) - o.Write(uint8((instr >> 16) & 0xFF)) - o.Write(uint8((instr >> 24) & 0xFF)) - - if VerboseMode { - fmt.Fprintln(os.Stderr) - } -} diff --git a/arch.go b/arch.go deleted file mode 100644 index adec7df..0000000 --- a/arch.go +++ /dev/null @@ -1,43 +0,0 @@ -// Completion: 100% - Utility module complete -package main - -import "fmt" - -// Architecture defines the interface for different CPU architectures -type Architecture interface { - // Instruction generation - MovImmediate(w Writer, dest, val string) error - Syscall(w Writer) error - - // Register validation - IsValidRegister(reg string) bool - - // ELF header information - ELFMachineType() uint16 - - // Architecture identification - Name() string -} - -// X86_64 implements Architecture for x86_64 -type X86_64 struct{} - -// ARM64 implements Architecture for aarch64 -type ARM64 struct{} - -// Riscv64 implements Architecture for riscv64 -type Riscv64 struct{} - -// NewArchitecture creates the appropriate architecture implementation -func NewArchitecture(machine string) (Architecture, error) { - switch machine { - case "x86_64": - return &X86_64{}, nil - case "aarch64": - return &ARM64{}, nil - case "riscv64": - return &Riscv64{}, nil - default: - return nil, fmt.Errorf("unsupported architecture: %s", machine) - } -} diff --git a/arena.go b/arena.go deleted file mode 100644 index 2fc35a1..0000000 --- a/arena.go +++ /dev/null @@ -1,311 +0,0 @@ -// Arena allocator for Tim runtime -// Provides fast bump allocation with scope-based deallocation -package main - -import ( - "fmt" - "slices" -) - -// ArenaScope represents different allocation scopes -type ArenaScope int - -const ( - ArenaGlobal ArenaScope = iota // Program lifetime - ArenaFrame // Per-frame allocation - ArenaFunction // Per-function call - ArenaBlock // arena { ... } block -) - -// Arena represents an allocation arena in the generated code -type Arena struct { - name string - scope ArenaScope - baseReg string // Register holding base pointer - currentReg string // Register holding current pointer - sizeReg string // Register holding size - labelNum int // Unique label number -} - -// Arena runtime structure (in generated code): -// struct Arena { -// void* base; // Start of arena memory -// void* current; // Current allocation pointer -// size_t size; // Total arena size -// size_t used; // Bytes used -// } - -// Code generation for arena operations - -// generateArenaInit generates code to initialize an arena -// This is called at program start or arena block entry -func (fc *TimCompiler) generateArenaInit(arena *Arena, sizeBytes int) { - // Allocate arena memory with mmap (Linux) or malloc (Windows/macOS) - if fc.eb.target.OS() == OSLinux { - // mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) - fc.out.XorRegWithReg("rdi", "rdi") // addr = NULL - fc.out.MovImmToReg("rsi", fmt.Sprintf("%d", sizeBytes)) // length - fc.out.MovImmToReg("rdx", "7") // PROT_READ|PROT_WRITE|PROT_EXEC = 7 - fc.out.MovImmToReg("r10", "34") // MAP_PRIVATE|MAP_ANONYMOUS = 0x22 = 34 - fc.out.MovImmToReg("r8", "-1") // fd = -1 - fc.out.XorRegWithReg("r9", "r9") // offset = 0 - fc.out.MovImmToReg("rax", "9") // sys_mmap = 9 - fc.out.Syscall() - } else { - // Fall back to malloc for Windows/macOS - if !fc.hasCFunction("malloc") { - fc.importCFunction("malloc", "msvcrt.dll") - } - fc.out.MovImmToReg("rdi", fmt.Sprintf("%d", sizeBytes)) - mallocSymbol := "__imp_malloc" - fc.out.CallSymbol(mallocSymbol) - } - - // rax now contains the arena base pointer - // Store in arena structure (we'll use stack for now) - fc.out.MovRegToMem("rax", "rbp", -16) // arena.base - fc.out.MovRegToMem("rax", "rbp", -24) // arena.current = base - fc.out.MovImmToReg("rcx", fmt.Sprintf("%d", sizeBytes)) - fc.out.MovRegToMem("rcx", "rbp", -32) // arena.size - fc.out.XorRegWithReg("rcx", "rcx") - fc.out.MovRegToMem("rcx", "rbp", -40) // arena.used = 0 -} - -// generateArenaAlloc generates code to allocate from current arena -// Returns pointer in rax -func (fc *TimCompiler) generateArenaAlloc(sizeBytes int) { - // Load current pointer - fc.out.MovMemToReg("rax", "rbp", -24) // rax = arena.current - - // Allocate by bumping pointer - fc.out.MovRegToReg("rcx", "rax") // Save allocation pointer - fc.out.AddImmToReg("rax", int64(sizeBytes)) - fc.out.MovRegToMem("rax", "rbp", -24) // Update arena.current - - // Update used count - fc.out.MovMemToReg("rdx", "rbp", -40) // Load arena.used - fc.out.AddImmToReg("rdx", int64(sizeBytes)) - fc.out.MovRegToMem("rdx", "rbp", -40) // Store arena.used - - // Return allocation pointer in rax - fc.out.MovRegToReg("rax", "rcx") -} - -// generateArenaReset generates code to reset arena to initial state -func (fc *TimCompiler) generateArenaReset() { - // Reset current = base - fc.out.MovMemToReg("rax", "rbp", -16) // Load arena.base - fc.out.MovRegToMem("rax", "rbp", -24) // Store to arena.current - - // Reset used = 0 - fc.out.XorRegWithReg("rax", "rax") - fc.out.MovRegToMem("rax", "rbp", -40) -} - -// generateArenaFree generates code to free arena memory -// This is called at program end or arena block exit -func (fc *TimCompiler) generateArenaFree() { - // Load base pointer - fc.out.MovMemToReg("rdi", "rbp", -16) // rdi = arena.base - - if fc.eb.target.OS() == OSLinux { - // munmap(addr, length) - fc.out.MovMemToReg("rsi", "rbp", -32) // rsi = arena.size - fc.out.MovImmToReg("rax", "11") // sys_munmap = 11 - fc.out.Syscall() - } else { - // Fall back to free for Windows/macOS - if !fc.hasCFunction("free") { - fc.importCFunction("free", "msvcrt.dll") - } - freeSymbol := "__imp_free" - fc.out.CallSymbol(freeSymbol) - } -} - -// Helper functions for calling C runtime - -func (fc *TimCompiler) callMalloc() { - // Import malloc if not already imported - if !fc.hasCFunction("malloc") { - fc.importCFunction("malloc", "libc.so.6") - } - - // Call malloc through PLT - mallocSymbol := "malloc@plt" - if fc.eb.target.OS() == OSWindows { - mallocSymbol = "__imp_malloc" - } - fc.out.CallSymbol(mallocSymbol) -} - -func (fc *TimCompiler) callRealloc() { - // Similar to malloc - if !fc.hasCFunction("realloc") { - fc.importCFunction("realloc", "libc.so.6") - } - - reallocSymbol := "realloc@plt" - if fc.eb.target.OS() == OSWindows { - reallocSymbol = "__imp_realloc" - } - fc.out.CallSymbol(reallocSymbol) -} - -func (fc *TimCompiler) callFree() { - // Import free if not already imported - if !fc.hasCFunction("free") { - fc.importCFunction("free", "libc.so.6") - } - - // Call free through PLT - freeSymbol := "free@plt" - if fc.eb.target.OS() == OSWindows { - freeSymbol = "__imp_free" - } - fc.out.CallSymbol(freeSymbol) -} - -// Check if C function is already imported -func (fc *TimCompiler) hasCFunction(name string) bool { - // Check in imported functions - return slices.Contains(fc.importedFunctions, name) -} - -// Import a C function for calling -func (fc *TimCompiler) importCFunction(name, library string) { - // Add to dynamic linker if we have one - if fc.eb.dynlinker != nil { - fc.eb.dynlinker.ImportFunction(library, name) - } - - // Track that we've imported it - fc.importedFunctions = append(fc.importedFunctions, name) -} - -// Arena-aware allocation functions - -// compileStringLiteral now uses arena allocation -func (fc *TimCompiler) compileStringLiteralWithArena(s string) { - // Calculate size needed (length + null terminator) - size := len(s) + 1 - - // Allocate from current arena - fc.generateArenaAlloc(size) - - // rax now points to allocated memory - // Copy string data - fc.out.MovRegToReg("rdi", "rax") // Save pointer - - // Store each byte (using standard mov instructions) - for i, ch := range []byte(s) { - fc.out.MovImmToReg("rax", fmt.Sprintf("%d", ch)) - fc.out.MovByteRegToMem("rax", "rdi", i) - } - // Null terminator - fc.out.XorRegWithReg("rax", "rax") - fc.out.MovByteRegToMem("rax", "rdi", len(s)) - - // Convert pointer to float64 for Tim - fc.out.Cvtsi2sd("xmm0", "rdi") -} - -// compileListLiteral now uses arena allocation -func (fc *TimCompiler) compileListLiteralWithArena(elements []Expression) { - // Lists in Tim are maps with integer keys - // Calculate size for map structure (simplified) - // For now: array of float64 values - size := len(elements) * 8 // 8 bytes per float64 - - // Allocate from current arena - fc.generateArenaAlloc(size) - - // rax points to array - fc.out.MovRegToReg("r14", "rax") // Save base pointer - - // Store each element - for i, elem := range elements { - fc.compileExpression(elem) // Result in xmm0 - fc.out.MovXmmToMem("xmm0", "r14", i*8) - } - - // Return pointer as float64 - fc.out.Cvtsi2sd("xmm0", "r14") -} - -// compileMapLiteral now uses arena allocation -func (fc *TimCompiler) compileMapLiteralWithArena(keys, values []Expression) { - // Similar to list, but with key-value pairs - // For now: simple array of alternating keys and values - size := len(keys) * 16 // 16 bytes per pair (key + value) - - // Allocate from current arena - fc.generateArenaAlloc(size) - - // rax points to map data - fc.out.MovRegToReg("r14", "rax") - - // Store key-value pairs - for i := range keys { - // Store key - fc.compileExpression(keys[i]) - fc.out.MovXmmToMem("xmm0", "r14", i*16) - - // Store value - fc.compileExpression(values[i]) - fc.out.MovXmmToMem("xmm0", "r14", i*16+8) - } - - // Return pointer as float64 - fc.out.Cvtsi2sd("xmm0", "r14") -} - -// Default arena sizes and growth parameters -const ( - // Initial sizes - generous defaults for typical applications - DefaultGlobalArenaSize = 16 * 1024 * 1024 // 16 MB (was 1MB) - DefaultFrameArenaSize = 4 * 1024 * 1024 // 4 MB (was 256KB) - DefaultFunctionArenaSize = 1024 * 1024 // 1 MB (was 64KB) - DefaultBlockArenaSize = 512 * 1024 // 512 KB (was 32KB) - - // Growth parameters - // 1.3x growth is gentler than 2x, wastes less memory - // Example: 16MB → 20.8MB → 27MB → 35.1MB → 45.6MB → 59.3MB → 77MB → 100MB - ArenaGrowthNumerator = 13 // Multiply by 13 - ArenaGrowthDenominator = 10 // Divide by 10 = 1.3x growth - - // Maximum arena size before failing (1GB) - MaxArenaSize = 1024 * 1024 * 1024 -) - -// callArenaAlloc generates code to allocate from current arena. -// Input: rdi = size to allocate (internal ABI; all call sites must put size in rdi) -// Output: rax = pointer to allocated memory -func (fc *TimCompiler) callArenaAlloc() { - fc.usesArenas = true - - arenaIndex := fc.currentArena - 1 - offset := arenaIndex * 8 - - // _tim_arena_alloc uses the internal (Linux SysV) ABI on all platforms: - // rdi = arena_ptr, rsi = size - // Translate the size in rdi to the correct pair before the call. - if fc.eb.target.OS() == OSWindows { - // Windows: move size from rdi to rdx (arg1), load arena into rcx (arg0). - // We use rcx/rdx to match _tim_arena_alloc's own entry code on Windows. - fc.out.MovRegToReg("rdx", "rdi") // rdx = size - fc.out.LeaSymbolToReg("rcx", "_tim_arena_meta") - fc.out.MovMemToReg("rcx", "rcx", 0) // rcx = meta-arena array pointer - fc.out.MovMemToReg("rcx", "rcx", offset) // rcx = arena struct pointer - } else { - // Linux/SysV: push size, load arena into rdi, pop size to rsi. - fc.out.PushReg("rdi") - fc.out.LeaSymbolToReg("rdi", "_tim_arena_meta") - fc.out.MovMemToReg("rdi", "rdi", 0) // rdi = meta-arena array pointer - fc.out.MovMemToReg("rdi", "rdi", offset) // rdi = arena struct pointer - fc.out.PopReg("rsi") // rsi = size - } - - fc.trackFunctionCall("_tim_arena_alloc") - fc.out.CallSymbol("_tim_arena_alloc") -} diff --git a/arithmetic_comprehensive_test.go b/arithmetic_comprehensive_test.go index cdcf77c..0318224 100644 --- a/arithmetic_comprehensive_test.go +++ b/arithmetic_comprehensive_test.go @@ -62,21 +62,6 @@ func TestArithmeticOperations(t *testing.T) { source: "x := 16 ** 0.5\nprintln(x)\n", expected: "4\n", }, - { - name: "caret_as_power", - source: "x := 2 ^ 3\nprintln(x)\n", - expected: "8\n", - }, - { - name: "caret_precedence", - source: "x := 2 + 3 ^ 2\nprintln(x)\n", - expected: "11\n", - }, - { - name: "caret_vs_double_star", - source: "a := 5 ^ 2\nb := 5 ** 2\nprintln(a)\nprintln(b)\n", - expected: "25\n25\n", - }, } for _, tt := range tests { @@ -244,42 +229,42 @@ func TestBitwiseOperations(t *testing.T) { }{ { name: "bitwise_and", - source: `x := 12 &b 10 + source: `x := 12 & 10 println(x) `, expected: "8\n", }, { name: "bitwise_or", - source: `x := 12 |b 10 + source: `x := 12 | 10 println(x) `, expected: "14\n", }, { name: "bitwise_xor", - source: `x := 12 ^b 10 + source: `x := 12 ^ 10 println(x) `, expected: "6\n", }, { name: "bitwise_not", - source: `x := ~b 0 + source: `x := ~ 0 println(x) `, expected: "-1\n", }, { name: "shift_left", - source: `x := 5 <>b 2 + source: `x := 20 >> 2 println(x) `, expected: "5\n", diff --git a/arithmetic_test.go b/arithmetic_test.go deleted file mode 100644 index b57f4af..0000000 --- a/arithmetic_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package main - -import ( - "testing" -) - -// Test ADD instruction -func TestAddX86RegToReg(t *testing.T) { - eb, _ := New("x86_64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.AddRegToReg("rax", "rbx") - - textBytes := eb.text.Bytes() - // REX.W + ADD + ModR/M = 48 01 d8 - expected := []byte{0x48, 0x01, 0xD8} - if len(textBytes) != 3 { - t.Fatalf("Expected 3 bytes, got %d", len(textBytes)) - } - for i, b := range expected { - if textBytes[i] != b { - t.Errorf("Byte %d: expected 0x%02X, got 0x%02X", i, b, textBytes[i]) - } - } -} - -func TestAddX86ImmToReg(t *testing.T) { - eb, _ := New("x86_64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.AddImmToReg("rax", 5) - - textBytes := eb.text.Bytes() - // REX.W + ADD imm8 + ModR/M + imm = 48 83 c0 05 - expected := []byte{0x48, 0x83, 0xC0, 0x05} - if len(textBytes) != 4 { - t.Fatalf("Expected 4 bytes, got %d", len(textBytes)) - } - for i, b := range expected { - if textBytes[i] != b { - t.Errorf("Byte %d: expected 0x%02X, got 0x%02X", i, b, textBytes[i]) - } - } -} - -func TestAddARM64RegToReg(t *testing.T) { - eb, _ := New("arm64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.AddRegToReg("x0", "x1") - - textBytes := eb.text.Bytes() - instr := uint32(textBytes[0]) | (uint32(textBytes[1]) << 8) | - (uint32(textBytes[2]) << 16) | (uint32(textBytes[3]) << 24) - - // Check ADD opcode - if (instr & 0xFFE00000) != 0x8B000000 { - t.Errorf("Wrong ADD opcode: 0x%08X", instr) - } -} - -func TestAddRISCVRegToReg(t *testing.T) { - eb, _ := New("riscv64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.AddRegToReg("a0", "a1") - - textBytes := eb.text.Bytes() - instr := uint32(textBytes[0]) | (uint32(textBytes[1]) << 8) | - (uint32(textBytes[2]) << 16) | (uint32(textBytes[3]) << 24) - - // Check opcode and funct7 for ADD - if (instr & 0x7F) != 0x33 { - t.Errorf("Wrong opcode: 0x%02X", instr&0x7F) - } - if ((instr >> 25) & 0x7F) != 0x00 { - t.Errorf("Wrong funct7 for ADD: 0x%02X", (instr>>25)&0x7F) - } -} - -// Test SUB instruction -func TestSubX86RegFromReg(t *testing.T) { - eb, _ := New("x86_64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.SubRegFromReg("rax", "rbx") - - textBytes := eb.text.Bytes() - // REX.W + SUB + ModR/M = 48 29 d8 - expected := []byte{0x48, 0x29, 0xD8} - if len(textBytes) != 3 { - t.Fatalf("Expected 3 bytes, got %d", len(textBytes)) - } - for i, b := range expected { - if textBytes[i] != b { - t.Errorf("Byte %d: expected 0x%02X, got 0x%02X", i, b, textBytes[i]) - } - } -} - -func TestSubARM64RegFromReg(t *testing.T) { - eb, _ := New("arm64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.SubRegFromReg("x0", "x1") - - textBytes := eb.text.Bytes() - instr := uint32(textBytes[0]) | (uint32(textBytes[1]) << 8) | - (uint32(textBytes[2]) << 16) | (uint32(textBytes[3]) << 24) - - // Check SUB opcode - if (instr & 0xFFE00000) != 0xCB000000 { - t.Errorf("Wrong SUB opcode: 0x%08X", instr) - } -} - -func TestSubRISCVRegFromReg(t *testing.T) { - eb, _ := New("riscv64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.SubRegFromReg("a0", "a1") - - textBytes := eb.text.Bytes() - instr := uint32(textBytes[0]) | (uint32(textBytes[1]) << 8) | - (uint32(textBytes[2]) << 16) | (uint32(textBytes[3]) << 24) - - // Check funct7 for SUB (0x20) - funct7 := (instr >> 25) & 0x7F - if funct7 != 0x20 { - t.Errorf("Wrong funct7 for SUB: 0x%02X, expected 0x20", funct7) - } -} - -// Test Jump instructions -func TestJumpX86Equal(t *testing.T) { - eb, _ := New("x86_64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.JumpConditional(JumpEqual, 100) - - textBytes := eb.text.Bytes() - if len(textBytes) != 6 { - t.Fatalf("Expected 6 bytes, got %d", len(textBytes)) - } - - // 0F 84 for JE + 32-bit offset - if textBytes[0] != 0x0F || textBytes[1] != 0x84 { - t.Errorf("Wrong JE opcode: %02X %02X", textBytes[0], textBytes[1]) - } - - // Check offset (little-endian) - offset := int32(uint32(textBytes[2]) | (uint32(textBytes[3]) << 8) | - (uint32(textBytes[4]) << 16) | (uint32(textBytes[5]) << 24)) - if offset != 100 { - t.Errorf("Wrong offset: %d, expected 100", offset) - } -} - -func TestJumpX86Unconditional(t *testing.T) { - eb, _ := New("x86_64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.JumpUnconditional(200) - - textBytes := eb.text.Bytes() - if len(textBytes) != 5 { - t.Fatalf("Expected 5 bytes, got %d", len(textBytes)) - } - - // E9 for JMP - if textBytes[0] != 0xE9 { - t.Errorf("Wrong JMP opcode: %02X", textBytes[0]) - } - - offset := int32(uint32(textBytes[1]) | (uint32(textBytes[2]) << 8) | - (uint32(textBytes[3]) << 16) | (uint32(textBytes[4]) << 24)) - if offset != 200 { - t.Errorf("Wrong offset: %d, expected 200", offset) - } -} - -func TestJumpARM64Equal(t *testing.T) { - eb, _ := New("arm64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.JumpConditional(JumpEqual, 32) // 8 instructions forward - - textBytes := eb.text.Bytes() - instr := uint32(textBytes[0]) | (uint32(textBytes[1]) << 8) | - (uint32(textBytes[2]) << 16) | (uint32(textBytes[3]) << 24) - - // Check B.cond opcode - if (instr & 0xFF000000) != 0x54000000 { - t.Errorf("Wrong B.cond opcode: 0x%08X", instr) - } - - // Check condition (EQ = 0) - cond := instr & 0xF - if cond != 0 { - t.Errorf("Wrong condition: %d, expected 0 (EQ)", cond) - } -} - -func TestJumpRISCVEqual(t *testing.T) { - eb, _ := New("riscv64") - out := NewOut(eb.target, eb.TextWriter(), eb) - - out.JumpConditional(JumpEqual, 8) - - textBytes := eb.text.Bytes() - instr := uint32(textBytes[0]) | (uint32(textBytes[1]) << 8) | - (uint32(textBytes[2]) << 16) | (uint32(textBytes[3]) << 24) - - // Check branch opcode (0x63) - if (instr & 0x7F) != 0x63 { - t.Errorf("Wrong branch opcode: 0x%02X", instr&0x7F) - } - - // Check funct3 for BEQ (0) - funct3 := (instr >> 12) & 0x7 - if funct3 != 0 { - t.Errorf("Wrong funct3 for BEQ: %d", funct3) - } -} diff --git a/arm64_backend.go b/arm64_backend.go deleted file mode 100644 index ce0fa24..0000000 --- a/arm64_backend.go +++ /dev/null @@ -1,832 +0,0 @@ -// Completion: 95% - Backend complete, production-ready for basic and intermediate programs -package main - -import ( - "strconv" - "strings" -) - -// ARM64Backend implements the CodeGenerator interface for ARM64 architecture -type ARM64Backend struct { - writer Writer - eb *ExecutableBuilder -} - -// NewARM64Backend creates a new ARM64 code generator backend -func NewARM64Backend(writer Writer, eb *ExecutableBuilder) *ARM64Backend { - return &ARM64Backend{ - writer: writer, - eb: eb, - } -} - -func (a *ARM64Backend) write(b uint8) { - a.writer.(*BufferWrapper).Write(b) -} - -func (a *ARM64Backend) writeUnsigned(i uint) { - a.writer.(*BufferWrapper).WriteUnsigned(i) -} - -func (a *ARM64Backend) emit(bytes []byte) { - for _, b := range bytes { - a.write(b) - } -} - -func (a *ARM64Backend) writeInstruction(instr uint32) { - // ARM64 instructions are little-endian - a.write(uint8(instr & 0xFF)) - a.write(uint8((instr >> 8) & 0xFF)) - a.write(uint8((instr >> 16) & 0xFF)) - a.write(uint8((instr >> 24) & 0xFF)) -} - -// ===== Data Movement ===== - -func (a *ARM64Backend) MovRegToReg(dst, src string) { - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // ARM64 MOV (register): ORR Xd, XZR, Xm - var instr uint32 - if dstReg.Size == 64 && srcReg.Size == 64 { - // 64-bit: ORR Xd, XZR, Xm - instr = 0xAA0003E0 | (uint32(srcReg.Encoding&31) << 16) | uint32(dstReg.Encoding&31) - } else { - // 32-bit: ORR Wd, WZR, Wm - instr = 0x2A0003E0 | (uint32(srcReg.Encoding&31) << 16) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) MovImmToReg(dst, imm string) { - dstReg, dstOk := arm64Registers[dst] - if !dstOk { - return - } - - // Strip ARM64 immediate prefix (#) if present - imm = strings.TrimPrefix(imm, "#") - - // Parse immediate value - var immVal uint64 - if val, err := strconv.ParseInt(imm, 0, 64); err == nil { - immVal = uint64(val) - } else if val, err := strconv.ParseUint(imm, 0, 64); err == nil { - immVal = val - } - - // Use MOVZ (move with zero) for immediate values - var instr uint32 - if dstReg.Size == 64 { - // MOVZ Xd, #imm16 - instr = 0xD2800000 | (uint32(immVal&0xFFFF) << 5) | uint32(dstReg.Encoding&31) - } else { - // MOVZ Wd, #imm16 - instr = 0x52800000 | (uint32(immVal&0xFFFF) << 5) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) MovMemToReg(dst, symbol string, offset int32) { - // ARM64: Load from memory at symbol+offset into register - // LDR Xt, [base, #offset] - dstReg, dstOk := arm64Registers[dst] - if !dstOk { - return - } - - // For now, use ADRP + LDR for symbol access - // ADRP loads page address, then LDR with page offset - offsetPos := uint64(a.writer.(*BufferWrapper).buf.Len()) - a.eb.pcRelocations = append(a.eb.pcRelocations, PCRelocation{ - offset: offsetPos, - symbolName: symbol, - }) - - // ADRP Xt, symbol (load page address) - instr := uint32(0x90000000) | uint32(dstReg.Encoding&31) - a.writeInstruction(instr) - - // LDR Xt, [Xt, #offset] - if offset < 0 || offset > 32760 { - // Offset out of range for immediate encoding - offset = 0 - } - // LDR: 1 11 11001 01 imm12 Rn Rt - instr = uint32(0xF9400000) | - (uint32(offset/8) << 10) | // imm12 (scaled by 8 for 64-bit) - (uint32(dstReg.Encoding&31) << 5) | // Rn (base) - uint32(dstReg.Encoding&31) // Rt (dest) - a.writeInstruction(instr) -} - -func (a *ARM64Backend) MovRegToMem(src, symbol string, offset int32) { - // ARM64: Store register to memory at symbol+offset - // STR Xt, [base, #offset] - srcReg, srcOk := arm64Registers[src] - if !srcOk { - return - } - - // Use temporary register (x16) for address calculation - offsetPos := uint64(a.writer.(*BufferWrapper).buf.Len()) - a.eb.pcRelocations = append(a.eb.pcRelocations, PCRelocation{ - offset: offsetPos, - symbolName: symbol, - }) - - // ADRP x16, symbol - instr := uint32(0x90000000) | 16 // x16 - a.writeInstruction(instr) - - // STR Xt, [x16, #offset] - if offset < 0 || offset > 32760 { - offset = 0 - } - // STR: 1 11 11001 00 imm12 Rn Rt - instr = uint32(0xF9000000) | - (uint32(offset/8) << 10) | // imm12 - (uint32(16) << 5) | // Rn = x16 - uint32(srcReg.Encoding&31) // Rt (source) - a.writeInstruction(instr) -} - -// ===== Integer Arithmetic ===== - -func (a *ARM64Backend) AddRegToReg(dst, src string) { - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // ADD Xd, Xd, Xm (shifted register form) - var instr uint32 - if dstReg.Size == 64 { - instr = 0x8B000000 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } else { - instr = 0x0B000000 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) AddImmToReg(dst string, imm int64) { - dstReg, dstOk := arm64Registers[dst] - if !dstOk { - return - } - - // ADD Xd, Xd, #imm12 - if imm < 0 || imm > 4095 { - imm = imm & 0xFFF - } - - var instr uint32 - if dstReg.Size == 64 { - instr = 0x91000000 | (uint32(imm&0xFFF) << 10) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } else { - instr = 0x11000000 | (uint32(imm&0xFFF) << 10) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) SubRegToReg(dst, src string) { - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // SUB Xd, Xd, Xm - var instr uint32 - if dstReg.Size == 64 { - instr = 0xCB000000 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } else { - instr = 0x4B000000 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) SubImmFromReg(dst string, imm int64) { - dstReg, dstOk := arm64Registers[dst] - if !dstOk { - return - } - - // SUB Xd, Xd, #imm12 - if imm < 0 || imm > 4095 { - imm = imm & 0xFFF - } - - var instr uint32 - if dstReg.Size == 64 { - instr = 0xD1000000 | (uint32(imm&0xFFF) << 10) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } else { - instr = 0x51000000 | (uint32(imm&0xFFF) << 10) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) MulRegToReg(dst, src string) { - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // MUL Xd, Xd, Xm (MADD Xd, Xd, Xm, XZR) - var instr uint32 - if dstReg.Size == 64 { - instr = 0x9B007C00 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } else { - instr = 0x1B007C00 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) DivRegToReg(dst, src string) { - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // SDIV Xd, Xd, Xm (signed division) - var instr uint32 - if dstReg.Size == 64 { - instr = 0x9AC00C00 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } else { - instr = 0x1AC00C00 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) IncReg(dst string) { - // ADD Xd, Xd, #1 - a.AddImmToReg(dst, 1) -} - -func (a *ARM64Backend) DecReg(dst string) { - // SUB Xd, Xd, #1 - a.SubImmFromReg(dst, 1) -} - -func (a *ARM64Backend) NegReg(dst string) { - dstReg, dstOk := arm64Registers[dst] - if !dstOk { - return - } - - // NEG Xd, Xd (SUB Xd, XZR, Xd) - var instr uint32 - if dstReg.Size == 64 { - instr = 0xCB0003E0 | (uint32(dstReg.Encoding&31) << 16) | uint32(dstReg.Encoding&31) - } else { - instr = 0x4B0003E0 | (uint32(dstReg.Encoding&31) << 16) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -// ===== Bitwise Operations ===== - -func (a *ARM64Backend) XorRegWithReg(dst, src string) { - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // EOR Xd, Xd, Xm - var instr uint32 - if dstReg.Size == 64 { - instr = 0xCA000000 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } else { - instr = 0x4A000000 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) XorRegWithImm(dst string, imm int64) { - // ARM64: EOR Xd, Xn, #imm (bitwise exclusive OR with immediate) - dstReg, dstOk := arm64Registers[dst] - if !dstOk { - return - } - - // Check if immediate can be encoded (ARM64 has complex immediate encoding) - // For simplicity, handle common cases - if imm < 0 || imm > 0xFFFF { - // For large immediates, need to load into register first - // This is a simplified implementation - return - } - - // EOR (immediate): sf 10 100100 N immr imms Rn Rd - // sf=1 for 64-bit, N=1 for 64-bit - instr := uint32(0xD2000000) | - (uint32(imm&0xFFFF) << 5) | // Simplified encoding - (uint32(dstReg.Encoding&31) << 5) | // Rn (same as Rd) - uint32(dstReg.Encoding&31) // Rd - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) AndRegWithReg(dst, src string) { - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // AND Xd, Xd, Xm - var instr uint32 - if dstReg.Size == 64 { - instr = 0x8A000000 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } else { - instr = 0x0A000000 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) OrRegWithReg(dst, src string) { - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // ORR Xd, Xd, Xm - var instr uint32 - if dstReg.Size == 64 { - instr = 0xAA000000 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } else { - instr = 0x2A000000 | (uint32(srcReg.Encoding&31) << 16) | (uint32(dstReg.Encoding&31) << 5) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) NotReg(dst string) { - dstReg, dstOk := arm64Registers[dst] - if !dstOk { - return - } - - // MVN Xd, Xd (ORN Xd, XZR, Xd) - var instr uint32 - if dstReg.Size == 64 { - instr = 0xAA2003E0 | (uint32(dstReg.Encoding&31) << 16) | uint32(dstReg.Encoding&31) - } else { - instr = 0x2A2003E0 | (uint32(dstReg.Encoding&31) << 16) | uint32(dstReg.Encoding&31) - } - - a.writeInstruction(instr) -} - -// ===== Stack Operations ===== - -func (a *ARM64Backend) PushReg(reg string) { - compilerError("ARM64Backend.PushReg not implemented (ARM64 doesn't have PUSH/POP)") -} - -func (a *ARM64Backend) PopReg(reg string) { - compilerError("ARM64Backend.PopReg not implemented (ARM64 doesn't have PUSH/POP)") -} - -// ===== Control Flow ===== - -func (a *ARM64Backend) JumpConditional(condition JumpCondition, offset int32) { - // B.cond offset (conditional branch) - // Offset is in instructions (4-byte units), signed 19-bit - immOffset := offset / 4 - if immOffset < -262144 || immOffset > 262143 { - compilerError("ARM64 conditional branch offset out of range: %d", immOffset) - return - } - - var cond uint32 - switch condition { - case JumpEqual: - cond = 0x0 // EQ - case JumpNotEqual: - cond = 0x1 // NE - case JumpGreater: - cond = 0xC // GT - case JumpGreaterOrEqual: - cond = 0xA // GE - case JumpLess: - cond = 0xB // LT - case JumpLessOrEqual: - cond = 0xD // LE - default: - compilerError("Unknown jump condition for ARM64: %v", condition) - return - } - - // B.cond: 0101010 0 imm19 0 cond - instr := uint32(0x54000000) | (uint32(immOffset&0x7FFFF) << 5) | cond - a.writeInstruction(instr) -} - -func (a *ARM64Backend) JumpUnconditional(offset int32) { - // B offset (unconditional branch) - // Offset is in instructions (4-byte units), signed 26-bit - immOffset := offset / 4 - if immOffset < -33554432 || immOffset > 33554431 { - compilerError("ARM64 unconditional branch offset out of range: %d", immOffset) - return - } - - // B: 000101 imm26 - instr := uint32(0x14000000) | uint32(immOffset&0x3FFFFFF) - a.writeInstruction(instr) -} - -func (a *ARM64Backend) CallSymbol(symbol string) { - // BL offset (branch with link) - a.write(0x94) // Placeholder - will be patched - - callPos := a.eb.text.Len() - a.writeUnsigned(0x000000) // 3 more bytes (total 4 bytes for BL) - - a.eb.callPatches = append(a.eb.callPatches, CallPatch{ - position: callPos - 1, // Point to start of instruction - targetName: symbol, - }) -} - -func (a *ARM64Backend) CallRelative(offset int32) { - // BL offset - immOffset := offset / 4 - if immOffset < -33554432 || immOffset > 33554431 { - compilerError("ARM64 BL offset out of range: %d", immOffset) - return - } - - // BL: 100101 imm26 - instr := uint32(0x94000000) | uint32(immOffset&0x3FFFFFF) - a.writeInstruction(instr) -} - -func (a *ARM64Backend) CallRegister(reg string) { - regInfo, regOk := arm64Registers[reg] - if !regOk { - return - } - - // BLR Xn (branch with link to register) - // BLR: 1101011 0 0 01 11111 000000 Rn 00000 - instr := uint32(0xD63F0000) | (uint32(regInfo.Encoding&31) << 5) - a.writeInstruction(instr) -} - -func (a *ARM64Backend) Ret() { - // RET (BR X30) - instr := uint32(0xD65F03C0) - a.writeInstruction(instr) -} - -// ===== Comparisons ===== - -func (a *ARM64Backend) CmpRegToReg(reg1, reg2 string) { - reg1Info, reg1Ok := arm64Registers[reg1] - reg2Info, reg2Ok := arm64Registers[reg2] - if !reg1Ok || !reg2Ok { - return - } - - // CMP is encoded as SUBS XZR, Xn, Xm - instr := uint32(0xEB000000) | - (uint32(reg2Info.Encoding&31) << 16) | - (uint32(reg1Info.Encoding&31) << 5) | - 31 // Rd = XZR - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) CmpRegToImm(reg string, imm int64) { - regInfo, regOk := arm64Registers[reg] - if !regOk { - return - } - - // ARM64 immediate must be 12-bit unsigned - if imm < 0 || imm > 4095 { - imm = 0 - } - - // SUBS XZR, Xn, #imm - instr := uint32(0xF1000000) | - (uint32(imm&0xFFF) << 10) | - (uint32(regInfo.Encoding&31) << 5) | - 31 // Rd = XZR - - a.writeInstruction(instr) -} - -// ===== Address Calculation ===== - -func (a *ARM64Backend) LeaSymbolToReg(dst, symbol string) { - // ARM64: Load effective address of symbol using ADRP + ADD - dstReg, dstOk := arm64Registers[dst] - if !dstOk { - return - } - - // Record relocation - offsetPos := uint64(a.writer.(*BufferWrapper).buf.Len()) - a.eb.pcRelocations = append(a.eb.pcRelocations, PCRelocation{ - offset: offsetPos, - symbolName: symbol, - }) - - // ADRP Xd, symbol (load page address) - instr := uint32(0x90000000) | uint32(dstReg.Encoding&31) - a.writeInstruction(instr) - - // ADD Xd, Xd, #pageoff (add page offset) - // ADD (immediate): sf 0 0 10001 shift imm12 Rn Rd - instr = uint32(0x91000000) | - (uint32(dstReg.Encoding&31) << 5) | // Rn (same as Rd) - uint32(dstReg.Encoding&31) // Rd - a.writeInstruction(instr) -} - -func (a *ARM64Backend) LeaImmToReg(dst, base string, offset int32) { - // ADD Xd, Xbase, #offset - dstReg, dstOk := arm64Registers[dst] - baseReg, baseOk := arm64Registers[base] - if !dstOk || !baseOk { - return - } - - if offset < 0 || offset > 4095 { - offset = offset & 0xFFF - } - - instr := uint32(0x91000000) | - (uint32(offset&0xFFF) << 10) | - (uint32(baseReg.Encoding&31) << 5) | - uint32(dstReg.Encoding&31) - - a.writeInstruction(instr) -} - -// ===== Floating Point (SIMD) ===== - -func (a *ARM64Backend) MovXmmToMem(src, base string, offset int32) { - // ARM64: Store FP register to memory - // STR Dt, [Xn, #offset] - srcReg, srcOk := arm64Registers[src] - baseReg, baseOk := arm64Registers[base] - if !srcOk || !baseOk { - return - } - - if offset < 0 || offset > 32760 { - offset = 0 - } - - // STR (FP): 1 11 11101 00 imm12 Rn Rt - instr := uint32(0xFD000000) | - (uint32(offset/8) << 10) | // imm12 (scaled by 8) - (uint32(baseReg.Encoding&31) << 5) | // Rn (base) - uint32(srcReg.Encoding&31) // Rt (source FP) - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) MovMemToXmm(dst, base string, offset int32) { - // ARM64: Load FP register from memory - // LDR Dt, [Xn, #offset] - dstReg, dstOk := arm64Registers[dst] - baseReg, baseOk := arm64Registers[base] - if !dstOk || !baseOk { - return - } - - if offset < 0 || offset > 32760 { - offset = 0 - } - - // LDR (FP): 1 11 11101 01 imm12 Rn Rt - instr := uint32(0xFD400000) | - (uint32(offset/8) << 10) | // imm12 - (uint32(baseReg.Encoding&31) << 5) | // Rn (base) - uint32(dstReg.Encoding&31) // Rt (dest FP) - - a.writeInstruction(instr) -} - -func (a *ARM64Backend) MovRegToXmm(dst, src string) { - // ARM64: Move integer register to FP/SIMD register - // FMOV Dd, Xn (convert GPR to FP register) - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // FMOV Dd, Xn: 0x9e 0x67 0x00 Xn Dd - // Format: 0 00 11110 01 1 00111 000000 Rn Rd - instr := uint32(0x9e670000) | - (uint32(srcReg.Encoding&31) << 5) | // Rn (source GPR) - uint32(dstReg.Encoding&31) // Rd (dest FP) - - a.writer.(*BufferWrapper).Write(uint8(instr & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 8) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 16) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 24) & 0xFF)) -} - -func (a *ARM64Backend) MovXmmToReg(dst, src string) { - // ARM64: Move FP/SIMD register to integer register - // FMOV Xd, Dn (convert FP to GPR) - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // FMOV Xd, Dn: 0x9e 0x66 0x00 Dn Xd - // Format: 0 00 11110 01 1 00110 000000 Rn Rd - instr := uint32(0x9e660000) | - (uint32(srcReg.Encoding&31) << 5) | // Rn (source FP) - uint32(dstReg.Encoding&31) // Rd (dest GPR) - - a.writer.(*BufferWrapper).Write(uint8(instr & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 8) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 16) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 24) & 0xFF)) -} - -func (a *ARM64Backend) Cvtsi2sd(dst, src string) { - // ARM64: Convert signed integer to double - // SCVTF Dd, Xn (signed convert float) - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // SCVTF Dd, Xn: 0x9e 0x62 0x00 Xn Dd - // Format: 0 00 11110 01 1 00010 000000 Rn Rd - instr := uint32(0x9e620000) | - (uint32(srcReg.Encoding&31) << 5) | // Rn (source int) - uint32(dstReg.Encoding&31) // Rd (dest fp) - - a.writer.(*BufferWrapper).Write(uint8(instr & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 8) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 16) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 24) & 0xFF)) -} - -func (a *ARM64Backend) Cvttsd2si(dst, src string) { - // ARM64: Convert double to signed integer (truncate) - // FCVTZS Xd, Dn (floating-point convert to signed, zero round) - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // FCVTZS Xd, Dn: 0x9e 0x78 0x00 Dn Xd - // Format: 0 00 11110 01 1 11000 000000 Rn Rd - instr := uint32(0x9e780000) | - (uint32(srcReg.Encoding&31) << 5) | // Rn (source fp) - uint32(dstReg.Encoding&31) // Rd (dest int) - - a.writer.(*BufferWrapper).Write(uint8(instr & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 8) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 16) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 24) & 0xFF)) -} - -func (a *ARM64Backend) AddpdXmm(dst, src string) { - // ARM64: FADD Dd, Dn, Dm (double-precision add) - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // FADD Dd, Dd, Dm: 0x1e 0x60 0x28 Dm Dd - // Format: 0 00 11110 01 1 Rm 001010 Rn Rd - instr := uint32(0x1e602800) | - (uint32(srcReg.Encoding&31) << 16) | // Rm (src) - (uint32(dstReg.Encoding&31) << 5) | // Rn (dst, also used as first operand) - uint32(dstReg.Encoding&31) // Rd (dst) - - a.writer.(*BufferWrapper).Write(uint8(instr & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 8) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 16) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 24) & 0xFF)) -} - -func (a *ARM64Backend) SubpdXmm(dst, src string) { - // ARM64: FSUB Dd, Dn, Dm (double-precision subtract) - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // FSUB Dd, Dd, Dm: 0x1e 0x60 0x38 Dm Dd - // Format: 0 00 11110 01 1 Rm 001110 Rn Rd - instr := uint32(0x1e603800) | - (uint32(srcReg.Encoding&31) << 16) | // Rm - (uint32(dstReg.Encoding&31) << 5) | // Rn - uint32(dstReg.Encoding&31) // Rd - - a.writer.(*BufferWrapper).Write(uint8(instr & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 8) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 16) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 24) & 0xFF)) -} - -func (a *ARM64Backend) MulpdXmm(dst, src string) { - // ARM64: FMUL Dd, Dn, Dm (double-precision multiply) - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // FMUL Dd, Dd, Dm: 0x1e 0x60 0x08 Dm Dd - // Format: 0 00 11110 01 1 Rm 000010 Rn Rd - instr := uint32(0x1e600800) | - (uint32(srcReg.Encoding&31) << 16) | // Rm - (uint32(dstReg.Encoding&31) << 5) | // Rn - uint32(dstReg.Encoding&31) // Rd - - a.writer.(*BufferWrapper).Write(uint8(instr & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 8) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 16) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 24) & 0xFF)) -} - -func (a *ARM64Backend) DivpdXmm(dst, src string) { - // ARM64: FDIV Dd, Dn, Dm (double-precision divide) - dstReg, dstOk := arm64Registers[dst] - srcReg, srcOk := arm64Registers[src] - if !dstOk || !srcOk { - return - } - - // FDIV Dd, Dd, Dm: 0x1e 0x60 0x18 Dm Dd - // Format: 0 00 11110 01 1 Rm 000110 Rn Rd - instr := uint32(0x1e601800) | - (uint32(srcReg.Encoding&31) << 16) | // Rm - (uint32(dstReg.Encoding&31) << 5) | // Rn - uint32(dstReg.Encoding&31) // Rd - - a.writer.(*BufferWrapper).Write(uint8(instr & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 8) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 16) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 24) & 0xFF)) -} - -func (a *ARM64Backend) Ucomisd(reg1, reg2 string) { - // ARM64: FCMP Dn, Dm (floating-point compare) - // This sets condition flags like x86's UCOMISD - reg1FP, reg1Ok := arm64Registers[reg1] - reg2FP, reg2Ok := arm64Registers[reg2] - if !reg1Ok || !reg2Ok { - return - } - - // FCMP Dn, Dm: 0x1e 0x60 0x20 Dm Dn - // Format: 0 00 11110 01 1 Rm 001000 Rn 0 0000 - instr := uint32(0x1e602000) | - (uint32(reg2FP.Encoding&31) << 16) | // Rm - (uint32(reg1FP.Encoding&31) << 5) // Rn - - a.writer.(*BufferWrapper).Write(uint8(instr & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 8) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 16) & 0xFF)) - a.writer.(*BufferWrapper).Write(uint8((instr >> 24) & 0xFF)) -} - -// ===== System Calls ===== - -func (a *ARM64Backend) Syscall() { - // SVC #0 - instr := uint32(0xD4000001) - a.writeInstruction(instr) -} diff --git a/arm64_codegen.go b/arm64_codegen.go deleted file mode 100644 index 6139b64..0000000 --- a/arm64_codegen.go +++ /dev/null @@ -1,8980 +0,0 @@ -// Completion: 95% - ARM64 codegen complete, all core features implemented -package main - -import ( - "fmt" - "maps" - "os" - "slices" - "strconv" - "strings" - "unsafe" -) - -// ARM64CodeGen handles ARM64 code generation for macOS -type ARM64CodeGen struct { - out *ARM64Out - eb *ExecutableBuilder - stackVars map[string]int // variable name -> stack offset from fp - mutableVars map[string]bool // variable name -> is mutable - lambdaVars map[string]bool // variable name -> is callable function (has signature) - varTypes map[string]string // variable name -> type (for type tracking) - stackSize int // current stack size - stackFrameSize uint64 // total stack frame size allocated in prologue - stringCounter int // counter for string labels - stringInterns map[string]string // string value -> label (for string interning) - labelCounter int // counter for jump labels - activeLoops []ARM64LoopInfo // stack of active loops for break/continue - lambdaFuncs []ARM64LambdaFunc // list of lambda functions to generate - lambdaCounter int // counter for lambda names - currentLambda *ARM64LambdaFunc // current lambda being compiled (for recursion) - cConstants map[string]*CHeaderConstants // C constants from imports - currentArena int // Arena depth (0=none, 1=first arena, 2=nested, etc.) - usesArenas bool // Track if program uses any arena blocks - currentAssignName string // Name of variable being assigned (for lambda self-reference) - deferredExprs [][]Expression // Stack of deferred expressions per scope (LIFO order) - globalSlots map[string]int // module-global var name -> slot index in the x28 globals array - globalMutable map[string]bool // module-global var name -> is mutable - cstructs map[string]*CStructDecl // cstruct name -> declaration (field layout) - varCStructType map[string]string // variable name -> cstruct type it points at - varListElemType map[string]string // list-variable name -> cstruct type of its elements (so `xs[i].field` works) - funcReturnsListElem map[string]string // lambda-var name -> cstruct elem type of the list its body returns - closurePtrOffset int32 // frame offset where the current lambda's closure pointer (x9) is saved - boxedVars map[string]bool // names that live in a heap cell in the current scope (captured-and-mutated locals) - returnsClosure map[string]bool // lambda-var name -> its body evaluates to a closure (so `g = f(x); g()` invokes it) - funcReturnsCStruct map[string]string // lambda-var name -> cstruct type its body returns (so `p = vadd(a,b)` is typed) - ctypes *CStructTypes // shared, platform-independent cstruct type oracle (over the maps above) - fpDepth int // expression-stack depth: BinaryExpr keeps its left operand in d24+fpDepth instead of spilling to memory, when the right operand has no call -} - -// ARM64LambdaFunc represents a lambda function for ARM64 -type ARM64LambdaFunc struct { - Name string - Params []string - ParamCStructTypes map[string]string // param name -> cstruct type (from `(a as V)`) - Body Expression - BodyStart int // Position where lambda body code starts (for tail recursion) - FuncStart int // Position where function starts (including prologue, for recursion) - VarName string // Variable name this lambda is assigned to (for recursion) - IsRecursive bool // Whether this lambda calls itself recursively - Captures []string // Names captured from the enclosing scope (closure env order) - // BoxedCaptures marks which of Captures are boxed: the env slot holds a heap - // cell pointer (by reference) rather than a value, so the lambda can mutate - // the captured variable and have it persist (the make_counter pattern). - BoxedCaptures map[string]bool -} - -// ARM64LoopInfo tracks information about an active loop -type ARM64LoopInfo struct { - Label int // Loop label (@1, @2, @3, etc.) - StartPos int // Code position of loop start (condition check) - ContinuePos int // Code position for continue (increment step) - EndPatches []int // Positions that need to be patched to jump to loop end - ContinuePatches []int // Positions that need to be patched to jump to continue position - IteratorOffset int // Stack offset for iterator variable - IndexOffset int // Stack offset for index counter (list loops only) - UpperBoundOffset int // Stack offset for limit (range) or length (list) - ListPtrOffset int // Stack offset for list pointer (list loops only) - IsRangeLoop bool // True for range loops, false for list loops -} - -// NewARM64CodeGen creates a new ARM64 code generator -func NewARM64CodeGen(eb *ExecutableBuilder, cConstants map[string]*CHeaderConstants) *ARM64CodeGen { - // Use the target from ExecutableBuilder (which has the correct OS) - acg := &ARM64CodeGen{ - out: &ARM64Out{out: NewOut(eb.target, eb.TextWriter(), eb)}, - eb: eb, - stackVars: make(map[string]int), - mutableVars: make(map[string]bool), - lambdaVars: make(map[string]bool), - varTypes: make(map[string]string), - stackSize: 0, - stringCounter: 0, - stringInterns: make(map[string]string), - labelCounter: 0, - cConstants: cConstants, - globalSlots: make(map[string]int), - globalMutable: make(map[string]bool), - cstructs: make(map[string]*CStructDecl), - varCStructType: make(map[string]string), - boxedVars: make(map[string]bool), - returnsClosure: make(map[string]bool), - funcReturnsCStruct: make(map[string]string), - varListElemType: make(map[string]string), - funcReturnsListElem: make(map[string]string), - } - // The cstruct type oracle reads/writes the same map instances (maps are - // references), so registrations by either side are mutually visible. - acg.ctypes = &CStructTypes{ - Decls: acg.cstructs, - VarType: acg.varCStructType, - FuncReturns: acg.funcReturnsCStruct, - VarListElem: acg.varListElemType, - FuncReturnsListElem: acg.funcReturnsListElem, - } - return acg -} - -// cStructTypeOf returns the cstruct type name that an expression evaluates to, -// or "". It delegates to the shared, platform-independent oracle (CStructTypes) -// so ARM64 and every other backend infer identical types. -func (acg *ARM64CodeGen) cStructTypeOf(expr Expression) string { - return acg.ctypes.TypeOf(expr) -} - -// ifStmtToMatchExpr lowers an `if`/`elif`/`else` statement to the value-producing -// MatchExpr the parser would have built for the same condition in expression -// position. Used to give a trailing `if` (the last statement of a value block) its -// value, instead of falling through to a 0 result. Each branch body becomes a -// BlockExpr (evaluating to its last expression); the `else` becomes the default. -func ifStmtToMatchExpr(s *IfStmt) *MatchExpr { - clauses := make([]*MatchClause, 0, len(s.Branches)) - for _, br := range s.Branches { - clauses = append(clauses, &MatchClause{Guard: br.Condition, Result: &BlockExpr{Statements: br.Body}}) - } - var def Expression = &NumberExpr{Value: 0.0} - if len(s.ElseBody) > 0 { - def = &BlockExpr{Statements: s.ElseBody} - } - return &MatchExpr{Condition: &NumberExpr{Value: 1.0}, Clauses: clauses, DefaultExpr: def} -} - -// ifStmtCStructType returns the cstruct type a trailing `if`/`elif`/`else` -// statement evaluates to. Delegates to the shared oracle. -func (acg *ARM64CodeGen) ifStmtCStructType(s *IfStmt) string { - return acg.ctypes.ifStmtType(s) -} - -// listElemCStructTypeOf infers the cstruct type of the ELEMENTS of a list-valued -// expression, or "". Delegates to the shared oracle. -func (acg *ARM64CodeGen) listElemCStructTypeOf(expr Expression) string { - return acg.ctypes.listElemTypeOf(expr) -} - -// lambdaReturnsListElemType returns the cstruct element type of the list a lambda -// body evaluates to, or "". Delegates to the shared oracle. -func (acg *ARM64CodeGen) lambdaReturnsListElemType(body Expression) string { - return acg.ctypes.LambdaReturnsListElem(body) -} - -// lambdaReturnCStructType returns the cstruct type a lambda body evaluates to -// (its last expression), or "". Delegates to the shared oracle. -func (acg *ARM64CodeGen) lambdaReturnCStructType(body Expression) string { - return acg.ctypes.LambdaReturnType(body) -} - -// registerBlockLocalCStructTypes records the cstruct type of each struct-valued -// local in a block into the shared type map. Delegates to the shared oracle. -func (acg *ARM64CodeGen) registerBlockLocalCStructTypes(stmts []Statement) []string { - return acg.ctypes.RegisterBlockLocals(stmts) -} - -// isLambdaValue reports whether expr is a lambda literal in any of its forms. -func isLambdaValue(expr Expression) bool { - switch expr.(type) { - case *LambdaExpr, *PatternLambdaExpr, *MultiLambdaExpr: - return true - } - return false -} - -// lambdaBodyReturnsClosure reports whether a lambda whose body is `body` -// evaluates to a closure — its value (the body itself, or the last expression of -// a block) is a lambda. This lets `g = make_counter(0)` mark g as callable so -// `g()` invokes the returned closure rather than yielding its value. -func lambdaBodyReturnsClosure(body Expression) bool { - if isLambdaValue(body) { - return true - } - if blk, ok := body.(*BlockExpr); ok && len(blk.Statements) > 0 { - switch s := blk.Statements[len(blk.Statements)-1].(type) { - case *ExpressionStmt: - return isLambdaValue(s.Expr) - case *JumpStmt: - return s.Value != nil && isLambdaValue(s.Value) - } - } - return false -} - -// markIfClosure records that variable `name` holds a callable closure when its -// initializer is a lambda, a call to a closure-returning function, or an alias of -// another closure variable. Tracking this is what makes a closure stored in a -// variable invocable with zero arguments (e.g. counter()). -func (acg *ARM64CodeGen) markIfClosure(name string, value Expression) { - switch v := value.(type) { - case *LambdaExpr: - acg.lambdaVars[name] = true - acg.returnsClosure[name] = lambdaBodyReturnsClosure(v.Body) - if ct := acg.lambdaReturnCStructType(v.Body); ct != "" { - acg.funcReturnsCStruct[name] = ct - } - if et := acg.lambdaReturnsListElemType(v.Body); et != "" { - acg.funcReturnsListElem[name] = et - } - case *PatternLambdaExpr, *MultiLambdaExpr: - acg.lambdaVars[name] = true - case *CallExpr: - if acg.returnsClosure[v.Function] { - acg.lambdaVars[name] = true - } - case *IdentExpr: - if acg.lambdaVars[v.Name] { - acg.lambdaVars[name] = true - acg.returnsClosure[name] = acg.returnsClosure[v.Name] - } - } -} - -// boxedCaptureVars returns the names that the function whose body is `body` must -// heap-box: variables that some nested lambda updates via `<-` and captures from -// the enclosing scope. Boxing gives them a shared heap cell so a closure's -// mutation persists and is visible across calls (the canonical make_counter -// pattern). Read-only captures are unaffected (still captured by value). -func boxedCaptureVars(body Expression) map[string]bool { - out := make(map[string]bool) - forEachChildLambda(body, func(lam *LambdaExpr) { - bound := make(map[string]bool) - for _, p := range lam.Params { - bound[p] = true - } - collectUpdatedFreeVars(lam.Body, bound, out) - }) - return out -} - -// forEachChildLambda walks expr and calls fn for each lambda nested within it, -// without descending into the lambdas' own bodies (so fn receives the immediate -// child lambdas; deeper nesting is handled by the recursion in -// collectUpdatedFreeVars, which tracks the correct bound names per level). -func forEachChildLambda(expr Expression, fn func(*LambdaExpr)) { - switch e := expr.(type) { - case *LambdaExpr: - fn(e) - case *BlockExpr: - for _, stmt := range e.Statements { - switch s := stmt.(type) { - case *AssignStmt: - forEachChildLambda(s.Value, fn) - case *ExpressionStmt: - forEachChildLambda(s.Expr, fn) - case *JumpStmt: - if s.Value != nil { - forEachChildLambda(s.Value, fn) - } - } - } - case *BinaryExpr: - forEachChildLambda(e.Left, fn) - forEachChildLambda(e.Right, fn) - case *CallExpr: - for _, a := range e.Args { - forEachChildLambda(a, fn) - } - case *MatchExpr: - forEachChildLambda(e.Condition, fn) - for _, c := range e.Clauses { - if c.Guard != nil { - forEachChildLambda(c.Guard, fn) - } - forEachChildLambda(c.Result, fn) - } - if e.DefaultExpr != nil { - forEachChildLambda(e.DefaultExpr, fn) - } - case *JumpExpr: - if e.Value != nil { - forEachChildLambda(e.Value, fn) - } - } -} - -// collectUpdatedFreeVars records, into out, the names that expr updates via `<-` -// that are not bound within expr (i.e. free variables it mutates). bound tracks -// names introduced by params and `:=` declarations as the walk descends; nested -// lambdas extend bound with their own params. -func collectUpdatedFreeVars(expr Expression, bound map[string]bool, out map[string]bool) { - switch e := expr.(type) { - case *BlockExpr: - local := make(map[string]bool) - maps.Copy(local, bound) - for _, stmt := range e.Statements { - switch s := stmt.(type) { - case *AssignStmt: - collectUpdatedFreeVars(s.Value, local, out) - if s.IsUpdate { - if !local[s.Name] { - out[s.Name] = true - } - } else { - local[s.Name] = true - } - case *ExpressionStmt: - collectUpdatedFreeVars(s.Expr, local, out) - case *JumpStmt: - if s.Value != nil { - collectUpdatedFreeVars(s.Value, local, out) - } - } - } - case *LambdaExpr: - inner := make(map[string]bool) - maps.Copy(inner, bound) - for _, p := range e.Params { - inner[p] = true - } - collectUpdatedFreeVars(e.Body, inner, out) - case *BinaryExpr: - collectUpdatedFreeVars(e.Left, bound, out) - collectUpdatedFreeVars(e.Right, bound, out) - case *CallExpr: - for _, a := range e.Args { - collectUpdatedFreeVars(a, bound, out) - } - case *MatchExpr: - collectUpdatedFreeVars(e.Condition, bound, out) - for _, c := range e.Clauses { - if c.Guard != nil { - collectUpdatedFreeVars(c.Guard, bound, out) - } - collectUpdatedFreeVars(c.Result, bound, out) - } - if e.DefaultExpr != nil { - collectUpdatedFreeVars(e.DefaultExpr, bound, out) - } - case *JumpExpr: - if e.Value != nil { - collectUpdatedFreeVars(e.Value, bound, out) - } - } -} - -// isCaptureName reports whether name is captured by the lambda currently being -// compiled (used to resolve multi-level captures). -func (acg *ARM64CodeGen) isCaptureName(name string) bool { - if acg.currentLambda == nil { - return false - } - return slices.Contains(acg.currentLambda.Captures, name) -} - -// computeCaptures returns the enclosing-scope variables a lambda body references -// that must be captured by value into its closure env: free variables (excluding -// the lambda's own params) that are neither mutable module globals (shared via -// x28) nor the lambda's self-reference, and that are resolvable in the current -// scope (a stack variable, or itself a capture one level up). Sorted for -// deterministic env layout. -func (acg *ARM64CodeGen) computeCaptures(body Expression, params []string) []string { - free := make(map[string]bool) - collectCapturedVariables(body, params, free) - var caps []string - for name := range free { - if _, isGlobal := acg.globalSlots[name]; isGlobal { - continue - } - if name == acg.currentAssignName { - continue - } - if _, inScope := acg.stackVars[name]; inScope { - caps = append(caps, name) - } else if acg.isCaptureName(name) { - caps = append(caps, name) - } - } - slices.Sort(caps) - return caps -} - -// arm64UnaryFPOps maps single-arg math functions to the ARM64 double-precision -// FP instruction (operating d0 -> d0) that computes them directly, avoiding a -// libm call. Encodings: fsqrt d0,d0 / fabs d0,d0 / frint{m,p,z,n} d0,d0. -var arm64UnaryFPOps = map[string]uint32{ - "sqrt": 0x1e61c000, // fsqrt d0, d0 - "fabs": 0x1e60c000, // fabs d0, d0 - "abs": 0x1e60c000, // fabs d0, d0 (Tim's float abs) - "floor": 0x1e654000, // frintm d0, d0 (round toward -inf) - "ceil": 0x1e64c000, // frintp d0, d0 (round toward +inf) - "trunc": 0x1e65c000, // frintz d0, d0 (round toward zero) - // `round` is intentionally NOT here: frintn is ties-to-even while C round() is - // ties-away-from-zero, so it keeps using the libm call for exact semantics. -} - -// emitFmovD emits `fmov d{dst}, d{src}` (register move between FP registers). -func (acg *ARM64CodeGen) emitFmovD(dst, src uint32) { - instr := uint32(0x1e604000) | (src << 5) | dst - acg.out.out.writer.WriteBytes([]byte{byte(instr), byte(instr >> 8), byte(instr >> 16), byte(instr >> 24)}) -} - -// exprHasCall conservatively reports whether compiling expr emits any `bl` -// (function call, libm, map access via a runtime helper, struct construction that -// may spill to malloc, match, etc.). It is used to decide whether the expression -// register stack (d24-d31, caller-saved) is safe to use: a call would clobber -// those registers, so when the right operand of a binary op has a call we fall -// back to a memory spill instead. -func (acg *ARM64CodeGen) exprHasCall(expr Expression) bool { - switch e := expr.(type) { - case *NumberExpr, *StringExpr, *IdentExpr: - return false - case *BinaryExpr: - return acg.exprHasCall(e.Left) || acg.exprHasCall(e.Right) - case *UnaryExpr: - return acg.exprHasCall(e.Operand) - case *CastExpr: - return acg.exprHasCall(e.Expr) - case *FMAExpr: - return acg.exprHasCall(e.A) || acg.exprHasCall(e.B) || acg.exprHasCall(e.C) - case *FieldAccessExpr: - // `Ctor(...).field` folds to the field argument (no call there). - if ctor, ok := e.Object.(*CallExpr); ok { - if decl, isS := acg.cstructs[ctor.Function]; isS && len(ctor.Args) == len(decl.Fields) { - for i := range decl.Fields { - if decl.Fields[i].Name == e.FieldName { - return acg.exprHasCall(ctor.Args[i]) - } - } - } - } - // A cstruct field is a plain typed load; a map field uses a helper call. - if acg.cstructForExpr(e.Object) != nil { - return acg.exprHasCall(e.Object) - } - return true - default: - return true - } -} - -// emitLoadBoxCellPtr loads the heap cell pointer of boxed variable `name` into -// the given GP register. The pointer lives either in the variable's stack slot -// (a boxed local) or in the closure env (a boxed capture, by reference). -func (acg *ARM64CodeGen) emitLoadBoxCellPtr(name, reg string) error { - if so, ok := acg.stackVars[name]; ok && so != -1 { - return acg.out.LdrImm64(reg, "x29", int32(16+so-8)) - } - if acg.currentLambda != nil { - if idx := slices.Index(acg.currentLambda.Captures, name); idx >= 0 { - if err := acg.out.LdrImm64(reg, "x29", acg.closurePtrOffset); err != nil { - return err - } - return acg.out.LdrImm64(reg, reg, int32(8+idx*8)) - } - } - return fmt.Errorf("boxed variable '%s' not found in scope", name) -} - -// emitStoreBoxedVar stores the value currently in d0 through the heap cell of -// boxed variable `name` (used for `<-` updates of a boxed local or capture). -func (acg *ARM64CodeGen) emitStoreBoxedVar(name string) error { - if err := acg.emitLoadBoxCellPtr(name, "x10"); err != nil { - return err - } - return acg.out.StrImm64Double("d0", "x10", 0) -} - -// emitBoxedDeclaration allocates a heap cell for a new boxed local, stores the -// value in d0 into it, and writes the cell pointer into the variable's stack -// slot at slotOffset (relative to x29). The slot then holds a shared pointer -// that nested closures capture by reference. -func (acg *ARM64CodeGen) emitBoxedDeclaration(slotOffset int32) error { - // Spill the value across the malloc call (malloc may clobber d0). - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64Double("d0", "sp", 0); err != nil { - return err - } - if err := acg.out.MovImm64("x0", 8); err != nil { - return err - } - if err := acg.eb.GenerateCallInstruction("_tim_malloc"); err != nil { - return err - } - if err := acg.out.LdrImm64Double("d0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - if err := acg.out.StrImm64Double("d0", "x0", 0); err != nil { // cell[0] = value - return err - } - return acg.out.StrImm64("x0", "x29", slotOffset) // slot = cell pointer -} - -// collectGlobals identifies module-level variables that are captured by a lambda -// and promotes them to globals stored in a heap array addressed via x28. This is -// how closures over module state (the common case: top-level `state`, counters, -// configuration) work on ARM64 — a captured global is a single shared slot that -// both the module body and any lambda read/write by reference. Per-invocation -// capture of an enclosing lambda's parameters (e.g. make_adder) is not handled -// here and remains a known limitation. -func (acg *ARM64CodeGen) collectGlobals(program *Program) { - // The module body is the program's top-level statements; if the program is - // auto-wrapped as `main = { ... }`, the block's statements are module-level - // too. Collect both so captured module vars are found in either shape. - var moduleStmts []Statement - moduleStmts = append(moduleStmts, program.Statements...) - for _, stmt := range program.Statements { - if as, ok := stmt.(*AssignStmt); ok && as.Name == "main" { - if lam, ok := as.Value.(*LambdaExpr); ok { - if blk, ok := lam.Body.(*BlockExpr); ok { - moduleStmts = append(moduleStmts, blk.Statements...) - } - } - } - } - - // Names that any lambda defined at module level captures from the enclosing - // scope. - captured := make(map[string]bool) - for _, stmt := range moduleStmts { - as, ok := stmt.(*AssignStmt) - if !ok || as.IsUpdate { - continue - } - if lam, ok := as.Value.(*LambdaExpr); ok { - collectCapturedVariables(lam.Body, lam.Params, captured) - } - } - - // A module var that some lambda captures becomes a shared global slot - // (captured by reference). Module-level lambdas may be compiled before the - // vars they reference (statement ordering), so these can't be snapshotted by - // value at definition the way an enclosing lambda's parameters are — the - // global slot gives them a stable home reachable from any lambda body. - for _, stmt := range moduleStmts { - as, ok := stmt.(*AssignStmt) - if !ok || as.IsUpdate { - continue - } - if captured[as.Name] { - if _, seen := acg.globalSlots[as.Name]; !seen { - acg.globalSlots[as.Name] = len(acg.globalSlots) - acg.globalMutable[as.Name] = as.Mutable - } - } - } -} - -// CompileProgram compiles a Tim program to ARM64 -func (acg *ARM64CodeGen) CompileProgram(program *Program) error { - // Register cstruct layouts up front so constructors, field access, and - // return-type inference work regardless of statement order (the per-stmt - // CStructDecl case still records any declared inline as well). - maps.Copy(acg.cstructs, program.CStructs) - - // Initialize arena tracking - acg.currentArena = 1 // Start at 1 to enable default global arena - - // Push defer scope for program-level defers - acg.pushDeferScope() - - // PHASE 1: Compile program to calculate needed stack size - // Save the current text buffer position to patch prologue later - prologueStart := acg.eb.text.Len() - - // Emit placeholder prologue (we'll patch this later with correct stack size) - // Reserve space for: sub sp, sp, #SIZE (4 bytes) - acg.out.out.writer.WriteBytes([]byte{0xff, 0x43, 0x04, 0xd1}) // placeholder: sub sp, sp, #0x110 - // Save frame pointer and link register - if err := acg.out.StrImm64("x29", "sp", 0); err != nil { - return err - } - if err := acg.out.StrImm64("x30", "sp", 8); err != nil { - return err - } - // Set frame pointer - if err := acg.out.AddImm64("x29", "sp", 0); err != nil { - return err - } - - // General-purpose heap backing store, reserved with a raw mmap syscall so no - // libc/libSystem allocator is linked. _tim_malloc bump-allocates from this - // region for lists, closures, boxed vars and the like. x25 = current top, - // x24 = limit; both callee-saved so they survive every libc/SDL/lambda call. - // libc is pulled in only for genuine C-FFI (c.malloc, SDL, ...), keeping a - // pure-Tim program free of any dynamic dependency. Emitted unconditionally so - // _tim_malloc is always safe to call; the reservation is overcommitted (pages - // are lazy) so an unused heap costs nothing. - if err := acg.emitMmapReserve(genHeapSize); err != nil { - return err - } - if err := acg.out.MovReg64("x25", "x0"); err != nil { // top = base - return err - } - if err := acg.out.MovImm64("x9", genHeapSize); err != nil { - return err - } - if err := acg.out.AddReg64("x24", "x25", "x9"); err != nil { // limit = base + size - return err - } - - // Identify module-level globals captured by lambdas and, if any, allocate a - // heap array for them whose base lives in x28 (callee-saved, so it survives - // every call and is visible from lambda bodies). See collectGlobals. - acg.collectGlobals(program) - if len(acg.globalSlots) > 0 { - if err := acg.out.MovImm64("x0", uint64(len(acg.globalSlots)*8)); err != nil { - return err - } - if err := acg.eb.GenerateCallInstruction("_tim_malloc"); err != nil { - return err - } - if err := acg.out.MovReg64("x28", "x0"); err != nil { - return err - } - } - - // Bump arena for cstruct values: x27 = current top, x26 = limit. cstruct - // constructors bump x27 instead of calling malloc, and `arena { }` frees a - // scope's allocations by restoring x27. The buffer is overcommitted (pages are - // lazy), so the reservation is cheap. x26/x27 are callee-saved, so they - // survive every libc/SDL/lambda call. Only set up when the program uses - // cstructs (the only bump-arena consumer). - if len(program.CStructs) > 0 { - if err := acg.emitMmapReserve(arenaBumpSize); err != nil { - return err - } - if err := acg.out.MovReg64("x27", "x0"); err != nil { // top = base - return err - } - if err := acg.out.MovImm64("x1", arenaBumpSize); err != nil { - return err - } - if err := acg.out.AddReg64("x26", "x27", "x1"); err != nil { // limit = base + size - return err - } - } - - // Compile each statement - for _, stmt := range program.Statements { - if err := acg.compileStatement(stmt); err != nil { - return err - } - } - - // Evaluate main (if it exists) to get the exit code - // main can be a direct value (main = 42) or a function (main = { 42 }) - if _, exists := acg.stackVars["main"]; exists { - // main exists - check if it's a lambda/function or a direct value - if acg.lambdaVars["main"] { - // main is a lambda/function - call it with no arguments - if VerboseMode { - debugf("DEBUG: Calling main function for exit code\n") - } - if err := acg.compileExpression(&CallExpr{Function: "main", Args: []Expression{}}); err != nil { - return err - } - } else { - // main is a direct value - just load it - if VerboseMode { - debugf("DEBUG: Loading main value for exit code\n") - } - if err := acg.compileExpression(&IdentExpr{Name: "main"}); err != nil { - return err - } - } - // Result is in d0 (float64) - if VerboseMode { - debugf("DEBUG: Main expression compiled, converting to int32\n") - } - } else { - // No main - use exit code 0 - if VerboseMode { - debugf("DEBUG: No main variable found, using exit code 0\n") - } - // fmov d0, xzr (d0 = 0.0) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x67, 0x9e}) - } - - // Pop defer scope and execute deferred expressions - if err := acg.popDeferScope(); err != nil { - return err - } - - // PHASE 2: Calculate actual stack frame size needed - // Stack frame = 16 bytes (saved fp+lr) + acg.stackSize (local vars) + padding - // Round up to 16-byte alignment - if VerboseMode { - debugf("DEBUG: CompileProgram finished, acg.stackSize = %d bytes\n", acg.stackSize) - } - actualStackSize := uint64((16 + acg.stackSize + 15) &^ 15) - acg.stackFrameSize = actualStackSize - if VerboseMode { - debugf("DEBUG: Calculated actualStackSize = %d bytes (0x%x)\n", actualStackSize, actualStackSize) - } - - // PHASE 3: Patch the prologue with correct stack size - if actualStackSize > 0xFFF { - // Stack frame too large for immediate encoding - // This requires using a different instruction sequence - if VerboseMode { - fmt.Fprintf(os.Stderr, "Warning: Stack frame size %d exceeds 12-bit immediate limit\n", actualStackSize) - } - // For now, cap at maximum encodable value - actualStackSize = 0xFFF - acg.stackFrameSize = actualStackSize - } - - // Patch the SUB sp instruction at prologueStart - // ARM64 SUB immediate encoding: 0xd10003ff | (imm12 << 10) - textBytes := acg.eb.text.Bytes() - subInstr := uint32(0xd10003ff) | (uint32(actualStackSize) << 10) - textBytes[prologueStart] = byte(subInstr) - textBytes[prologueStart+1] = byte(subInstr >> 8) - textBytes[prologueStart+2] = byte(subInstr >> 16) - textBytes[prologueStart+3] = byte(subInstr >> 24) - - // Function epilogue (if no explicit exit) - // Convert d0 (float64 result from main) to w0 (int32 exit code) - // fcvtzs w0, d0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x1e}) - - // For static Linux builds, exit with syscall instead of returning - if acg.eb.target.OS() == OSLinux && !acg.eb.useDynamicLinking { - // mov x8, #93 (sys_exit on ARM64 Linux) - acg.out.out.writer.WriteBytes([]byte{0xa8, 0x0b, 0x80, 0xd2}) - // svc #0 - acg.out.out.writer.WriteBytes([]byte{0x01, 0x00, 0x00, 0xd4}) - // Don't return here - continue to generate lambdas and helpers - } else { - // Dynamic builds or macOS: restore frame and return - if err := acg.out.LdrImm64("x30", "sp", 8); err != nil { - return err - } - if err := acg.out.LdrImm64("x29", "sp", 0); err != nil { - return err - } - if err := acg.out.AddImm64("sp", "sp", uint32(acg.stackFrameSize)); err != nil { - return err - } - if err := acg.out.Return("x30"); err != nil { - return err - } - } - - if VerboseMode { - debugf("DEBUG: About to generate lambda functions (count=%d)\n", len(acg.lambdaFuncs)) - } - - // Generate lambda functions after main program - if err := acg.generateLambdaFunctions(); err != nil { - return err - } - - if VerboseMode { - debugf("DEBUG: Finished generating lambda functions\n") - } - - // Generate runtime helper functions - if err := acg.generateRuntimeHelpers(); err != nil { - return err - } - - if VerboseMode { - debugf("DEBUG: CompileProgram completed successfully\n") - } - - return nil -} - -// compileStatement compiles a single statement -func (acg *ARM64CodeGen) compileStatement(stmt Statement) error { - switch s := stmt.(type) { - case *ExpressionStmt: - // Handle PostfixExpr as a statement (x++, x--) - if postfix, ok := s.Expr.(*PostfixExpr); ok { - return acg.compilePostfixStmt(postfix) - } - return acg.compileExpression(s.Expr) - case *AssignStmt: - return acg.compileAssignment(s) - case *LoopStmt: - return acg.compileLoopStatement(s) - case *WhileStmt: - return acg.compileWhileStatement(s) - case *CStructDecl: - // Cstruct declarations generate no runtime code, but we record the field - // layout so field access (p.x) and indexed field writes (p[i] <- v) can - // emit typed loads/stores at the right offsets. - acg.cstructs[s.Name] = s - return nil - case *CImportStmt: - // C imports are handled at compile-time to populate cConstants - // No runtime code generation needed - return nil - case *ArenaStmt: - return acg.compileArenaStmt(s) - case *WithStmt: - // A with-block is transparent at runtime: the subject was already - // injected into each body call at parse time, so just run the body. - for _, bodyStmt := range s.Body { - if err := acg.compileStatement(bodyStmt); err != nil { - return err - } - } - return nil - case *DeferStmt: - // Defer statement: collect for execution at scope exit - if len(acg.deferredExprs) == 0 { - return fmt.Errorf("defer can only be used inside a function or block scope") - } - currentScope := len(acg.deferredExprs) - 1 - acg.deferredExprs[currentScope] = append(acg.deferredExprs[currentScope], s.Call) - return nil - case *SpawnStmt: - // Process spawning with fork() - // Full implementation needs process management - return fmt.Errorf("spawn statements not yet implemented in ARM64 (requires fork/exec support)") - case *RegisterAssignStmt: - // Register assignment in unsafe blocks - return acg.compileRegisterAssignment(s) - case *MemoryStore: - // Memory store operation in unsafe blocks - return acg.compileMemoryStore(s) - case *SyscallStmt: - // System call instruction - // Registers must be set up before calling syscall: - // ARM64: x8=syscall#, x0-x6=args - // svc #0 instruction - acg.out.out.writer.WriteBytes([]byte{0x01, 0x00, 0x00, 0xd4}) // svc #0 - return nil - case *JumpStmt: - return acg.compileJumpStatement(s) - case *MapUpdateStmt: - return acg.compileMapUpdate(s) - case *IfStmt: - return acg.compileIfStatement(s) - case *MultipleAssignStmt: - return acg.compileMultipleAssign(s) - default: - return fmt.Errorf("unsupported statement type for ARM64: %T", stmt) - } -} - -// compileBitCount compiles the popcount/clz/ctz bit builtins. The argument is -// converted from float64 to a 64-bit integer, the bit operation is applied, and -// the integer result is converted back to float64. -func (acg *ARM64CodeGen) compileBitCount(call *CallExpr, op string) error { - if len(call.Args) != 1 { - return fmt.Errorf("%s requires exactly 1 argument", op) - } - if err := acg.compileExpression(call.Args[0]); err != nil { - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x0", "d0"); err != nil { - return err - } - switch op { - case "popcount": - if err := acg.out.FmovGPToDouble("d1", "x0"); err != nil { - return err - } - if err := acg.out.CntVec8b("d1", "d1"); err != nil { - return err - } - if err := acg.out.AddvBytes8b("d1", "d1"); err != nil { - return err - } - if err := acg.out.FmovSingleToGP("x0", "d1"); err != nil { - return err - } - case "clz": - if err := acg.out.Clz64("x0", "x0"); err != nil { - return err - } - case "ctz": - if err := acg.out.Rbit64("x0", "x0"); err != nil { - return err - } - if err := acg.out.Clz64("x0", "x0"); err != nil { - return err - } - } - return acg.out.ScvtfInt64ToDouble("d0", "x0") -} - -// compileIfStatement compiles an if / elif / else chain. Each branch's -// condition is treated as a boolean: zero is false, anything else is true. -func (acg *ARM64CodeGen) compileIfStatement(stmt *IfStmt) error { - var endJumps []int - for _, branch := range stmt.Branches { - if err := acg.compileExpression(branch.Condition); err != nil { - return err - } - // Skip the body when the condition is 0.0. - acg.out.out.writer.WriteBytes([]byte{0xe1, 0x03, 0x67, 0x9e}) // fmov d1, xzr - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) // fcmp d0, d1 - nextJump := acg.eb.text.Len() - acg.out.BranchCond("eq", 0) - - for _, s := range branch.Body { - if err := acg.compileStatement(s); err != nil { - return err - } - } - endJump := acg.eb.text.Len() - acg.out.Branch(0) - endJumps = append(endJumps, endJump) - - nextPos := acg.eb.text.Len() - acg.patchJumpOffset(nextJump, int32(nextPos-nextJump)) - } - - for _, s := range stmt.ElseBody { - if err := acg.compileStatement(s); err != nil { - return err - } - } - - endPos := acg.eb.text.Len() - for _, j := range endJumps { - acg.patchJumpOffset(j, int32(endPos-j)) - } - return nil -} - -// compileMapUpdate compiles an in-place element update: name[index] <- value. -// Lists/maps are laid out as [count(8)][elem0(8)][elem1(8)]..., so element i -// lives at base + 8 + i*8 (matching the IndexExpr read path). -func (acg *ARM64CodeGen) compileMapUpdate(stmt *MapUpdateStmt) error { - // C-struct indexed field write: p[i] <- v writes field i (by declaration - // order) as raw typed memory. The index must be a constant. - if decl := acg.cstructForExpr(&IdentExpr{Name: stmt.MapName}); decl != nil { - idxExpr := stmt.Index - if cast, ok := idxExpr.(*CastExpr); ok { - idxExpr = cast.Expr - } - num, ok := idxExpr.(*NumberExpr) - if !ok { - return fmt.Errorf("cstruct field write requires a constant index") - } - fi := int(num.Value) - if fi < 0 || fi >= len(decl.Fields) { - return fmt.Errorf("cstruct %s has no field index %d", decl.Name, fi) - } - // Evaluate value -> d0, spill; load pointer -> x9; store. - if err := acg.compileExpression(stmt.Value); err != nil { - return err - } - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64Double("d0", "sp", 0); err != nil { - return err - } - if err := acg.compileExpression(&IdentExpr{Name: stmt.MapName}); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0x09, 0x00, 0x78, 0x9e}) // fcvtzs x9, d0 (numeric ptr) - if err := acg.out.LdrImm64Double("d0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - return acg.emitCStructFieldStore(&decl.Fields[fi]) - } - - // Evaluate the new value into d0 and stash it. - if err := acg.compileExpression(stmt.Value); err != nil { - return err - } - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64Double("d0", "sp", 0); err != nil { - return err - } - - // Load the container pointer (variable holds it as a float64) into x0, stash. - if err := acg.compileExpression(&IdentExpr{Name: stmt.MapName}); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) // fcvtzs x0, d0 - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x0", "sp", 0); err != nil { - return err - } - - // Evaluate the index into x1. - if err := acg.compileExpression(stmt.Index); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0x01, 0x00, 0x78, 0x9e}) // fcvtzs x1, d0 - - // Restore container pointer (x0); value is now at [sp,0]. - acg.out.LdrImm64("x0", "sp", 0) - acg.out.AddImm64("sp", "sp", 16) - - // Maps/strings (key,value pairs) update by key; lists update by flat index. - updType := acg.getExprType(&IdentExpr{Name: stmt.MapName}) - if updType == "map" || updType == "string" { - // x7 = address of the value slot for key x1 (0 if the key is absent). - if err := acg.emitMapValueSlotAddr(); err != nil { - return err - } - if err := acg.out.LdrImm64Double("d0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - // Skip the store when the key was not found. - skipPos := acg.eb.text.Len() - acg.out.out.writer.WriteBytes([]byte{0x07, 0x00, 0x00, 0xb4}) // cbz x7, +0 - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x00, 0x00, 0xfd}) // str d0, [x7] - skipLabel := acg.eb.text.Len() - acg.patchJumpOffset(skipPos, int32(skipLabel-skipPos)) - return nil - } - - // List: element address = x0 + 8 + index*8. - acg.out.AddImm64("x0", "x0", 8) - acg.out.out.writer.WriteBytes([]byte{0x21, 0xf0, 0x7d, 0xd3}) // lsl x1, x1, #3 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x01, 0x8b}) // add x0, x0, x1 - - // Restore the value and store it. - if err := acg.out.LdrImm64Double("d0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0xfd}) // str d0, [x0] - return nil -} - -// cstructForExpr returns the cstruct declaration for an expression that holds a -// pointer to one, else nil. Delegates to the shared, platform-independent oracle. -func (acg *ARM64CodeGen) cstructForExpr(expr Expression) *CStructDecl { - return acg.ctypes.DeclForExpr(expr) -} - -// emitLDST emits a single scaled load/store with base register x9 and Rt=0 -// (x0/w0 or d0/s0 depending on baseOp). baseOp is the opcode with imm/Rn/Rt=0. -func (acg *ARM64CodeGen) emitLDST(baseOp uint32, scale, off int) { - imm := uint32(off/scale) & 0xfff - instr := baseOp | (imm << 10) | (9 << 5) - acg.out.out.writer.WriteBytes([]byte{byte(instr), byte(instr >> 8), byte(instr >> 16), byte(instr >> 24)}) -} - -// emitCStructFieldLoad loads the field at its offset (pointer in x9) and leaves -// the value in d0 (numeric, or pointer-bits for ptr/cstr fields). -func (acg *ARM64CodeGen) emitCStructFieldLoad(f *CStructField) error { - w := acg.out.out.writer - off := f.Offset - ucvtf := []byte{0x00, 0x00, 0x63, 0x9e} // ucvtf d0, x0 - scvtf := []byte{0x00, 0x00, 0x62, 0x9e} // scvtf d0, x0 - switch f.Type { - case "uint8": - acg.emitLDST(0x39400000, 1, off) // ldrb w0 - w.WriteBytes(ucvtf) - case "int8": - acg.emitLDST(0x39800000, 1, off) // ldrsb x0 - w.WriteBytes(scvtf) - case "uint16": - acg.emitLDST(0x79400000, 2, off) // ldrh w0 - w.WriteBytes(ucvtf) - case "int16": - acg.emitLDST(0x79800000, 2, off) // ldrsh x0 - w.WriteBytes(scvtf) - case "uint32": - acg.emitLDST(0xb9400000, 4, off) // ldr w0 - w.WriteBytes(ucvtf) - case "int32": - acg.emitLDST(0xb9800000, 4, off) // ldrsw x0 - w.WriteBytes(scvtf) - case "uint64": - acg.emitLDST(0xf9400000, 8, off) // ldr x0 - w.WriteBytes(ucvtf) - case "int64": - acg.emitLDST(0xf9400000, 8, off) // ldr x0 - w.WriteBytes(scvtf) - case "float32": - acg.emitLDST(0xbd400000, 4, off) // ldr s0 - w.WriteBytes([]byte{0x00, 0xc0, 0x22, 0x1e}) // fcvt d0, s0 - case "float64": - acg.emitLDST(0xfd400000, 8, off) // ldr d0 - case "cstr", "ptr": - acg.emitLDST(0xf9400000, 8, off) // ldr x0 - w.WriteBytes([]byte{0x00, 0x00, 0x67, 0x9e}) // fmov d0, x0 (keep bits) - default: - return fmt.Errorf("unsupported cstruct field type for ARM64 load: %s", f.Type) - } - return nil -} - -// emitCStructFieldStore stores d0 into the field at its offset (pointer in x9), -// converting d0 to the field's width/type. -func (acg *ARM64CodeGen) emitCStructFieldStore(f *CStructField) error { - w := acg.out.out.writer - off := f.Offset - fcvtzuW := []byte{0x00, 0x00, 0x79, 0x1e} // fcvtzu w0, d0 - fcvtzsW := []byte{0x00, 0x00, 0x78, 0x1e} // fcvtzs w0, d0 - fcvtzuX := []byte{0x00, 0x00, 0x79, 0x9e} // fcvtzu x0, d0 - fcvtzsX := []byte{0x00, 0x00, 0x78, 0x9e} // fcvtzs x0, d0 - switch f.Type { - case "uint8": - w.WriteBytes(fcvtzuW) - acg.emitLDST(0x39000000, 1, off) // strb w0 - case "int8": - w.WriteBytes(fcvtzsW) - acg.emitLDST(0x39000000, 1, off) - case "uint16": - w.WriteBytes(fcvtzuW) - acg.emitLDST(0x79000000, 2, off) // strh w0 - case "int16": - w.WriteBytes(fcvtzsW) - acg.emitLDST(0x79000000, 2, off) - case "uint32": - w.WriteBytes(fcvtzuW) - acg.emitLDST(0xb9000000, 4, off) // str w0 - case "int32": - w.WriteBytes(fcvtzsW) - acg.emitLDST(0xb9000000, 4, off) - case "uint64": - w.WriteBytes(fcvtzuX) - acg.emitLDST(0xf9000000, 8, off) // str x0 - case "int64": - w.WriteBytes(fcvtzsX) - acg.emitLDST(0xf9000000, 8, off) - case "float32": - w.WriteBytes([]byte{0x00, 0x40, 0x62, 0x1e}) // fcvt s0, d0 - acg.emitLDST(0xbd000000, 4, off) // str s0 - case "float64": - acg.emitLDST(0xfd000000, 8, off) // str d0 - case "cstr", "ptr": - w.WriteBytes([]byte{0x00, 0x00, 0x66, 0x9e}) // fmov x0, d0 (bits) - acg.emitLDST(0xf9000000, 8, off) // str x0 - default: - return fmt.Errorf("unsupported cstruct field type for ARM64 store: %s", f.Type) - } - return nil -} - -// arenaBumpSize is the size of the cstruct bump-arena buffer reserved at startup -// (1 GiB). It is overcommitted, so only pages actually touched cost memory; it -// must comfortably hold the live allocations of one `arena { }` scope. -const arenaBumpSize = 0x40000000 - -// genHeapSize is the size of the general-purpose _tim_malloc heap region reserved -// at startup (1 GiB, overcommitted). Lists, closures, boxed vars and other heap -// allocations bump-allocate from it; when a region is exhausted _tim_malloc mmaps -// a fresh one, so this is a working-set hint, not a hard cap. -const genHeapSize = 0x40000000 - -// emitMmapSyscall emits a raw mmap(2) syscall reserving anonymous, private, -// read/write memory. The length must already be in x1; the base address is -// returned in x0. Using a direct syscall (rather than libc malloc) is what keeps -// a pure-Tim program free of any dynamic-library dependency — libSystem/libc is -// linked only when the program makes a genuine C-FFI call. Errors are not checked -// (a 1 GiB overcommit effectively never fails); clobbers x0-x5 and x8/x16. -func (acg *ARM64CodeGen) emitMmapSyscall() error { - if err := acg.out.MovImm64("x0", 0); err != nil { // addr = NULL - return err - } - if err := acg.out.MovImm64("x2", 3); err != nil { // prot = PROT_READ|PROT_WRITE - return err - } - flags := uint64(0x22) // Linux: MAP_ANONYMOUS|MAP_PRIVATE - if acg.eb.target.OS() == OSDarwin { - flags = 0x1002 // macOS: MAP_ANON|MAP_PRIVATE - } - if err := acg.out.MovImm64("x3", flags); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0x04, 0x00, 0x80, 0x92}) // movn x4, #0 (fd = -1) - if err := acg.out.MovImm64("x5", 0); err != nil { // offset = 0 - return err - } - if acg.eb.target.OS() == OSDarwin { - if err := acg.out.MovImm64("x16", 0x2000000|197); err != nil { // mmap - return err - } - acg.out.out.writer.WriteBytes([]byte{0x01, 0x10, 0x00, 0xd4}) // svc #0x80 - } else { - if err := acg.out.MovImm64("x8", 222); err != nil { // mmap - return err - } - acg.out.out.writer.WriteBytes([]byte{0x01, 0x00, 0x00, 0xd4}) // svc #0 - } - return nil -} - -// emitMmapReserve reserves `size` bytes via emitMmapSyscall, leaving the base in x0. -func (acg *ARM64CodeGen) emitMmapReserve(size uint64) error { - if err := acg.out.MovImm64("x1", size); err != nil { - return err - } - return acg.emitMmapSyscall() -} - -// emitBumpAlloc allocates `size` bytes (rounded up to 16) from the x27 bump arena, -// leaving the pointer in x0. On overflow it falls back to malloc (so correctness -// never depends on the arena being large enough — only memory reuse does). -func (acg *ARM64CodeGen) emitBumpAlloc(size int) error { - rounded := (size + 15) &^ 15 - if err := acg.out.MovReg64("x0", "x27"); err != nil { // x0 = current top - return err - } - if rounded <= 4095 { - if err := acg.out.AddImm64("x1", "x27", uint32(rounded)); err != nil { - return err - } - } else { - if err := acg.out.MovImm64("x1", uint64(rounded)); err != nil { - return err - } - if err := acg.out.AddReg64("x1", "x27", "x1"); err != nil { - return err - } - } - // cmp x1, x26 (subs xzr, x1, x26) - cmp := uint32(0xEB000000) | (26 << 16) | (1 << 5) | 31 - acg.out.out.writer.WriteBytes([]byte{byte(cmp), byte(cmp >> 8), byte(cmp >> 16), byte(cmp >> 24)}) - fbJump := acg.eb.text.Len() - if err := acg.out.BranchCond("hi", 0); err != nil { // overflow -> fall back to malloc - return err - } - if err := acg.out.MovReg64("x27", "x1"); err != nil { // commit new top - return err - } - doneJump := acg.eb.text.Len() - if err := acg.out.Branch(0); err != nil { - return err - } - fbPos := acg.eb.text.Len() - acg.patchJumpOffset(fbJump, int32(fbPos-fbJump)) - if err := acg.out.MovImm64("x0", uint64(size)); err != nil { - return err - } - if err := acg.eb.GenerateCallInstruction("_tim_malloc"); err != nil { - return err - } - donePos := acg.eb.text.Len() - acg.patchJumpOffset(doneJump, int32(donePos-doneJump)) - return nil -} - -// pureBaseExprEqual reports whether two expressions are syntactically equal AND -// side-effect-free (so one may be compiled in place of the other, or compiled -// once and reused). Only the shapes that appear as a vec-arithmetic operand base -// are handled — identifiers, nested field reads, numeric/`self` literals. -func pureBaseExprEqual(a, b Expression) bool { - switch ae := a.(type) { - case *IdentExpr: - be, ok := b.(*IdentExpr) - return ok && ae.Name == be.Name - case *FieldAccessExpr: - be, ok := b.(*FieldAccessExpr) - return ok && ae.FieldName == be.FieldName && pureBaseExprEqual(ae.Object, be.Object) - case *NumberExpr: - be, ok := b.(*NumberExpr) - return ok && ae.Value == be.Value - } - return false -} - -// isPureVecBase reports whether an expression is a side-effect-free struct-valued -// operand base (safe to compile to a pointer without double-evaluating a call). -func isPureVecBase(e Expression) bool { - switch e.(type) { - case *IdentExpr, *FieldAccessExpr: - return true - } - return false -} - -// tryCompileNEONVec3 recognizes a 3-field f64 cstruct constructor whose arguments -// are an element-wise binary op over the fields of one or two operand structs and -// emits NEON: the x/y lanes are computed with a single .2D op, z stays scalar. -// Returns (true, _) when it emitted code; (false, nil) to fall back to the scalar -// constructor. It only fires when operand layouts are statically known to match -// `decl` (so the 128-bit load of lanes x,y is sound). -func (acg *ARM64CodeGen) tryCompileNEONVec3(decl *CStructDecl, args []Expression) (bool, error) { - // Exactly three contiguous f64 fields at offsets 0, 8, 16. - if len(decl.Fields) != 3 { - return false, nil - } - for i, f := range decl.Fields { - if (f.Type != "float64" && f.Type != "f64") || f.Offset != i*8 { - return false, nil - } - } - - // Every argument must be `.field_i ` with the same operator. - op := "" - lefts := make([]*FieldAccessExpr, 3) - rights := make([]Expression, 3) - for i, a := range args { - be, ok := a.(*BinaryExpr) - if !ok || (be.Operator != "+" && be.Operator != "-" && be.Operator != "*") { - return false, nil - } - if op == "" { - op = be.Operator - } else if be.Operator != op { - return false, nil - } - fa, ok := be.Left.(*FieldAccessExpr) - if !ok || fa.FieldName != decl.Fields[i].Name || !isPureVecBase(fa.Object) { - // `*` may also be written `s * v.field`; try the mirrored shape. - fa, ok = be.Right.(*FieldAccessExpr) - if op != "*" || !ok || fa.FieldName != decl.Fields[i].Name || !isPureVecBase(fa.Object) { - return false, nil - } - lefts[i], rights[i] = fa, be.Left - } else { - lefts[i], rights[i] = fa, be.Right - } - } - - // The left operand base (the receiver) must be identical across lanes and have - // `decl`'s layout, so loading lanes x,y as one 128-bit vector is correct. - baseA := lefts[0].Object - for i := 1; i < 3; i++ { - if !pureBaseExprEqual(baseA, lefts[i].Object) { - return false, nil - } - } - if acg.cStructTypeOf(baseA) != decl.Name { - return false, nil - } - - // Determine the right-hand side: either element-wise (`b.field_i`, base B of - // `decl`'s type) or a broadcast scalar (the same value across all lanes). - rhsIsField := false - if rfa, ok := rights[0].(*FieldAccessExpr); ok && rfa.FieldName == decl.Fields[0].Name && isPureVecBase(rfa.Object) { - rhsIsField = true - } - var baseB Expression - var scalarS Expression - if rhsIsField { - for i := range 3 { - rfa, ok := rights[i].(*FieldAccessExpr) - if !ok || rfa.FieldName != decl.Fields[i].Name || !isPureVecBase(rfa.Object) { - return false, nil - } - } - baseB = rights[0].(*FieldAccessExpr).Object - for i := 1; i < 3; i++ { - if !pureBaseExprEqual(baseB, rights[i].(*FieldAccessExpr).Object) { - return false, nil - } - } - if acg.cStructTypeOf(baseB) != decl.Name { - return false, nil - } - } else { - // Broadcast-scalar (scale): only valid for `*`, with the same scalar value - // on every lane and the scalar not itself a struct field of decl. - if op != "*" { - return false, nil - } - scalarS = rights[0] - for i := 1; i < 3; i++ { - if !pureBaseExprEqual(scalarS, rights[i]) { - return false, nil - } - } - } - - return true, acg.emitNEONVec3(decl, op, baseA, baseB, scalarS) -} - -// emitNEONVec3 emits the NEON body recognized by tryCompileNEONVec3: it allocates -// the result struct, computes lanes x,y with one .2D op and lane z scalar, and -// leaves the result pointer (as a numeric double) in d0. -func (acg *ARM64CodeGen) emitNEONVec3(decl *CStructDecl, op string, baseA, baseB, scalarS Expression) error { - vecOp := map[string]func(d, a, b string) error{ - "+": acg.out.FaddV2D, "-": acg.out.FsubV2D, "*": acg.out.FmulV2D, - }[op] - sclOp := map[string]func(d, a, b string) error{ - "+": acg.out.FaddScalar64, "-": acg.out.FsubScalar64, "*": acg.out.FmulScalar64, - }[op] - - // Reserve a 16-byte spill slot for operand pointers / the scalar. - acg.out.SubImm64("sp", "sp", 16) - - // ptrA = &baseA, spilled to [sp, 0]. - if err := acg.compileExpression(baseA); err != nil { // d0 = pointer (float bits) - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x9", "d0"); err != nil { - return err - } - if err := acg.out.StrImm64("x9", "sp", 0); err != nil { - return err - } - - if baseB != nil { - if err := acg.compileExpression(baseB); err != nil { // d0 = pointer - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x9", "d0"); err != nil { - return err - } - if err := acg.out.StrImm64("x9", "sp", 8); err != nil { - return err - } - } else { - if err := acg.compileExpression(scalarS); err != nil { // d0 = scalar value - return err - } - if err := acg.out.StrImm64Double("d0", "sp", 8); err != nil { - return err - } - } - - // Allocate the result struct (pointer in x0); may call malloc, so operands are - // reloaded from the spill slot afterward. - if err := acg.emitBumpAlloc(decl.Size); err != nil { - return err - } - if err := acg.out.LdrImm64("x9", "sp", 0); err != nil { // x9 = ptrA - return err - } - if err := acg.out.LdrQ("v0", "x9", 0); err != nil { // A.x, A.y - return err - } - if err := acg.out.LdrImm64Double("d3", "x9", 16); err != nil { // A.z - return err - } - if baseB != nil { - if err := acg.out.LdrImm64("x10", "sp", 8); err != nil { // x10 = ptrB - return err - } - if err := acg.out.LdrQ("v1", "x10", 0); err != nil { // B.x, B.y - return err - } - if err := acg.out.LdrImm64Double("d4", "x10", 16); err != nil { // B.z - return err - } - } else { - if err := acg.out.LdrImm64Double("d4", "sp", 8); err != nil { // scalar - return err - } - if err := acg.out.DupV2D("v1", "d4"); err != nil { // broadcast to both lanes - return err - } - } - if err := vecOp("v2", "v0", "v1"); err != nil { - return err - } - if err := acg.out.StrQ("v2", "x0", 0); err != nil { // result.x, result.y - return err - } - if err := sclOp("d3", "d3", "d4"); err != nil { - return err - } - if err := acg.out.StrImm64Double("d3", "x0", 16); err != nil { // result.z - return err - } - acg.out.AddImm64("sp", "sp", 16) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) // scvtf d0, x0 (numeric ptr) - return nil -} - -// compileCStructConstructor compiles a cstruct value constructor, e.g. -// `Point(3.0, 4.0)`: allocate the struct, store each argument into its field, -// and yield a numeric pointer to the value (the convention cstruct field access -// expects). A field whose type is itself a cstruct receives another struct -// pointer (stored as an 8-byte pointer), so nested structs compose. -func (acg *ARM64CodeGen) compileCStructConstructor(decl *CStructDecl, args []Expression) error { - if len(args) != len(decl.Fields) { - return fmt.Errorf("%s constructor expects %d arguments, got %d", decl.Name, len(decl.Fields), len(args)) - } - // NEON fast path for element-wise vec3 arithmetic (`V(a.x±b.x, ...)`, - // `V(a.x*s, ...)`): processes the x/y pair with one .2D instruction. - if handled, err := acg.tryCompileNEONVec3(decl, args); err != nil { - return err - } else if handled { - return nil - } - size := decl.Size - if size <= 0 { - size = len(decl.Fields) * 8 - } - // Allocate the struct value from the bump arena (freed by the enclosing - // `arena { }`), falling back to malloc on overflow. - if err := acg.emitBumpAlloc(size); err != nil { - return err - } - // Spill the struct pointer so it survives compiling each field value. - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x0", "sp", 0); err != nil { - return err - } - for i := range decl.Fields { - if err := acg.compileExpression(args[i]); err != nil { // value in d0 - return err - } - if err := acg.out.LdrImm64("x9", "sp", 0); err != nil { // struct pointer - return err - } - if err := acg.emitCStructFieldStore(&decl.Fields[i]); err != nil { - return err - } - } - if err := acg.out.LdrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) // scvtf d0, x0 (numeric ptr) - return nil -} - -// emitMapValueSlotAddr searches a map/string for key x1 (pointer in x0) and -// leaves the address of its value slot in x7, or 0 if the key is absent. Same -// layout as emitMapKeyLookup. -func (acg *ARM64CodeGen) emitMapValueSlotAddr() error { - if err := acg.out.LdrImm64Double("d1", "x0", 0); err != nil { // count - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x2", "d1"); err != nil { - return err - } - if err := acg.out.MovImm64("x3", 0); err != nil { // i - return err - } - loopStart := acg.eb.text.Len() - if err := acg.out.CmpReg64("x3", "x2"); err != nil { - return err - } - notFoundJump := acg.eb.text.Len() - acg.out.BranchCond("ge", 0) - if err := acg.out.LslImm64("x5", "x3", 4); err != nil { - return err - } - if err := acg.out.AddReg64("x4", "x0", "x5"); err != nil { - return err - } - if err := acg.out.AddImm64("x4", "x4", 8); err != nil { // &key[i] - return err - } - if err := acg.out.LdrImm64Double("d1", "x4", 0); err != nil { - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x6", "d1"); err != nil { - return err - } - if err := acg.out.CmpReg64("x6", "x1"); err != nil { - return err - } - foundJump := acg.eb.text.Len() - acg.out.BranchCond("eq", 0) - if err := acg.out.AddImm64("x3", "x3", 1); err != nil { - return err - } - backPos := acg.eb.text.Len() - acg.out.Branch(0) - acg.patchJumpOffset(backPos, int32(loopStart-backPos)) - - foundLabel := acg.eb.text.Len() - acg.patchJumpOffset(foundJump, int32(foundLabel-foundJump)) - if err := acg.out.AddImm64("x7", "x4", 8); err != nil { // value slot - return err - } - endJump := acg.eb.text.Len() - acg.out.Branch(0) - - notFoundLabel := acg.eb.text.Len() - acg.patchJumpOffset(notFoundJump, int32(notFoundLabel-notFoundJump)) - if err := acg.out.MovImm64("x7", 0); err != nil { - return err - } - - endLabel := acg.eb.text.Len() - acg.patchJumpOffset(endJump, int32(endLabel-endJump)) - return nil -} - -// compileMultipleAssign compiles `a, b = expr` where expr yields a flat tuple -// [count][elem0][elem1]... (e.g. pop returns [new_list, popped]). Element i is -// unpacked from offset 8+i*8 into the i-th name. -func (acg *ARM64CodeGen) compileMultipleAssign(stmt *MultipleAssignStmt) error { - if err := acg.compileExpression(stmt.Value); err != nil { - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x0", "d0"); err != nil { // tuple ptr - return err - } - // Spill the pointer so it survives across element loads. - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x0", "sp", 0); err != nil { - return err - } - - for i, name := range stmt.Names { - var offset int32 - if stmt.IsUpdate { - so, ok := acg.stackVars[name] - if !ok { - return fmt.Errorf("cannot update undefined variable '%s'", name) - } - offset = int32(16 + so - 8) - } else if so, exists := acg.stackVars[name]; exists && acg.mutableVars[name] { - offset = int32(16 + so - 8) - } else { - acg.stackSize += 8 - acg.stackVars[name] = acg.stackSize - acg.mutableVars[name] = stmt.Mutable - offset = int32(16 + acg.stackSize - 8) - } - - if err := acg.out.LdrImm64("x0", "sp", 0); err != nil { - return err - } - if err := acg.out.LdrImm64Double("d0", "x0", int32(8+i*8)); err != nil { - return err - } - if err := acg.out.StrImm64Double("d0", "x29", offset); err != nil { - return err - } - } - - acg.out.AddImm64("sp", "sp", 16) - return nil -} - -// compileJumpStatement compiles jump statements (ret, @label) -func (acg *ARM64CodeGen) compileJumpStatement(stmt *JumpStmt) error { - // Handle function return: ret with Label=0 - if stmt.Label == 0 && stmt.IsBreak { - // Return from function - if stmt.Value != nil { - if err := acg.compileExpression(stmt.Value); err != nil { - return err - } - // d0 now contains return value - } - - // Restore frame pointer and link register, then return - if err := acg.out.LdrImm64("x30", "sp", 8); err != nil { - return err - } - if err := acg.out.LdrImm64("x29", "sp", 0); err != nil { - return err - } - // Add back stack frame size - if acg.currentLambda != nil { - // Lambda has its own frame size - // Calculate based on stored params - simplified version - paramCount := len(acg.currentLambda.Params) - frameSize := uint32((16 + paramCount*8 + 2048 + 15) &^ 15) - if err := acg.out.AddImm64("sp", "sp", frameSize); err != nil { - return err - } - } else { - // Main program frame - if err := acg.out.AddImm64("sp", "sp", uint32(acg.stackFrameSize)); err != nil { - return err - } - } - // ret instruction - if err := acg.out.Return("x30"); err != nil { - return err - } - return nil - } - - // All other cases require being inside a loop - if len(acg.activeLoops) == 0 { - keyword := "@" - if stmt.IsBreak { - keyword = "ret" - } - return fmt.Errorf("%s used outside of loop", keyword) - } - - // Resolve which loop this jump targets. Default to the innermost; a positive - // label selects an enclosing loop by its @N label. - target := len(acg.activeLoops) - 1 - for j := range acg.activeLoops { - if acg.activeLoops[j].Label == stmt.Label { - target = j - break - } - } - - // Emit a placeholder branch; the loop's epilogue patches it to the loop end - // (break) or the continue/step position (continue). - pos := acg.eb.text.Len() - if err := acg.out.Branch(0); err != nil { - return err - } - if stmt.IsBreak { - acg.activeLoops[target].EndPatches = append(acg.activeLoops[target].EndPatches, pos) - } else { - acg.activeLoops[target].ContinuePatches = append(acg.activeLoops[target].ContinuePatches, pos) - } - return nil -} - -// pushDeferScope creates a new defer scope for collecting deferred expressions -func (acg *ARM64CodeGen) pushDeferScope() { - acg.deferredExprs = append(acg.deferredExprs, []Expression{}) -} - -// popDeferScope executes all deferred expressions in reverse order and removes the scope -func (acg *ARM64CodeGen) popDeferScope() error { - if len(acg.deferredExprs) == 0 { - return nil - } - - currentScope := len(acg.deferredExprs) - 1 - deferred := acg.deferredExprs[currentScope] - - // Execute deferred expressions in LIFO order - for i := len(deferred) - 1; i >= 0; i-- { - if err := acg.compileExpression(deferred[i]); err != nil { - return err - } - } - - acg.deferredExprs = acg.deferredExprs[:currentScope] - return nil -} - -// compileArenaStmt compiles an arena block with auto-cleanup -func (acg *ARM64CodeGen) compileArenaStmt(stmt *ArenaStmt) error { - // Mark that this program uses arenas - acg.usesArenas = true - - // Save previous arena context and increment depth - previousArena := acg.currentArena - acg.currentArena++ - - // Save the bump-arena top (x27) so it can be restored when the block exits, - // freeing every cstruct value allocated within it. - acg.stackSize += 8 - arenaSaveSlot := int32(16 + acg.stackSize - 8) - if err := acg.out.StrImm64("x27", "x29", arenaSaveSlot); err != nil { - return err - } - - // Push defer scope for arena - acg.pushDeferScope() - - // Compile statements in arena body - for _, bodyStmt := range stmt.Body { - if err := acg.compileStatement(bodyStmt); err != nil { - return err - } - } - - // Pop defer scope and execute deferred expressions - if err := acg.popDeferScope(); err != nil { - return err - } - - // Restore previous arena context - acg.currentArena = previousArena - - // Restore the bump-arena top — frees this scope's cstruct allocations. - if err := acg.out.LdrImm64("x27", "x29", arenaSaveSlot); err != nil { - return err - } - return nil -} - -// compileExpression compiles an expression and leaves result in d0 (float64 register) -func (acg *ARM64CodeGen) compileExpression(expr Expression) error { - switch e := expr.(type) { - case *BooleanExpr: - // Booleans are just 1.0 / 0.0 in Tim's uniform float64 world. - v := 0.0 - if e.Value { - v = 1.0 - } - return acg.compileExpression(&NumberExpr{Value: v}) - - case *NumberExpr: - // Tim uses float64 for all numbers - // For whole numbers, convert via integer; for decimals, load from .rodata - if e.Value == float64(int64(e.Value)) { - // Whole number - convert to int64, then to float64 - val := int64(e.Value) - // Load integer into x0 - if err := acg.out.MovImm64("x0", uint64(val)); err != nil { - return err - } - // Convert x0 (int64) to d0 (float64) - // scvtf d0, x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) // scvtf d0, x0 - } else { - // Decimal number - store in .rodata and load - labelName := fmt.Sprintf("float_%d", acg.stringCounter) - acg.stringCounter++ - - // Convert float64 to 8 bytes (little-endian) - bits := uint64(0) - *(*float64)(unsafe.Pointer(&bits)) = e.Value - var floatData []byte - for i := range 8 { - floatData = append(floatData, byte((bits>>(i*8))&0xFF)) - } - acg.eb.Define(labelName, string(floatData)) - - // Load address of float into x0 using PC-relative addressing - offset := uint64(acg.eb.text.Len()) - acg.eb.pcRelocations = append(acg.eb.pcRelocations, PCRelocation{ - offset: offset, - symbolName: labelName, - }) - // ADRP x0, label@PAGE - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x90}) - // ADD x0, x0, label@PAGEOFF - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x91}) - - // Load float64 from [x0] into d0 - // ldr d0, [x0] - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x40, 0xfd}) - } - - case *StringExpr: - // Strings are represented as map[uint64]float64 - // Map format: [count][key0][val0][key1][val1]... - - // Check if we've already interned this string - var labelName string - if existingLabel, exists := acg.stringInterns[e.Value]; exists { - // Reuse existing label for this string - labelName = existingLabel - } else { - // Create new label and intern it - labelName = fmt.Sprintf("str_%d", acg.stringCounter) - acg.stringCounter++ - acg.stringInterns[e.Value] = labelName - - // Build map data: count followed by key-value pairs - var mapData []byte - - // Count (number of characters) - count := float64(len(e.Value)) - countBits := uint64(0) - *(*float64)(unsafe.Pointer(&countBits)) = count - for i := range 8 { - mapData = append(mapData, byte((countBits>>(i*8))&0xFF)) - } - - // Add each character as a key-value pair - for idx, ch := range e.Value { - // Key: character index as float64 - keyVal := float64(idx) - keyBits := uint64(0) - *(*float64)(unsafe.Pointer(&keyBits)) = keyVal - for i := range 8 { - mapData = append(mapData, byte((keyBits>>(i*8))&0xFF)) - } - - // Value: character code as float64 - charVal := float64(ch) - charBits := uint64(0) - *(*float64)(unsafe.Pointer(&charBits)) = charVal - for i := range 8 { - mapData = append(mapData, byte((charBits>>(i*8))&0xFF)) - } - } - - acg.eb.Define(labelName, string(mapData)) - } - - // Load address into x0 - offset := uint64(acg.eb.text.Len()) - acg.eb.pcRelocations = append(acg.eb.pcRelocations, PCRelocation{ - offset: offset, - symbolName: labelName, - }) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x90}) // ADRP x0, label@PAGE - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x91}) // ADD x0, x0, label@PAGEOFF - - // Convert pointer to float64: scvtf d0, x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - - case *IdentExpr: - // Module globals live in the heap array based at x28 (visible from any - // lambda body), addressed by their fixed slot. - if slot, isGlobal := acg.globalSlots[e.Name]; isGlobal { - return acg.out.LdrImm64Double("d0", "x28", int32(slot*8)) - } - // Load variable from stack into d0 - stackOffset, exists := acg.stackVars[e.Name] - if !exists { - // Captured variable: read from the closure env at [closurePtr+8+idx*8]. - if acg.currentLambda != nil { - if idx := slices.Index(acg.currentLambda.Captures, e.Name); idx >= 0 { - if err := acg.out.LdrImm64("x9", "x29", acg.closurePtrOffset); err != nil { - return err - } - if acg.boxedVars[e.Name] { - // Boxed capture: the env slot holds a cell pointer; deref it. - if err := acg.out.LdrImm64("x9", "x9", int32(8+idx*8)); err != nil { - return err - } - return acg.out.LdrImm64Double("d0", "x9", 0) - } - return acg.out.LdrImm64Double("d0", "x9", int32(8+idx*8)) - } - } - if VerboseMode { - fmt.Fprintf(os.Stderr, "Error: undefined variable '%s'\n", e.Name) - } - return acg.undefinedVariableError(e.Name) - } - // ldr d0, [x29, #offset] - // x29 points to saved fp location, variables start at offset 16 - offset := int32(16 + stackOffset - 8) - if acg.boxedVars[e.Name] { - // Boxed local: the slot holds a cell pointer; deref it. - if err := acg.out.LdrImm64("x9", "x29", offset); err != nil { - return err - } - return acg.out.LdrImm64Double("d0", "x9", 0) - } - if err := acg.out.LdrImm64Double("d0", "x29", offset); err != nil { - return err - } - - case *BinaryExpr: - // Check for list concatenation with + operator - if e.Operator == "+" { - leftType := acg.getExprType(e.Left) - rightType := acg.getExprType(e.Right) - - if leftType == "list" && rightType == "list" { - // List concatenation: [1, 2] + [3, 4] -> [1, 2, 3, 4] - // Compile left list (result in d0) - if err := acg.compileExpression(e.Left); err != nil { - return err - } - // Convert d0 (float) to x0 (pointer) - acg.out.SubImm64("sp", "sp", 16) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x00, 0xfd}) // str d0, [sp] - if err := acg.out.LdrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - - // Push x0 (left ptr) to stack - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x0", "sp", 0); err != nil { - return err - } - - // Compile right list (result in d0) - if err := acg.compileExpression(e.Right); err != nil { - return err - } - // Convert d0 to x1 - acg.out.SubImm64("sp", "sp", 16) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x00, 0xfd}) // str d0, [sp] - if err := acg.out.LdrImm64("x1", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - - // Restore left ptr to x0 - if err := acg.out.LdrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - - // Call _tim_list_concat(x0, x1) -> x0 - if err := acg.eb.GenerateCallInstruction("_tim_list_concat"); err != nil { - return err - } - - // Convert result x0 back to d0 - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x40, 0xfd}) // ldr d0, [sp] - acg.out.AddImm64("sp", "sp", 16) - - return nil - } - - if leftType == "list" && rightType != "list" { - // List + element: append the element (shorthand for list += elem). - if err := acg.compileExpression(e.Left); err != nil { - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x0", "d0"); err != nil { - return err - } - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x0", "sp", 0); err != nil { - return err - } - if err := acg.compileExpression(e.Right); err != nil { - return err - } - if err := acg.out.LdrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - if err := acg.eb.GenerateCallInstruction("_tim_append"); err != nil { - return err - } - return acg.out.ScvtfInt64ToDouble("d0", "x0") - } - - if leftType == "string" && rightType == "string" { - // String concatenation. String pointers are stored numerically - // (scvtf), so recover them with fcvtzs — a bit reinterpret would - // pass garbage to the helper and crash. - if err := acg.compileExpression(e.Left); err != nil { - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x0", "d0"); err != nil { - return err - } - // Spill left pointer across the right operand's evaluation. - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x0", "sp", 0); err != nil { - return err - } - if err := acg.compileExpression(e.Right); err != nil { - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x1", "d0"); err != nil { - return err - } - if err := acg.out.LdrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - - // Call _tim_string_concat(x0, x1) -> x0 - if err := acg.eb.GenerateCallInstruction("_tim_string_concat"); err != nil { - return err - } - // Result pointer back to the numeric convention. - return acg.out.ScvtfInt64ToDouble("d0", "x0") - } - } - - // String equality/inequality compares contents, not pointer identity. - // (Identical literals are interned to one address, but runtime-built - // strings are not, so a numeric compare would be wrong.) - if (e.Operator == "==" || e.Operator == "!=") && - acg.getExprType(e.Left) == "string" && acg.getExprType(e.Right) == "string" { - if err := acg.compileExpression(e.Left); err != nil { - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x0", "d0"); err != nil { - return err - } - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x0", "sp", 0); err != nil { - return err - } - if err := acg.compileExpression(e.Right); err != nil { - return err - } - if err := acg.out.FcvtzsDoubleToInt64("x1", "d0"); err != nil { - return err - } - if err := acg.out.LdrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - if err := acg.eb.GenerateCallInstruction("_tim_string_eq"); err != nil { - return err - } - // x0 = 1 if equal else 0. Invert for "!=". - if e.Operator == "!=" { - if err := acg.out.CmpImm64("x0", 0); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x17, 0x9f, 0x9a}) // cset x0, eq - } - return acg.out.ScvtfInt64ToDouble("d0", "x0") - } - - // Special handling for or! operator (railway-oriented programming) - // or! requires conditional execution: only evaluate right side if left is error/null - if e.Operator == "or!" { - // Compile left expression into d0 - if err := acg.compileExpression(e.Left); err != nil { - return err - } - - // Check if d0 is NaN by comparing with itself - // fcmp d0, d0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x60, 0x1e}) - // b.vs (branch if overflow/NaN) to execute_default - executeDefaultPos1 := acg.eb.text.Len() - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x54}) // b.vs placeholder - - // Not NaN, now check if d0 == 0.0 (null pointer) - // fmov d1, xzr (d1 = 0.0) - acg.out.out.writer.WriteBytes([]byte{0xe1, 0x03, 0x67, 0x9e}) - // fcmp d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) - // b.eq (branch if equal to 0) to execute_default - executeDefaultPos2 := acg.eb.text.Len() - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x54}) // b.eq placeholder - - // Value is valid (not NaN and not 0), skip to end without evaluating right side - skipDefaultPos := acg.eb.text.Len() - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x14}) // b placeholder - - // execute_default label: evaluate right expression (could be block or value) - executeDefaultLabel := acg.eb.text.Len() - if err := acg.compileExpression(e.Right); err != nil { // Result goes to d0 - return err - } - - // End label - endLabel := acg.eb.text.Len() - - // Patch the jumps - // ARM64 branch offsets are in instructions (4 bytes each), not bytes - // Offset = (target - current_pc) / 4 - - // Patch NaN check (b.vs) to execute_default - offset1 := int32((executeDefaultLabel - executeDefaultPos1) / 4) - bytes1 := acg.eb.text.Bytes() - // b.vs encoding: 0x54 with imm19 in bits [23:5] and cond=0110 (VS) in bits [3:0] - instr1 := uint32(0x54000006) | (uint32(offset1&0x7ffff) << 5) - bytes1[executeDefaultPos1] = byte(instr1 & 0xFF) - bytes1[executeDefaultPos1+1] = byte((instr1 >> 8) & 0xFF) - bytes1[executeDefaultPos1+2] = byte((instr1 >> 16) & 0xFF) - bytes1[executeDefaultPos1+3] = byte((instr1 >> 24) & 0xFF) - - // Patch zero check (b.eq) to execute_default - offset2 := int32((executeDefaultLabel - executeDefaultPos2) / 4) - instr2 := uint32(0x54000000) | (uint32(offset2&0x7ffff) << 5) - bytes1[executeDefaultPos2] = byte(instr2 & 0xFF) - bytes1[executeDefaultPos2+1] = byte((instr2 >> 8) & 0xFF) - bytes1[executeDefaultPos2+2] = byte((instr2 >> 16) & 0xFF) - bytes1[executeDefaultPos2+3] = byte((instr2 >> 24) & 0xFF) - - // Patch skip jump (b) to end - offset3 := int32((endLabel - skipDefaultPos) / 4) - instr3 := uint32(0x14000000) | uint32(offset3&0x3ffffff) - bytes1[skipDefaultPos] = byte(instr3 & 0xFF) - bytes1[skipDefaultPos+1] = byte((instr3 >> 8) & 0xFF) - bytes1[skipDefaultPos+2] = byte((instr3 >> 16) & 0xFF) - bytes1[skipDefaultPos+3] = byte((instr3 >> 24) & 0xFF) - - // d0 now contains either original value (if not NaN/null) or result of right side - return nil - } - - // Compile left operand (result in d0) - if err := acg.compileExpression(e.Left); err != nil { - return err - } - - // Keep the left operand in a scratch FP register (d24+depth) across the - // right operand instead of spilling to memory — but only when the right - // operand has no call (which would clobber the caller-saved d24-d31) and - // the stack isn't exhausted. Deeper nesting uses higher registers. - if acg.fpDepth < 8 && !acg.exprHasCall(e.Right) { - reg := uint32(24 + acg.fpDepth) - acg.emitFmovD(reg, 0) // fmov d{reg}, d0 (save left) - acg.fpDepth++ - if err := acg.compileExpression(e.Right); err != nil { - return err - } - acg.fpDepth-- - acg.out.out.writer.WriteBytes([]byte{0x01, 0x40, 0x60, 0x1e}) // fmov d1, d0 (right) - acg.emitFmovD(0, reg) // fmov d0, d{reg} (restore left) - } else { - // Memory spill (16-byte aligned). - acg.out.SubImm64("sp", "sp", 16) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x00, 0xfd}) // str d0, [sp] - if err := acg.compileExpression(e.Right); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0x01, 0x40, 0x60, 0x1e}) // fmov d1, d0 - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x40, 0xfd}) // ldr d0, [sp] - acg.out.AddImm64("sp", "sp", 16) - } - - // Perform operation: d0 = d0 op d1 - switch e.Operator { - case "+": - // fadd d0, d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x28, 0x61, 0x1e}) - case "-": - // fsub d0, d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x38, 0x61, 0x1e}) - case "*": - // fmul d0, d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x08, 0x61, 0x1e}) - case "/": - // Division by zero yields an error-NaN (0x7FF8000064763000, "dv0") - // that or! can detect, rather than +/-inf. - // fcmp d1, #0.0 - acg.out.out.writer.WriteBytes([]byte{0x28, 0x20, 0x60, 0x1e}) - // b.ne do_div (divisor non-zero: normal division) - nePos := acg.eb.text.Len() - acg.out.BranchCond("ne", 0) - // divisor is zero: load the error-NaN sentinel into d0 - if err := acg.out.MovImm64("x0", 0x7FF8000064763000); err != nil { - return err - } - if err := acg.out.FmovGPToDouble("d0", "x0"); err != nil { - return err - } - // b end_div - bEndPos := acg.eb.text.Len() - acg.out.Branch(0) - // do_div: - doDivLabel := acg.eb.text.Len() - acg.patchJumpOffset(nePos, int32(doDivLabel-nePos)) - // fdiv d0, d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x18, 0x61, 0x1e}) - // end_div: - endDivLabel := acg.eb.text.Len() - acg.patchJumpOffset(bEndPos, int32(endDivLabel-bEndPos)) - case "==": - // fcmp d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) - // cset x0, eq (x0 = 1 if equal, else 0) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x17, 0x9f, 0x9a}) - // scvtf d0, x0 (convert to float) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "!=": - // fcmp d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) - // cset x0, ne - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x07, 0x9f, 0x9a}) - // scvtf d0, x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "<": - // fcmp d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) - // cset x0, lt - acg.out.out.writer.WriteBytes([]byte{0xe0, 0xa7, 0x9f, 0x9a}) - // scvtf d0, x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "<=": - // fcmp d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) - // cset x0, le - acg.out.out.writer.WriteBytes([]byte{0xe0, 0xc7, 0x9f, 0x9a}) - // scvtf d0, x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case ">": - // fcmp d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) - // cset x0, gt - acg.out.out.writer.WriteBytes([]byte{0xe0, 0xd7, 0x9f, 0x9a}) - // scvtf d0, x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case ">=": - // fcmp d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) - // cset x0, ge - acg.out.out.writer.WriteBytes([]byte{0xe0, 0xb7, 0x9f, 0x9a}) - // scvtf d0, x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "mod", "%": - // Modulo by zero yields the same error-NaN as division by zero. - // fcmp d1, #0.0 - acg.out.out.writer.WriteBytes([]byte{0x28, 0x20, 0x60, 0x1e}) - neModPos := acg.eb.text.Len() - acg.out.BranchCond("ne", 0) // b.ne do_mod - if err := acg.out.MovImm64("x0", 0x7FF8000064763000); err != nil { - return err - } - if err := acg.out.FmovGPToDouble("d0", "x0"); err != nil { - return err - } - bModEndPos := acg.eb.text.Len() - acg.out.Branch(0) // b end_mod - doModLabel := acg.eb.text.Len() - acg.patchJumpOffset(neModPos, int32(doModLabel-neModPos)) - // Modulo: a % b = a - b * floor(a / b) - // d0 = dividend (a), d1 = divisor (b) - // fmov d2, d0 (save dividend in d2) - acg.out.out.writer.WriteBytes([]byte{0x02, 0x40, 0x60, 0x1e}) - // fmov d3, d1 (save divisor in d3) - acg.out.out.writer.WriteBytes([]byte{0x23, 0x40, 0x60, 0x1e}) - // fdiv d0, d0, d1 (d0 = a / b) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x18, 0x61, 0x1e}) - // fcvtzs x0, d0 (x0 = floor(a / b) as int) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // scvtf d0, x0 (d0 = floor(a / b) as float) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - // fmul d0, d0, d3 (d0 = floor(a / b) * b) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x08, 0x63, 0x1e}) - // fsub d0, d2, d0 (d0 = a - floor(a / b) * b) - acg.out.out.writer.WriteBytes([]byte{0x40, 0x38, 0x60, 0x1e}) - modEndLabel := acg.eb.text.Len() - acg.patchJumpOffset(bModEndPos, int32(modEndLabel-bModEndPos)) - case "and": - // Logical AND: returns 1.0 if both non-zero, else 0.0 - // Compare d0 with 0.0 - // fmov d2, xzr (d2 = 0.0) - acg.out.out.writer.WriteBytes([]byte{0xe2, 0x03, 0x67, 0x9e}) - // fcmp d0, d2 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x62, 0x1e}) - // cset x0, ne (x0 = 1 if d0 != 0, else 0) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x07, 0x9f, 0x9a}) - // Compare d1 with 0.0 - // fcmp d1, d2 - acg.out.out.writer.WriteBytes([]byte{0x20, 0x20, 0x62, 0x1e}) - // cset x1, ne (x1 = 1 if d1 != 0, else 0) - acg.out.out.writer.WriteBytes([]byte{0xe1, 0x07, 0x9f, 0x9a}) - // and x0, x0, x1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x01, 0x8a}) - // scvtf d0, x0 (convert result to float) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "or": - // Logical OR: returns 1.0 if either non-zero, else 0.0 - // Compare d0 with 0.0 - // fmov d2, xzr (d2 = 0.0) - acg.out.out.writer.WriteBytes([]byte{0xe2, 0x03, 0x67, 0x9e}) - // fcmp d0, d2 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x62, 0x1e}) - // cset x0, ne (x0 = 1 if d0 != 0, else 0) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x07, 0x9f, 0x9a}) - // Compare d1 with 0.0 - // fcmp d1, d2 - acg.out.out.writer.WriteBytes([]byte{0x20, 0x20, 0x62, 0x1e}) - // cset x1, ne (x1 = 1 if d1 != 0, else 0) - acg.out.out.writer.WriteBytes([]byte{0xe1, 0x07, 0x9f, 0x9a}) - // orr x0, x0, x1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x01, 0xaa}) - // scvtf d0, x0 (convert result to float) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "xor": - // Logical XOR: returns 1.0 if exactly one non-zero, else 0.0 - // Compare d0 with 0.0 - // fmov d2, xzr (d2 = 0.0) - acg.out.out.writer.WriteBytes([]byte{0xe2, 0x03, 0x67, 0x9e}) - // fcmp d0, d2 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x62, 0x1e}) - // cset x0, ne (x0 = 1 if d0 != 0, else 0) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x07, 0x9f, 0x9a}) - // Compare d1 with 0.0 - // fcmp d1, d2 - acg.out.out.writer.WriteBytes([]byte{0x20, 0x20, 0x62, 0x1e}) - // cset x1, ne (x1 = 1 if d1 != 0, else 0) - acg.out.out.writer.WriteBytes([]byte{0xe1, 0x07, 0x9f, 0x9a}) - // eor x0, x0, x1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x01, 0xca}) - // scvtf d0, x0 (convert result to float) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "shl": - // Shift left: convert to int64, shift, convert back - // fcvtzs x0, d0 (x0 = int64(d0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // fcvtzs x1, d1 (x1 = int64(d1)) - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x78, 0x9e}) - // lsl x0, x0, x1 (x0 <<= x1) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0xc1, 0x9a}) - // scvtf d0, x0 (d0 = float64(x0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "shr": - // Shift right: convert to int64, shift, convert back - // fcvtzs x0, d0 (x0 = int64(d0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // fcvtzs x1, d1 (x1 = int64(d1)) - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x78, 0x9e}) - // lsr x0, x0, x1 (x0 >>= x1) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x24, 0xc1, 0x9a}) - // scvtf d0, x0 (d0 = float64(x0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "rol": - // Rotate left: convert to int64, rotate, convert back - // fcvtzs x0, d0 (x0 = int64(d0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // fcvtzs x1, d1 (x1 = int64(d1)) - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x78, 0x9e}) - // neg x2, x1 (x2 = -x1 for rotate) - acg.out.out.writer.WriteBytes([]byte{0xe2, 0x03, 0x01, 0xcb}) - // ror x0, x0, x2 (rotate left by negating right rotate) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x2c, 0xc2, 0x9a}) - // scvtf d0, x0 (d0 = float64(x0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "ror": - // Rotate right: convert to int64, rotate, convert back - // fcvtzs x0, d0 (x0 = int64(d0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // fcvtzs x1, d1 (x1 = int64(d1)) - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x78, 0x9e}) - // ror x0, x0, x1 (x0 rotate right by x1) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x2c, 0xc1, 0x9a}) - // scvtf d0, x0 (d0 = float64(x0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "&b": - // Bitwise AND: convert to int64, AND, convert back - // fcvtzs x0, d0 (x0 = int64(d0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // fcvtzs x1, d1 (x1 = int64(d1)) - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x78, 0x9e}) - // and x0, x0, x1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x01, 0x8a}) - // scvtf d0, x0 (d0 = float64(x0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "|b": - // Bitwise OR: convert to int64, OR, convert back - // fcvtzs x0, d0 (x0 = int64(d0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // fcvtzs x1, d1 (x1 = int64(d1)) - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x78, 0x9e}) - // orr x0, x0, x1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x01, 0xaa}) - // scvtf d0, x0 (d0 = float64(x0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "^b": - // Bitwise XOR: convert to int64, XOR, convert back - // fcvtzs x0, d0 (x0 = int64(d0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // fcvtzs x1, d1 (x1 = int64(d1)) - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x78, 0x9e}) - // eor x0, x0, x1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x01, 0xca}) - // scvtf d0, x0 (d0 = float64(x0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "<>b": - // Right shift: convert to int64, shift, convert back (same as shr) - // fcvtzs x0, d0 (x0 = int64(d0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // fcvtzs x1, d1 (x1 = int64(d1)) - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x78, 0x9e}) - // lsr x0, x0, x1 (x0 >>= x1) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x24, 0xc1, 0x9a}) - // scvtf d0, x0 (d0 = float64(x0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - case "**": - // Power: call pow(base, exponent) from libm - // d0 = base, d1 = exponent - // Result in d0 - if err := acg.eb.GenerateCallInstruction("pow"); err != nil { - return err - } - case "::": - // Cons: prepend element to list - // d0 = element, d1 = list pointer - // Convert to pointers for function call - acg.out.SubImm64("sp", "sp", 16) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x00, 0xfd}) // str d0, [sp] - if err := acg.out.LdrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - - acg.out.SubImm64("sp", "sp", 16) - acg.out.out.writer.WriteBytes([]byte{0xe1, 0x03, 0x00, 0xfd}) // str d1, [sp] - if err := acg.out.LdrImm64("x1", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - - // Call _tim_list_cons(element, list) -> new_list - if err := acg.eb.GenerateCallInstruction("_tim_list_cons"); err != nil { - return err - } - - // Convert result back to d0 - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x40, 0xfd}) // ldr d0, [sp] - acg.out.AddImm64("sp", "sp", 16) - case "?b": - // Bit Test: (int64(d0) >> int64(d1)) & 1 - // fcvtzs x0, d0 (x0 = int64(d0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // fcvtzs x1, d1 (x1 = int64(d1)) - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x78, 0x9e}) - // lsr x0, x0, x1 (x0 >>= x1) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x24, 0xc1, 0x9a}) - // mov x1, #1 - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x80, 0xd2}) - // and x0, x0, x1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x01, 0x8a}) - // scvtf d0, x0 (d0 = float64(x0)) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - default: - return fmt.Errorf("unsupported binary operator for ARM64: %s", e.Operator) - } - - case *ListExpr: - // Lists are stored as: [count][elem0][elem1]... - // If every element is a number literal we can emit the whole thing as - // static rodata. Otherwise we build it on the heap at runtime, evaluating - // each element expression (e.g. struct constructors or variables). - allNumbers := true - for _, elem := range e.Elements { - if _, ok := elem.(*NumberExpr); !ok { - allNumbers = false - break - } - } - - if !allNumbers { - n := len(e.Elements) - if err := acg.out.MovImm64("x0", uint64((n+1)*8)); err != nil { - return err - } - if err := acg.eb.GenerateCallInstruction("_tim_malloc"); err != nil { - return err - } - // Spill the base pointer across element evaluation. - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x0", "sp", 0); err != nil { - return err - } - // Store the count (as float64) at [base]. - if err := acg.out.MovImm64("x1", uint64(n)); err != nil { - return err - } - if err := acg.out.ScvtfInt64ToDouble("d0", "x1"); err != nil { - return err - } - if err := acg.out.LdrImm64("x9", "sp", 0); err != nil { - return err - } - if err := acg.out.StrImm64Double("d0", "x9", 0); err != nil { - return err - } - for i, elem := range e.Elements { - if err := acg.compileExpression(elem); err != nil { // value in d0 - return err - } - if err := acg.out.LdrImm64("x9", "sp", 0); err != nil { - return err - } - if err := acg.out.StrImm64Double("d0", "x9", int32(8+i*8)); err != nil { - return err - } - } - if err := acg.out.LdrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - return acg.out.ScvtfInt64ToDouble("d0", "x0") // numeric pointer - } - - // For now, store list data in rodata - labelName := fmt.Sprintf("list_%d", acg.stringCounter) - acg.stringCounter++ - - var listData []byte - - // Count - count := float64(len(e.Elements)) - countBits := uint64(0) - *(*float64)(unsafe.Pointer(&countBits)) = count - for i := range 8 { - listData = append(listData, byte((countBits>>(i*8))&0xFF)) - } - - // Elements (number literals) - for _, elem := range e.Elements { - numExpr := elem.(*NumberExpr) - elemBits := uint64(0) - *(*float64)(unsafe.Pointer(&elemBits)) = numExpr.Value - for i := range 8 { - listData = append(listData, byte((elemBits>>(i*8))&0xFF)) - } - } - - acg.eb.Define(labelName, string(listData)) - - // Load address into x0 - offset := uint64(acg.eb.text.Len()) - acg.eb.pcRelocations = append(acg.eb.pcRelocations, PCRelocation{ - offset: offset, - symbolName: labelName, - }) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x90}) // ADRP x0, label@PAGE - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x91}) // ADD x0, x0, label@PAGEOFF - - // Convert pointer to float64 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) // scvtf d0, x0 - - case *IndexExpr: - // Compile the list/map expression - if err := acg.compileExpression(e.List); err != nil { - return err - } - - // d0 now contains pointer to list (as float64) - // Convert to integer pointer: fcvtzs x0, d0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - - // Save list pointer (maintain 16-byte stack alignment) - acg.out.SubImm64("sp", "sp", 16) - acg.out.StrImm64("x0", "sp", 0) - - // Compile index expression - if err := acg.compileExpression(e.Index); err != nil { - return err - } - - // Convert index from float64 to int64: fcvtzs x1, d0 - acg.out.out.writer.WriteBytes([]byte{0x01, 0x00, 0x78, 0x9e}) - - // Restore list pointer - acg.out.LdrImm64("x0", "sp", 0) - acg.out.AddImm64("sp", "sp", 16) - - // x0 = pointer, x1 = index/key. - // Maps and strings store [count][key0][val0]... (16-byte pairs), so - // m[k] is a key lookup. Lists store [count][elem0][elem1]... (flat). - idxType := acg.getExprType(e.List) - if idxType == "map" || idxType == "string" { - if err := acg.emitMapKeyLookup(); err != nil { - return err - } - } else { - // Skip past count (8 bytes) and index by (index * 8) - acg.out.AddImm64("x0", "x0", 8) - // x1 = x1 << 3 (multiply by 8) - acg.out.out.writer.WriteBytes([]byte{0x21, 0xf0, 0x7d, 0xd3}) // lsl x1, x1, #3 - // x0 = x0 + x1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x01, 0x8b}) // add x0, x0, x1 - // Load element into d0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x40, 0xfd}) // ldr d0, [x0] - } - - case *CallExpr: - return acg.compileCall(e) - - case *DirectCallExpr: - return acg.compileDirectCall(e) - - case *MatchExpr: - return acg.compileMatchExpr(e) - - case *FMAExpr: - // FMA pattern: a * b + c (or variants) - // Compile A (result in d0) - if err := acg.compileExpression(e.A); err != nil { - return err - } - // Save d0 to stack (op1) - maintain 16-byte alignment - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64Double("d0", "sp", 0); err != nil { - return err - } - - // Compile B (result in d0) - if err := acg.compileExpression(e.B); err != nil { - return err - } - // Save d0 to stack (op2) - maintain 16-byte alignment - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64Double("d0", "sp", 0); err != nil { - return err - } - - // Compile C (result in d0) - if err := acg.compileExpression(e.C); err != nil { - return err - } - // Move C to d3 (accumulator) - // fmov d3, d0 (double-precision register move) - acg.out.out.writer.WriteBytes([]byte{0x03, 0x40, 0x60, 0x1e}) - - // Pop B to d1 (Dm) - if err := acg.out.LdrImm64Double("d1", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - - // Pop A to d2 (Dn) - if err := acg.out.LdrImm64Double("d2", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - - // Result in d0 (Dd). ARM64 fused ops are defined as: - // FMADD = Da + Dn*Dm FMSUB = Da - Dn*Dm - // FNMADD = -(Da + Dn*Dm) FNMSUB = Dn*Dm - Da - // with Dn=a (d2), Dm=b (d1), Da=c (d3). - if e.IsNegMul { - if e.IsSub { - // -(a*b) - c -> -(Dn*Dm + Da) -> FNMADD - return acg.out.FnmaddScalar64("d0", "d2", "d1", "d3") - } - // -(a*b) + c -> Da - Dn*Dm -> FMSUB - return acg.out.FmsubScalar64("d0", "d2", "d1", "d3") - } - if e.IsSub { - // a*b - c -> Dn*Dm - Da -> FNMSUB - return acg.out.FnmsubScalar64("d0", "d2", "d1", "d3") - } - // a*b + c -> FMADD - return acg.out.FmaddScalar64("d0", "d2", "d1", "d3") - - case *LambdaExpr: - // Generate a unique function name for this lambda - acg.lambdaCounter++ - funcName := fmt.Sprintf("lambda_%d", acg.lambdaCounter) - - // A lambda value is a heap closure object [func_ptr, cap0, cap1, ...]. - // Captures are the enclosing-scope variables the body references, taken - // by value at creation time. (Mutable module state is shared via x28 and - // is NOT captured here.) - caps := acg.computeCaptures(e.Body, e.Params) - - acg.lambdaFuncs = append(acg.lambdaFuncs, ARM64LambdaFunc{ - Name: funcName, - Params: e.Params, - ParamCStructTypes: e.ParamCStructTypes, - Body: e.Body, - VarName: acg.currentAssignName, // Store variable name for self-recursion - Captures: caps, - }) - lambdaIdx := len(acg.lambdaFuncs) - 1 - - // Allocate the closure object: 8 (func ptr) + 8*len(caps). - if err := acg.out.MovImm64("x0", uint64(8+len(caps)*8)); err != nil { - return err - } - if err := acg.eb.GenerateCallInstruction("_tim_malloc"); err != nil { - return err - } - acg.out.MovReg64("x10", "x0") // x10 = closure object pointer - - // Store the function address at [obj]. ADRP/ADD target x0 (the relocation - // patcher expects Rd=0), then store into the object held in x10. - off := uint64(acg.eb.text.Len()) - acg.eb.pcRelocations = append(acg.eb.pcRelocations, PCRelocation{offset: off, symbolName: funcName}) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x90}) // ADRP x0, funcName@PAGE - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x91}) // ADD x0, x0, funcName@PAGEOFF - acg.out.out.writer.WriteBytes([]byte{0x40, 0x01, 0x00, 0xf9}) // str x0, [x10] - - // Stash the object pointer and store each captured value at [obj+8+i*8]. - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64("x10", "sp", 0); err != nil { - return err - } - for i, name := range caps { - if acg.boxedVars[name] { - // Captured by reference: the env slot holds the shared cell - // pointer so the closure's mutations persist and are visible. - if err := acg.emitLoadBoxCellPtr(name, "x11"); err != nil { - return err - } - if err := acg.out.LdrImm64("x9", "sp", 0); err != nil { - return err - } - if err := acg.out.StrImm64("x11", "x9", int32(8+i*8)); err != nil { - return err - } - if acg.lambdaFuncs[lambdaIdx].BoxedCaptures == nil { - acg.lambdaFuncs[lambdaIdx].BoxedCaptures = make(map[string]bool) - } - acg.lambdaFuncs[lambdaIdx].BoxedCaptures[name] = true - continue - } - if err := acg.compileExpression(&IdentExpr{Name: name}); err != nil { - return err - } - if err := acg.out.LdrImm64("x9", "sp", 0); err != nil { - return err - } - if err := acg.out.StrImm64Double("d0", "x9", int32(8+i*8)); err != nil { - return err - } - } - if err := acg.out.LdrImm64("x0", "sp", 0); err != nil { - return err - } - acg.out.AddImm64("sp", "sp", 16) - // Closure pointer -> numeric double convention in d0. - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) // scvtf d0, x0 - - case *UnaryExpr: - // Compile the operand first (result in d0) - if err := acg.compileExpression(e.Operand); err != nil { - return err - } - - switch e.Operator { - case "-": - // Unary minus: negate the value - // Use fneg d0, d0 instruction - acg.out.out.writer.WriteBytes([]byte{0x00, 0x40, 0x61, 0x1e}) // fneg d0, d0 - - case "not": - // Logical NOT: returns 1.0 if operand is 0.0, else 0.0 - // Compare d0 with 0.0 - // fmov d1, xzr (d1 = 0.0) - acg.out.out.writer.WriteBytes([]byte{0xe1, 0x03, 0x67, 0x9e}) - // fcmp d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) - // cset x0, eq (x0 = 1 if equal, else 0) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x17, 0x9f, 0x9a}) - // scvtf d0, x0 (convert to float64) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - - case "~b": - // Bitwise NOT: convert to int64, NOT, convert back - // fcvtzs x0, d0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // mvn x0, x0 (bitwise NOT) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x20, 0xaa}) - // scvtf d0, x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - - case "#": - // Length operator: the operand is a list/map pointer (numeric double); - // the length is stored in the first 8 bytes. - // fcvtzs x0, d0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - // ldr d0, [x0] - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x40, 0xfd}) - - default: - return fmt.Errorf("unsupported unary operator for ARM64: %s", e.Operator) - } - - case *LengthExpr: - // Compile the operand (should be a list/map, returns pointer as float64 in d0) - if err := acg.compileExpression(e.Operand); err != nil { - return err - } - - // Convert pointer from float64 to integer in x0 - // fcvtzs x0, d0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) - - // Load length from list/map (first 8 bytes) - // ldr d0, [x0] - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x40, 0xfd}) - - // Length is now in d0 as float64 - - case *MapExpr: - // Map literal stored as: [count (float64)] [key1] [value1] [key2] [value2] ... - labelName := fmt.Sprintf("map_%d", acg.stringCounter) - acg.stringCounter++ - - var mapData []byte - - // Add count - count := float64(len(e.Keys)) - countBits := uint64(0) - *(*float64)(unsafe.Pointer(&countBits)) = count - for i := range 8 { - mapData = append(mapData, byte((countBits>>(i*8))&0xFF)) - } - - // Add key-value pairs (only number literals supported for now) - for i := range e.Keys { - if keyNum, ok := e.Keys[i].(*NumberExpr); ok { - keyBits := uint64(0) - *(*float64)(unsafe.Pointer(&keyBits)) = keyNum.Value - for j := range 8 { - mapData = append(mapData, byte((keyBits>>(j*8))&0xFF)) - } - } else { - return fmt.Errorf("unsupported map key type for ARM64: %T", e.Keys[i]) - } - - if valNum, ok := e.Values[i].(*NumberExpr); ok { - valBits := uint64(0) - *(*float64)(unsafe.Pointer(&valBits)) = valNum.Value - for j := range 8 { - mapData = append(mapData, byte((valBits>>(j*8))&0xFF)) - } - } else { - return fmt.Errorf("unsupported map value type for ARM64: %T", e.Values[i]) - } - } - - acg.eb.Define(labelName, string(mapData)) - - // Load address into x0 - offset := uint64(acg.eb.text.Len()) - acg.eb.pcRelocations = append(acg.eb.pcRelocations, PCRelocation{ - offset: offset, - symbolName: labelName, - }) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x90}) // ADRP x0, label@PAGE - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0x91}) // ADD x0, x0, label@PAGEOFF - - // Convert pointer to float64 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) // scvtf d0, x0 - - case *InExpr: - // Compile value to search for (result in d0) - if err := acg.compileExpression(e.Value); err != nil { - return err - } - - // Save search value to stack - acg.out.SubImm64("sp", "sp", 16) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x00, 0xfd}) // str d0, [sp] - - // Compile container expression (result in d0 as float64 pointer) - if err := acg.compileExpression(e.Container); err != nil { - return err - } - - // Save container pointer - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x07, 0x00, 0xfd}) // str d0, [sp, #8] - - // Convert container pointer from float64 to integer in x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) // fcvtzs x0, d0 - - // Load count from container (first 8 bytes) - acg.out.out.writer.WriteBytes([]byte{0x01, 0x00, 0x40, 0xfd}) // ldr d1, [x0] - - // Convert count to integer in x1 - acg.out.out.writer.WriteBytes([]byte{0x21, 0x00, 0x78, 0x9e}) // fcvtzs x1, d1 - - // x0 = container pointer, x1 = count - // x2 = loop index (start at 0) - if err := acg.out.MovImm64("x2", 0); err != nil { - return err - } - - // Load search value into d2 - acg.out.out.writer.WriteBytes([]byte{0xe2, 0x03, 0x40, 0xfd}) // ldr d2, [sp] - - // Loop start - loopStartPos := acg.eb.text.Len() - - // Compare index with count: cmp x2, x1 - acg.out.out.writer.WriteBytes([]byte{0x5f, 0x00, 0x01, 0xeb}) // cmp x2, x1 - - // If index >= count, jump to not_found - notFoundJumpPos := acg.eb.text.Len() - acg.out.BranchCond("ge", 0) // Placeholder - - // Calculate element address: x0 + 8 + (x2 * 8) - // x3 = x2 * 8 - acg.out.out.writer.WriteBytes([]byte{0x43, 0xf0, 0x7d, 0xd3}) // lsl x3, x2, #3 - // x3 = x0 + x3 - acg.out.out.writer.WriteBytes([]byte{0x03, 0x00, 0x00, 0x8b}) // add x3, x0, x3 - // x3 = x3 + 8 (skip count) - if err := acg.out.AddImm64("x3", "x3", 8); err != nil { - return err - } - - // Load element into d3 - acg.out.out.writer.WriteBytes([]byte{0x63, 0x00, 0x40, 0xfd}) // ldr d3, [x3] - - // Compare element with search value: fcmp d2, d3 - acg.out.out.writer.WriteBytes([]byte{0x40, 0x20, 0x63, 0x1e}) - - // If equal, jump to found - foundJumpPos := acg.eb.text.Len() - acg.out.BranchCond("eq", 0) // Placeholder - - // Increment index: x2++ - if err := acg.out.AddImm64("x2", "x2", 1); err != nil { - return err - } - - // Jump back to loop start - loopBackOffset := int32(loopStartPos - (acg.eb.text.Len() + 4)) - acg.out.Branch(loopBackOffset) - - // Not found: return 0.0 - notFoundPos := acg.eb.text.Len() - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x67, 0x9e}) // fmov d0, xzr - - // Jump to end - endJumpPos := acg.eb.text.Len() - acg.out.Branch(0) // Placeholder - - // Found: return 1.0 - foundPos := acg.eb.text.Len() - if err := acg.out.MovImm64("x0", 1); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) // scvtf d0, x0 - - // End - endPos := acg.eb.text.Len() - - // Clean up stack - acg.out.AddImm64("sp", "sp", 16) - - // Patch jumps - acg.patchJumpOffset(notFoundJumpPos, int32(notFoundPos-notFoundJumpPos)) - acg.patchJumpOffset(foundJumpPos, int32(foundPos-foundJumpPos)) - acg.patchJumpOffset(endJumpPos, int32(endPos-endJumpPos)) - - case *ParallelExpr: - return acg.compileParallelExpr(e) - - case *NamespacedIdentExpr: - // Handle namespaced identifiers like sdl.SDL_INIT_VIDEO or data.field - // Check if this is a C constant - if constants, ok := acg.cConstants[e.Namespace]; ok { - if value, found := constants.Constants[e.Name]; found { - // Found a C constant - load it as a number - if err := acg.out.MovImm64("x0", uint64(value)); err != nil { - return err - } - // Convert to float64: scvtf d0, x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - if VerboseMode { - fmt.Fprintf(os.Stderr, "Resolved C constant %s.%s = %d\n", e.Namespace, e.Name, value) - } - } else { - return fmt.Errorf("undefined constant '%s.%s'", e.Namespace, e.Name) - } - } else { - // Not a C import - treat as field access (obj.field) - // Convert to IndexExpr and compile it - hashValue := hashStringKey(e.Name) - indexExpr := &IndexExpr{ - List: &IdentExpr{Name: e.Namespace}, - Index: &NumberExpr{Value: float64(hashValue)}, - } - return acg.compileExpression(indexExpr) - } - - case *MoveExpr: - // Compile the expression being moved (loads into d0) - // The move operator (!) just compiles the inner expression - // Tracking of moved variables would be done at a higher level - return acg.compileExpression(e.Expr) - - case *FStringExpr: - // F-string: concatenate all parts - if len(e.Parts) == 0 { - // Empty f-string, return empty string - return acg.compileExpression(&StringExpr{Value: ""}) - } - - // Compile first part - firstPart := e.Parts[0] - // Convert to string if needed - if acg.getExprType(firstPart) == "string" { - if err := acg.compileExpression(firstPart); err != nil { - return err - } - } else { - // Not a string - wrap with str() for conversion - if err := acg.compileExpression(&CallExpr{ - Function: "str", - Args: []Expression{firstPart}, - }); err != nil { - return err - } - } - - // Concatenate remaining parts - for i := 1; i < len(e.Parts); i++ { - // Save left pointer (current result) to stack - acg.stackSize += 8 - leftOffset := acg.stackSize - offset := int32(16 + leftOffset - 8) - if err := acg.out.StrImm64Double("d0", "x29", offset); err != nil { - return err - } - - // Evaluate right string (next part) - part := e.Parts[i] - if acg.getExprType(part) == "string" { - if err := acg.compileExpression(part); err != nil { - return err - } - } else { - // Not a string - wrap with str() for conversion - if err := acg.compileExpression(&CallExpr{ - Function: "str", - Args: []Expression{part}, - }); err != nil { - return err - } - } - - // Save right pointer to stack - acg.stackSize += 8 - rightOffset := acg.stackSize - offset = int32(16 + rightOffset - 8) - if err := acg.out.StrImm64Double("d0", "x29", offset); err != nil { - return err - } - - // Load arguments: x0 = left ptr, x1 = right ptr - // Convert float64 pointers to integers - offset = int32(16 + leftOffset - 8) - if err := acg.out.LdrImm64Double("d0", "x29", offset); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x78, 0x9e}) // fcvtzs x0, d0 - - offset = int32(16 + rightOffset - 8) - if err := acg.out.LdrImm64Double("d0", "x29", offset); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0x01, 0x00, 0x78, 0x9e}) // fcvtzs x1, d0 - - // Call string_concat(left, right) - if err := acg.compileCall(&CallExpr{ - Function: "string_concat", - Args: []Expression{}, // Args already in registers - }); err != nil { - return err - } - - // Clean up stack (2 slots) - acg.stackSize -= 16 - } - - case *JumpExpr: - // Compile the value expression of return/jump statements - // The value will be left in d0 - if e.Value != nil { - return acg.compileExpression(e.Value) - } - // No value - leave 0.0 in d0 - if err := acg.out.MovImm64("x0", 0); err != nil { - return err - } - // Convert to float64: scvtf d0, x0 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - - case *BlockExpr: - // Blocks execute all statements in sequence and return the last expression's value - if len(e.Statements) == 0 { - // Empty block: return 0 - return acg.compileExpression(&NumberExpr{Value: 0.0}) - } - - // Execute all statements - for i, stmt := range e.Statements { - isLast := (i == len(e.Statements)-1) - - if isLast { - // For the last statement, make sure its value ends up in d0 - if exprStmt, ok := stmt.(*ExpressionStmt); ok { - // Expression statement: compile it (result goes to d0) - return acg.compileExpression(exprStmt.Expr) - } else if assignStmt, ok := stmt.(*AssignStmt); ok { - // Assignment: compile it, then load the assigned value into d0 - if err := acg.compileStatement(stmt); err != nil { - return err - } - return acg.compileExpression(&IdentExpr{Name: assignStmt.Name}) - } else if ifStmt, ok := stmt.(*IfStmt); ok { - // A trailing `if`/`else` is the block's value: compile it as the - // equivalent value-producing match so its arm result lands in d0. - return acg.compileExpression(ifStmtToMatchExpr(ifStmt)) - } - } - - // Not the last statement, or last statement is not an expression/assignment - if err := acg.compileStatement(stmt); err != nil { - return err - } - } - - // If we get here, the last statement wasn't an expression or assignment - // Return 0 - return acg.compileExpression(&NumberExpr{Value: 0.0}) - - case *CastExpr: - // For now, just compile the expression being cast - // Actual type casting would be more complex - return acg.compileExpression(e.Expr) - - case *PipeExpr: - // Pipe operator: left | right - // For now, implement basic scalar pipe (full list mapping would need ParallelExpr) - leftType := acg.getExprType(e.Left) - - if leftType == "list" { - // List mapping: would need ParallelExpr support - return fmt.Errorf("pipe operator on lists not yet supported in ARM64 (requires ParallelExpr)") - } - - // Scalar pipe: evaluate left, then apply right - if err := acg.compileExpression(e.Left); err != nil { - return err - } - - // For now, just evaluate right (which should use the value in d0) - // Full implementation would handle lambda calls - return acg.compileExpression(e.Right) - - case *UnsafeExpr: - // Inline assembly: execute ARM64-specific block - if len(e.ARM64Block) > 0 { - // Compile ARM64 block statements - for _, stmt := range e.ARM64Block { - if err := acg.compileStatement(stmt); err != nil { - return err - } - } - // Handle return value if specified - if e.ARM64Return != nil { - // Return value is already in the appropriate register - // Just need to ensure it's in d0 if it's a float - return nil - } - } else { - // No ARM64 block - this is expected for x86_64-only unsafe code - return fmt.Errorf("unsafe block has no ARM64 implementation") - } - - case *PatternLambdaExpr: - // Pattern matching lambda with multiple clauses - // Full implementation would need pattern matching codegen - return fmt.Errorf("pattern lambdas not yet implemented in ARM64 (requires pattern matching)") - - case *FieldAccessExpr: - // SROA fast path: `Struct(e0, e1, ...).field` folds straight to the - // matching argument expression — no allocation, no memory round-trip. After - // inlining, the hot path is full of these (e.g. `vadd(...).x` becomes - // `V(a.x+b.x, ...).x` -> `a.x+b.x`), so this keeps intermediate vectors out - // of the bump arena entirely. - if ctor, ok := e.Object.(*CallExpr); ok { - if decl, isStruct := acg.cstructs[ctor.Function]; isStruct && len(ctor.Args) == len(decl.Fields) { - for i := range decl.Fields { - if decl.Fields[i].Name == e.FieldName { - return acg.compileExpression(ctor.Args[i]) - } - } - } - } - - // C-struct field access (p.x) reads raw typed memory at the field offset. - if decl := acg.cstructForExpr(e.Object); decl != nil { - for i := range decl.Fields { - if decl.Fields[i].Name == e.FieldName { - if err := acg.compileExpression(e.Object); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0x09, 0x00, 0x78, 0x9e}) // fcvtzs x9, d0 (numeric ptr) - return acg.emitCStructFieldLoad(&decl.Fields[i]) - } - } - return fmt.Errorf("cstruct %s has no field %s", decl.Name, e.FieldName) - } - // Otherwise Tim map field access: m.x is sugar for m[hashStringKey("x")]. - indexExpr := &IndexExpr{ - List: e.Object, - Index: &NumberExpr{Value: float64(hashStringKey(e.FieldName))}, - } - return acg.compileExpression(indexExpr) - - default: - return fmt.Errorf("unsupported expression type for ARM64: %T", expr) - } - - return nil -} - -// compileAssignment compiles an assignment statement -func (acg *ARM64CodeGen) compileAssignment(assign *AssignStmt) error { - // Module globals: store to the shared heap slot based at x28. Define and - // update both resolve to the same slot (capture-by-reference). - if slot, isGlobal := acg.globalSlots[assign.Name]; isGlobal { - if assign.IsUpdate && !acg.globalMutable[assign.Name] { - return fmt.Errorf("cannot update immutable variable '%s' (use <- only for mutable variables)", assign.Name) - } - oldName := acg.currentAssignName - acg.currentAssignName = assign.Name - if err := acg.compileExpression(assign.Value); err != nil { - return err - } - acg.currentAssignName = oldName - acg.markIfClosure(assign.Name, assign.Value) - acg.varTypes[assign.Name] = acg.getExprType(assign.Value) - return acg.out.StrImm64Double("d0", "x28", int32(slot*8)) - } - - // Validate assignment semantics - _, exists := acg.stackVars[assign.Name] - isMutable := acg.mutableVars[assign.Name] - - // A nested closure updating a variable it captured by reference: the name is - // not a local of this lambda but is one of its boxed captures. - isBoxedCaptureUpdate := assign.IsUpdate && !exists && acg.boxedVars[assign.Name] && - acg.currentLambda != nil && slices.Contains(acg.currentLambda.Captures, assign.Name) - - if assign.IsUpdate { - // <- Update existing mutable variable - if !exists && !isBoxedCaptureUpdate { - return fmt.Errorf("cannot update undefined variable '%s'", assign.Name) - } - if exists && !isMutable { - return fmt.Errorf("cannot update immutable variable '%s' (use <- only for mutable variables)", assign.Name) - } - } else if assign.Mutable { - // := Define mutable variable - if exists { - return fmt.Errorf("variable '%s' already defined (use <- to update)", assign.Name) - } - } else { - // = assignment. If the name already exists in this function's scope, update - // it IN PLACE (reuse its slot) instead of allocating a fresh shadow slot. - // Shadowing a live variable mid-body is what silently broke reassignment - // inside a loop: `lo = m` allocated a new slot, but the loop-top read of - // `lo` had already been compiled against the original slot, so the write - // never fed back. Reusing the slot gives `=` the mutable-by-default - // reassignment semantics idiomatic Tim expects (e.g. bisect/march loops). - if exists { - assign.IsReuseMutable = true - } - } - - // Set the assignment name context for lambda self-reference - oldAssignName := acg.currentAssignName - acg.currentAssignName = assign.Name - - // Compile the value - if err := acg.compileExpression(assign.Value); err != nil { - return err - } - - // Restore previous assignment context - acg.currentAssignName = oldAssignName - - // Boxed variables live in a heap cell shared with nested closures: store the - // value through the cell pointer rather than into the stack slot directly. - // This covers both `<-` updates of a boxed local and a closure updating a - // boxed capture by reference. - if assign.IsUpdate && (isBoxedCaptureUpdate || acg.boxedVars[assign.Name]) { - return acg.emitStoreBoxedVar(assign.Name) - } - - var offset int32 - if assign.IsUpdate { - // <- Update existing mutable variable - look up its offset - stackOffset := acg.stackVars[assign.Name] - offset = int32(16 + stackOffset - 8) - } else { - // = or := - Allocate stack space for new variable (8-byte aligned) - // This includes shadowing for immutable variables - // HOWEVER: if updating existing mutable variable with =, reuse its offset - if assign.IsReuseMutable { - // Reuse existing variable's offset (update with =) - stackOffset := acg.stackVars[assign.Name] - offset = int32(16 + stackOffset - 8) - if VerboseMode { - debugf("DEBUG: Updating existing mutable variable '%s' at offset=%d\n", - assign.Name, offset) - } - } else { - // Allocate new stack space for new variable or shadowing - // Variables are stored at positive offsets from frame pointer - acg.stackSize += 8 - acg.stackVars[assign.Name] = acg.stackSize - acg.mutableVars[assign.Name] = assign.Mutable - if VerboseMode { - debugf("DEBUG: Allocated variable '%s' at stackSize=%d (offset from fp=%d)\n", - assign.Name, acg.stackSize, 16+acg.stackSize-8) - } - // Track the type of the value being assigned - acg.varTypes[assign.Name] = acg.getExprType(assign.Value) - // Track if this is a lambda/function (or a variable holding a closure - // returned from a call) so it can be invoked, including with no args. - acg.markIfClosure(assign.Name, assign.Value) - // Track cstruct-typed pointers (e.g. `p := malloc(n) as Point`, - // `p = Point(...)`, or `p = f(...)` where f returns a cstruct). - if ct := acg.cStructTypeOf(assign.Value); ct != "" { - acg.varCStructType[assign.Name] = ct - } - // Track lists of cstructs so `xs[i].field` resolves the element layout. - if et := acg.listElemCStructTypeOf(assign.Value); et != "" { - acg.varListElemType[assign.Name] = et - } - // x29 points to saved fp location, variables start at offset 16 - offset = int32(16 + acg.stackSize - 8) - - // A new mutable local that a nested closure mutates is boxed: its - // slot holds a heap cell pointer instead of the value. - if acg.boxedVars[assign.Name] { - return acg.emitBoxedDeclaration(offset) - } - } - } - - // Store result on stack: str d0, [x29, #offset] - return acg.out.StrImm64Double("d0", "x29", offset) -} - -// compileMatchExpr compiles a match expression (if/else equivalent) -func (acg *ARM64CodeGen) compileMatchExpr(expr *MatchExpr) error { - // Compile the condition expression (result in d0) - if err := acg.compileExpression(expr.Condition); err != nil { - return err - } - - // The condition is only needed by value-match clauses, guardless `cond { … }` - // clauses, and the no-clause form. A plain guard match (`{ | g => r ~> d }`) - // never reads it — so skip spilling it (and the per-clause reload) entirely, - // which is the common case in heavily-branchy code. - condUsed := len(expr.Clauses) == 0 - for _, c := range expr.Clauses { - if c.IsValueMatch || c.Guard == nil { - condUsed = true - break - } - } - if condUsed { - acg.out.SubImm64("sp", "sp", 16) - if err := acg.out.StrImm64Double("d0", "sp", 0); err != nil { - return err - } - } - - var endJumpPositions []int - - for _, clause := range expr.Clauses { - var nextClauseJumpPos int - - if clause.IsValueMatch { - // Compare condition (d0) with guard value. (Note: the parser usually - // rewrites value matches into a `condition == guard` boolean guard, so - // string matches are handled by the == operator's string path below.) - if err := acg.compileExpression(clause.Guard); err != nil { - return err - } - // Guard value in d0, move to d1 - acg.out.out.writer.WriteBytes([]byte{0x01, 0x40, 0x60, 0x1e}) // fmov d1, d0 - // Load condition back to d0 - if err := acg.out.LdrImm64Double("d0", "sp", 0); err != nil { - return err - } - // fcmp d0, d1 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) - - // Jump to next clause if not equal - nextClauseJumpPos = acg.eb.text.Len() - acg.out.BranchCond("ne", 0) - } else if clause.Guard != nil { - // Boolean guard: evaluate and check if non-zero - if err := acg.compileExpression(clause.Guard); err != nil { - return err - } - // fcmp d0, #0.0 (0.0 is not FMOV-immediate-encodable; zero via xzr) - acg.out.out.writer.WriteBytes([]byte{0xe1, 0x03, 0x67, 0x9e}) // fmov d1, xzr - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) // fcmp d0, d1 - - // Jump to next clause if false (== 0.0) - nextClauseJumpPos = acg.eb.text.Len() - acg.out.BranchCond("eq", 0) - } else { - // Guardless clause from the `cond { body }` form: the condition itself - // is the boolean test. Load it (it was spilled) and run the body if true. - if err := acg.out.LdrImm64Double("d0", "sp", 0); err != nil { - return err - } - acg.out.out.writer.WriteBytes([]byte{0xe1, 0x03, 0x67, 0x9e}) // fmov d1, xzr - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x61, 0x1e}) // fcmp d0, d1 - nextClauseJumpPos = acg.eb.text.Len() - acg.out.BranchCond("eq", 0) // skip body if condition == 0 - } - - // Matched! Compile result - if clause.Result != nil { - if err := acg.compileExpression(clause.Result); err != nil { - return err - } - } - - // Jump to end - endJumpPos := acg.eb.text.Len() - acg.out.Branch(0) - endJumpPositions = append(endJumpPositions, endJumpPos) - - // Every clause now emits a guard branch; patch it to skip to the next clause. - currentPos := acg.eb.text.Len() - acg.patchJumpOffset(nextClauseJumpPos, int32(currentPos-nextClauseJumpPos)) - } - - // Default clause - if expr.DefaultExpr != nil { - if err := acg.compileExpression(expr.DefaultExpr); err != nil { - return err - } - } else if len(expr.Clauses) == 0 { - // No clauses and no default - restore condition - if err := acg.out.LdrImm64Double("d0", "sp", 0); err != nil { - return err - } - } else { - // Default is 0.0 (0.0 is not FMOV-immediate-encodable; zero via xzr) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x67, 0x9e}) // fmov d0, xzr - } - - // End position - endPos := acg.eb.text.Len() - - // Patch all end jumps - for _, jumpPos := range endJumpPositions { - offset := int32(endPos - jumpPos) - acg.patchJumpOffset(jumpPos, offset) - } - - // Clean up the condition spill slot (only allocated when condUsed). - if condUsed { - acg.out.AddImm64("sp", "sp", 16) - } - - return nil -} - -// patchJumpOffset patches a branch instruction's offset -func (acg *ARM64CodeGen) patchJumpOffset(pos int, offset int32) { - // ARM64 branch offsets are in words (4 bytes), not bytes - if offset%4 != 0 { - // Offset not aligned - this shouldn't happen but handle gracefully - offset = (offset >> 2) << 2 - } - - imm := offset >> 2 // Convert to word offset - - textBytes := acg.eb.text.Bytes() - - // Read existing instruction - instr := uint32(textBytes[pos]) | (uint32(textBytes[pos+1]) << 8) | - (uint32(textBytes[pos+2]) << 16) | (uint32(textBytes[pos+3]) << 24) - - // Check if it's a conditional branch (B.cond) or unconditional branch (B) - if (instr & 0xff000010) == 0x54000000 { - // Conditional branch: B.cond - imm19 at bits [23:5] - instr = (instr & 0xff00001f) | ((uint32(imm) & 0x7ffff) << 5) - } else if (instr & 0x7e000000) == 0x34000000 { - // Compare-and-branch: CBZ/CBNZ - imm19 at bits [23:5] - instr = (instr & 0xff00001f) | ((uint32(imm) & 0x7ffff) << 5) - } else if (instr & 0xfc000000) == 0x14000000 { - // Unconditional branch: B - imm26 at bits [25:0] - instr = (instr & 0xfc000000) | (uint32(imm) & 0x3ffffff) - } - - // Write back patched instruction - textBytes[pos] = byte(instr) - textBytes[pos+1] = byte(instr >> 8) - textBytes[pos+2] = byte(instr >> 16) - textBytes[pos+3] = byte(instr >> 24) -} - -// compileParallelExpr compiles a parallel map operation (||) -func (acg *ARM64CodeGen) compileParallelExpr(expr *ParallelExpr) error { - // For now, only support: list || lambda - lambda, ok := expr.Operation.(*LambdaExpr) - if !ok { - return fmt.Errorf("parallel operator (||) currently only supports lambda expressions") - } - - if len(lambda.Params) != 1 { - return fmt.Errorf("parallel operator lambda must have exactly one parameter") - } - - const ( - parallelResultAlloc = 2080 - lambdaScratchOffset = parallelResultAlloc - 8 - savedLambdaSpillOffset = parallelResultAlloc + 8 - ) - - // Compile the lambda to get its function pointer (result in d0) - if err := acg.compileExpression(expr.Operation); err != nil { - return err - } - - // Save lambda function pointer (currently in d0) to stack - // str d0, [sp, #-16]! (pre-indexed: decrement sp by 16, then store) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0xef, 0x1f, 0xfd}) - // Convert d0 to integer pointer: fmov x11, d0 - acg.out.out.writer.WriteBytes([]byte{0x0b, 0x00, 0x67, 0x9e}) - // Save integer pointer: str x11, [sp, #8] - acg.out.out.writer.WriteBytes([]byte{0xeb, 0x07, 0x00, 0xf9}) - - // Compile the input list expression (returns pointer as float64 in d0) - if err := acg.compileExpression(expr.List); err != nil { - return err - } - - // Save list pointer and load as integer pointer - // str d0, [sp, #-8]! (pre-indexed: decrement sp by 8, then store) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0xff, 0x1f, 0xfd}) - // Load as integer: ldr x13, [sp] - acg.out.out.writer.WriteBytes([]byte{0xed, 0x03, 0x40, 0xf9}) - - // Load list length from [x13] into x14 - // ldr d0, [x13] - acg.out.out.writer.WriteBytes([]byte{0xa0, 0x01, 0x40, 0xfd}) - // fcvtzs x14, d0 - convert float64 to int64 - acg.out.out.writer.WriteBytes([]byte{0x0e, 0x00, 0x78, 0x9e}) - - // Allocate result list on stack - // sub sp, sp, #parallelResultAlloc - if err := acg.out.SubImm64("sp", "sp", parallelResultAlloc); err != nil { - return err - } - - // Store result list pointer in x12 - // mov x12, sp - acg.out.out.writer.WriteBytes([]byte{0xec, 0x03, 0x00, 0x91}) - - // Move the saved lambda pointer into the reserved scratch slot - // ldr x10, [x12, #savedLambdaSpillOffset] - spillOffsetImm := (savedLambdaSpillOffset / 8) << 10 - strInstr := uint32(0xf9400000) | uint32(10) | uint32(12<<5) | uint32(spillOffsetImm) - acg.out.out.writer.WriteBytes([]byte{ - byte(strInstr), - byte(strInstr >> 8), - byte(strInstr >> 16), - byte(strInstr >> 24), - }) - // str x10, [x12, #lambdaScratchOffset] - scratchOffsetImm := (lambdaScratchOffset / 8) << 10 - strInstr = uint32(0xf9000000) | uint32(10) | uint32(12<<5) | uint32(scratchOffsetImm) - acg.out.out.writer.WriteBytes([]byte{ - byte(strInstr), - byte(strInstr >> 8), - byte(strInstr >> 16), - byte(strInstr >> 24), - }) - - // Store length in result list - // ldr d0, [x13] - acg.out.out.writer.WriteBytes([]byte{0xa0, 0x01, 0x40, 0xfd}) - // str d0, [x12] - acg.out.out.writer.WriteBytes([]byte{0x80, 0x01, 0x00, 0xfd}) - - // Initialize loop counter to 0 - // mov x15, xzr - acg.out.out.writer.WriteBytes([]byte{0xef, 0x03, 0x1f, 0xaa}) - - // Loop start - loopStart := acg.eb.text.Len() - - // Check if index >= length: cmp x15, x14 - acg.out.out.writer.WriteBytes([]byte{0xdf, 0x01, 0x0e, 0xeb}) - // b.ge loop_end - loopEndJumpPos := acg.eb.text.Len() - acg.out.BranchCond("ge", 0) // Placeholder - - // Load element from input list: input_list[index] - // Element address = x13 + 8 + (x15 * 8) - // mov x0, x15 - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x0f, 0xaa}) - // lsl x0, x0, #3 (multiply by 8) - acg.out.out.writer.WriteBytes([]byte{0x00, 0xf0, 0x7d, 0xd3}) - // add x0, x0, #8 (skip length) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x00, 0x91}) - // add x0, x0, x13 (x0 = address of element) - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x0d, 0x8b}) - - // Load element into d0 - // ldr d0, [x0] - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x40, 0xfd}) - - // Save loop index x15 to stack (will be clobbered by environment pointer) - // str x15, [sp, #-16]! (pre-indexed: decrement sp by 16, then store) - acg.out.out.writer.WriteBytes([]byte{0xef, 0xef, 0x1f, 0xf8}) - - // Load lambda closure object pointer from scratch slot - // ldr x0, [x12, #lambdaScratchOffset] - scratchOffsetImm = (lambdaScratchOffset / 8) << 10 - ldrInstr := uint32(0xf9400000) | uint32(0) | uint32(12<<5) | uint32(scratchOffsetImm) - acg.out.out.writer.WriteBytes([]byte{ - byte(ldrInstr), - byte(ldrInstr >> 8), - byte(ldrInstr >> 16), - byte(ldrInstr >> 24), - }) - - // Extract function pointer from closure object (offset 0) - // ldr x11, [x0, #0] - acg.out.out.writer.WriteBytes([]byte{0x0b, 0x00, 0x40, 0xf9}) - - // Extract environment pointer from closure object (offset 8) into x15 - // ldr x15, [x0, #8] - acg.out.out.writer.WriteBytes([]byte{0x0f, 0x04, 0x40, 0xf9}) - - // Call the lambda function with environment in x15: blr x11 - acg.out.out.writer.WriteBytes([]byte{0x60, 0x01, 0x3f, 0xd6}) - - // Restore loop index from stack - // ldr x15, [sp], #16 - acg.out.out.writer.WriteBytes([]byte{0xef, 0x07, 0x41, 0xf8}) - - // Result is in d0, store it in output list: result_list[index] - // mov x0, x15 - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x0f, 0xaa}) - // lsl x0, x0, #3 - acg.out.out.writer.WriteBytes([]byte{0x00, 0xf0, 0x7d, 0xd3}) - // add x0, x0, #8 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x20, 0x00, 0x91}) - // add x0, x0, x12 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x0c, 0x8b}) - // str d0, [x0] - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x00, 0xfd}) - - // Increment index: add x15, x15, #1 - acg.out.out.writer.WriteBytes([]byte{0xef, 0x05, 0x00, 0x91}) - - // Jump back to loop start - loopBackJumpPos := acg.eb.text.Len() - backOffset := int32(loopStart - loopBackJumpPos) - acg.out.Branch(backOffset) - - // Loop end - loopEndPos := acg.eb.text.Len() - - // Patch conditional jump - acg.patchJumpOffset(loopEndJumpPos, int32(loopEndPos-loopEndJumpPos)) - - // Return result list pointer as float64 in d0 - // mov x0, x12 - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x0c, 0xaa}) - // scvtf d0, x0 - convert pointer to float64 - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) - - // Adjust stack pointer - // add sp, sp, #(parallelResultAlloc + 16 + 8) - if err := acg.out.AddImm64("sp", "sp", parallelResultAlloc+24); err != nil { - return err - } - - return nil -} - -// compileCall compiles a function call -// Confidence that this function is working: 75% -func (acg *ARM64CodeGen) compileCall(call *CallExpr) error { - // CStruct value constructor: Point(x, y, ...) allocates the struct value. - if decl, ok := acg.cstructs[call.Function]; ok { - return acg.compileCStructConstructor(decl, call.Args) - } - - // Check if this is a namespaced call (e.g., sdl.SDL_Init, c.sin) - if strings.Contains(call.Function, ".") { - parts := strings.SplitN(call.Function, ".", 2) - if len(parts) == 2 { - namespace := parts[0] - funcName := parts[1] - - // Check if this is a C library function call - if constants, ok := acg.cConstants[namespace]; ok { - // Check if function signature exists in C header - if sig, found := constants.Functions[funcName]; found { - if VerboseMode { - fmt.Fprintf(os.Stderr, "Calling C function %s.%s with signature: %s %s(...)\n", - namespace, funcName, sig.ReturnType, funcName) - } - // Compile as external C function call - return acg.compileCFunctionCall(funcName, call.Args, sig) - } - return fmt.Errorf("undefined C function '%s.%s'", namespace, funcName) - } - // If the "namespace" is actually a variable, this is method-call - // syntax: desugar xs.append(a) -> append(xs, a) and fall through. - if _, isVar := acg.stackVars[namespace]; isVar { - call.Function = funcName - call.Args = append([]Expression{&IdentExpr{Name: namespace}}, call.Args...) - } else { - // Not a C import - might be a method call or other namespaced access - return fmt.Errorf("undefined namespace '%s' for function call", namespace) - } - } - } - - // C.printf / c.printf explicitly select the C library implementation, - // whereas the bare printf builtin uses Tim's own formatter. - if call.IsCFFI && call.Function == "printf" { - return acg.compilePrintf(call) - } - - switch call.Function { - case "println": - return acg.compilePrintln(call) - case "str": - if len(call.Args) != 1 { - return fmt.Errorf("str() requires exactly 1 argument") - } - if err := acg.compileExpression(call.Args[0]); err != nil { - return err - } - // Arg in d0 - if err := acg.eb.GenerateCallInstruction("_tim_str"); err != nil { - return err - } - // Result is a string pointer in x0. Strings use the numeric pointer - // convention (scvtf to store, fcvtzs to recover) — matching string - // literals and _tim_string_concat — so convert with scvtf, not fmov. - acg.out.out.writer.WriteBytes([]byte{0x00, 0x00, 0x62, 0x9e}) // scvtf d0, x0 - return nil - case "eprint", "eprintln", "eprintf": - return acg.compileEprint(call) - case "exit": - return acg.compileExit(call) - case "exitf", "exitln": - return acg.compileExitf(call) - case "print": - return acg.compilePrint(call) - case "getpid": - return acg.compileGetPid(call) - case "printf": - // Tim's own formatter (no libc). C.printf / c.printf still routes to - // the C library via the namespaced-call path. - return acg.compilePrintfNative(call, 1) - case "me": - // Tail recursion - only valid inside a lambda - if acg.currentLambda == nil { - return fmt.Errorf("'me' keyword can only be used inside a lambda") - } - return acg.compileTailCall(call) - case "call": - return acg.compileFFICall(call) - case "alloc": - return acg.compileAlloc(call) - case "popcount", "clz", "ctz": - return acg.compileBitCount(call, call.Function) - case "head": - return acg.compileHead(call) - case "tail": - return acg.compileTail(call) - case "append": - return acg.compileAppend(call) - case "pop": - return acg.compilePop(call) - case "is_nan": - return acg.compileIsNan(call) - case "error": - return acg.compileError(call) - case "_error_code_extract": - return acg.compileErrorCodeExtract(call) - case "string_concat": - // Internal string concatenation function - // Arguments should already be in x0 and x1 - if err := acg.eb.GenerateCallInstruction("_tim_string_concat"); err != nil { - return err - } - // Result pointer in x0 -> numeric double convention in d0. - return acg.out.ScvtfInt64ToDouble("d0", "x0") - case "write_i8", "write_i16", "write_i32", "write_i64", - "write_u8", "write_u16", "write_u32", "write_u64", "write_f32", "write_f64": - return acg.compileMemoryWrite(call) - case "read_i8", "read_i16", "read_i32", "read_i64", - "read_u8", "read_u16", "read_u32", "read_u64", "read_f32", "read_f64": - return acg.compileMemoryRead(call) - case "dlopen": - // dlopen(path, flags) - sig := &CFunctionSignature{ - ReturnType: "void*", - Params: []CFunctionParam{ - {Type: "const char*", Name: "path"}, - {Type: "int", Name: "mode"}, - }, - } - return acg.compileCFunctionCall("dlopen", call.Args, sig) - case "dlsym": - // dlsym(handle, symbol) - sig := &CFunctionSignature{ - ReturnType: "void*", - Params: []CFunctionParam{ - {Type: "void*", Name: "handle"}, - {Type: "const char*", Name: "symbol"}, - }, - } - return acg.compileCFunctionCall("dlsym", call.Args, sig) - case "dlclose": - // dlclose(handle) - sig := &CFunctionSignature{ - ReturnType: "int", - Params: []CFunctionParam{ - {Type: "void*", Name: "handle"}, - }, - } - return acg.compileCFunctionCall("dlclose", call.Args, sig) - case "dlerror": - // dlerror() - sig := &CFunctionSignature{ - ReturnType: "char*", - Params: []CFunctionParam{}, - } - return acg.compileCFunctionCall("dlerror", call.Args, sig) - default: - // Check if this is a self-recursive call within a lambda - if acg.currentLambda != nil && call.Function == acg.currentLambda.VarName { - // Mark lambda as recursive - acg.currentLambda.IsRecursive = true - // This is a recursive call - compile arguments and call current function - return acg.compileSelfRecursiveCall(call) - } - - // Check if it's a variable holding a function pointer or value - if _, exists := acg.stackVars[call.Function]; exists { - // Check if this is actually a lambda/function or just a value - isLambda := acg.lambdaVars[call.Function] - - // If calling a non-lambda value with no args, just return the value - if !isLambda && len(call.Args) == 0 { - stackOffset := acg.stackVars[call.Function] - offset := int32(16 + stackOffset - 8) - if err := acg.out.LdrImm64Double("d0", "x29", offset); err != nil { - return err - } - return nil - } - - // Convert to DirectCallExpr and compile - directCall := &DirectCallExpr{ - Callee: &IdentExpr{Name: call.Function}, - Args: call.Args, - } - return acg.compileDirectCall(directCall) - } - - // Hardware-instruction fast path: these map to a single ARM64 FP op, so - // emit it inline instead of a libm call (which on macOS also pays an - // expensive dynamic-link stub indirection — the stub cost dominated sqrt - // in profiles of the metaballs). Must run BEFORE the implicit-`c` libm - // route below, which would otherwise emit a call for sqrt/floor/etc. - if op, ok := arm64UnaryFPOps[call.Function]; ok && len(call.Args) == 1 { - if err := acg.compileExpression(call.Args[0]); err != nil { // arg in d0 - return err - } - acg.out.out.writer.WriteBytes([]byte{byte(op), byte(op >> 8), byte(op >> 16), byte(op >> 24)}) - return nil - } - - // Check if it's a C function from the "c" namespace (implicit) - // This handles bare function names like sin(), cos(), etc. from libm - if constants, ok := acg.cConstants["c"]; ok { - if sig, found := constants.Functions[call.Function]; found { - if VerboseMode { - fmt.Fprintf(os.Stderr, "Calling implicit C function c.%s\n", call.Function) - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - } - } - - // This is critical for macOS/ARM64 where header parsing might be flaky in tests - switch call.Function { - case "sin", "cos", "tan", "asin", "acos", "atan", - "cbrt", "exp", "exp2", "log", "log2", "log10": - // Single-argument libm functions: double f(double) - sig := &CFunctionSignature{ - ReturnType: "double", - Params: []CFunctionParam{{Type: "double", Name: "x"}}, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "pow", "atan2", "fmod", "hypot", "copysign", "fdim", "fmax", "fmin", "nextafter": - // Two-argument libm functions: double f(double, double) - sig := &CFunctionSignature{ - ReturnType: "double", - Params: []CFunctionParam{ - {Type: "double", Name: "x"}, - {Type: "double", Name: "y"}, - }, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "malloc": - sig := &CFunctionSignature{ - ReturnType: "void*", - Params: []CFunctionParam{{Type: "size_t", Name: "size"}}, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "free": - sig := &CFunctionSignature{ - ReturnType: "void", - Params: []CFunctionParam{{Type: "void*", Name: "ptr"}}, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "strlen": - sig := &CFunctionSignature{ - ReturnType: "size_t", - Params: []CFunctionParam{{Type: "const char*", Name: "s"}}, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "fopen": - sig := &CFunctionSignature{ - ReturnType: "void*", - Params: []CFunctionParam{{Type: "const char*", Name: "path"}, {Type: "const char*", Name: "mode"}}, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "fwrite": - sig := &CFunctionSignature{ - ReturnType: "size_t", - Params: []CFunctionParam{ - {Type: "void*", Name: "ptr"}, {Type: "size_t", Name: "size"}, - {Type: "size_t", Name: "nmemb"}, {Type: "void*", Name: "stream"}, - }, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "fputc": - sig := &CFunctionSignature{ - ReturnType: "int", - Params: []CFunctionParam{{Type: "int", Name: "c"}, {Type: "void*", Name: "stream"}}, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "fclose": - sig := &CFunctionSignature{ - ReturnType: "int", - Params: []CFunctionParam{{Type: "void*", Name: "stream"}}, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "fork", "getpid": - sig := &CFunctionSignature{ReturnType: "int", Params: nil} - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "mmap": - sig := &CFunctionSignature{ - ReturnType: "void*", - Params: []CFunctionParam{ - {Type: "void*", Name: "addr"}, {Type: "size_t", Name: "len"}, - {Type: "int", Name: "prot"}, {Type: "int", Name: "flags"}, - {Type: "int", Name: "fd"}, {Type: "long", Name: "offset"}, - }, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "munmap": - sig := &CFunctionSignature{ - ReturnType: "int", - Params: []CFunctionParam{{Type: "void*", Name: "addr"}, {Type: "size_t", Name: "len"}}, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "waitpid": - sig := &CFunctionSignature{ - ReturnType: "int", - Params: []CFunctionParam{ - {Type: "int", Name: "pid"}, {Type: "void*", Name: "status"}, {Type: "int", Name: "options"}, - }, - } - return acg.compileCFunctionCall(call.Function, call.Args, sig) - case "proc_exit": - // Immediate process exit (libc _exit): no atexit handlers, no stdio - // flush, no framework teardown — essential for a forked worker that - // must not run the parent's Metal/CoreFoundation cleanup. - sig := &CFunctionSignature{ReturnType: "void", Params: []CFunctionParam{{Type: "int", Name: "code"}}} - return acg.compileCFunctionCall("_exit", call.Args, sig) - } - - // A call to a module-level named function that isn't visible as a stack - // variable in this scope (e.g. recursion or a function referenced from a - // different lambda). Emit a direct branch to its generated label. - for idx := range acg.lambdaFuncs { - if acg.lambdaFuncs[idx].VarName == call.Function { - return acg.compileNamedCall(acg.lambdaFuncs[idx].Name, call.Args) - } - } - - return fmt.Errorf("unsupported function for ARM64: %s", call.Function) - } -} - -// compileNamedCall emits a direct call (bl) to a generated function label, -// passing arguments in d0-d7 per AAPCS64. The branch is left as a placeholder -// and resolved by the writer's internal call-patching using eb.labels. -func (acg *ARM64CodeGen) compileNamedCall(label string, args []Expression) error { - if len(args) > 8 { - return fmt.Errorf("too many arguments to call %s (max 8)", label) - } - // Evaluate arguments left to right, spilling each to the stack. - for _, arg := range args { - if err := acg.compileExpression(arg); err != nil { - return err - } - acg.out.SubImm64("sp", "sp", 16) - acg.out.out.writer.WriteBytes([]byte{0xe0, 0x03, 0x00, 0xfd}) // str d0, [sp] - } - // Reload into d0..d(n-1) (reverse, since the last pushed is on top). - for i := len(args) - 1; i >= 0; i-- { - instr := uint32(0xfd400000) | uint32(i) | (31 << 5) // ldr dN, [sp] - acg.out.out.writer.WriteBytes([]byte{byte(instr), byte(instr >> 8), byte(instr >> 16), byte(instr >> 24)}) - acg.out.AddImm64("sp", "sp", 16) - } - // bl