From d49b3837923ebd4b166bb56f18d20a3d51705fab Mon Sep 17 00:00:00 2001 From: ZVN DEV <78920650+zvndev@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:37:52 -0400 Subject: [PATCH 1/7] build: remove the experimental LLVM backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cranelift is now the single code-generation backend. The LLVM path was a partial second implementation (~half the builtins, no push/str_*/math/http), did not build cleanly from the documented steps (zstd link error), and offered no measured speedup over Cranelift — which already edges out cc -O2 on the fib benchmark. Removing it eliminates ~6.8K LOC of duplicated codegen and the cost of keeping two backends at parity. - delete turbo-codegen-llvm crate; drop it from the workspace + turbo-cli deps - remove the `--llvm` cargo feature - drop the LLVM jobs/steps from CI, nightly, and release workflows - update RELEASE.md / CLAUDE.md test commands (no more --exclude turbo-codegen-llvm) A future optimizing backend, if pursued, should lower through a shared mid-level IR rather than re-walking the AST a second time. --- .github/workflows/ci.yml | 78 +- .github/workflows/nightly.yml | 49 +- .github/workflows/release.yml | 37 +- AUDIT-REPORT.md | 48 - CLAUDE.md | 2 +- FINDINGS-REPORT.md | 138 - SPRINT-PLAN-0.8.0.md | 164 -- SPRINT-PLAN-2026-03.md | 103 - SPRINT-PLAN.md | 71 - STDLIB-ROADMAP.md | 174 -- TODO.md | 38 - benchmark-final.png | Bin 117332 -> 0 bytes docs/RELEASE.md | 9 +- turbo/Cargo.lock | 102 - turbo/Cargo.toml | 1 - turbo/PHASE1-POLISH-SPRINT.md | 391 --- turbo/crates/turbo-cli/Cargo.toml | 2 - turbo/crates/turbo-codegen-llvm/Cargo.toml | 9 - .../crates/turbo-codegen-llvm/src/builtins.rs | 1817 ------------ turbo/crates/turbo-codegen-llvm/src/ctx.rs | 125 - turbo/crates/turbo-codegen-llvm/src/expr.rs | 2615 ----------------- .../crates/turbo-codegen-llvm/src/helpers.rs | 592 ---- turbo/crates/turbo-codegen-llvm/src/lib.rs | 1355 --------- turbo/crates/turbo-codegen-llvm/src/stmt.rs | 156 - turbo/crates/turbo-codegen-llvm/src/types.rs | 151 - 25 files changed, 24 insertions(+), 8203 deletions(-) delete mode 100644 AUDIT-REPORT.md delete mode 100644 FINDINGS-REPORT.md delete mode 100644 SPRINT-PLAN-0.8.0.md delete mode 100644 SPRINT-PLAN-2026-03.md delete mode 100644 SPRINT-PLAN.md delete mode 100644 STDLIB-ROADMAP.md delete mode 100644 TODO.md delete mode 100644 benchmark-final.png delete mode 100644 turbo/PHASE1-POLISH-SPRINT.md delete mode 100644 turbo/crates/turbo-codegen-llvm/Cargo.toml delete mode 100644 turbo/crates/turbo-codegen-llvm/src/builtins.rs delete mode 100644 turbo/crates/turbo-codegen-llvm/src/ctx.rs delete mode 100644 turbo/crates/turbo-codegen-llvm/src/expr.rs delete mode 100644 turbo/crates/turbo-codegen-llvm/src/helpers.rs delete mode 100644 turbo/crates/turbo-codegen-llvm/src/lib.rs delete mode 100644 turbo/crates/turbo-codegen-llvm/src/stmt.rs delete mode 100644 turbo/crates/turbo-codegen-llvm/src/types.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a879a85..bece61f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('turbo/Cargo.lock') }} - name: cargo clippy - run: cargo clippy --manifest-path turbo/Cargo.toml --workspace --exclude turbo-codegen-llvm --all-targets -- -D warnings + run: cargo clippy --manifest-path turbo/Cargo.toml --workspace --all-targets -- -D warnings doc: name: Rustdoc @@ -65,12 +65,11 @@ jobs: # Build all crate docs with warnings as errors. Catches broken # intra-doc links, invalid HTML in doc comments, and busted Rust - # examples in doc-tests. The LLVM backend is excluded to keep this - # job hermetic — it has its own dedicated job below. + # examples in doc-tests. - name: cargo doc env: RUSTDOCFLAGS: "-D warnings" - run: cargo doc --no-deps --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo doc --no-deps --workspace --manifest-path turbo/Cargo.toml test: name: Unit tests (${{ matrix.os }}) @@ -95,7 +94,7 @@ jobs: key: ${{ runner.os }}-cargo-test-${{ hashFiles('turbo/Cargo.lock') }} - name: cargo test - run: cargo test --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo test --workspace --manifest-path turbo/Cargo.toml integration: name: Integration tests (${{ matrix.os }}) @@ -120,7 +119,7 @@ jobs: key: ${{ runner.os }}-cargo-integration-${{ hashFiles('turbo/Cargo.lock') }} - name: Build release - run: cargo build --release --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo build --release --workspace --manifest-path turbo/Cargo.toml - name: Run integration tests run: ./tests/run_tests.sh @@ -145,7 +144,7 @@ jobs: key: ${{ runner.os }}-cargo-overflow-${{ hashFiles('turbo/Cargo.lock') }} - name: cargo test (overflow-checks enabled) - run: cargo test --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo test --workspace --manifest-path turbo/Cargo.toml env: RUSTFLAGS: "-C overflow-checks=on" @@ -168,7 +167,7 @@ jobs: key: ${{ runner.os }}-cargo-build-${{ hashFiles('turbo/Cargo.lock') }} - name: Build - run: cargo build --release --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo build --release --workspace --manifest-path turbo/Cargo.toml - name: Report binary size run: | @@ -193,67 +192,6 @@ jobs: run: bash tests.sh working-directory: turbo/crates/turbo-codegen-cranelift/runtime - llvm-backend: - name: LLVM backend (build + test) - runs-on: ubuntu-latest - # Hard-required now that struct destructuring, if-let, and optional - # chaining are implemented. The three features were the last TODOs - # gating the fence; see CHANGELOG.md under [Unreleased] for details. - # Only run when LLVM-related files change, or on pushes to master - if: >- - github.event_name == 'push' || - github.event_name == 'pull_request' - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 - id: filter - with: - filters: | - llvm: - - 'turbo/crates/turbo-codegen-llvm/**' - - 'turbo/crates/turbo-ast/**' - - '.github/workflows/ci.yml' - - - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 - with: - toolchain: stable - if: steps.filter.outputs.llvm == 'true' || github.event_name == 'push' - - - name: Install LLVM 18 - if: steps.filter.outputs.llvm == 'true' || github.event_name == 'push' - run: | - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - sudo apt-get update - sudo apt-get install -y llvm-18-dev libpolly-18-dev - - - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 - if: steps.filter.outputs.llvm == 'true' || github.event_name == 'push' - with: - path: | - ~/.cargo/registry - ~/.cargo/git - turbo/target - key: ${{ runner.os }}-cargo-llvm-${{ hashFiles('turbo/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-llvm- - - - name: Build LLVM backend - if: steps.filter.outputs.llvm == 'true' || github.event_name == 'push' - run: cargo build --manifest-path turbo/Cargo.toml -p turbo-codegen-llvm - env: - LLVM_SYS_180_PREFIX: /usr/lib/llvm-18 - - - name: Test LLVM backend - if: steps.filter.outputs.llvm == 'true' || github.event_name == 'push' - run: cargo test --manifest-path turbo/Cargo.toml -p turbo-codegen-llvm - env: - LLVM_SYS_180_PREFIX: /usr/lib/llvm-18 - - - name: Skip notice - if: steps.filter.outputs.llvm != 'true' && github.event_name != 'push' - run: echo "No LLVM-related files changed — skipping build and test." - website: name: Website lint + build runs-on: ubuntu-latest @@ -301,7 +239,7 @@ jobs: key: ${{ runner.os }}-cargo-parity-${{ hashFiles('turbo/Cargo.lock') }} - name: Build release - run: cargo build --release --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo build --release --workspace --manifest-path turbo/Cargo.toml - name: Run parity tests run: ./tests/parity/run_parity.sh diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7de7a22..4832b56 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -31,13 +31,13 @@ jobs: run: cargo fmt --manifest-path turbo/Cargo.toml --all -- --check - name: cargo clippy -D warnings - run: cargo clippy --manifest-path turbo/Cargo.toml --workspace --exclude turbo-codegen-llvm --all-targets -- -D warnings + run: cargo clippy --manifest-path turbo/Cargo.toml --workspace --all-targets -- -D warnings - name: cargo test - run: cargo test --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo test --workspace --manifest-path turbo/Cargo.toml - name: Build release for integration + parity - run: cargo build --release --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo build --release --workspace --manifest-path turbo/Cargo.toml - name: Integration tests run: ./tests/run_tests.sh @@ -50,7 +50,7 @@ jobs: - name: cargo doc -D warnings env: RUSTDOCFLAGS: "-D warnings" - run: cargo doc --no-deps --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo doc --no-deps --workspace --manifest-path turbo/Cargo.toml - uses: taiki-e/install-action@9545a0634ccdca487a09cde61d362f0bc5fb5df9 # v2.61.4 with: @@ -74,13 +74,6 @@ jobs: # in a dashboard nobody watches. Future improvement: add it back # if/when we want to track soft-fail trends across days (e.g. # feed the hash diff into a cross-build observability bucket). - # - # `llvm-backend` is *not* omitted: ci.yml path-gates it so PRs - # don't pay the LLVM build cost on unrelated changes, but the - # nightly canary runs on master nightly and exists exactly to - # catch backend rot, so we run it for real (no path filter, no - # continue-on-error). The LLVM toolchain install step matches - # the one in ci.yml exactly. - name: C runtime tests run: bash tests.sh @@ -89,36 +82,6 @@ jobs: - name: Lint error codes run: ./scripts/check_error_codes.sh - - name: Install LLVM 18 (for llvm-backend) - run: | - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - sudo apt-get update - sudo apt-get install -y llvm-18-dev libpolly-18-dev - - # Mirror the LLVM cache step from ci.yml so the nightly canary hits the - # same cache layer as PR builds. Keeping the key and restore-keys in - # lockstep means a cold LLVM 18 build is rare — otherwise this would - # add ~10 minutes to every nightly run. - - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - turbo/target - key: ${{ runner.os }}-cargo-llvm-${{ hashFiles('turbo/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-llvm- - - - name: Build LLVM backend - run: cargo build --manifest-path turbo/Cargo.toml -p turbo-codegen-llvm - env: - LLVM_SYS_180_PREFIX: /usr/lib/llvm-18 - - - name: Test LLVM backend - run: cargo test --manifest-path turbo/Cargo.toml -p turbo-codegen-llvm - env: - LLVM_SYS_180_PREFIX: /usr/lib/llvm-18 - - name: Build codegen fuzz binary run: cargo build --release --bin codegen-fuzz --manifest-path turbo/fuzz/Cargo.toml @@ -142,7 +105,7 @@ jobs: toolchain: stable - name: Build release binary - run: cargo build --release --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo build --release --workspace --manifest-path turbo/Cargo.toml - name: Smoke test run: ./turbo/target/release/turbolang --version @@ -161,7 +124,7 @@ jobs: toolchain: stable - name: Build release - run: cargo build --release --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml + run: cargo build --release --workspace --manifest-path turbo/Cargo.toml - name: Package canary run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2616000..45ace87 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,18 +17,12 @@ jobs: - os: macos-latest target: aarch64-apple-darwin artifact: turbolang - llvm: true - llvm_prefix: /opt/homebrew/opt/llvm@18 - os: macos-latest target: x86_64-apple-darwin artifact: turbolang - llvm: false - llvm_prefix: "" - os: ubuntu-latest target: x86_64-unknown-linux-gnu artifact: turbolang - llvm: true - llvm_prefix: /usr/lib/llvm-18 runs-on: ${{ matrix.os }} @@ -40,17 +34,6 @@ jobs: toolchain: stable targets: ${{ matrix.target }} - - name: Install LLVM 18 (macOS) - if: runner.os == 'macOS' && matrix.llvm - run: brew install llvm@18 zstd - - - name: Install LLVM 18 (Linux) - if: runner.os == 'Linux' && matrix.llvm - run: | - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - sudo apt-get update - sudo apt-get install -y llvm-18-dev libpolly-18-dev - - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: | @@ -62,17 +45,7 @@ jobs: ${{ runner.os }}-cargo-release-${{ matrix.target }}- ${{ runner.os }}-cargo-release- - - name: Build (Cranelift + LLVM) - if: matrix.llvm - run: | - cd turbo - cargo build --release --target ${{ matrix.target }} -p turbo-cli --features turbo-cli/llvm - env: - LLVM_SYS_180_PREFIX: ${{ matrix.llvm_prefix }} - LIBRARY_PATH: ${{ runner.os == 'macOS' && '/opt/homebrew/lib' || '' }} - - - name: Build (Cranelift only) - if: "!matrix.llvm" + - name: Build (Cranelift) run: | cd turbo cargo build --release --target ${{ matrix.target }} -p turbo-cli @@ -92,11 +65,11 @@ jobs: echo 'fn main() { print("turbo works") }' > /tmp/test.tb ./turbo/target/${{ matrix.target }}/release/turbolang run /tmp/test.tb - - name: Smoke test - LLVM AOT build - if: matrix.target != 'x86_64-apple-darwin' && matrix.llvm + - name: Smoke test - AOT build + if: matrix.target != 'x86_64-apple-darwin' run: | - ./turbo/target/${{ matrix.target }}/release/turbolang build --llvm /tmp/test.tb -o /tmp/test_llvm - /tmp/test_llvm + ./turbo/target/${{ matrix.target }}/release/turbolang build /tmp/test.tb -o /tmp/test_aot + /tmp/test_aot - name: Package run: | diff --git a/AUDIT-REPORT.md b/AUDIT-REPORT.md deleted file mode 100644 index fddcf65..0000000 --- a/AUDIT-REPORT.md +++ /dev/null @@ -1,48 +0,0 @@ -# TurboLang Product + OSS Audit Report -Generated: 2026-05-09 -Source: Combined `$product-review` + `$gold-standard-os` review in this Codex session. - -## Product Verdict -TurboLang is real software: Rust compiler workspace, CLI, LSP, docs, examples, release engineering, CI, security policy, fuzzing, and runnable demos. Current rating before sprint: 7.1/10. OSS score before sprint: 39-40/50. - -## P0/P1 Findings - -### P0 / Critical Sprint Blockers -1. **AOT/JIT array parity failure** - - Command: `./tests/parity/run_parity.sh` - - Failing program: `turbo/tests/parity/programs/arrays.tb` - - JIT prints `5,15,1,5`; AOT fails with `runtime error: array index 0 out of bounds (length 0)`. - - Impact: core compiler correctness issue. - -2. **Dependency install path traversal** - - Files: `turbo/crates/turbo-cli/src/main.rs:503-505`, `877-889`, `934-975`, `1127-1137`. - - Issue: dependency key is used in `turbo_modules.join(dep.name)` without validating against `..`, separators, or absolute names. - - Impact: malicious `turbo.toml` can delete/replace files outside `turbo_modules` during install/update. - -3. **Wildcard CORS in Turbo HTTP runtime** - - Files: `turbo/crates/turbo-codegen-cranelift/src/runtime.rs:1417-1424`, `turbo/crates/turbo-codegen-cranelift/runtime/turbo_rt.c:1963-1967`. - - Issue: typed responses emit `Access-Control-Allow-Origin: *` by default. - - Impact: arbitrary websites can read local/public Turbo HTTP server responses. - -4. **Website vulnerable dependency** - - File: `website/package.json:12`. - - `npm audit` reports one high Next.js advisory and one moderate PostCSS advisory. Upgrade Next to patched version. - -### P1 / High Trust-Breaking Issues -5. **Formatting drift** - - `cargo fmt --manifest-path turbo/Cargo.toml --all -- --check` fails. - -6. **Parity not PR-gated** - - CI has parity in nightly only; add PR CI parity job. - -7. **Website not root-CI-gated** - - Add `npm ci`, `npm run lint`, and `npm run build` to root CI. - -8. **Version/docs drift** - - Runtime reports `turbolang 0.8.0`; flagship demo says `v1.0` / JSON `1.0.0`. - - `SECURITY.md` claims no request body size limits while runtime has limits. - -### P2 / Important Follow-ups -9. Add coverage reporting. -10. Add CodeQL/secret scanning/dependency review. -11. Split large files over time. diff --git a/CLAUDE.md b/CLAUDE.md index e1321c5..1b979aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ cargo build --manifest-path turbo/Cargo.toml cargo build --release --manifest-path turbo/Cargo.toml # Run all unit tests -cargo test --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml +cargo test --workspace --manifest-path turbo/Cargo.toml # Run a .tb source file via JIT cargo run --manifest-path turbo/Cargo.toml -- run turbo/tests/phase1/hello.tb diff --git a/FINDINGS-REPORT.md b/FINDINGS-REPORT.md deleted file mode 100644 index f16ac83..0000000 --- a/FINDINGS-REPORT.md +++ /dev/null @@ -1,138 +0,0 @@ -# TurboLang v0.9.0 — Findings & Fixes Report - -**Date:** 2025-05-16 -**Scope:** Full product review, code review, and implementation sprint -**Commits:** 14 commits across TurboLang, TurboServo, turbo-agent - ---- - -## Executive Summary - -TurboLang is in strong shape at v0.9.0. The compiler is correct, the type system is sound, generics and traits are fully working, and the stdlib is comprehensive (104 builtins). The main findings were **thread-safety bugs in the runtime** (3 critical use-after-free / data-race issues) and **type system holes** in edge cases. All critical and high-severity issues have been fixed and pushed. - ---- - -## Feature Status Clarification - -| Feature | Status | Notes | -|---------|--------|-------| -| **Generics** | FULLY WORKING | `fn`, `struct`, `enum`, type inference at call sites | -| **Traits** | FULLY WORKING | `trait Foo {}`, `impl Foo for Bar {}`, trait bounds `` | -| **Trait bounds** | FULLY WORKING | Sema validates bounds, codegen uses name-mangled dispatch | -| **Impl blocks** | FULLY WORKING | Multiple methods, self parameter, static dispatch | -| **Result types** | FULLY WORKING | `i64 ! str`, `?` operator, `ok()`/`err()`, match arms | -| **Async/Spawn** | WORKING (fixed) | Was crashing under arena — now uses malloc for thread contexts | -| **Channels** | WORKING (fixed) | Was use-after-free under arena — now uses malloc for nodes | - -**Key insight:** The turbo-agent DESIGN.md was overly conservative. Generics, traits, and impl blocks all work today. The actual blockers for turbo-agent are: (1) no `http_post` with custom headers, (2) no JSON object builder beyond `json_stringify(key, value)`. Both have workarounds (`shell_exec` + curl, string concatenation). - ---- - -## Issues Found & Fixed - -### CRITICAL (3) — All Fixed - -| # | File | Issue | Fix | -|---|------|-------|-----| -| 1 | `turbo_rt.c:1062` | `rt_spawn_with_args` allocates spawn context via `turbo_alloc` — use-after-free when arena reclaims while thread runs | Changed to `malloc` directly | -| 2 | `turbo_rt.c:919` | `rt_random_seeded` is a non-atomic global + `rand()` is not thread-safe — data race with `spawn` | Replaced with thread-local xorshift32 PRNG | -| 3 | `runtime.rs:926` | JIT random hashes `subsec_nanos()` — identical values within same nanosecond | Replaced with thread-local xorshift64 with proper seeding | - -### HIGH (4) — All Fixed - -| # | File | Issue | Fix | -|---|------|-------|-----| -| 4 | `turbo_rt.c:1529` | Channel nodes allocated via `turbo_alloc` — use-after-free across threads | Changed to `malloc`/`free` | -| 5 | `expr.rs:547` | `Expr::Try` hardcodes return type as `TurboTy::Int` regardless of actual Ok type | Extracts Ok type from `TurboTy::Result` | -| 6 | `sema/lib.rs:194` | `U32` literal check only validates `n >= 0`, missing upper bound | Added `n <= 4_294_967_295` and `I32` range | -| 7 | `turbo_rt.c:805` | `rt_str_join` returns static `""` literal — crash if caller frees it | Returns heap-allocated empty string | - -### MEDIUM (6) — Documented, Not Yet Fixed - -| # | File | Issue | Severity | Notes | -|---|------|-------|----------|-------| -| 8 | `expr.rs:78` | `.unwrap()` on binary op compile — panics if operand is Unit type | MEDIUM | Sema prevents this in practice | -| 9 | `turbo_rt.c:1375` | C runtime `rt_json_get` uses naive substring search — wrong for nested JSON | MEDIUM | JIT uses serde_json (correct); C version is AOT-only | -| 10 | `turbo_rt.c:1599` | `rt_mutex_clone` returns same pointer without refcount — dangling risk | MEDIUM | Mutexes are effectively immortal in current programs | -| 11 | `builtins.rs:344` | `compile_abs` only handles i64, incorrect for f64 | MEDIUM | Sema restricts to integers currently | -| 12 | `builtins.rs:356` | `compile_min`/`compile_max` use signed comparison for unsigned types | MEDIUM | No user-facing u8/u16 arithmetic yet | -| 13 | `turbo_rt.c:2585` | `rt_list_dir` uses `turbo_realloc` to grow array — corrupts entries under arena | MEDIUM | Only affects AOT builds with >initial capacity dirs | - -### LOW (5) — Documented - -| # | Issue | -|---|-------| -| 14 | JIT `rt_str_split`/`rt_str_join` don't null-check inputs (C versions do) | -| 15 | `rt_hashmap_keys` uses insertion sort — O(n²) for large maps | -| 16 | JIT and AOT `rt_str_split("")` have different semantics | -| 17 | Array literal element type inferred from first element only | -| 18 | `int_literal_fits_in_type` returns `true` for non-integer types | - ---- - -## Sprint Deliverables (Tracks 1-5) - -### Track 1: C Runtime Security Hardening -- JSON escape handling: added `\t`, `\b`, `\f` to `rt_json_root()` + encode symmetry in `rt_json_escape_dup()` -- Padding validation: `rt_pad_left`/`rt_pad_right` now handle negative width -- HTTP parsing: `sscanf` return value checked -- JSON bounds: `rt_json_get` uses `json_end` pointer for safe traversal - -### Track 2: Parser Safety -- Eliminated double-peek `.unwrap()` on soft keyword EOF edge case - -### Track 3: CLI Robustness -- `init` command: replaced 4 bare `.unwrap()` calls with proper error messages -- Added `default-run = "turbolang"` to prevent ambiguous binary builds - -### Track 4: Codegen CString Dedup -- Extracted `cstring_or_empty()` helper -- Replaced 30+ raw `CString::new(x).unwrap_or_else(...)` patterns - -### Track 5: Integration Tests -- `json_escapes.tb` / `.expected` — verifies escape handling -- `pad_edge_cases.tb` / `.expected` — verifies padding validation -- `adversarial/soft_keyword_eof.tb` / `.expected` — verifies parser safety - ---- - -## TurboServo Improvements - -- **v0.9.0 alignment** — stdlib builtins replace hand-rolled parsing (-165 lines) -- **CRUD Notes API** — file-backed persistence endpoint (CREATE, READ, LIST, DELETE) -- **Parse function dedup** — 4 identical `parse_int` wrappers consolidated to 1 import - ---- - -## turbo-agent Demos - -Two working `.tb` files now exist: - -1. **`mock_agent.tb`** — Self-contained agent loop: mock LLM → tool dispatch → response. Runs without API keys. Demonstrates the full agent pattern. -2. **`simple_agent.tb`** — Real Anthropic API call via `shell_exec` + curl. Requires `ANTHROPIC_API_KEY` env var. - -Both compile and run on TurboLang v0.9.0 today. - ---- - -## Remaining Work (Priority Order) - -1. **MEDIUM fixes** — `rt_list_dir` arena corruption is the most impactful remaining bug -2. **JIT/AOT parity** — `rt_str_split("")` semantics and `rt_print_f64` formatting diverge -3. **http_post with headers** — would eliminate the `shell_exec` workaround in turbo-agent -4. **JSON object builder** — `json_build()` or similar for constructing nested JSON without string concat -5. **TurboServo templating** — move from 280-line string concat to `.html` template files -6. **Package manager** — needed for TurboServo to be importable without path hacks - ---- - -## Test Results - -``` -Unit tests: ALL PASSING -Integration: json_escapes ✓, pad_edge_cases ✓, soft_keyword_eof ✓ -Random: Produces distinct values on consecutive calls ✓ -Try operator: Correctly extracts and propagates typed Ok/Err values ✓ -TurboServo: Compiles and serves CRUD API correctly ✓ -turbo-agent: mock_agent runs full tool-calling loop ✓ -``` diff --git a/SPRINT-PLAN-0.8.0.md b/SPRINT-PLAN-0.8.0.md deleted file mode 100644 index edf08cf..0000000 --- a/SPRINT-PLAN-0.8.0.md +++ /dev/null @@ -1,164 +0,0 @@ -# Sprint Plan — Turbo 0.8.0 "The Safe Core" -Generated: 2026-04-15 -Based on: product review findings (conversation), v0.7.7 baseline - -## Sprint Goal -Close the correctness, memory-safety, and stdlib gaps that block "use Turbo in production" messaging. No new features — only safety and completeness of existing surface. After this sprint, `read_file` can fail gracefully, hashmaps support int values, the runtime has no shell-injection path, and integer overflow is at least detectable. - -## Success Criteria -- [ ] `rt_exec` shell injection path removed or gated behind argv-array API (no `/bin/sh -c`) -- [ ] `rt_pow` and core arithmetic helpers checked for overflow (trap via `rt_int_overflow` infra) -- [ ] `read_file` / `write_file` return `Result`; existing tests updated -- [ ] Hashmap supports `int` values in addition to `str` values -- [ ] `read_fd_to_string` realloc corruption fixed for responses >8KB -- [ ] AOT linker: `-l` arguments validated against allowlist regex `^[A-Za-z0-9_.+-]+$` -- [ ] `./tests/run_tests.sh` passes (ideally 185/185, minimum no new failures) -- [ ] Release build succeeds (`cargo build --release`) - -## Dev Tracks - -### Track 1: Runtime C Hardening — turbo_rt.c safety -**Agent:** C / runtime safety specialist -**Files touched (exclusive):** -- `turbo/crates/turbo-codegen-cranelift/runtime/turbo_rt.c` lines 811–1073 ONLY (I/O + exec + arithmetic zone) -**Tasks:** -- **TASK-1A (P0): Patch `rt_exec` shell injection** at `turbo_rt.c:1026–1073`. - The current `execlp("/bin/sh","sh","-c",cmd,...)` is RCE with any user-controlled input. - Fix: Replace with argv-vector execution: tokenize `cmd` on whitespace (no shell metacharacters), reject if any of `; | & $ \` ( ) < > \n` appear in the string, then `execvp(argv[0], argv)`. Preserve the pipe+capture semantics. Keep the exported symbol name `rt_exec` and the `const char *rt_exec(const char *cmd)` signature unchanged. -- **TASK-1B (P0): Checked `rt_pow`** at `turbo_rt.c:842–847`. - The current loop `result *= base` wraps silently. Fix: use `__builtin_mul_overflow` on the per-iteration multiply; on overflow call the existing `rt_int_overflow` helper (search the file — it's already exported). Keep signature `long long rt_pow(long long base, long long exp)`. -- **TASK-1C (P0): Fix `read_fd_to_string` realloc corruption** at `turbo_rt.c:904–918`. - When inside an arena the `turbo_realloc` only copies `size/2` bytes on grow (see comment block at lines 180–194). Fix: track `len` (not `cap/2`) and pass `len` as the copy size when replacing the arena pointer. Simplest fix: stop using `turbo_realloc` here — allocate a fresh buffer via `malloc`/`realloc` directly and, once complete, copy into arena memory in a single shot. Document inline. Ensure HTTP responses ≥16KB round-trip correctly. -- **TASK-1D (P1): Quick adversarial test** — add `turbo/tests/adversarial/exec_injection.tb` and `.expected` that invokes `shell_exec("echo hi; echo pwned")` — expected output should be a rejection or escaped literal, NOT `hi\npwned`. (If you have to pick one `.expected`: make it the rejection path — print an error string via `fprintf(stderr, ...)` and return `""` from rt_exec on rejection.) - -**Files you must NOT edit (other agents own):** -- `turbo_rt.c` lines OUTSIDE 811–1073 (Track 2 owns hashmap 1360–1500, Track 3 owns nothing in this file — you have full I/O zone) -- `src/aot.rs` (Track 4) -- Any `.rs` file under `src/` — do not touch compiler code. - -**Commit message:** `Track 1: Runtime C hardening — remove shell exec path, check rt_pow, fix read_fd_to_string realloc` - ---- - -### Track 2: Generic Hashmap (int values) — runtime + compiler -**Agent:** Compiler + runtime specialist -**Files touched (exclusive):** -- `turbo/crates/turbo-codegen-cranelift/runtime/turbo_rt.c` lines **1360–1500 ONLY** (hashmap section) -- `turbo/crates/turbo-codegen-cranelift/src/builtins.rs` (add new entries; do not reorder existing) -- `turbo/crates/turbo-codegen-cranelift/src/expr.rs` (add new builtin dispatch; do not reorder) -- `turbo/crates/turbo-codegen-cranelift/src/lib.rs` (declare_rt_fn for new symbols) -- `turbo/crates/turbo-codegen-cranelift/src/jit.rs` (symbol table entries) -- `turbo/crates/turbo-sema/src/lib.rs` (add builtin signatures) -- `docs/stdlib.md` (document new variants) - -**Tasks:** -- **TASK-2A (P1): Add int-valued hashmap variants.** New runtime functions in `turbo_rt.c` (append at end of hashmap section before line 1500): - - `void rt_hashmap_set_int(void *map_ptr, const char *key, long long value)` - - `long long rt_hashmap_get_int(const void *map_ptr, const char *key)` — returns 0 on miss (document this) - Store ints inline in the existing hashmap value field by stringifying with a tag byte — OR extend the internal value union to carry either `char*` or `int64`. Pick the simpler approach that doesn't break existing `rt_hashmap_set`/`get`/`has`/`keys`/`remove`. -- **TASK-2B (P1): Wire into compiler** — builtin dispatch for `hashmap_set_int`, `hashmap_get_int` in `expr.rs`; JIT symbol registration in `jit.rs` at BOTH call sites (lines ~126 and ~274); `declare_rt_fn` in `lib.rs`; sema signatures in `turbo-sema/src/lib.rs`. -- **TASK-2C (P1): Test** — `turbo/tests/phase1/hashmap_int.tb` + `.expected` doing a word-frequency counter: - ``` - fn main() { - let words = ["a", "b", "a", "c", "a", "b"] - let mut m = hashmap() - let mut i = 0 - while i < len(words) { - let w = words[i] - if hashmap_has(m, w) { - m = hashmap_set_int(m, w, hashmap_get_int(m, w) + 1) - } else { - m = hashmap_set_int(m, w, 1) - } - i = i + 1 - } - print(hashmap_get_int(m, "a")) - print(hashmap_get_int(m, "b")) - print(hashmap_get_int(m, "c")) - } - ``` - Expected: `3\n2\n1\n`. -- **TASK-2D (P2): Update `docs/stdlib.md`** in the HashMap section — document both variants, clarify the `str→str` vs `str→int` distinction honestly. - -**Files you must NOT edit (other agents own):** -- `turbo_rt.c` lines OUTSIDE 1360–1500 (Track 1 owns 811–1073; ALL other sections are off-limits) -- `src/aot.rs` (Track 4) -- The I/O builtins (`read_file`, `write_file`) — Track 3 owns those. - -**Commit message:** `Track 2: Generic hashmap — add int-valued variants (str→int)` - ---- - -### Track 3: Result-Returning I/O -**Agent:** Compiler + stdlib specialist -**Files touched (exclusive):** -- `turbo/crates/turbo-codegen-cranelift/runtime/turbo_rt.c` — ADD NEW FUNCTIONS ONLY at end of file (after line 2050). DO NOT modify any existing function in this file. -- `turbo/crates/turbo-codegen-cranelift/src/builtins.rs` -- `turbo/crates/turbo-codegen-cranelift/src/expr.rs` -- `turbo/crates/turbo-codegen-cranelift/src/lib.rs` -- `turbo/crates/turbo-codegen-cranelift/src/jit.rs` -- `turbo/crates/turbo-sema/src/lib.rs` -- `docs/stdlib.md` (I/O section) - -**Tasks:** -- **TASK-3A (P0): New runtime helpers** — append to the end of `turbo_rt.c`: - - `const char *rt_read_file_checked(const char *path, char **err_out)` — returns file contents on success (err_out set to NULL), or NULL on failure (err_out set to an allocated error message string). Use `rt_str_dup` or equivalent arena-aware allocation already available in the file. - - `long long rt_write_file_checked(const char *path, const char *content, char **err_out)` — returns 0 on success, -1 on failure with err_out set. -- **TASK-3B (P0): New language-level builtins** that return Turbo `Result`: - - `try_read_file(path: str) -> str ! str` - - `try_write_file(path: str, content: str) -> bool ! str` (or `unit ! str` — match existing Result builtin patterns in the codebase) - Leave the existing `read_file` and `write_file` alone — do NOT change their signatures (backward compat). Callers that want to recover from errors use `try_*`. - Wire through `expr.rs` dispatch, JIT symbol table (both sites), `declare_rt_fn` in `lib.rs`, and builtin signatures in `turbo-sema/src/lib.rs`. -- **TASK-3C (P0): Test** — `turbo/tests/phase1/try_read_file.tb` + `.expected`: - ``` - fn main() { - let r = try_read_file("/tmp/nonexistent-turbo-xyz-12345.txt") - match r { - Ok(s) => print("ok: {s}"), - Err(e) => print("err"), - } - } - ``` - Expected: `err`. -- **TASK-3D (P1): Docs** — add a "Fallible I/O" subsection to `docs/stdlib.md` documenting both APIs and when to use each. - -**Files you must NOT edit (other agents own):** -- `turbo_rt.c` lines 1–2050 (ANY existing function — append only) -- `src/aot.rs` (Track 4) - -**Commit message:** `Track 3: Result-returning I/O — add try_read_file / try_write_file builtins` - ---- - -### Track 4: AOT Linker Allowlist -**Agent:** Compiler build specialist -**Files touched (exclusive):** -- `turbo/crates/turbo-codegen-cranelift/src/aot.rs` - -**Tasks:** -- **TASK-4A (P0): Linker-flag injection fix** at `aot.rs:110–112`. - Current code: `for lib in link_libs { cmd.arg(format!("-l{}", lib)); }`. - Risk: `--link "m -o /etc/passwd"` or `--link "@/tmp/attacker.rsp"` injects linker flags. - Fix: before the loop, validate every `lib` against the regex `^[A-Za-z0-9_.+-]+$`. On any mismatch, return `CodegenError { code: ErrorCode::E0XXX, message: format!("invalid library name '{lib}' in --link; must match [A-Za-z0-9_.+-]+") }`. Pick an unused error code (check `turbo-ast/src/errors.rs` first). If adding a new code, also add the docs entry per `CONTRIBUTING.md` — `docs/errors/EXXXX.md` AND the symlinked source-of-truth at `turbo/crates/turbo-cli/src/errors/EXXXX.md`. Follow the checklist precisely or the build script will fail. -- **TASK-4B (P1): Same allowlist for any other user-supplied linker arg you find** in `aot.rs`. Grep for `.arg(format!` in aot.rs — validate every `{user_value}` formatted into a flag. (Lines ~341, 387 look like build-internal values, not user-supplied — leave those unless you confirm otherwise.) -- **TASK-4C (P2): Unit test** in `aot.rs` `#[cfg(test)]` block verifying rejection of `"m -o /tmp/x"`, `"@/etc/passwd"`, `"../../lib"`, and acceptance of `"m"`, `"ssl"`, `"c++"`, `"z_.+-"`. - -**Files you must NOT edit (other agents own):** -- `turbo_rt.c` (Tracks 1, 2, 3) -- Any other `.rs` file beyond `aot.rs` (avoid merge conflicts) -- `turbo-sema/src/lib.rs`, `src/builtins.rs`, `src/expr.rs`, `src/jit.rs`, `src/lib.rs` — all reserved for Tracks 2 and 3 - -**Commit message:** `Track 4: AOT linker-flag allowlist — reject injection in --link values` - ---- - -## Intentionally Deferred -- **Full generic hashmap** `HashMap` with arbitrary types — would require generics + monomorphization work in codegen; deferred to v0.9.0. This sprint ships the pragmatic `str→int` add-on only. -- **Checked arithmetic everywhere** — only `rt_pow` this sprint; comprehensive checked-arith for `+ - * /` in codegen is a bigger change (debug/release split). Deferred. -- **String interpolation sigil flip** — ergonomic change that breaks every existing program; needs a deprecation cycle, wrong sprint. -- **Module system, Windows, browser WASM, regex** — out of scope. - -## Manual Actions Required After Sprint -- Bump version to `0.8.0-dev` in `turbo/Cargo.toml` (and ecosystem repos) once this lands and is reviewed. -- Update CHANGELOG.md with the four tracks. -- No credential rotation needed. diff --git a/SPRINT-PLAN-2026-03.md b/SPRINT-PLAN-2026-03.md deleted file mode 100644 index 90a13cb..0000000 --- a/SPRINT-PLAN-2026-03.md +++ /dev/null @@ -1,103 +0,0 @@ -# Sprint Plan — Turbo -Generated: 2026-04-13 -Based on: product review findings from this session - -## Sprint Goal -Make Turbo materially more trustworthy by removing misleading shipped-feature claims, fixing high-signal runtime/security issues, and tightening demo/docs consistency. - -## Success Criteria -- [ ] All P0/P1 trust-breaking issues resolved -- [ ] AI/agent product messaging matches actual compiler behavior -- [ ] `exec()` no longer functions as a safe-context shell escape -- [ ] HTTP demo surface returns sane content types for browser-facing flows -- [ ] Insecure roadmap auth defaults are removed or clearly hardened -- [ ] Version/docs/example inconsistencies reduced -- [ ] Build + core verification commands pass after integration - -## Priority Triage - -### P0 - CRITICAL -- None from the inherited review - -### P1 - HIGH -- TRACK-01 / TASK-01: Remove or reframe shipped AI-agent claims that overstate compiler support -- TRACK-02 / TASK-02: Gate `exec()` behind `@unsafe` semantics instead of allowing shell execution from safe code -- TRACK-02 / TASK-03: Fix HTTP response content-type behavior that makes browser demos render incorrectly -- TRACK-03 / TASK-04: Remove insecure fallback JWT secret and public-facing unsafe defaults in roadmap API - -### P2 - MEDIUM -- TRACK-03 / TASK-05: Stop passing WebSocket auth tokens in query strings in the roadmap example/docs -- TRACK-04 / TASK-06: Fix stale version / docs drift that undermines trust -- TRACK-04 / TASK-07: Clean up deprecated error-code docs that no longer match source of truth - -### P3 - LOW -- Leave broad compiler simplification / architecture refactors out of this sprint -- Leave LLVM-path expansion out of this sprint - -## Dev Tracks - -### Track 1: Trust Surface & Product Honesty — executor -**Files touched:** `README.md`, `website/src/app/page.tsx`, `website/src/app/docs/page.tsx`, `website/src/app/docs/agents/page.tsx`, `website/src/app/docs/examples/page.tsx` - -**Tasks:** -- [ ] TASK-01: Remove or soften claims that Turbo already ships first-class AI agents/tool-calling when the compiler does not parse `agent` / `tool fn` -- [ ] TASK-08: Make docs/examples language accurately distinguish runnable examples vs roadmap/planned examples -- [ ] TASK-09: Keep differentiation honest: compiler/tooling integration is shipped; AI integration is planned - -### Track 2: Runtime Safety & Demo Correctness — executor -**Files touched:** `turbo/crates/turbo-sema/src/type_check/expr.rs`, `turbo/crates/turbo-codegen-cranelift/src/runtime.rs`, `turbo/crates/turbo-codegen-cranelift/runtime/turbo_rt.c`, `turbo/tests/phase1/exec_env_get.tb`, `turbo/tests/phase1/http_server.tb` - -**Tasks:** -- [ ] TASK-02: Make `exec()` require `@unsafe` context, consistent with other dangerous builtins -- [ ] TASK-03: Fix HTTP runtime response typing so HTML/browser-facing flows do not get mislabeled as JSON -- [ ] TASK-10: Update regression coverage so unsafe exec and HTTP response behavior are tested - -### Track 3: Roadmap Example Hardening — executor -**Files touched:** `examples/roadmap/web-api/src/auth.tb`, `examples/roadmap/web-api/src/main.tb`, `examples/roadmap/web-api/src/routes/ws.tb`, `examples/roadmap/web-api/README.md` - -**Tasks:** -- [ ] TASK-04: Remove known default JWT secret fallback and make configuration expectations explicit -- [ ] TASK-05: Replace query-string WebSocket token guidance with a safer pattern or explicit warning/placeholder flow -- [ ] TASK-11: Align roadmap example README with hardened/non-runnable status so readers do not cargo-cult insecure defaults - -### Track 4: Trust Cleanup & Consistency — executor -**Files touched:** `docs/errors.md`, `turbo/crates/turbo-cli/src/playground.html` - -**Tasks:** -- [ ] TASK-06: Update playground footer/version so shipped tooling reflects current release version -- [ ] TASK-07: Remove stale deprecated agent error-code references from docs that no longer exist in source - -## Intentionally Skipped This Sprint -- Full AI-agent language implementation (`agent` / `tool fn`) — too large for a trust/hardening sprint -- Broad runtime/server security redesign (thread-pool limits, TLS, auth framework) -- Large-scale architecture cleanup across compiler crates - -## Manual Follow-Ups After Sprint -- Consider rotating any secrets copied from old roadmap docs/examples -- Consider pinning GitHub Actions by commit SHA in release-bearing workflows -- Decide whether `exec()` should remain in the language long-term or be replaced by a more explicit process API - ---- - -## Follow-on Sprint (Iteration 2) - -### Goal -Turn the first trust sprint into a stronger shipping story by hardening the release/install path, replacing response content sniffing with explicit APIs, making shell execution more explicit, tightening server runtime safety, and elevating the dashboard into a true flagship demo. - -### Tracks - -#### Track 5: Supply-chain hardening -**Files touched:** `distribution/install.sh`, `.github/workflows/ci.yml`, `.github/workflows/release.yml`, `.github/workflows/nightly.yml`, `SECURITY.md` -- [ ] Pin GitHub Actions to immutable SHAs -- [ ] Tighten installer verification flow and documentation - -#### Track 6: Runtime API & server hardening -**Files touched:** `turbo/crates/turbo-sema/src/type_check/expr.rs`, `turbo/crates/turbo-sema/src/type_check/mod.rs`, `turbo/crates/turbo-codegen-cranelift/src/expr.rs`, `turbo/crates/turbo-codegen-cranelift/src/builtins.rs`, `turbo/crates/turbo-codegen-cranelift/src/lib.rs`, `turbo/crates/turbo-codegen-cranelift/src/runtime.rs`, `turbo/crates/turbo-codegen-cranelift/runtime/turbo_rt.c`, `turbo/crates/turbo-codegen-cranelift/runtime/turbo_rt_wasm.c`, `turbo/tests/phase1/http_server.tb`, `turbo/tests/phase1/exec_env_get.tb` -- [ ] Add explicit HTTP response helpers instead of relying on content sniffing -- [ ] Add server-id bounds checks and active-connection limits -- [ ] Make shell execution more explicit than plain `exec` - -#### Track 7: Flagship demo elevation -**Files touched:** `README.md`, `examples/README.md`, `examples/web-dashboard/README.md`, `examples/web-dashboard/main.tb`, `website/src/app/page.tsx`, `website/src/app/docs/examples/page.tsx` -- [ ] Make `web-dashboard` the clear flagship runnable demo -- [ ] Improve demo guidance and public positioning around it diff --git a/SPRINT-PLAN.md b/SPRINT-PLAN.md deleted file mode 100644 index 96273e9..0000000 --- a/SPRINT-PLAN.md +++ /dev/null @@ -1,71 +0,0 @@ -# Sprint Plan — TurboLang v0.9.1 (MEDIUM Fixes + P1 Features) - -Generated: 2026-05-17 -Based on: FINDINGS-REPORT.md (issues #8-13) + strategic gaps - -## Sprint Goal - -Fix all 6 remaining MEDIUM-severity bugs and add 3 P1 builtins (http_post_with_headers, json_build, float_to_int/int_to_float) to unblock turbo-agent and harden the runtime. - -## Success Criteria - -- [ ] All 6 MEDIUM issues resolved -- [ ] http_post_with_headers builtin working end-to-end (C runtime + JIT + codegen + sema) -- [ ] json_build builtin working end-to-end -- [ ] float_to_int / int_to_float builtins working end-to-end -- [ ] All existing tests still pass -- [ ] New integration tests for each fix and feature - -## Dev Tracks - -### Track 1: C Runtime Fixes + New Runtime Functions -**Agent:** C Runtime Specialist -**Files touched:** `turbo/crates/turbo-codegen-cranelift/runtime/turbo_rt.c` -**Tasks:** -- [ ] TASK-02: Fix `rt_json_get` naive substring search for nested JSON (line ~1375) -- [ ] TASK-03: Fix `rt_mutex_clone` to use refcounted wrapper (line ~1599) -- [ ] TASK-06: Fix `rt_list_dir` to use malloc/realloc instead of turbo_alloc/turbo_realloc (line ~2579) -- [ ] TASK-07a: Implement `rt_http_post_with_headers(url, body, headers)` in C runtime -- [ ] TASK-08a: Implement `rt_json_build(pairs_csv)` in C runtime -- [ ] TASK-09a: Implement `rt_float_to_int(f64) -> i64` and `rt_int_to_float(i64) -> f64` in C runtime - -### Track 2: Codegen Fixes + New Builtin Compilation -**Agent:** Codegen Specialist -**Files touched:** `turbo/crates/turbo-codegen-cranelift/src/builtins.rs`, `turbo/crates/turbo-codegen-cranelift/src/expr.rs` -**Tasks:** -- [ ] TASK-01: Replace `.unwrap()` on binary op compile (expr.rs:78) with proper CodegenError -- [ ] TASK-04: Fix `compile_abs` to handle f64 via fabs -- [ ] TASK-05: Fix `compile_min`/`compile_max` to use unsigned comparison for unsigned types -- [ ] TASK-07b: Add `http_post_with_headers` codegen in builtins.rs -- [ ] TASK-08b: Add `json_build` codegen in builtins.rs -- [ ] TASK-09b: Add `float_to_int` / `int_to_float` codegen in builtins.rs - -### Track 3: JIT Runtime + Sema Type Signatures -**Agent:** Type System Specialist -**Files touched:** `turbo/crates/turbo-codegen-cranelift/src/jit.rs`, `turbo/crates/turbo-codegen-cranelift/src/runtime.rs`, `turbo/crates/turbo-sema/src/lib.rs` -**Tasks:** -- [ ] TASK-07c: Register `rt_http_post_with_headers` in JIT symbol table + add JIT wrapper in runtime.rs -- [ ] TASK-08c: Register `rt_json_build` in JIT symbol table + add JIT wrapper in runtime.rs -- [ ] TASK-09c: Register `rt_float_to_int` / `rt_int_to_float` in JIT symbol table + add JIT wrappers -- [ ] TASK-07d: Add `http_post_with_headers` type signature in sema -- [ ] TASK-08d: Add `json_build` type signature in sema -- [ ] TASK-09d: Add `float_to_int` / `int_to_float` type signatures in sema - -### Track 4: Integration Tests -**Agent:** Test Engineer -**Files touched:** `turbo/tests/phase1/` (new .tb + .expected files only) -**Tasks:** -- [ ] TEST-01: `json_nested.tb` — test rt_json_get with nested JSON objects -- [ ] TEST-02: `list_dir.tb` — test rt_list_dir basic functionality -- [ ] TEST-03: `http_post_headers.tb` — test http_post_with_headers -- [ ] TEST-04: `json_build.tb` — test json_build with multiple key-value pairs -- [ ] TEST-05: `type_conversions.tb` — test float_to_int and int_to_float -- [ ] TEST-06: `abs_float.tb` — test abs() with float values -- [ ] TEST-07: `min_max_types.tb` — test min/max with various numeric types - -## File Conflict Analysis -- Track 1: turbo_rt.c (exclusive) -- Track 2: builtins.rs, expr.rs (exclusive) -- Track 3: jit.rs, runtime.rs, sema/lib.rs (exclusive) -- Track 4: tests/phase1/*.tb (exclusive, new files only) -- **No conflicts between tracks.** diff --git a/STDLIB-ROADMAP.md b/STDLIB-ROADMAP.md deleted file mode 100644 index 8033eac..0000000 --- a/STDLIB-ROADMAP.md +++ /dev/null @@ -1,174 +0,0 @@ -# TurboLang Standard Library Roadmap -Generated: 2026-05-15 -Goal: Batteries-included language — zero third-party dependencies needed for real-world software. - -## Philosophy -Every dependency is liability. TurboLang ships no package manager by design. -The stdlib must be complete enough that developers never need one. - ---- - -## What We Have (67 builtins) - -### Strings (12) ✅ -trim, upper, lower, split, join, replace, contains, starts_with, ends_with, index_of, char_at, repeat, to_str, len - -### Math (5) ✅ -abs, min, max, pow, sqrt - -### Collections — Arrays (5) ✅ -len, push, map, filter, reduce - -### Collections — HashMap (9) ✅ -hashmap, hashmap_set, hashmap_get, hashmap_set_int, hashmap_get_int, hashmap_has, hashmap_len, hashmap_keys, hashmap_remove - -### JSON (4) ✅ -json_get, json_stringify, to_json, to_json_array - -### HTTP Client (2) ✅ -http_get, http_post - -### HTTP Server (11) ✅ -http_server, http_server_public, route, http_listen, respond, respond_html, respond_json, request_body, request_method, request_path, request_query, request_header - -### Async/Concurrency (7) ✅ -channel, send, recv, mutex, mutex_get, mutex_set, sleep - -### I/O (6) ✅ -print, read_line, read_file, write_file, try_read_file, try_write_file - -### System (2) ✅ -exec, env_get - -### Testing (4) ✅ -assert, assert_eq, assert_ne, panic - -### Other (2) ✅ -clone, to_str - ---- - -## Tier 1 — Can't Build Anything Real Without These - -### System Essentials -- [ ] T1-01: `args()` — CLI argument array (runtime exists, register in sema) -- [ ] T1-02: `exit(code)` — Exit process with status code -- [ ] T1-03: `type_of(val)` — Get type name as string - -### Date/Time -- [ ] T1-04: `time_now()` — Current Unix timestamp in seconds (f64) -- [ ] T1-05: `time_ms()` — Current Unix timestamp in milliseconds (i64) -- [ ] T1-06: `format_time(timestamp, format)` — Format timestamp to string (basic: RFC3339, ISO8601) - -### Filesystem -- [ ] T1-07: `file_exists(path)` — Check if file exists (bool) -- [ ] T1-08: `delete_file(path)` — Delete a file -- [ ] T1-09: `list_dir(path)` — List directory contents as array of strings -- [ ] T1-10: `mkdir(path)` — Create directory (recursive) -- [ ] T1-11: `path_join(a, b)` — Join path segments -- [ ] T1-12: `path_dir(path)` — Get directory component -- [ ] T1-13: `path_base(path)` — Get filename component -- [ ] T1-14: `path_ext(path)` — Get file extension - -### Collections -- [ ] T1-15: `sort(arr)` — Sort array (COW, returns new array) -- [ ] T1-16: `reverse(arr)` — Reverse array (COW) -- [ ] T1-17: `array_contains(arr, val)` — Check if value in array -- [ ] T1-18: `find(arr, closure)` — Find first element matching predicate -- [ ] T1-19: `any(arr, closure)` — True if any element matches -- [ ] T1-20: `all(arr, closure)` — True if all elements match -- [ ] T1-21: `zip(arr1, arr2)` — Combine two arrays into array of tuples/pairs -- [ ] T1-22: `flatten(arr)` — Flatten nested array one level -- [ ] T1-23: `slice(arr, start, end)` — Sub-array extraction -- [ ] T1-24: `array_remove(arr, index)` — Remove element at index (COW) - -### Math -- [ ] T1-25: `floor(x)` — Floor of float → i64 -- [ ] T1-26: `ceil(x)` — Ceiling of float → i64 -- [ ] T1-27: `round(x)` — Round float → i64 -- [ ] T1-28: `random()` — Random f64 in [0.0, 1.0) -- [ ] T1-29: `random_range(min, max)` — Random i64 in [min, max] -- [ ] T1-30: `sin(x)`, `cos(x)`, `tan(x)` — Trig functions (f64 → f64) -- [ ] T1-31: `log(x)`, `log2(x)`, `log10(x)` — Logarithms (f64 → f64) -- [ ] T1-32: `exp(x)` — e^x (f64 → f64) -- [ ] T1-33: `PI`, `E` — Math constants - -### HTTP Client -- [ ] T1-34: `http_request(method, url, headers, body)` — General HTTP with custom headers -- [ ] T1-35: `http_status(response)` — Get status code from response -- [ ] T1-36: `http_headers(response)` — Get response headers - -### String Additions -- [ ] T1-37: `substring(s, start, end)` — Extract substring by index -- [ ] T1-38: `pad_left(s, width, char)` — Left-pad string -- [ ] T1-39: `pad_right(s, width, char)` — Right-pad string -- [ ] T1-40: `str_to_int(s)` — Parse string to integer (returns Result) -- [ ] T1-41: `str_to_float(s)` — Parse string to float (returns Result) -- [ ] T1-42: `format(template, args...)` — sprintf-style formatting (or just ensure interpolation covers it) - ---- - -## Tier 2 — Needed for Web/API Work - -### Encoding -- [ ] T2-01: `base64_encode(s)` — Base64 encode string -- [ ] T2-02: `base64_decode(s)` — Base64 decode string (Result) -- [ ] T2-03: `url_encode(s)` — Percent-encode string -- [ ] T2-04: `url_decode(s)` — Percent-decode string (Result) -- [ ] T2-05: `hex_encode(s)` — Hex encode bytes -- [ ] T2-06: `hex_decode(s)` — Hex decode string (Result) - -### Crypto/Hashing -- [ ] T2-07: `sha256(s)` — SHA-256 hash as hex string -- [ ] T2-08: `md5(s)` — MD5 hash as hex string -- [ ] T2-09: `hmac_sha256(key, msg)` — HMAC-SHA256 - -### Regex -- [ ] T2-10: `regex_match(pattern, s)` — Check if string matches pattern (bool) -- [ ] T2-11: `regex_find(pattern, s)` — Find first match (str? optional) -- [ ] T2-12: `regex_find_all(pattern, s)` — Find all matches (array) -- [ ] T2-13: `regex_replace(pattern, s, replacement)` — Regex replace - -### HTTP Extensions -- [ ] T2-14: `http_put(url, body)` — HTTP PUT -- [ ] T2-15: `http_delete(url)` — HTTP DELETE -- [ ] T2-16: `http_patch(url, body)` — HTTP PATCH - -### Data Formats -- [ ] T2-17: `csv_parse(s)` — Parse CSV string to array of arrays -- [ ] T2-18: `csv_stringify(data)` — Convert to CSV string -- [ ] T2-19: `toml_parse(s)` — Parse TOML to hashmap -- [ ] T2-20: `json_parse(s)` — Full JSON parse to typed values - ---- - -## Tier 3 — Differentiators (Vendored Libraries) - -### Database -- [ ] T3-01: `db_open(path)` — Open SQLite database -- [ ] T3-02: `db_exec(db, sql)` — Execute SQL statement -- [ ] T3-03: `db_query(db, sql)` — Query returning array of rows - -### Networking -- [ ] T3-04: `tcp_connect(host, port)` — TCP client socket -- [ ] T3-05: `tcp_listen(port)` — TCP server socket -- [ ] T3-06: `udp_socket()` — UDP socket -- [ ] T3-07: `dns_lookup(hostname)` — DNS resolution - -### TUI/Terminal -- [ ] T3-08: `color(text, color)` — ANSI colored text -- [ ] T3-09: `bold(text)`, `italic(text)`, `underline(text)` — Text styling -- [ ] T3-10: `cursor_move(row, col)` — Move terminal cursor -- [ ] T3-11: `clear_screen()` — Clear terminal -- [ ] T3-12: `term_size()` — Get terminal width/height - ---- - -## Progress Tracker - -| Tier | Total | Done | Remaining | -|------|-------|------|-----------| -| Tier 1 | 42 | 0 | 42 | -| Tier 2 | 20 | 0 | 20 | -| Tier 3 | 12 | 0 | 12 | -| **Total** | **74** | **0** | **74** | diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 1c7388d..0000000 --- a/TODO.md +++ /dev/null @@ -1,38 +0,0 @@ -# Turbo Language — TODO - -## Reference counting (Priority: Critical, v0.6) -The runtime currently does not reference-count heap allocations: `rt_release` -is a no-op. This produces a ~2.5 KB/request leak on the example HTTP server -and makes long-running services impractical. Real ARC is planned for v0.6 -and is tracked as the top P1 follow-up from the v0.5.1 hardening sprint. - -Scope: -- Per-allocation refcount header (already allocated in `turbo_alloc`) -- `rt_retain` / `rt_release` that atomically inc/dec the count -- Codegen insertion of retains on assignment and releases at scope exit -- Deep release for array/hashmap element types -- Re-enable the leak test that was disabled while this is a no-op - -## Visual Library (Priority: High) -Turbo needs a visual output library so programs can produce graphics, charts, and UI. - -### Approach: Server-rendered SVG/Canvas via HTTP -- Add `svg_*` and `canvas_*` builtins to the runtime (turbo_rt.c + codegen) -- Programs generate SVG/HTML strings and serve them via the HTTP server -- Browser renders the visuals — no native GUI toolkit needed for v1 -- Enables: charts, data visualization, dashboards, generative art - -### Builtins to add -- `svg_rect(x, y, w, h, fill)` → SVG `` string -- `svg_circle(cx, cy, r, fill)` → SVG `` string -- `svg_line(x1, y1, x2, y2, stroke)` → SVG `` string -- `svg_text(x, y, content, size)` → SVG `` string -- `svg_path(d, fill, stroke)` → SVG `` string -- `svg_group(children)` → SVG `` wrapper -- `svg_document(w, h, children)` → full `` document string -- Bar chart, line chart, pie chart helpers built on top - -### Future: Native GUI (v1.3 roadmap) -- Turbo/ui declarative framework compiling to native widgets -- Cross-platform: macOS (AppKit/SwiftUI), Windows, Linux -- See `design/ROADMAP.md` v1.3 section diff --git a/benchmark-final.png b/benchmark-final.png deleted file mode 100644 index 628e086c943ab035216a4df8b43aef88b8e507ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 117332 zcmdSBWl)w|_&2H|ARr(OQUU@G-QA5ycO%{1y#)cKk?xZ22I=nZmhNtNfV15D|GwwT znRm{oGjo`6#=$4<`(D?&u3s&G$jeF~Bfdv`@!|#YCrMGo7cX9Cf1{8;et5l@D$|B|C)$>_)Tzdx5lJ8rr7 zyR?*U+|j92{l&}>_`xwfj*+p=^pBqtb(zg|&sX1}{r$mXiQd@zd0L9t%h4 zT@;aC{N3e6)oP!9x^A;M^)LoAvXP>M=-;2cHsNs720FPs!l5M3i{fWyDlw6~F&6h! zw&)r!!=(+Xk5v6>?5UGK@;;>~B$?hynUW40&iwaYs%@jINN$#8Db$ObZG@@_TlZ^2 zMTgF0q8apy*5r;LpZ%Yy5qa?@{4zT+FKK_d%lZC8MfmTXW--W>Aou{IOwC7G|^OFNpUHQ0Q9{P#w`OEV9g4Y7J9 z4lFw5_Kf3&MV?y?xj9e>wu?@c#SJQuxtaY14AVOneC=OE@!^oi{D&gkqW-X%sf98T>!oiS|@A}pjuv`Tuo z^qGpg=Z;4A@DDGN{*-=0MUPcpU2&H`?tPCul>3H`(4!Im+=3V!bl+Df8H{W`v1F^ zA9D#ze zeS$93SW-PIK+2R0D*J4Lv1r&GHv4jAk~c?EwJub&wU^d9C48V~y#Yu#h&;?++77ZM zB2;h87i*WJ$RtE(s8k9!o7^vgvFQpY_hQmGRj}!`MU4u49`C)c=iQvs&o+AH()mT^ zn%e#0Q3>x`exssj5j5@d^R8i_jbD)QIr0({r>Af^T6h#*@U^`F-y$CKc-n~?N3XNc z`aCEJ6;S z3gwq(Gb(56UrBi&3Z(KZJNe!6J8u78q9ggB0X`xtA-5B!Gxh6CHKPEr5bUJT8`0Xm z^R3Suwkz^abXv7j_x1|G%N2$aqiGE4(UPy>;A|Fa=^**l8E?z&)>?ZIbX_X+J3X#s z-L4PuqGV3L$%%@N#?qFyUS3>iHM)fLA(DTNV?D;h$A9?K4g_I|8LhrZJr#aq3=l#^N-d+hKjQ_dl&Oy5h zxsT-Ji3AyLXX}PNp=gOnVi?0UF5fTP+po^IE*O;aPdkH9Vdta1eVY;(ASEFYGIGq3 zinW?6&$Rd5#M0N-r<6-$y&FRK6+$=aqfhDn+W(mx(N<$DkHq=dd&$8V*)5n?Y!r>T5}{il4R67`C#v*=?) zEb7bcWZM{8jY9cMKkn3Ha3eb-sn1*ScqAlwp_Io52aA~^p3dpypPw2XcSefJS|$pV zD1CjOA8t-=juz{z<{i3&u}Tzkf=w=)+#ih!@h~tRMg^YDTEss0fma7o^uB^U=i%i1 zB1^Abe|oc8b$znRAZ8ykVcpsQVH;|=yR|J`PErSTvq7Z-zWli9>ChN0oA zy1JXbC^8zA!tY(Thx06pbtkuvH>=YTB*WkiN7MKYW=b&L$wM#q#z2YC>iouZq^G4; z$P$gF2(z3mCB($U#Jl?giUw@!rAp(XehaJUkPmMIrd(IOIX-;&0J@Dj0=N;aCbx=y zx-8XF?S^tcTsoGyhUww-IksM-xYs|*xn|S%#MJ9(vx%%}byBetx;b1nOSb~g zP^D$l)fVrqf!J1ut;+j*X#oMf9Q+H7W)HTlY=?x522`zD+0+GcshH5vP)^6~L(q3- z%Jdk1M#l|XJT6pCfUV!=m;V4t_zZe?vCn@?#~L(~uRu6#b9QEbOZO3BGHNVTNJt+Z_azD+s_3^sv z2L|ip;c}zv)&6uWzgckt-vhBgt5I6v&PMcSHYp{{`RQ&7HcXA4@W;)Q*5{eaR=gTh7`zp}uD|8%h|L zm0f^(EmWIli-)murAx*NJRGuYD15{u(DS*&8m?v0eZ3hCDvrHnpMi=B0pGGO@ z#dZby5(X{-|LJs11awaoep0c^tE-2jI=x1xJpo$Ar zf4$f|>xSzX*Z1FKtE6L-FSk~ zmFYGwbc7J_^72Nak0!Ec{UY9g)7^L!-Cw-vUTV02JV$nEnh_k&p5&za-1l!{h?Z+` zSgA+2uy2%2RBEso93kVL{jvW(w7hFzMgF_gx#ygqwQVXH->%9OmmZUW2BbVwL$iib!Q- zTWSvdoIDDS*yr};!hA7arAQD;f+-Tr=0NPAu)PejdqG24xva@>Qh+LhKGAyuuz|CV ztBeOtX!M@LIx5bvWDsfnwi>JUrwX#EoHLD}`$c7MXquj%A_251c*yw2%>5JjN8G+I zN|)dIFa1&)j0dr?;o6{N2dXzh?SyprGfmAe5Jb57Pc{MgNaCL}bUj%KqnN3Vd1{58 ze@0q0c%)Tl)VuikPqo!dsJbWTQs`u8>(jj@_`=GERbQMK8a4X)y)UItccyV=Wo446 z8V=i^B+M=+Qv!zEH^N?;RVv+7{QDRnT75|bqqj^mU}krW?XtQa6Kt%FC)hndd2c)k zJYHsRKSdgmQ&4EIsDq}XsN5(1iQJp!T&7T`QMJ-|2}jpWXwPOmiwnEU4cBoL@%gyX zR_uL7n8IGEd}ha3W^UOE>)p&le{`#plT*xc!VX}_qi(o|Fg-OeedKZUk8*P#ox^vI zk8RF{$Li}jqa#uXSb?)_`3vm#KlR@pRObFV|+>-ou$X_oc2y7Rv<%P8WuX7_(- z0j8B7dMyDw_<_D)ibV=V>ZDWTa@H>y%EH=(5~=L(foD1l;h*$V3ge7ZlGaMNDI1H*f7Qf0X#5SYrUvnD-)-8HmQ{B zwOFWfz+~9--qf-WCiXa9cY=PqBLL}h4CIF~XmLxv&*q;c)w%V@|H9<3_hEE?>Fo5^ zSk%hud*VeI4Oj_0u0$WRl7#Wu$u%#GI2^==>%k!Ju`(oXgR_{*uh{N~%%z4B8%J>+ zFEy~3Py7P)k4pShVb8>ByOKAXTw}c`9*vpG_bF>T~ixZO@Opx@7(9`BLEi3M_fCKYau`G)w+L~;4x1cimx);i6y)9j`2 z2$7|Fx@%5a9#9J651&8{*eum^Sufn|zXSv-u%jaP?JvP?)dmMFTEFr)MOmsP3+HD} zi*>dVEFotlO3S~Uu;6{2>t2l9lUo{#(XJT6$N zWZpX(Bf_Lch=Op5T3O3EV>4)f`q>Pc)$%$7qJ^t5zIFuVky|pVft$1G{{R2bF|vH3*|aR2^tW1UB32X6y&nBFul28JP$ zX_^t=JOYNywc2Q?$v@GQA~8WNSuoQtpP?+D`2<_M?#h{2cH1I2%Dp-$0a+HnTMSqG z?Nm?$2e2rg*XDguEQj%GWi&4N6rqD*B&U?8c)T_P(EY9zC`u ze;z9^kH><3N@&3KBDdCIffGS!m?=29M@jYl3#EbEsI+`xb|Nc)d1I;hqS>$*h#=BJ zNN;~~=)=m$hCq>Ju=mB{IOq#AzZsq9@`MQoAUf}kffn5Zi-di8el83X5)>44rSW>e zx~GKHbTn;pNpo*Ee>FQ)Eu-K1tJM7~*fd=~$mb*D0kbk3@4Kz|hs(X}y@GjUhfT?O z>%=|bf@Jx_`6`JEf)CN7Yp2)OHj}x|7u$4^=!RNu9v<#)^?8>r^)VeSQ^gvNtS({r z9Q>U1HkAVLG-{=>G-|A7WAX_VR`bjoX^;R*_Bbnb+1}x#>l(EtH+z|MY&uQtMi-e$ zj6uI;U7dh$A2)SiQfllz0^<3Q?)${#?jV^;%X{%F8;mvcB*8_zKgup(=RZE6esEtb z*Y`ImwsQIdRKod|p%zsoG7^$Zk)ZHO@FXFQ%fVwYaXOr9PhvCw`nz39-^M1a zY?fI=x!F4Mu)ZUBxow(F=5svaoO4*Rg!FNc1wfh$l<8hS!;zh+umGQq{;>t9gSZ4ETUt?eI94DA{DTjTv3D{JpwGl7Pu3 zXBpu~NiqMIFuD?2lk43fK()fc!Xlrb_*+)HlXAUah%)qeVzRp;_~YpAe!aj|+^sO| zr9LqqqKE%WscIwHMyF%s?R85uowx3p1X`8(-Iuhne5|ZOC9#IXxP^Ub7Q{B%Ly<1G ziO$WT3EzgIz}3!BEvq`0IOrSvkp1?=0~`JX^GAnz4@?(q#1XBW@g#nh>5blp~;<_1%=Pd+|AjV?!+=7t=mM8Dkgd2Sz0TG?EYEIE0281!1C zTee4Z|K3s-ZB)gqMH;_1Ae!6*65mXbkO?z0Gn4osYR*RsGDg1v95BC!puUx7X_+3o z{feQH4;Cm1Md|oFs)m3aEzM5;5#EWd!T52+U|-M)Y`_$6_P82|L6}I1`j#0`3sy%d zPqt?8Z%>328CiwLCnUriE*l`ml(NckMlys>*f~2}&y{~YXQHRqeN5?hs_=dGkx93h zF3gm~!MKp_R13kTWA#t;RP~I@*K1A79lhL}lnBKwI96(Dsu{MVsNb-P%@SiS9bJ!Wt@qse2 zF=Rv^{uxrr5d5l`lg7_QL9tE3Y_#RF1f&#N?c1{L$9(XCBH^a-|88^D{&mw*GXh?> zcgF9x$f&z}L-DfRpW8t*^3&AcB3ppF*d8{$V=jzYXmF&ID&4GQHGON#G_tBmg=<phX#f@mj)9ila=mbn%xth6j zX5z9PajC_7F+<4zJ)6ZPxU*)!50hR3P<+-&2jsc3@nD8d2B^Yu`3#Wddi!XT7b$I)vp!x2xm8sxGvy{Pwy3=ycus*uTN!L!vq7hR zdQz_6$y%m;VEDx*MovIxSZgGj&1}pQfTo!x2c=pojp1b}MCF`pAI&-R7#|9mq+iGW zf$@MwU_XoYc%C%3TQ5{k7YlB_=ppCb>j_H+s~Wcq1K+aPl>JJxclFuw* ztiyh`=Pt~YN|3+04xFcwz-#BB-|@r>QI5ZO{FM( z)e_BVz%-07WT&!$O_x#4Yeus18XjIt@&YKuxec@A-X*%tZ~^5;Y9FNX&Dh(A@;$Sp zR~uc^jmkQ@yH}EsBLj|?i<;Zc!#Gy*wKbmAlYo+n>udQo z5zggmZ_@Pp<(EOzG%(^QJ^A*5IB2-FJqL0Uz=@ppK0IjyylMa)n2_c~PZXENWm8Rc z`#_>F9*hIMN7K!%{;aO=SiUb`yJ6r@y9n)b+!+)?#2?D^ey$5?a<)V(f-6zL_iO*B z%nyF7xgJ;Vdu%_=VCD{E1(YQ2bak2Pk`v(I*xuE#Dv5M78FVAcL?em+695_Q7K$nS+>S%xK?lzEr(p;wSlMX!8gO<>L> zczQZluCFWBHd+j%QmB33dfW#pFcf+*s^FXH&UXnokH6mlsAUs< zv1KjyzunH-fF7h_=G!TExY7*YpaF0i?WXO-O0#E$eFU*U>0Sid8E|ftr+FvE{`Qxv z1(p>^Ma8JqF1^lgvun9(smJTZG6#($4%?>znzA$j-<8d`hQ>Av)diwIeyZxqf8j-o zjvn4+iMX3*S@~LnY<#rns$OfAxTTg7fyV;ej^FSo1dQ%xhMBUdh+?WsIUzq>(Q|E3 z$E}pk4ufm{6Sz2>h=|fU0%IWxSpboPoi7^os$q2`wQ;IUW>h+VmO&|3T2N5XU_BeO z_H=HnO4HGm80^eS`EUX*u=e|hhfb$|GRw;uqxK4w3;eLui`AKmtAYCjl&`lmV*~a@ zDn)_zNWezhRp>a|pmaG}h%H(5KHS}%%U2*15LhiQX91)`dJooTE(Y|vV)@Kh12wX; zvH%-BUW~RXUQUI*f<^pd3Q;f5X!YfnehqsD#7$yur@7nt$6!!87HK+(lo4eqj=&2B z_8<`G9-kf`08g0z?VKE|HkM;km1}>L6Hr&@+8KyE7)R%?Yyn!e+4|r&y}DtRh$X~x zeQF?6)!8hIW2E#a3o{wjSg82>`x6on*iG5aS5Ds}(H)(GHK>xpAT{ivk-S|MM69fA_MWY#^PNySty?dbqm-aAbb}a0?W2 z8q|AAa`GQT>&M4yJMkPOB>aEvVNzf>`#!(A#mC1dvl8Skg*P?bS?`8fUgjesBNLtJ zFQiM(WWLAkj~`v_?&hNCp}6jUYj6w|N&JvK>gnP>1rSM&DUYr{To@>*Os~+eFeD@- zJ~zA75-kp~1+Vk|9eeu_4Pq zILe+An9>RPJZ?dkSDpLkQWF($^po*y4sKG?V^n;0b)(ecg-SeXkqES@cWZM^UU%!? z5DWw8_7-o91q1}5^~A))KrTV%*SCg?QO7K~bRVF1D)Ebpil%Wp8$3TXqY{gUgvO8W z@HO)8g9$&u(o3U4ov&QLyw4BxRv^+Z$_aS;ZuSWYxmZv9qRM|m%(gEQCA|FmN)%%< ziH!!xht$k!g*%BKQ)TBUQrw%G9h0EcEQ`=EHk+klrM%;do#!g+MU$uo#mC85YL(%r zPOarUkEs zn#p@Y!e-Oa(#Xh-eRBhg1S@&@`HP)VaF1ZJ7ghQK@e&CWQg77X3I=CK9j@!cH60jj z%HmFrjz#Jfi2LOVhzQ7hz<**NM#cwEt3b6RH#&Oq2SzVrDO7=LKIbzI7`W9&{S%<) z2)RlRbr!1p)%lS+3HUs4qZLR0RvpN|1mnFy=BqL6#n#vr%7)BL^n_UgK&;o|B?c9h z>_Zq<%r@Y+)qNHTx-K%+N({oHi8COQihoA>=GatwJLv&I?5+ zSb9OzK0N%`5#R{q+9)-BeKtH8ENKtpXGQ)gC(aa`|SDFal&TjYkZ)_UbXGVqumI2aC-S-+em8ltoJHx`77wbc;}CE1xH<<;<5ISQo@NI3cNCgK=R4qO`VW$oMlc^g3`nL?-6b zgG7whZmV@BS7(I~I`q8I$VzSZ|x*PT(5MPQ%r-7;i7+ZSjo z3fux7mt$#sJh6LD2QvpC2!zj38ckgBRKcZMrt5Q}TR#@R&9_){;B~x&#}(W_1}vQw zK*PGMrpi_seV_aUo^Exdr9-!(#ijH^T=&j9wgz#gJ;i2A830xAyx6{?*bze6I6R!F zG)@K`gsBe53vG8YeDhgiA^H>mgL!NZ?fJvs=(PF_-{xsmoB4aI)B7ARxtCLy3oHI+ zm#6bvv)63~(7sS*5c-Jps zh~(FW^ihZv-f}xqSlym7LH-$d8QJzlNIZfl@SNDkyX(7L1#M-Qzn;}f(^c0GL))c# z-9{IkkN7~UR&{8iRw>M^ciHSCaSjiP4Xw#6#{NhGDb?=-P!WDhAg1&b1J|-QyoAN{ zDK~AD+u3icyLpa@ku=R;99enS&R>0c-SoNqRp*DPRH6u!*+kXbO9x960ufNV}tQInj zDt}UUN7Tx%PZvPhU$2j@+T7esAqxr#h!&^*()dR$GAG{=-6{hBp}v$ZH=@l|U@yN< zEYUW{CGx~{QRDK6r{}Yq>a($;=zP@i#nnRxXvGuC&287x(fV-H8D*AZW{pX^%i*w@ z_nUII&p>INvm2$#^9JSc1@<-KM!NWKl&M?3CWT|Fx7UTB&e z$AojWJ3*mP;A@h$c5^iu_Lk-wByiZqZH2j$9W0clNV+YLgk zuj+cV5dL-msGDkK@$$FgnrwzWs1BkrnorORoGJM1;XJ+8Sp`m!PoE2#B&zzqqn-42 zb@|+`^+E5DGIm4JNf1f%4J5>aee#Fwq||u=$24&#I`+DkUYdOkP`ZyI zgRkh5Y^@L&3^dbtNy1`{lqmsH!t-SCQ7iIGLSx+mF|~QpcW7ug9RU~ltr9}STVk(0 zt4T5(-Uny%R5CIVO2j+ttdq~_EWUSkUcr8FPsT&s z%0HLrhq?F%wpY79aQFv?fKZm-HX4-GAwi`m4qU{i#1Q~{fV=wLgw^>$8jQsZCqDz^ z1ua_M6MXU!FuSt81+R)FzYP@SIzL|KQO^Rl5mAxaNVyeThehuTTy&ODbL`Pz`57TPthSQm&AfOugHDgF8OO(pk4U{C?khKQVfjkTX z))NRz!TMJ0_+Cwm((vv=8HG6O|!?R{*p1mZlcmuW$%Q-xB7mUeq$`2WC!6xFu z!8E8MPy>(wsj70Mz7}NtnT;AAFqtQ(|M-Ot&^~EAht-zljz}(C6FJ}SYkk5!ki`tC zINE@X_ufDGCMp*H4E-aDwzzQqiEJZ(M`-Vdw<0~~`{DA$NX!1-H`(XH8gz4ZpcBU| z%9hd(Dps>2;?Vnvc9*k1wNnA5H922Ip|yro%u`GX)yrMg_pDnD%uVz{x-kjeclsLl zX}Yv26BSvJS8G~Eu?}CVcQ#IPWezGW82&Oe;l0(rMlc>!i9d^g(e;>iUJrL+$>>gy zq^wuzkv}JoG5huVX$6G3GT~^&(mEZlIKW@J2xrw%T1DfiP216I-X5yLlDc9Z~5tt z!u$y;ChO-e@@agQqYSv0DRQizs9>_EAZY=rQc#hgVjP_9%uhlt88Xtkzb;33L}F~W z_Qlv8{HXj$CvyB_9tMiF*JcwHwWbB~62b9Yg^c;=CzlX1 z=XgR$N7h{7Nbq=|x%#_2%;C^qD^Yf#U{tE$*R$5 z*;fa?u+tVBy}kv<{;3z)5i210 z&|sx3U{BxH%pH>Slv@XJ13s$UZ2W|m!WVvi5BYqu5grnv7M3s&ze9=~8T$wsGmMH| z%)-PD)&{DP*hL7VoO{+k4Q#gc)4fOMTOsM*?75CR$HoIOB5Yhw`mjD1!#h$lG2E@s zPb|4h<@(Vb0U_(e%r7xdeD1a+Xe8A5GDY)(z?+~wP+N4p&S21G@B z4I7j9E7zzMgK2q#+q?q7S#o|yc)7Y%G#Ad+&zKi;8Eopi@TcDHogFc38`IFi^3RYW# z$e@O#EhX;vSrvvKHpDP20ig?6Lr0Br_5{j`0=CpM0qPe1yn(;b3Bb+(KzJX_3$3#XUaZY>&Sb|fpm42*v0s(fB1m_-R#vNR4Ff1go!ftdTo0fo5)9PchO}8 zWGbMb)+)Di`Pnq|%XoXmzGi`#T`)4+Fhn=I%O9KTmq>@az;fNmU|es%7VJ-=NUyt=+;Qoz@{90hwxAtUbUG`9RBJTb`7de%=#Jb2D$ zF%T>9X1gy+L|vlg9Ob!O!`~l2D^jgQQ>J>`oaki|?GQdiEJJ$h?>w)6e) z`}_AtrDjbddJ?q)V=7I25|VpE_!7l|5OGG3<5`&hOVk=R$z)Wu<+;8j&Z620ff- z#5|F2!I3Ks21aq9gDGb4D}Ole4}&r1ncsO_ZK7cbt6@NP^JQ+?lr`Xaeq-^?mpMga z^4^6rr8*K@OisW(A!0Vd)oi1clh=i)7bs%68^)%jgYfJl03jilN*Vt=aRqVMXh$0h z4H*HF9!+=vTsRP!PXI{Bsr64YTa0lA&CwOatQBk7Bpl>E5?75LCa)0hk zt+~RefvKh7r)&bl<&`Zy{H>+WQ}|byM_M%DLuf{ z{iaLnNv!ygbK%botM8Uk1pU6IV5ElnVKro|#!^ndURME^e!HudMy~}Ooy5JyIyl^E zkP=zH-dW!LZ9(iW+K^5aqt5}*U?II9rG`3m~&`0 z$s$ZFcL-()vXQ-6`9OMWn`K#Ztbngj%5WemakkZ&(GB9ToEGOPoz`zBG?aZnnPaJx zbU)Vd(>(`Y!DqxAQh85OJVq;k82ib=L6C`2qAx`w4%mnD>7m}G+e3-1VT3#tKv6Sm z7rcY<)vC4Pu~~{YTNg~s6%Sj0zynKP6xW7X$R6GvCqUTM)wRnIG~n(K1dNsrOnDRJ zH<0jp`gC`@(?9&m)!D=Ft3-SrbAvSS=dUqQz^NTGcfb4>PbaMbK(NE(yJu0x%~lXz z>1{RW_9cD%7MLd5%wu9ZmXY~)E)pr)h5->M2BLuIJWcGwDa9YNdVZSUCNBJa8d&+` z0IgquYWd~BLxefJJvklxMAP)rx6~lZG#jv3$^Ne!0C$7R32bNTW_sx z^vJK!DdiH(%zcIN93OdytBxK`r%LG{TKz|}owOoaT!&h5C ziGX;E;jqXG-Q-YJxdFR$smO37NX5~r$+jLs8o}`ncJm2sTy_o3TDL;e9>188(bngY zc2s>|AZs%Y9H={=+dyD7tr%e>I{-GINN#dDS^4UMb;!=+Vinu@Yge*Yls1Lcj*~3D zXGFkk?8Oyqet*YMcI*>qIg72nsP9?EClU@J>I!C?qA-N}5#@jKO9W@AaV*w4+(FVH zdNu0_Rs-I}t=c`QNe7wkbAEK8QeIk6P;VfzB|uAWt#3DaLKFX?u)q5Qp>5>t?te*p zf|ZnBYxhSh5G|foshNj@v8CU~}|Ijs4H2ZG44p3;FTC0k&_~`8{jh@a< zj7J48p|!p1BdvV|&3egmaK;2^cM^C=2t>hXKme)nN+Aeq}$0WYzun9!;bVie}A}S|TXcHbZ z1+v-cSqMW92y9{`5(UThA)Zb~Q+O9@59MmMZH8>m`2n1avHzw#D{_XLi|b^$@nKev zZh67fApjpWH_>!ZYX&rv1f~sN5CuLv>z~e*2?AMA#LgaJ7%Qo|dPZlSIH=|05N=r{ zB~om{#2o_aEU`8S#R%b}pm0=k;;nvhzpsAM)t6d-wN2*s1khoU}2qvG*)Z$%ig<#%?!y4h>is?2(x8gUpO0n?pzIx+_tpqMI z;2XGd`#v24HC(2p=$$3%GzKR*K$n2@XT+2u7msF&tlnMCrUR2F9CHhRtwztA(({t7 zX5ouTY!s-`EZl?|0jK0nB$K9YdYrGA^5 z%POqUKR(VR7Lh8=_OVl)G=?lJ`6)7b*XiD&2{^KzkOHlXZJ4`t+Z?w3a-m20r_bGR zY+Ux6BosT0VbLn1Ral+LIzbi?X~TJ$W9Pm4?vfO|K}BwX<71+^cjk%D$^?59a_$q=&5SCT^V4A{;K=!Ovxqb-v|YW+C@BQLc~wXD4h&U{z0X#73s zsvrd#?l=g5#L~pyoUHoS(y|(nl>Bzu>*)HC{sz#KEXu!!Ta8hHzKzFeC;0@0jpfgi zOTT}5u$9)ukpg&jl5q~CRK``><%(P1OTf4&uj zh3Wt3W&Q8B{J(UfINv1i930?aD$fiL%kY*tYz?SniGC8=%QPD|0M3a>P|@-5IePOu1Xov7DKCh5}p@jgsMLBWu#W`hq$wtH)r!T|4R#~$nm)H=pSWB?_V8u@bPh%h(q5Oeb}()EeJ#o%TMK2ZL`yDO2|O$u#ddHG5-}MQ4-XCN>JR9JfFM@-6oZfn zi06;0BbX;j2CJ`lX&eA!;_U3qbmUv`(0LC&OvMwpYQs#qxW4#N;;OHjsCP^lE^m>T z^&8bMFhG$?NlJ3M9vAoVGxHJujRx4UdxuGZ8a=%!1BX>T*`;}VTkcGL_F)1G;b6H@ z4`8h^ll)`7GF>vLPMr<;Q%@JKlZy+zdbwR1Z%a~jm39Lnu(Njsh2-acB_t%E^1G@1 zdWSvG-7S_|Uy$~Fxvhl>49}<^@>l;{MKsL{r_-)&Zidc-gj60hp@v7C!V1UI$KdXq z{FV}I1<2M`b*#`&6YfPzGc>noWk!LHY(BK8V?PK&4cB zdXZ;mJX->Vm_I{W=;=tozZly z1_uo>IJtvo$?%8y!m5#q#+ueP?W zcVCR_Qa)rH&NVoq7Drzl%zo&mRxclGaE-2FW3);+QZ*SF4ji_+0#3Vv z>gxF^@Wzi?RfOX`-&LA@9xbXXrV5l$d4_-i1ab%8Yhmwb074WpM4Bl)Yf>rG#dx4Q zhaXy8X>Kgl9@oQP$`Zprv_01vS_3{5Kz7-Ql9i4&`lFu+IXGNd^9?@hH`rl$BY1X$Sz*IoWke659c&{0-<`l{yOuHg>mv=R0cq>MapLq`EET z$oO=-;o7GgliqU=PU;@@NB5Q^Y^H!@H?D-Xwl?kc5uFyhX1z3%TB$a8!Z#NeJisLf zr&R1jA$p0t(kZF-m`?G?BS+vA;$ z;N0uh+o1$Sc=SfLhLD4kO!iOEy9xz%EA4TXe^E26*T zfk#$&9?M3f(X11b{UU51&|Y!+VCPEfMT(6a$?^7_`AdpeVDc^613L|*va5J^u&)L6 z=fJWTDAZatfH0WBw*4C#J&o_L98wJx+uM;tu#noU41*NZ#N0hSH#UDPKrXdwEE-I< zw?4r*hk%;raxxR}Q3DOgSPleyG7q}V%gPrcun7q^M5F2{h8P@%nkiJ#n%8OZ z>IT3c9JWdT()J(2Xo_KCey{HiX-yidgM;{caRikn!`!za!i&Hi7I<=_m2#-`?G9D} zz14obt3pbHpjEd(QNfl@bvwNUp*ShE#Gp?+0d$Mwbm_dIDT$ zIaAywA?*SjybA4etx)IFKTms;dFHA*6$ahFY!xq%ZHR|6wueHP5jzLW13PA*ch4Da zQ>8kTAJrUihXg3<^*i5hPLuLwJRSinOx;ku4#L%+rY?5d=>33w^+nwdGxq7};ijug zbRHu>3=CkJ)M_A9I0U&;v|v~D9EUHM zE=+3aKHS11)Kb+z;tq3cLgmN(s-u?3aVUH1T>SbF?pz;zuITIpC_0;N`#(PNfJIK3 zYA<}cocuH(8*qq*(3!y0$tVNW2LE{hbFZVTE6A|YaJw8Y(MK?gv}ht1z%qxeb@qN( zjZ*?g=_qm;JuVqF`eq11``(R-=Rkj#l`;5qPflXMdRs-Ik@0umxBu>L1K22`!Z3*D z|Kjeg!m5tHbzc>c?(XhRX#^$>(gK3gA)=&6gLF3tNP~ocq#}wSA>E-8(k)0!NSrZU z=YQ7ooU6Sr_P$uRI)ORo?;9iD_cMG#Gg|^KA@A$d>N^VLjM-aK?xPTTp1$0jSm?53 zTg#So{ag{VawAyb)}z#;HPV5_B1#XTEjIap+TIr=K^?T>Io@4gc9fKrpFwS4M!lvx z%ug)0swy|@hASsGQ<^@xZeeS%Y|=$vYI%eeHQGz5^$OFT&27f`AW(4?K2`CLR=BlR ze|52AGN4|Jv9kN9s{SCOb8iP-qol*1;2C`0H(ZkIZHhSeR z6|zksbP0h%RlxC2Yi1PtVEHZLF!U&Sa!eBl{3Yy%sj$cf6{#=Q3w6{w6cZ1Tb-sYh zo&*M`!t4dmQ>3NNFe&>7J|(6+g9g#C|4J`Q%69?m%^e&)@=AZlnEz^5&6|uX^~tc? z^}jkElH6%J11bmRg|vTVd;1QcHvR^qwRrt;B#*km{s_bG;I19BC%e<)$IG-28)Y}!vVv2*H1`@5yK~em{z`$ zAZ{n-O$>uyGoAkAT=*Nx=(PbglRBjUilQtc4zUl-qh>FRU~5-%ofn zcxd?UCx6v@O^#FggEXv9ELG-2M}NBz-KgOy%IgwSq_bVO<<%ehfA*cD5~GB()kXGXf3uOE98kBA!7GLDwQzWJqWJL1%W z%RF0;*6(O-g=c;Yc)U6@lauVgzGPW41vy$c9d6qQ^ z#p1gcn2(D1XI_L&o}B#}iVbh&Kz*WSB=iHXW{v4ttM}!8-%$0(Wz<-BG(=&4YUF}t zzAWrcJIOBfM=nuziTyFk$bW6mQcl9Iy<3u7ylAB0Bgq`OsG?u{)6(8jM<6^nF5H?FqgV3>a1?tI5{N+3`(Qo!rr&9v z9EzXYFc>C)eBh-IwYpDgt2&`rvb;$J#QR^#mOvVTPjoQ6oT7slTQCGRmN-gF>|l99 z&gTzIO|@MO!jO$Y`;IRSYAnZJqt~E*7aeaunLP9wA-+2M**Rnv&@AW|OM@u8WSSQ2 zq#)LAZx0_~=PK-u%Ta5`omYnU9TGGJ#Q>YfuN>c|#4ny~l|{fJGw zO?+=3%>3Bqi|m1xm_~Q#uL=vDop%(YI8sFKjebJKS7i!xRPFJ^a=vn+vt_-xlj7ws>{R)>)Y0*xjBa}G zs|&Q1x`>gt@gmtDoGk`+G%N+IRKbs_(>n6oUy@j`st0Q!wsy&bU1`D?%4Pfn+-}|3&J|EggW8%~+pYszvci8a z#KrWsDP%1^jfbj6#O3YZKhr%%Y!4qkY>Q$Kd>7T50&z7&^I+N4Bd7hvi5=@%`mohV ztYPVk+ytKg1HU!?INTU6zO4>Zi+g)~pB-+(>jjO^WZi5)E2eflQh28BZLO-_IX{@~ z!#;NlHI*ccs&qN_A$mD0-(HfGv~=QC_lLKcmjvTGX}mNcgn?UD=k94>mgD@{J*Tab zW`mnB1X3q+_MpP`7zCB^#gMs;(eIYtF_p{ahNA)lAq+(-eLai)5tWtc6tx!wg zNk}q<%Gb)o<1{=aR1cks8vl!j?Qf&4oV((lsF?TT#0WN=%}aZ1vEzoootp2=S0SUJ zq1M;9@~f%@JnooW=lgwE<9|~{hAE5P`yAyaAB8St+Ig)wL=wIZZQf{pA(a(+`Lup! zQI$q7>R#>C#+VjeU}Pal?DW&8#mAe(G^XE#Zm*>s-^tkMQ8!|-?zW#M{-$_+T!~|v z@(6vsvrKgz&)`Ds9-Wxjnw3Cm6B$(lq+8G%qv*ojRD1vQr}f1r2UsAEnrCaC?0TF( zOUo2%9?B$WzsKSb`PHVqz=_SGf28c@>w6eVt6AxQ2x)dv^}C+5hp6z?A*s!j@jL5T z(CO95@hi|SLBqh%aw?X;uESfrC!8A8>|GX`Pi!g0&-qh_P+lBQoc1!xjxRnXfhvYt z{O;^f@C`LbM~Rtx*4P8|MpVcqUYuc_?cvdFlK<2Ko^VgSeJ~w^21RlL*QWPRB=cVv z7;s&%*g^fXj^7xIOHC$2yY7B9Np;hHGo{6mN6KYa*DEtYA#xel z`w9vR6)}w)Dcx2^o%H&{Y;n538y3U$Q$}Y|Amzr+bC99Cx;nE@o6Zz2Rs5SuYdDkL z{HZDrwb&>S4q~#v#lo6ya^5cfk{5jo|+AP`sM` zvqW{7m;3z`7v4YKJw}>hleXN41;a$qn5iB<6koPtTN%4CG29$}6Av}<(O7YJTkk6; z+*)4}OHOljv<)#?RTHZ2X>>eE}_6Sw}srcl)5UPuxz3CYAQD-+&7){+4uNmV54jb!g}Y zm0fjisShc;wl+ELP3~1F+YR3cTQ*&%nm9IhsSX<%=FDA*$j0PgKg;v8g(f2705YQu zz{SN~=`-A#JW#A}H@5*-LZ4?hup-K3dlm1r)lCqqibo%h#3A!5~wOlD-WFoS}r zAi$?l#<_nme3KLQPVKA17P=~*uG@bCnN&D7MRD`mzzyPu zI;?l}Vtj+5`Gp-s9Et*jRa8{&o5jazpfsZS%d>i}k5w2y=ITntcmhJIR2ih03B=Wv zmR~t*#U8$Mz&Sl9vH0-KemD=WU;4_bJxnA$xnY(vq@^>~b5T|261sMRt?lE>$rqzE zQ-q-mFNn{+FII}){_*iyIe)qRb%kRHSB-OuLrI=NNqa%!$L*AG>w5cSkNjz^ zya1UH$8i)2+MoOfSdXtoO6Y2C4XH5nzN`wdQkKD@_2Xxz6kS1$9gXH|7WCXzu4NhX zCV%`T@WtD=Z_`&3nZiN$$&^#CMxn47J$dir%7AT&RyeCqdiPNkZ`|XvqP>|UhuVDZ z?J7$=9O=-O?Wt$_Z&X0i5__s_@=;|$?LEi8ZN%>O2AAd|J6Qw}0rtS+)OGDq{x-syY}H(!&1Jc)yxMz z?1q4wdD7SSTzis2*ROV3-0)eU3l_VG)mui6&BC8D0nQThl@y(Ut63kq=F2ll28OPk zxfZh;OB-fe`?or`^NVmDoNe^JtdHb^aIW9^mG&d9t_L5SpQdt7=$I?wIs<{OJ}qxN z?i=B$e91)lOVA;Cz#rZtI8}(He5bQ0a2z)e4e3H^J-7vlZI)vlD8q4S?$l^CvoNx< zD$SzWS+ zl$AMltcAK@SB{WAN$)mb)i}W%YrO{bu{JgB*^i03Mm*`n#WAKgDihmwXVkIUmy~Oc z5)J9)+7{Whic+c{REs8Xd3$=&;~BQ>6PPehxW&10s(m5%gdB&}?6vvRgG$rJ`e(oX zSasc%@;=1x&F-G7{-=<&c?yEr_tQ02&t z;U$AYdC7BYF5uc5^UI6pyhgxye}1{sA`3M}npILMV>GWM~~TGh&&p-ah>ER5nG%=RBGpOh&^p`U=sn6 zGO9Dk&7m^l;pz&y#s=RX4XB+%_qF|1{8Q6Q`5r=AybPMsZi(R=fnLcRFT73Nh8!cJ zqRv2j3_*@*wK+Hd_)*;h&Nfs|dDNDvMAzwz!Y#SJVf4OZ&+D_>dzEEynnIc4s&Oei z%0uj~%`R6=Og%FX)z*Crzt!LJ2p?RB-ia&iG~MQb+?Kpm6Q`Xl*TeMm@LFgp?fGra z$xGgfDv_VN(RiEaw_EK!N#(l2Jo<96w6$;mEBBdFFePuKK45Bu5Bcfi=RZNqz~; zmz@|nD+9-K0aznJQ8oPK(1LC6%mjV$K8yHoaICCMRQvpq^6%V$n&7e{yVFeCP*ZAr zQ5SCfqKGaQ^_xrz{*94BgiSs`1?He~;@s5mXiTm-=Qq^FMf~0Bvnc&;3>(Q>+F{U>7uJNT5&It3kDMs3-g=v`QFO=k zB(qtPeXX4zF1`Qm400>b;U&MrxcAoj$V2D2CM*^027^K^WbwZ zM+kqlvitH;@g}rlgB+u@L@|wid_yLjM68*UkQ7I{<4)j|7V0!#EJnMCc~l?d$)lMN z9YE*1q}IZWHKFr+9C)QBxwBJ;sH$&e?hH2{elt1asu(7qpm?;ex`JL~F9cnzxU88mX_j^RY?q`|IqSS~cJf@Jnu3p50Y0lP~ z?@&82vUHwC2qmQSjIh#h`*KpXauwMBv9YnCp`pov&i`FUM@L9V$jQkEX#Oe36NWr7w`XE381Je7 zP>cjkQe0yg22||C#5Q!FjBXva?jt+O?btHO+Kdi2AZ846LBq{M#Q3JRP8iGOa z)6!CcNchh4)h@CyQTRzS3d--J;#~?NYz5}9k}WL@kiSnhg4uC}RHoIpwp?cN*S*g#T?<0-GEE*~9-6 z*wyWF)^>0ih&|i19>^3K20Tv{6v@|qRkd(B8eN`WLZf=NB>=>x(eS(l6={CL!lDvlVp6^*&TS!)-I>j2n^cLK+@>E&q42q|u%NLEzuE-dB2xyC&?KBG zbM=q>-Z_ZWiGCE9kjVUt`KW}Rv0#gSdM5t)Vn@$NVHYPI80I5eK(vH%Y0D`8`#-gS z$do(QntJpus(wOIA>=7jo*$MpLF%}n96i*TI3_(XEs8!yf7 ze)QaH=?c{JOUasKV+PrpA~#^l#71Q?4m0OxCy2@1Qz;e3>I*5;or0a_Dp40r+z z-aArgU|wZqXQP7h3m@tnG(xK`EVj@d>!=%fGE#uLo+G&O3Q}N(Jku6`8<0Lg%Q>wb z?G~vrB3TD7_~PQ&tTs^_%d5qJD^ph$5b46->|KnFq&GNbM>`U!d2eV5?M1dje z2ZVDrSy~z2<>ekLa@6a%ZV}?ACrcLO*Y=1H0x7<<&`7=qsj6v4W@y#f@eUcGD>YH8 ztVSicCSZMxs{Zj`RfAhRA^7xcRp(5BGd>X7=aCkvGvkC)B$VnE>=lK~T@qpQmbp#i*)f&KD@ zcG%b$;-i;vtXk zM59{?U-JodwAYpY%~fi>c0*XzWN;qR)@lO#-xsyMgyX7T@kU2WhR_x7J#OcYh{QI0 zF}Azs*fdN^zA-J(0O!(W&U2i^#G{q^k^pASLpAb_H%Q`vc76dcVu|QL^AUSr7yRDV zejvWto<3&rJpzU{mclKgdV3vr5Y^LO(uz6fIWJg3V>&V_s!dq-Y+a$x430xp$d#ns zSPvJ!y1}N1kBj>~QC`)^FjDmGwfZrLC32;_bk}p|4afqZp5X<~4k5ci#$kFDXr0Uq zA8d??5lkgaO_b_<@;!|LE2z9^XiinrC|v*+are$rG7*hli9eq%I|IYQ(f2v9Wh{qa zr_&>|)L&V*$EV@GqVZg)bRZ{sDrm1qzE$mN0i^e-&m6KWmCg2SXI zzq=IJb>F@Xf&XK)5TXUf!(TezT>=Rx%Nzsap>U(Zw6V5SEoIx5@AEj`yCN|1p8oz+ z#-to~N2ElM@k}e{TtosmoCk~1egtGt5)<(|!wylh_KqknwLUsJN=1Qluy<(4aMlm( zv9gE(V01${ung275xM~05jswV1H}6VN#ee@%I@wix$Nbb?ZAD=V7ooLftycHKzNqo zNXf}A;0y((;S#mf&GkD`=^)TGZS-9hPOZTY_CDGy^aW@RSy;+#rY5N6o)@a zM&hZj{yLRyr5VNo$ZH9VY07Bww3J95y8UT66crZuD49VQS`Xhd2=`Z7{55C>Eg zXrNaC1>$tGya(7OPEtj_qvUxa8XjU25&+`^s0`9`n*Qn%8i3<;Em!`Ou1!duL|=iPi}w4!X=UFIRV9>wL_!&{PKQNGZe&VcE2GbK zKb^8o_v-Q@MCSEOT$Ivs21q}n&(+>DG6v?~5D^sACC|AXJEgZAPTEWe!H^PsEx ziNp&>Yq=#(KR;^zL0oW5;Q`ag;Wt;&yY3ZGnFWPz!(joQOkhzkv}6Op0~Ewi2V@`E z+U}u)lLO?cBxiq*dkxBq15#`N{DmO%62PWhSvKECtpY|yt(1tD99EkhuuVY&eGKq= zE;9}S^^8>z$_?ep3OUU>Vwqt;7|CS+Fgboh;`#5aBP)>T&;-EJJO49{a&>C=USpaY zR{Upv=J4)jz<2Uh38-RCeldFIF!Ap3_XRUYM{Y*Pt4lrx_xmT9doF`n0Z=y+_uNeY z1K1sHG`krCyOg)y3dlZC$^ivwNEy=Lz9K0Y>ZpR3G9&c0S(8y?2@H7V6*$RBED zHOU!=g%_Hy_mnLpst)Z)WO>$5|OZ}H7`@t2C|DzUJsJkL)OVGprL0)OwUM5*ui7;Y8aqRApv zvw5dj$_WB?Fu4ji&4QTdv8T^k{kg$HTj+@IZLfW@SE7~5O=((!NU}bJ5Qhm*T$|W# zzkYSW$)S4n`#bF4EXn5&n{O<@1l)+x>1Pv}XYCEP<5Qct6*B9{M>-Uzv+H;OHAebOHTt4k-VjRUn7X>90Fo z0h#Hi;-(5y841Pa(`y4R(%y$Kd!bG+9h+FtnZ1yG0p$WoAEH>=U+G(0T4H)EG!abE zl}W^Q7s@+Y9$MKB6QP;KH@=O@07G>RJQ?(FEN|Vo5tZ-d3=%lNSyZ1-K;Xxad!$>e zkpaKNVgrOzLn+OFvLhn;N4z{eqYmfSUTV#5+G1flk-F;TR2PuqYj3p%ms(ei>MKY- zj3i`%u?S#se(W0f<;x;~z=TiTNSi_SS`XmO=4Zdcx>ZC;ruto;KUc0ZGBQd}Lc1U6 zpj=P=@xuq;x7C0tf$@TV2qtAndkJ=&H0F^uV`5^;r!2dbL`EP!c8dH(;#azV3v{WM z1zcq#H3?q%wp9=P0O-dK|6vPpd|mrrB+#XsoN^#5D+`)^{MOyttRSiN;GS-hd;$M8 zT4353g^WT`{u}1<-{_bBUAgT4qN?_P@nvG@f}qi(%7a9 z^h-dUr(y^fU&ksw1&7^Tm=@3^-*W%lb$OQ*fiQ0U`LpoO3;E5sLxXzzKyV(ayfSTc zU0UYrmWYdsd-38mHMP+UV}ZuXznm|)q{PtCv7-AL;79@DM0sgYVT9`S9d3ceMZx9p zakVsTAjzMsk0>zyE!BPPZM}Q}W@|ST7_UM3N|JN)`bSWorURd)(ajFw(D?DA$JP%X z=4DS)Q|kSXw{S9r9e>orH~mBR2L*(3DJdz-JGm_;4YRHX3tUB75b$<Pl@tJLaMYk=1umL(0BcuX%@d=Dm;#_!qOv@W>2cn=%x6++hcQ@M` zqLv5M=I^`Bkx`G=etP#kxU0NZdTlzK;7j2&SOsk$$Rmit3^+$`sZK%R5QsL2=h+fR zI?{mnsD*oYxo0WdGc5tX1<(Kthflnr9v8`}A>4(EcH8%a|7^b1Z@p|xTCc2Z?6uEd zGY9jFyg?Cq`du(*$stk_6F#C}WQemX9I&tm=gn^Kv;?%9CH^c8Hhe$7k%3bF;MOOQ z0-m3)#cX|;iexwGqZOyUA7y#;lvbj?>e1Le_E0!&OFj1pC*DlBKR>=RHoQ9UYXnW< z)~z%|_UrKQwQR|T+fVLCmxldeaR=m870y?~r%#{47ofn<3?XEg< zMBUae2D%mQyEolN+87N$-GQEIXlN)vb#4?wFjS0)99@sZ{2GSyikMw{9Zp6#WFT#6 zvJH#1Ly?AnFnA=f7swqRppX??qOvWNN_%VG+6I@_3|M4Xe{F7tdBUZ63wyXs@45RD zQjW66wVsil9$|0z!FlB9mmx;`vzT{}(fIBQR{@q_mjJ`reqEK>5Jc+jK|Wq9U$o#> zU4Rm}W@i2FLEkI3c)lrlh=R1s@BIu3xp`g?O5t;|YmqOnnD8D8bulpgMb;HD zHyC-2V?$ig0NRJ8rS0k44Ns;+Yz4sXZ1gRN>2cePWsu<3>xilfD!h1jQ7n%MY*L_W5Jh=Io^b^MZIDO5r`eR=x*rZD@$NJ3Avb zdF`$-i8_p9|6&#tyzxr)v=T&TRwG~%&y%q{D{;DU`D3gHW%!v(N_8Bf3?>A-m(@%VRedWM?nk8hwqGIyZWgFKdWkWY;7e zS>T7HB6v!ng1Sf-J<&*!bqyhXu$o3A65rh1Y{VX7b(6B=d3*2oyL5?Q5uwe`X8`By z^v7pRwhDDI`N0yef zi@vdA(yru)cOs9|3KvQ}w!Pb&<0%Fs#SY#+{7jwyS~WRM-Yn&L7si9QIE|ixfd}h_ zg+i8{pi?z`dyXf7&9DqvIDF%+J@xl0&9Uc~m${wpfADBN&*}Y7Enxdig##+H{C>>L z2N$6tE&KO?QX%%b@o_@vk*nzNCJC`Vu3bP@MdBUCN^e1`(}V)ZB#2KjGF$Ms@f8y+ zR|bB?U&mMHXJl*z^Hu^HP<8sMi3g3?PfY-uhHka~g?N{Y!T6zB)U_~iv-geynNwg+ zijx5&@Rw1GFgQ=R4xh9>J+brZ#1EC+F?SlA|#Dx8bjed2btO+Lz?`1QcICE zZexeB;-iRcc8%`si6T|E3MEG-Vn1zqZGSARP{ufV#$FY%*M*qaYq?-S`Wl6rNrk;8 z(0;T@5OL4GTRuHu%{QyH>9e16l#Hiy05lbW!XHmW!@09dpkP@+DRRi>25Zr@9b=5& z8!IcpNjpDMhBa8QI8f09 z8Yr6*Be6m}mh5+%_Nk-!*ETjnWll;ujI+vZvJC8l$ViFCoc31+$mv+ulfWXw6 zo?o2JVKwdNaGp2qEQ5Yuo9~pF ze53z~nh=4S2{!|yTH@A79Av`^^~(!IF+_}mTkFBx=o8`4GA@9rU7t(I5LcF8ZzPOT zi@V}WgU~-RWVlT@ajK`Tn%`PYWz*7XDMpB>BNZkD^nD^tBxNL%XMC{>Z*cU}Efv{a z0+;&~UzZ)t!|-UP_)O0K{QenvkE8u{%#EW6bp7=^&zLfEmao}+@BbO(s8W|RSMuYO z7+wwYbsWmcW+g+DLk&oLSFlJ*16VPpm%k1-^dcnE?etYG6Za}BR`t_g35<*Vo^CK1 zymqf@bxk`mx@AAgf+C^+XKhff(kt4_0?co7vMt!7r1VmKQq*njJKbO9c-fq0+nc00 zByQ$S?*6E0U+)ywp{Q8?%*v3~mvr+zE92S57=wysB1+)|!7qwXo)ul4l!nn?tOksX zZV5WwZjyJOu6L^TESp1L2OVdh5x1Ui!50{a*JC1VOIP}IqMs8DAVBtvujVX&`K!t- zBtILoOcCa0gzDHQILj-U>)s87fF41ga9bU~*g6lt2Igsha2C+Nw}{QhwI8yO#YdIm zeD>9_&H2SGhnbL!ycnlFJUc@#Xz+BjwaE)OsYLOI1_mykjKIl$h;VP`H&SBux1ZLp zy1q0Y`h&UZ=~rEI3mu%Od#!NGfn5{L?Xm0K3gbV3>h5ulaW7YYNtSSw-#XxVCckb% zKO)8MWWe7TW3(Vo*LL_zjdfyG^tI`9COQeB>x_I&NJKZ~BvY3{FL-Q>-#dC`eM_uI zSJ(Va@sDIg?zF^$^lt57YSCE9|MOz!Nl86QOOBb67MLd)1_sDiP5&SUt)yr=XwOrHxN}JZ3Fu^EL{EuoDezq=e;GG!Bi#OG z?-RJi)kbg4)>uWG`n*ZSkBf*tZB^^U)L9*_rf=kyicxMfraKj4Ea_L@nG0x||MAdk zU&>#DnzT3bNC}bRAcnE#y@=NiPlVzpVz=}Kmh@9ZM+@>Z|@n>eA z-?YTsV0#OBx>aqDOq-63w2}CA%BNcFteGZ_Yj5yyeKs2@`Ow?h=0w3)sLFc#_0~58yG4zWJi zKr|TFrR|6DqB?Lzli_u^TYGa1eFcR!+JTj;1FId%b!<0Yp8~v1)5<0k6R#B6&Llb1 zr5;;hiw{W%>1vIX<*z~!_(tsgs^Pc~xHV*-39*`NCp^t>)CyBbL4l}pCOtAt^QM#W zvnWp}t5V14zR{;xVUnkG&{%QrH(kz9$Y@u-MQ)^8xpI{8gL=Qfw7w1QeB-Cl$vg=^ zLai<2Gny^(O?hU>6%Z*N`#+_^?iDiE&+1p+7Xm*=Q^{QvJ5evi+~tJ@V0$pM;}3=l z^2kL+Y?a!N1Ok=U}x+}^`1tNYRL!7$Gy$ECP1U1mk!nq0k5u18`j|3rWW(d z^MKuGFvldfes#W`$F)itAdp5)=t8Qa@O}fMT-p==#n@4WaSNLy8zxrY+nYDMN^S)) z^PeRn3AVs=4DVK- zfiW2hK8&vOO);DOfS|$v-pL_KMpC>Mudj`iG4^>ImY;yg%|1ZzQRLegy$WVgo(G`?zmC=qq?Arl(}+q6UeOg zy`*c@Jl#)CVIYX&PcB6Os;}gC=>|d>dIl-i6BsSIHTRQ0QY7=(s$F^=uIJx>7gR%= zFa{XeDxE@GHG|hkkg$?W*l?%;bFN=d{XpANQ(szT;Cx~h0CXYi34EMlio`>iJLnKf0)qBKDRCF|qxOm4e^=IkJi_c))XjP?PTZinj zU=6Y}ZAmCMYFQJA)lubgjWCB&cCc#RM_1s6ibl5WHD7&!rjXli;y|%5NcI?pHkVH!7@gk zkIrQWuBph1MCsI-K z*zX>bb1Ck8eCFZNscLqhZQED(A|aTrmOn}f&hErfddvJEQ5l)ROtY@T70ceoXQ@8t z;|Jzhny=kE>Dfz>*$hOcd6$ofneg)~b8*!e(ka|-@0VFD-n~1RnM#lSOU~^PiyDRz zHCI)GyNip#+}Cgdf;ZnQjCto!7+gcl3JS4BPgL<)OD@;IEk+anP*QhlF@8 zB{vaL3Kudd6*&ndx@e116pJ|jhL-x!LOU8yQ^q&zUGIG&R{5rtxe0E`P4>hVWNw%& z$cw>j62gCKvM;UGi30gZr>+h+L%|j=&C-2QpXWgW$#%|5b!>x4Y3=0Anj0C!#vv=# zot~oHYay%V+)Fc^GgVyC8Rq72XZRGmcgibh74R+Z^6ws`r;WbuE7(uPpe2~3Og6qg_$*UTV(<~G);=o}y^_}PuWPZvuC*8s zXPYizGKInyUD^Dp^w7uB8eyJ&Esvj0YNp*x;*n23=W!bu&b+Br^ad`&qV7N2o__!n zi*j9(moh#Q+MZ8=z|_i1|uA zltvLMnLRjjyI5Jvk{hI)uiAzoG+Ibs0+H08!n-*lyS*d5WG0kP;H; z+_QkU(2L!_=QviUqg~P%@pU1CFDK-4QJ36Z|GU1d)~x?t_6_+?dzs;738C!kyY(-B zPcr1{hfy*X2wE>D&vzTCKzT0$Nb`#0g zdLvG!ER)T^EJ1!LL&JgrKY9l5v+K41xn(|Y;!!qy#Ux0Hg&>ui)O5bkfvQ1OS$t50 z11_$Ffwr%!-Nywvt23_qhjM%zI#&S}w4C1aUl)2PPy!5^Od_lL3E zq)du^_GKw{4-!danIBA$3PK zDwvFq_Um`oF39t~U$Mhn(BDbD z)0mPuy!smN$)bzC&;A>!7D42%GD%<3!F=>sB2{uIcc#JI^Mw48MY<91qUXW-{>JEH zMu(Qy_k=ws{yzjdwj&#!onFnxG*gFkE2x}mlCw*8<*;oMacV9;bW#tAmZ79C=vak) z!W$rx3oZK7#&seyN52`J0jbjTH$u^#n#O{tUSZ!}n5UZ^otH&bkjvHyS)BK`8GPO&T>@va6Q;OsT>n%%Hcs zn+xV2A0Hp+PMbExL=kba4VHT@AG%rACVs3360v#X6a&WRB zTj&|Gr*g?yPxjS`wmpr>rUb)Zy#lhA2<#@HfM=$c1`GkULlFf|jfeT0jj zC4K3&5%_!_`fO`z4ndJNhEA4s(glV+PL}C?hYS4Sr!>>J;|jLMENx4Z(WOs&v}od! zdneh|YA3#wxI#>XFp*~-MyD6R1U{tF1tvl(Jcmg=qXg&`S zF|Voh{1=8=NnTzkfcX3*bNOr>;*-57!@psH0h&3 zLN=Nww*+!G4n5Z6jRReCa|}GZq3-T(n6JuYneVrgYIsZd$-a!n4^7aOPn209rLoYWL*gc0Xv-;Nkv&iXsfa3C68gW=NK6@Xc9K z=ldL_4?W1M{7?7aQ3C!TNBEKNWGxA>2GMd_OJKf}9K=xef{+jz5M*4WffoE_AhDBKG;aE!TS(jzfiP(V}e4wJ3YgcPTiqwNnp{f?XW|D|78_cbAri^V{3okj3+p zRx~<*sMkQH-u^7v&K9T`peQJZ85i(uAJ*Hypr#d91&kDQeSv2&9!E1R>S)Rr&?XPd z?0eixMObDtEmiTs=qZrI5YW)XwxFlLNL8knZmzEKQ-5|X+&_8nq20XsMmbCLiJIrh z&qOBKuV>03ii^?3-}Qw{2@@|CgbYVDfk1=5Ykk{60s5R7vOBhVg z7#SNECx0tC9;Qzz`FevFKqXSg{!S1Zn$N+&^Xd9W@$q%G7aV^f$=QHOzom76cI0VG zg;5oNqZtmkJfDP}=02F!0s<==M!>0sRD%X`MY)Lj@)Ffr8Spk@4RjLqX8r~roC$8W z!u$jJUp~JoUzAR{xwy!2l9L-*&NY9IK;M>he`+uGn#zl{zUQvq@zSHP1}<=+MaReg znylmFj3B@SEROZn>}Ru07=zGD2$S7YCR))G;WW6@1t(evfHZ{97%TzWK>lJMOvrnK z(u_&zOEzI<0oj*eKV2l|mW2X2jG6X-CAiWDH8AK~;P@r=9sM+oKh6T%Y#b>B_8TvOPxj_r_cHJ>cjP1ezUZ~prA zt7iHxhs@=l>7lr|yC62=hQ8n36KGarx!{c8u$`P0yGbz>Pw#CBH24$%BwJ+}e;=l? z#`Ri`C-+`LGZ;q7V^Qv%9d58a_OHpYiAZ@l^6}$GT5(tM{1*98H6k{D1E9tUAH%7* z>X`w3a`nx4uz#n}0B6Mw7-k1lgy}GTraQ;N?(pRYVy^rB`@#b)bbNg$XJ?-e2FSPoY0v2s-@CX$>piJ=WG(m`w zaW?+t(}ZdI*NH#(lf2#bR}Pdt!s7s~y|lEXgF;3_1Ls}*IjK0_4eKYO6*nwZUoxU2-q+zAJWC`HHfBLk5+g-C!3({}3PFfrr?p~Nf_Me zpFk~9ODe+3Y73KqU}cFvye}o*E)!;5>^SKPy=u9DA}>(D3EZ}{UoQhS9G1fDejnf2 z?OB-61p_Ng2QuH?(P*Fa?Tn$LMdUZ@y)6BlDZ0@ki?!3`bKcSq4e7s~(#PA=!2!_5 z{0+VJ?>~UpgG>D*nzYIOGrKrU1Zp3*vLHWKJZtbiQme=S(d56a)$6I|4uWB5=n*;a zm_f*MliTgPsrd=4QB>5_%6pZ!Hn)Ixws7?8ECg;yI$-(Pd0d$I6@^doqaT&r{aGg&2(3>5vL%+3V3 zP+LDFCK3cibl=*E?V?B%a5X3;pO^b~SFLZYN3 z+Q<%NRaFdCdlq&15KaBBDhi)6L`|pmLirIXU$w81Y~W~KK!k-)5t*+^ek1MGt4|`Q z3USo4TDN>Cr(iw@LN-Rwv?|fpp6h5zl%kKeOQb{0?!pY>+FB7&+V4N&UJh3HQg}DL zC}xw-^WAF@K=3~pT_RiRQM-F^yu?z*>)kU&bqII%v=BwLhF}KT=2B+HS}}R}`z}9N zM#${gICf~F-az%um2~6QrOUnWO!Y+TKNp^*5rTZ>B@a>e*4@ zf)MYs;8ZTJB<#P9`)cG&xgLQU}dHUm5n3+FTb&S_LWsohV2~ zH8-(zuv1?~YVm4&nI>jfUtV}>aY`P&%W-8??M3+j`$FfNicI}Ri@=4y)W5*pJkHK4 zDd#kI%h&s5_N8tbLLo$G#{Tl8CsXTkW9esdjw$$ zMQ~AsV2J2vUG~SW+bmK~3w_Q4#ODjwB?;_qH+rw^?czAQ5C4>`+Z!*?(YY;uw1Iku%DER$ zC1X(X6TVweURDV;Nq>{GJF!EL!j;G-{uBvXyP>MYN`9ui->F&wc6esy-7%d6 zSY~?vsw1ROG-2AvI2$a%btyo>Si2_|u>7vJZzsD+4i%#+4u=B8}BzfYJ z9iIHHf8~!GkoiWjl5Rd0!@1CYvJqHaOtcyC2VENbDErL613+=l|3KsF3TQ)gsQW zE+;m$R#5Mld5hwy4&T!D$z6{4TmEOUACCSMZmcaTFd7(Eqc2I^ZDm)10$Sl5iTb4W zem+lkk4s>12=6p(|WTfx`{2;6pIWS1Piv@J(HL+f8IJKU9D=4@Rb@5WUrb%n`5QK_Kf z<$sknYz5OdxMW6xjvW)mmAkKBAJlpa22N(iSM&Dt5jlcXQ@1=nQiZ=ae2@iSM7M`^ zG%}sK(^syB^iXM5WZm@qIIjQs+{BzldmsW6qPwj?-M0Wz>4R4h2ccXt*A}cs}LQ5IVb`~DzH+{lIRqvjC}c$1hO~KBRBeAWYROLVXS&?Y`k(R=6JcL1XuZI z@0BuR4(J4azQI{IVc(Iw%Q*V2V*-yM5mQ0jQbOAOPG;EB60sc zF~ohExtGDYX0ssJOf9T;ntQaj7yycKR`_gT00<;0`3-P|RY1-Lje>|&@fI#MiF;-`Oi#!v|oCm@4ltJlu2eF7v8`#xa(J_K_=aM(#GDJ?IE z6XqG@?$_BGc=Yc;2UuX{pby|n80A#ZyI<1kSUtmfXCd6%+ z;GHS$aCe?((j7DV-%XcgN4H>rdtsBy|36#47%xly?{v8iEJu!$wFETaHU~6QJPel& zqju_usDZQ{sBk1_K&Y#OAp{U@iv-zp#XW%61NzZ@q3sk#M^{4NbF?Ya!C$E4SL?C8 z34?_r9BqDnoTy!bR%Z8}>4TrcG$cn~rl|&e#6dNdoRp-kkdcs3{p{B&s5bbqZvhpO zI>5|#&nAR7-)Zt!4ASmTDXAAKv+Gt8Q1V*Cvl)+@lm z!GqQD^gP*V8T!%y8ZLm8UqVh8I{fX^aWoS3GE`kX3<$~i_;@nwX{Q%Z%q{hThnT9!;Prv_O2@Je*6fst;iGkN53A2MrEtNtP8yDkc{l&fZKhPJf z(ay!c)Byr(Yzr)}Rx?n0LeQIVadCkKS-LVvcEJJTid1q@eSgVGGTw6a7-W?I9~Et$ zY6;kZFdy-U7?9XFE+Zp4u=-lNybf%cWIpe8-l+&d5+PfYgFm0Hegfbnw3LDQ=U3g` zouFgaXwiN@gsVPO)P5NK0&=5^y)K2d|A)1&j;caU`#lyQf`BL?(g+Ahhe%0^hyqG? zgLFuj(kUXKG;ENNl zhFGcsE&zRiAEnb&FZ#+4UxI*H?8Y*DO+xArSVG@3FCo;e*M@lrP6aOeZB$sX7Y7ukYD(?9;?x$s+oQSSyJ^`2YvV1j! z1U#6aAcFKHY;6Hnuwz$++XYcJCh1I7@v*S506_^RjL(1Xsv#Fz_9IImQ49$Qxp{Mk zmVTNt>Wbdi;Yz2RTpb!vI)KbSY@j=V-Suo4)``a&7x5L&Nft8{NMZP(0>scGf;|=> zB?%WF|8T2%59+n}-7aEEtuGz+Y01eFO32rRI7hll#)6j^w$m@I`o!Mi5)#_)vl|BHf^EfVrjC1ZO-dwe0b&-_vsE3~Nn3(% z-~I$^DkvL4`w!;z`>(?}%tAfKdrO}$$m7U= zMwSaFK^{(8DY$HicKKxAB!+zECc%+O+1YCgNGS!c4VmUbGeT;h`;#nenzGVRLi-*> zS&-uiay@!faQ@gq9fpU;+DsT@P8}?-g^b2~PiNEoiSrUy*Io}Mux;=+ci zRc8VPB8ZQS%s)k-z)w0rDdAb|@GyZ^NoUoYWIFMymPTg%X+NB?2LW91y5P8G3K~KK z1ZFp9%VLT1@!5Y`bFnf1bwot)Zy;2?+%DwgSljXO-q zLf*VD#RW)OPV>nRUn%yXc1wFCZ=pNnGj=>pusP%%hkxWU9qmpK)L~zMeg0WoBQbGL z_cu>A-bPVT16kSMRq_w#GL`c0lqy+RxMUVX;unQHvIua1o|4Q86t~3*7yB*K$VGPx z{JQe|N&)wAaXrT21Va20=Wlql0R?-aBD`Bn`2ZJPB3l|a?1kLignAIdKC#@mzt0b+ zz%+Qg@J-{n&RruexDNUF_`ETlNDJR@Rq&IAE(V51w6PC%TD-yzUP$%|EE7Jq?{Sp*`W)!y4!}U{?Vm)QUdPc>A?)$? zFiL=rpf8N-zk6Tifc8es7Oo37NG@w0j{8q^siCax z#_Eyhh1!pf(c-PZVITk3U<8+41*_-eZr8>^n<=zq`s%uS5!d+t(n?evc0bTB*%HPl zaUI(ARV@B%*X?p*38ZBku?ITn+Y5`l&PcW;z@o!>p9{5-+=)v|Gf4cFl=N16ma^mX z{So>m%EYAy))nh*7HTfpAZy6b827Iec6L_H+Y)+qGPAOx=d!uOyJRmCRyl01I#(DK zdHp81d8xB21O5D1HY15OKD-7e#mx=#qtunibvc4J+S<^f2%|CSe|1pYcF#+t%5Z|Q z@Nt{eXNotlO~@2!o>~H%U|YD5;+Gm}TKUVT5IdwJWb2YjVvzGK*6Mgj&AE@SNx()E z2TY0sScsk|^y|(suACsyANlee#l0E5kCd1FwrO=EE0Tk1!Ta;q+3dR3-5T38uOf`s zMf<2?_OPOa=P4Dd4E!VS@87>)zb2>~``Od;TODOlqrh#`F_5^j}C_&58f$sne7huJsTk!fv@k473;EU&AFV|@a}{Jb+5b!0r64v z=g(xmnaGu{h=J%A2ddx*ZaQ~3E$xiXwJk1~EgxI5!*4`#oMYb}|M?pw)SD7Np1c+m z(ba)bJ{&ortNers-Iybv{?)5guQ(uTU#8Ce@XJK<+avCOzW%V2!O#EWc>ng%YN?Z$ zkdTm+lqn7iytd%f=!zb?gq&1za+)yIwzASvxeR&8#+W_YN%Hga%S#2r2_#H|8RY#+ zuPD&?BOk#=_)XyxKsoNLkoI~30%Pdsc7!w?=l6DYObXfVIXP1>+&~4@eNs|L`DTDg zfE>CP5ikQbJ9N8%kN5K2e02{S2M}v{zzIi!RL$U4f!P*(;mtK4g>}11N`^sjG@Sv( zx2UKBRX(muou&c!m-NVKeE~w7VVZar^>Gst7eJg1mJbO*!s*IGep=~-1i-1b1VA{i zT5NU>_XhnnC>GoS5Dwlg-hZt7-3$=cO)u_iyYUoc{QIMY^ljfR?G{=WK>ItlUIB~8 z_wQ68uJ@N5n{+R;LIeUBW>;5>%%|dR+*(YW$p)hjlvzXKb0lXfL;Ujdi9E2AWMq)U-Enboyf?o?|2BmoiVw=npFDXYA|{4buM^PC2`h{! zB<{-^q-;H8%R}EA zj_F+_>8)A@(e;q7183lC5OVwDVZw-qyDA2}51@s3L`3-5(hrCzsOGOL{|3Jiqg?uX zNl6Te`;bT-PR=StG6j}BQ1a1IFKWGiUqF@^s5NI9v|+y@ncBwzEZ#^n^cX+tLdp$K zA9~hTI@z^pd1D+m|N6BMz{KAdXq@e4n+pxKBbEoNa$4fY+lP>ZeAWThARix^r`YD$ zHJ?i%+v)-sRgm4?#9;6v7%Z`TOXals<5e{0ji7kA{r#P>jPKC@hWS}=7y^>QN`V#f z`_jZzefA#sOoYG~(n-h!6GxHjxia@3qp|!ahV%VDG~)T3#P||RFLYnSGPH9DMNr@n z0ti!dzv20N*b@myh*u@OW=;}dIX(HkkwcOzpA{aYGyFFeFbYQHzT}s|%7v@V{Jsdl zEAd74Rgpt$H;I>t?oc+dL_i;e2W?C71$uaN&f_X{9CWEmSHtJhfT5})5cS{_gn1*$ zoiebC-o1a{U9X`SC{nNmJ+r0RCrk)tR-N=4Z3XEn12oybO*I4R?w0_SJ&Xx)(3A~b z@KfdZjDXYh&0w4@%eYH!f0s$}{()LkIy&#WcoNEO&>3Skn5hRv2rh8itM1!5CwSjI zI04PI;q&@}gct^r6~l9=`$4881XvViVs zzvW1#c({d4_2NMlu4P!RlB@mbDuq8Gtb5=Or*}+T#l3Pj8s&x`vZk~Pa^XW3F-iGR zAmx<#-e1oWXfg^6sRnqKscP&}k4P8f@ZbxMPfBW#cL`&iNYU*4{66HfK~T2N=C~5F z)Q}It2=3wI^H?u#rM-W^@BDrso#XRdN4$VjN7Oy>pN7(o~B1FUZ%9w(Aeu5Aefcn5tu>e`=B|C=^5gPDe`% zA&czt)_dR43Wxe4(`cC$C8z_9oFk0pb6SLr#b_WMysMg*7dgQXjTVH+NPU!ZEUd!iBY>)%}JwM0Hj;9lq8o*}A>bJ>P=sh1fxNLw;g^Mx_04Ibyzl zFX#v#&(;yDyWxBC+|&%D&Dgn~Ro<)^ZZHSgLH7lP<1b(Y+bj9WnSWB{l_NQuH@HEr zd0)Q3QyYO{sK;40PZ#pprCRvSD|S9bN1xhFxd1iT^fVSQ3W}|Yy~#jAfmtdh(BTio ze*vRlfMQqL2n=62tag?O$);hKqy@KpUIY>yRMw8$_UBPnOM5zZTO7NqkfoGzN%Z?VjD7 zJ9X^`sk(3iB*QnYZo9qcc+cS0Ib}+$?!u$lIV6qG5RpOEo+o_lyV`FwVARZ?My}`n ze1E*RC6zcOHeRF3*B4C?g{g8M{dM=Q`rh*LGPJH;fG?}HxYDV5etjLHNUDt*S%;>+ zTmF)YSQnU)xTob*QL*1es-TX#1>l5zrgb~uC=9{AHoirIf%F>5&qPLK#VS75jE$WR zW)V?PvJ^Z5PM=e=Mg?rB^G$Gm!@kSL!Xo3kzt_fNG5tB-Esok;Q95GH{Pk~iQ^JOh zj*hA7XZA<2VBn_bQ&UrOakhd$PnF3ygDb5z+*b3?=+aDxAu$N}4xk?H#=iDpN1<89 zka$gi=0{5e$SNrW1Vbr(kmqBMT~t6CTNzDwvv-s`0>6ZXy@%Qlczt!55;1^b7~%Y{ zp*#<+>q)(!XlX@`HUUgxlt?uA|IVNHPAu{*yM9JIhD*H>0vW`yl|BL=l6PY)7!+H1 z8JKpd@6tJUEMGQA!Qc4Gi=5E@o~NRcKqVrM6&I&YxD$oOx>TE_lA6fnz_Zjbrz%U3 z&W%5He&p!vOGc50`QpV3!ux3OGJpf^H}?gGX(TdT$V3TrVa}*e0h|VG9{y(px9S?$ zt&u;Hn!q?Be{=3EJpc8tjVOK&?fZl2(6O^~c@l{*50`X5_d|}Ey8GqY zEsWtUEBi>K+Tj3Ux|Zd+VlXE}K?uk%wQdp;tt}UFKU82ep9l@Eef`NMc3dOKv6(&C z*TnO6DkwGTrrsqRQLvyMEU;DX6Jk z|9cR9b}P=(kVrZP`P9XWe336Oy@Owhm>LbOPaprB|ArV7|ePmelU8*aplv; z4)FQ;=6!wL6pE|l_0PBFAFt9n>c4Ity`dQy#;}Tl=DQk}QgDA zICpSK1xPyj`Z7Qe0CNp=Ga;j_C@6tehY1gZ81?r0TSv%sDR{ra3Zo5=B zP618;nCwGX$Kmx64kTpbr=we4%c1dGl3ePXfHDro@{l%=Fth-UBt8!{*DbYzmmf$) zVj*sY_tB#tKv_dI$NXn7)T&e*X~3tKGexI~5%ekH6A-(V_T?1HxRR2R-mqf!K<`GN zJ=B~CKI$)c7XS~e&FoB1AOOZGa4NWR0(I8w@LI9lvb`7*+ zGcsl~GIq`7$4SNCKHuxocph}WNT*4j%I2|hflwDr5zFt(kO=j7DH|BJAj!6&X|&J- zKu!pjUx0)`$YvGRrBi#F2L085v^K-52WvV6gjq#Fo+23d-gPIiRNaFK%THBJ&9YHF zPj?1%Ct$<^4Ftw~dxB2(-`4%&05P9imf`$;cRjqWCBf!%j zU?dhy-U6Io+K&hu+qdvzf|sT>fXZMdQG=FbL8K`LUt@i1JyR+WB2oTFT(}f&NEox~ zpj!zLZ21xknV6cj{+lpiEtFU$;o{=LkOi}?;l&v@psp?P)z7A?&O>-bL`A_u12#T2 zoyNfDUwsL<9QR9h=9!tI~9<;@qo94iE3Wtmw&jW zndnRW&WhUF15bn(x`A%YkC4&I-=opK?r>RKSzD7bPn#ON&8co<1jRStl!3~P9LDnK z^#_q~t{HY@z~sRX7!YM)eX(K5t=ncF)?X7O;#{bM)#DMV1O^djrs!$_IT5>|=LK*G z5E+56UOamY!|0m5UlL^r=j);hf&jC!Kc!BHvhks@{;!)Y~4+(c~pN1&Ftfiv<5WE7bY4^gaHJKo%kR$vG>D>qrf5qvG5+yZ?3g|8Ba zQGXpXn7iGMm>tiKdSJ?wl8^`t3>5KD2fhZ}&6IiMTwKM_SR&MX33un`s>Cjc$S=29 z8-W%l?mt>l(YOtFGSn;ekq}+n5tu@+k-rCbGiWpGx-oo}wfZl>WOysc$yW|J%uP6L zRR-_bf@rNLssOzI)*j>w^$tV^7jILAAQH-boRVVp0wiTf-1T@j8G!qn5^TrJ`9P*n z*7AU}7CKP78%8sg=&x%OEZ#u*T2l-t@@-5^9Pj@7Gxrw!!Pmf^oA}QWHVJASo?(Z? z!l0gOfhYDh&dQkZb?s5P>DVHu!jf!pxk);!L7*U#p_cl3qAi@BiB$3`pt}@AR#sMy zTa{SADzHF#_I08WT$^I=HgR{j{72Jd-bNlF-kHH1>kA3O9D;8r!5kly>OFh)DnZm0 z_z0+i8#3$;BUIMCdaD6pYxMm5S5ob$>|rE~s)m}vsSd*aASUJktl^|i3$wFUm#$Zzo*cSu0L29a5@ggU!1+gG9t|ldT=!D| z-2MgrF^Ka!&4xH|V z*U{2V=K$T9t>#4bL0dSXWfKmb-G}IgO-No0unn-mnOeE0X#?pn4jJ6t5I1jDPzbqJ z&#^-$v_5?^!*1_3@{zsn7BYwU^?_2(oxs{p{NGU&^Q(>{no^ELgf6DaR^F>Eyte));m|EBWQ; z?=&mlPVHIY>~XFHN^Bdc<#UV`ecB8@FAEf`j*uo>zcC5Gn9 zDp|L8x${!$^=p)nxI^A4r^aPVf%E-yz%@KiL_Wd1NyuwoqFU40={i~AB`B!CWuh3Y zF9Q_re6zlo*weh{=O=L=KVET%hFZtCytu2>pR+@Dwy{_oc4ZydPrWhMx_yOR!XSaf z<9KT_C~S;KC?l@5oPh3EO-77|V)KXRx|w%5Ig1jXl$6Rrv^w@2s>7?lu>d`S!RWWC zouA)5W@P*wDusveSeol|srf+OYy~4AH8r&&Pn)$O`Ao&hYM8I@HUK_~X;N=kbs9|^ zY8;UZ$!u@32I8iC&De2icprDQPF<*4QQCX5&Y9?1&X2$4dUk>k9zH!y?(xQ2S}W`8 z@VO7jx-KZ<{b;CVD&R)r2WSKkd7=G|h}#N7j5$@x?UoDg_<5Jxr05o!7e?0i%QFLN z`MC&(b4O;1aR5_Tj7F+26U-(YTdi{;gGXy5FP^z|Bw{fb%7arZbm2?;An$v}PT#MrDX zHSO8C>IY{k2|pfo@$V{nhwu2N1?n`iNyy6&mb`lTav;v{gAaKdXuwr*>iYX7QGlME z!wErj@0B&Wa0K9b_jO0Lao~z(GhdNPd+pV3FD4>_La9&|8YYoEP9yNG35Az}f{w8r z;(d@mjUI%-e@=zX_V8=|{2|0r&uNr?bc-+4cX^~YrZ_fDlK})o*!$JlEOUhElx450 z9T?X9x)72gvb}M$`2?2}ETh`SXPc+0krd(+pB;D1R(jrttRF zRsi$yYGO+=ffpG?FP20ytI8|vd-q=w;jBEqmTpY#j)9v#z4?)vFa2g1|JnPTsQf}H z`6Md|L6PsIchS8fqsZtUAfLAZiY)SZ-}C0a7yQX&cCw(6+uc_@J|g{QqLAi0N{Vv> zY?~m$F6owYMXMG0aIeq4`?uLh?%jd^5aIRn9OH2>cz4kMe0Sc^mRKbUX~948NVtT5 z^zUyaNw5NMcdm^!y~dG?bu@=@PtMdc?v3f`;pCS?zc+aN^1iF{b;)N{WM*d0t4SjB z_blg9sHw3osCUB4$`zVe!Bq~ppzNxNiHQsXz4WJ-*RK*wTyOy}YysDu217<#pu2zL zuV00t0rE#ZaegPmNzcvicBtNhRpEvWWbeMhmKkZ}SC=6V4J5t1${Ib+!o|)$-$eQ? zB{lWl8AE1NF2y9MkW=8oGdDAHJ$LMedNri}w8S#hbgW3%z~JVbm&V>Nd>SnZ)?FY| z>r!{XQ&^lZWGFCTqCZWH}N5|g$dR)UH5Y;mCt{{|ldzTco7 zdH&k?;^t_=mSn6k0u197xITBwFCLx_BaS__bA`BI=d z^zwA77t%L@G&LSCSt32neE6~oR2zq35_$laGXDn5r7`(sb))ljUeb6HU*|geA8&U= zHn{m5)6j=6FAgVW>Z|bZz5+^%pI+d64yv8e0HoeLniz1nG--r3n5Hvif6y%@xs3kQ z(jkrCB;Z_JIra7RM-%=EFU`GTw~ON@^Xl`kn3(;uJ*Lr-5&W}E&{<#!oN4YYVnzVp z`9e&ru|4v#)N0|*=I_-3{9KXl(0baXNH*7%m6cJp9lZsNb@lWR;RfbgWJ|#$ z=P`?UYQvvvgbR(GrlX(J{CpMtJL<%@Joc#rU_2ok{1lYPmti1d$xm)0U9OFV7~A3@ z3kyzUM{TX;$$liL?Rl*i@47do$>^}4-6tTJUKoJd?T@5x6$$23A2&BQ)4TwZiIPSY z{r+uwIsp{GXmSQy9x!S^c&-LP(#1LI$nfxF2@cdpzT&3g=f^9CO*JCeFD&ft_~vl# zz2exXHa)HsRS!45DVObMK)&k`GA?1 z-17#L|@1!~Ln%&lq-_Z7g|u;?ctV6kGSaqRLe|91H8 z47<#)OI?>}Yx6sE8a1tiKeo;f@flQ~NOgxkvFjui0JguVpdic|donZ2^z(W5{cykA z!4U5OYmj4H9A<8^Om$VNbhvElQe1S}`5TavsO8PSWdDQ1f#?Qs8D8$bfjFo3qBiG_ z*y%&ingV>Mui?^KIZXY;02M3VC^q>q>O?~SHq)rGLN-J8eLQ@FdwY8!0a5YCoA8p7 zC)|yGGhIMff^MrpnaGaC&~8n>gjvk67MOIxsXMNA9g+>D?d=#C&t^|+nOrxNHC?5; zT3SMS03$pQa(Q+&%60fliT%#m`beMKndLj(d1BbVMX`xp@ads2I4TsTYw` z_||wSF+RRZRum5%apN51@xCoKbCG2?Sh9je*e^I3zJQ{+fPz$6%%~a}8R_d&Q&6PO z0DQa?We+-w)a&;md4=wHAmDDlBNQdw#J#@^jZ(hsn_F9{mCWgnFCNJd;s6vR*CfVi zJa|<&5oB%=T;_wYVcftWe57BI2yJ+5tr5{G$03wWjEdVVjgN42JRKE;1MRfy>XXy~ zvNa-4f#DD5?)z7Js&`*UDq^pK0nML3Ze4I=I2W_8j)1%wF$oJN6hF-}q7a94gTq|k z%*=A7*DEcp8BRRJUgT6C8)z_ez&5GY@`m8>3V(+Di#T~Y?DVUPI~AMc4)nPMDYNxzDyxIcR~1$5OKPSqvHt# z8G~jV0t#$1lPM|c7A6U0MMg$Ogj}|-Hi|9aI2xwGenjLzG{LKD-Y)o1bV@EQiQ^*q z4lZu!mpcnpQNy5v*w0kjgLP}DB>0SmZZ2ZSA3b>uUQ;0Jv$m>EQ9Qo+GQLfz2s&0A z7#$u?Q1cmswG|Ertcei~wVzLWaKol3_3D+d6hxN}F5gGE zc;=RZG*An|1fzkiKioI@aQ1;zF!^eh41!;O7m6f8Q2T#1G$gpu^*5~)zUxy@`$X{r z`jAayrGQQP1&2wF29{Wp&1{2$JvL8B?(B}e=s@))S*8UR@h&EMnScCBpMiYVDx>Ua zcf6F; z#v24(%|E5xa`T3kmY3PJ>RukZn8Pv0ZanxP81lT$!Q~g#XN!x|)As|SEXr+(yKRB) z+B=vaeei%?r*R$lf_UyZA6A9@l+|H47%y^*it2tU%gftpeR7wleUfs;3)&%-++39a(2TwJ0Yhj1!_AHc-|oDzzyS;qtVTqHw{rCD|>0HdPsW$ z6h+wWVG8_DoPG!L9_*A_Ux+*IlD9J)-C$$wmBCyaI>f-hz}~0&xsbr^dIgu@tyrWm zliBY4kKGDAz(m?QI~!?JdE+$VzMXDPG|$gd9Wy8~lH$Gkm@8YLyYQev2xeEEUx5m; z^zvu?daeCXZ#MTl?KKYJVV~4gn-6orNRg?N(wRj72Z#3{x+@3V#P-DI=A_ov$Z#CA zIXIN999Z9!fSVAow~%G*4{$Cmw0qr}>e?!83eFIkZjZ!b{J~|=8ECbjx;@^|09%5H zsK>@d^@TFPN@+vtPH)rdYoIo-rxq70lthe~<TFt(QHf0+}&+nObq(A5WStB60QC}b_NvU;j1`0HrB?rhDO2q z2EBAnwFhJXHO=v~84QoJ;M{qCxT)69E2Eo|qR-7e5+%bGOQF$UIrZ{DAn171QA9TZ zebQ@Y1xaj!`2CA)<4Y9~xIq{@1?ARV)nN3S$s4&e$H>W27_np%;aw~V) zKAj%k#Kpu!3eg?p5J|`JUuWijSqtdCve`;2)%1#qpa@>B-DitKj1pz*ELNdic-#q@ zY_V~)OFa7t6S`pcWm)4v3gV<+fMy8o>ZXfL$Yp}aca6M(>oVlXzi`3k`{!RMv)9-k zkIAP|mAl-|=c@aDc{~3l3p#RihMDQ>zmCrKnC~H1U%UbZpnpPK^}nJ2_nM7=c8XB2 zlKj^v@gE0&jEKm$*TzuHqZHQNS2ELt2?Yi8n^3`%b)Fzgmk*B>kqeJ?k`>mCK!Y^t z8?nxVw-ELg#X|V`=&^4<1?oMfqToYF)3&!mrBrzMLtpfd$w9vRkyi#da0R_@*2hHA z9eu8=o8y2dBqjO(Ja_ng^R5h~niWv6utKQMOGgDe0xQ~y#!yL4%FLU)j?_3k`QN22 z21L9XkT3T$&g2Y3xFJV1@ibY zSf=65y~?T%hZN+3L$UYQ^>tRq^c@gAzKY|OBWdx^u^Y_1)YM+>C!`gy`Uqorwfzn+ zp8erOsjaPTL|=x&6cia_Xk?O6uo`K&9H>o3vgu1cc5GShO7c3c%WohQmV02>k#&RHs!gw&O~dUN$EXBp`q&sH}_@`Garbt|;$#Xuz4y3*et zhQ)6$^;$(q=~d)zQDo!@h)ZoZMj>gHmcXpS27EW_wX|#C?>JFHE z$v{p{l-=to&F6BZpfH2iB$2r*R=~|nS=s2yEwbt3qkRz38bo&3Q;G!&x(Y|`?D4Fu zj9^>CK@j-C2d4@8++M|<4Su*H{CNj_hCv_ffK)c|g0i@DUPCArcq{i7XM};rH&o5BCTY(Jhg7^mJs%blCe!z!>O@hMzw3^MMRIz`n$+ zy4MHOAif4Vt{@6{80ME~p2LYcUVJG~Slh$Bb@gkl_=CZ*cKc+MF#&_lBS3~5dVAkE z@%Ns>IL3^qe8lRwZ;fUjRc?5goN555`@Ktb+E@@pzm$+@a3&&BsUW*L1Jo(pYu9mB zxRN^TAD@9puIJwUk7`4G`a+TZ-O9&OGa&!WRQ8xJEMeE1TG z3(O`)x3`Ie3WxmLmV4%y**H0EL8i3Sd3;N5KFwVfpi)LjR1|eWerH)RF>mR35gSl1 z9;`W`Bc72dq}v#R()f$J?#s4nSM>Y z>zn)~e*_PStbYeQ{py#eTh;9ykEJb{wSArSv=g6zg&Q!p`2bBSobaB}QIvXhdWDO) z$Vk6e4(%GnWn}KPwMQ)e1;0Lj=%lXJ6-?7#n!~F_7#FbuU zs7csT=+1Kgi}0lOjt-n-P(6D+RRGw55KF5f-AL|`Ybr)xPcP`k=X0a}G#gN~Gq73C zwgG?E*Z{N9G6TYw(`>xIuI{R+jD9uP==0_1bHGA6t0p}Y=XN2A;Fm_oXFY{SRc$fj ztgQ`fK=`(52khCr_(&V|p5jI#DXmj5for)9y=oX=QIYNN-DfyL#iMMdxe0~gvXDjYe;$jK2ezh$4FU^ZL@6*ln` zi{?tFo>ZxL*arw#iSuJQbawNm*ZC2g}Op z^tA6EAGjyV56i&(0dpdA5UH#%ceUH4Ag!*Tpdgx$A_-FfBdM8;sZ>1w!{tb!d3c59 z`hT$fd_mc-(>K@nN@Q~H@UTxELxn8DPrJr|%$1KPs*nW}78!#Lvb2_!wn=TfOq6 z<$1oWERPn&(fP$`7cO;0;FbozX9rr~3mS0v>glyU3Cg*f!)83l>pTGK2$X;{F4TN< zNi!tS<+Yk8Lm|zzi33?inKGF3v|mP;A1M}GWRC;sXke~+bn$!!Zd6P_|2jjiH=w1Y|!#I?U38g-{vVtOxIqk6ud)r!5EIN$}Ki(9= zTuv*irG@pWyWHj_hpv}jhQc*3M>~97HbnNDsHIsYp~}tE-8QLQ%>kLKoE##vc=Sr~%015FlZXZ^Y3 zT~ElXoWmmV)$K>x+aKJ5b{H*J@n)NbcP1&I+gv(~f+f2q;(avdhVei;pXDt5R^{O* zUh!bx<|mr<#FP>0>ZVt(% zhmg=)6K?IltCmS>)H1n~Bu_BW-`+I*JqEzGt7f^3p)aQ40vcK``ZO659)9qdydL^6 zgKSCV9gdD?10F8pPgD|l-Dv&B#Pm%>Qy38r5~Amo1_9hruYGC7nMMsR6=+2w?-4qn zPLYVA>qX1PVPoVz2+0A+6kxuOAJge<76VC9vOuoDX|swk{&-sY0&tAPxQ&T&T-?si z1!m75r6*T$LM*rUcs{x+r@mIhUOGE_ZL|pFtY92wvMM(|x3722R|ta#MZ~+WFQlXn zca2mE(|ME~((GWjYWf0kXJGMRl^_@6mHEz$0w|0(#%GO!J`3xN?d>eN8#2~W^j?W3 z#6-I$2>wLxR{Doz*>mQ9O!U79O-cBoJue&Ag)wppp#}I%aZT(+zTG{Q?^h^Be|n%` zww$^jt<~YRfo0|MVK=?lkwGJ6ueBcIoSuP!*7tmu)7P^-W<;w~aNk0w5ahXtkBfS? zwYLl9iUgZ&jLLz34$vgf_;)u3xh0iCk$VPMt-w-(njsU$$9s!177R#=qS~2vyFiI5 zB+~Gz;1vCW*}U=~9vmBvPImPYZ?g?{D1>a#^IU=P4$n%@Tg<7Jue>;kCJ=3;{)H?v zee50ycxuxSW4}*qCIrExJ+-erYLtGbs>;A(4VDzR3U=@5UX!0hKftyDe0AXguh*4e z-@q;~IY~?WB+q74kBmB|6-$%f+#F!BwFR>eV)7f9WjMjE(Wv~sI{^XX6R_5H%+4dF z`Nbeh$=TwEX@}SUo<6WtDLn!8PYII&Dp7~)3Fh43TUED7Mn4prKi3p5cuTXT%Ym5W zx%Md0)kTV1{Q1Kybv`Kv65<*WA)zyvApzZ%0WB)xY3gf+Mm{}8oNL(+qq1WCVk0dZ`uuB{HvFm}g2#ykN z&fl)i&K3GCFrpXd<>dh?%Eq$I(6w&^Vx0~Av(*f#+(>P6^Wcw?r96zVGKPx2a`FQ4 zw1-?ww2Ff#-^N1^kDM0V4~Rys*hl^4IXv9lSOm-jIss{R1Pvn8aF%NVA<*nfAYIQI zvDd~Vpp6-*=!?S+sQB9y$zRdh_g#2PbM#%Ip9cktBzw|}f1!M<>MI*yDiiqz@vzhH zZ4}eoqH_Su+-E#jehhj4#l(h$koUzF!A8_-U4T9iS!rM-;EBg#1~SAFXn=*|eqNZ$ z77k*mj~yB^55~vosHmvWgg=n^e4T*NjJzpF}+{A}v7D!$NQ75WDV=pH!>>Itkq^?dmDC7i3g|*KQb6w*ZceL@4fOupK=p+do6)26cf`XPvK6HM(v-%UhB)Zw5}pgy&p`^{DFFN7egeIw(zlgAo( zc7IM_B2f4EqqwFQxNwtM6l}siqx=f}e40D|MxP03Y>{g1%b)rFBDCk5a4O^0v}THO zcI5Jv#m8C%rtMo6i*Mb^n8({$WVYL)jBD!;V>?3a{J79In7 z10^LTh?q%CM1F)0R_~FM4;5lPgToH;*n{Thd+AL9Iss_{M1T|{0eoXPT+~Xf9&NQl z364w|AQ&?NPr3z zX&~9i-jDx*!uyYi1(K-zH>%Q19 zJe%*u7->izmrk}8hvQNP&8uR~Y)-8sr7cAzN$HlKxD@SObU?+%VSxIJk95(0X44_^ zS*wmYH3eOwWUTJk2A*zaLB8fM5!2~@=w1MhV7OlZ0se0+toxmW`+s8r zz{pBS1EB*Xh8uiXZ`}%j`3C~GfG8Yuz|VjxqGB^XP=tiW#W9Ne0$LB;km077i05;$ zNkOR!Wsbw$1$ru~1`Y5>?*Sgh%eyL{ArEHOoim5af&zBH_8zwM_h-IMWmL#PNr-_h zBN1d^F#ipf>H>h0y9%8Gfn z&f5i4R0fxr$e*;CQ>G-@Yw;VJ5T}anjqx^x02cDzSQJWkv`WM&NUj?g%b(Z47%HJ z?;iZ#OyOICVF>NkEn?W!!J8klRCaAkDq!@>p$y5>T0p2UeLLu zjlB;|EImDTH-5kr(cap6AJ^sdHmySE!pVWAB;_Ad6%NfUAl zjy6^Z1IDzbOxd8u1LRIA$;s%w*MOD;`c6g)V(n;`VARY}JuZ(S;rZ=gQt#|U<F@`5Xfe7=H1h7<_n5_ti+F~8diwex`5>`lF$q-J z2=rlkAYdg_S;1}A6zq0Is-Xc;8H=&%%V}qa1Y+3QzN+Z%e(2PRuVfQR;k+HARPQN8kk9GKYpY?yJ#Y<9t4J;XD&DW4k4|6 zh4$R^G?cfPasxX@go~Tx(A?hM-_rw(`7x9f8FGO|q!m7=1pLnZKu4I5qh-h>;gJ+V zqa6oFy~yEa<<)D~+}*SN^Ri6GeiI1&J`{+ZBFpvEQ+HgElUyIkf7uBKwkm?Tmf8!3 z9pJK$>;$tL&%jbJHvr*g;IB!ukRF0Z8u{uJ;jh>wpZ+B-&RdRNLS=99F)Ru)@Wp^> zXu1dNDlPTja{(-}`SRrpsNv!xBFMz!?VaqJHh{FarRXge-i*yMcP|>e)l~}LCa z%vs$oqQX806)rTqcp2GILXiO@yHUR`P(}paB7R_8mwf_*!+>P89Q#$+jn^fkHKDmeoc^+3GV&PqHnh-#`E;5CFA+opcJ_> zw&GU8P}7H0ALg(Acl-o{*B<<{}0fc$d1s)yQZ_MrN0`N+j83d z*M{8;T47&q0?V%faGLT1I0jC3Nhe26D+`P9e1p_Zd|kPRy>4)xro2gR^)!EE76 zKI3_Pz>=7jd3V_&klk31cYxe^_Zj~fK4aOnu?)Uy3TVm^pR$CPK+R1W^bEMpNPC&_tSN33j;7&d%K2TtdQ%ZeAQ2E%00^ zv&8~>(dGZcg^pq0ni@b-#O8GEIYC6Uz-1CZ&F7$`yV*-*&=uDR92883%{Kg!{Y@1H z&FwZbO7e2R26gjeS;=UIiT{>MdoQ^I&B7fTLQ#PJ^S!N+X_dn4@LXP|mHDDLDnW?U z9-;H)7_a|djo80m-TxF&`S_95f4|!*)Ja9*SQI9a9hD}n2tEn2zm#EIU!3iWFH1c%&Jpz#*LYWO#Hm zyg`qGR^X9RkJgJ*(+>J4%Kg}@TCbTt6!$Ji{)1GiSu9g<>FJQOE=~@@biJ>rd)xDzOfeUpULopnh+id%)UMT*K~ z+PWkWk?;Sis*_Op=L{$K9ciqCY{ZSgz*7zYMpmvL@Uo+O}U<;pb_^YNoYKs6o^69Y_Z`tCf z`Uv*oYs5e$cQX$T&P*(i6MR7>@`pjj{^re_>oPCk5}~uU9*|=_JD2kD@&Y?EXvR4E zQYzW37Yo|kB?Waq06^~b!N&wTl6C=_wzj?%8}N{t8qzgcn3)p*5R%OUtr5i>sNL{X zJ~lgXa&jWM>8{a2H*GB!#x7XV!Q^@|sHB2r$2Ira(o!4W<;&qyaT z6g04c9a&foA>*Or9{txFsHj>O`z!hG9yz9?`@yOt9EH|P*&yiv^!+yREJAH{`}ceE^w!u6&Vb8XQ^oLF0-BLl6nMHKYkXY-IYvWR#82-C3&71*R{*&fnMzwcJUl+$-WMlp!B)?3Z$Zc} z&e#tE2n*8J1C?)RXkWp*EeN{)E=SLliT#sR@J4&*q903(Jry-|v!?e2rx_&g0gk#Q zfabObfM(*^J3KJ~q}=$*6@{N4eaZpF+8VR_@H=B;s@h;Edjo_$aRCAJ7c{?iU0rD) zQ<>g34m~GWp&h2$ncqN54h{|y4CNNvecpr7Sus~zkK5G9Xt2=u5Nh1Cu6p6FY&Hfl zvkT7`aTI--7;p9QnYt^|B~)saUOfXgszTE-t=6z&(8ej$)a6wErcRfYm;d@8PUJ=X z2H>Elhopfw&on-a0efIsZZ@$B_n6L{6n4m@G@Z4l(OTbim5h>&{uY(aCR(be7|Rn# z-k{vDx()N~GyMQxv?gJotUmx!)920yX$;8AW4_S?_M)YUO97mq4{Kq2CWSZ4Yjpb23ng^PkFZ5YZn*6tzZUM4C8% z2>Y@LfjD1DC1q6J*T8T{_%#Hn0LQA_38Wy5UxG6fFn!8t*qWf3*1kPrX}da{%uyMJ zv1`|u4fPU?^mTO^pg?lkOJ;Tc${Ktp;ag3xTf6$lSS`%*qFKLI$;hTz7ts<8AZU+c z-~QZDT2kC}xG`?#p0%oPdxAPh@{RA}alM9|6zr-OTSaX43_o{6`2v!{hG^SE zv%4tLjlph^C-@jCwT2LQf2~ds3D~aA!_oTkOn^c7K8nu#)#0)MhCnhOnV7IJ-2=}c z-kUU|Cm@8YFRK9-{Hsc$!Bv-1&|}-I%+Afr-gZkPu>8CS@!3!{T7T;@U$e(}geI$FzYzGdmMzG8G_4YD?5l;eh0; zS2VAkZxA1V%Asnzj;iBQUPS66Va^lNk-S`&(@#Kz3e5@Gt~xuH>CC6R>s#FA)xVF# z@nQQcFdZWqtFn^fG&xAj8p@ff+27w!NXT$sCKNOgZm%ab1=d4bd$SFf8O`QW$%$n@ z+jvk`4ODJim^)3umY8>o0E=n8fy_!-4WpYwR72MhJ~!?qPs|fbN$e1HA$m{+_C$2Y z?7t*=>uKaACNen}@EuaZog4~9&(Se42|0`&4Bmr9PElnHjY5Y8ODp{O?29ynRiaWp zUb8(pc`Yr{6)PxrZX*`@gpek|Yk_Ry4tqr99gCIEr5}O z;-{y(kKx7s`@4Av)%UO&-MMq;Iv={}Q+oQf1`*<^z`{Zfly?P8Nw7rC!zSx;G=mBV z66t9x`Qzw&+i6S5NN|*^w=Y0oGD#F>tcMbBl;};twF;AAwe90=U?dE?zLh85bxwHl zwH8klr|XWTjl-MTTkHwstgxIl{TjGLeAO%JS_(B5Y z0SEsr(3$DB+n>18fZsXMfEG!Ubg_Zz?5R2_OuX@bO}q?$O}qwaXQ6L*U1%R79~$Pv zt~7BkYtH8NAEXaKSO7SB0lcz?zr;IOSvCebH98X`o6cpLdBot9&{mnbf`S6c7mr5R zp5+yUS5e}>s^5w#eL_cHHc#8%G#v};iM~h2?!@5V>xpp!F7Pt{z^o(w#H^9lq3_rJ zqYe%ENB@Jb=wEh5gJBe#X2Oloo^i^Th&369mEr2!%BBME9{Ku8{2%7tIw;G3ZTD6X zQ9wdk6cnUeN?J;!yAcE=1Qetj>6C71q@@%@q`N^RMY_8?_j&7YJ!|dRGkc!dduG2c ze+m-!eO=${jN|wmY7)BiF~Nh_YOy7SOpNBQcVajPI4+~2I9T6m&Ca0UC@lZeGZ}?; zCXUcozeAC~r{(anZ0p&MP49(%();`5bYf^e4IpkWhrNPlj^*FxilbIsVOVo?WGdSu zdN4b@**fzDdAcxsFC+3cmNYLl%esEfG-$dxeV)hp=Mcps3^_z=D~eC*`VTzKDlTM0 zJpFS9;N?cxn=)JjGq=z~|8!ExDE~B09ET2{Nw*`Jz>lO2 z9-#Yl-UMIeEY*e3Nl3k(i1zyanSqP!e{|OvYk>BN|4=kD&=$zjg8!jmEc!T*s|0D$ zJ7A24L1}Y6cx?cp9=CNUSP-vKm?-tds2ikSZzih3Cvn%;)<9-EFxK1v3zN9w&!0_y zeYO=AZU9dU$nUT9DY0^JfDWT`F#qbGW=hltY8Ql@$JyBrymioypfJ2LH62YA%8-nv zYi$}6wC~Zi)|BEahe>W6kW`Xi=5JwFj^M<^H5eQ+YaOs%1n+1~bmkY7 zmF>gIlx2N=eRlcRJ6LE0iou*7lphq?9K=O%=m5mBMYKp~=45{g)C97N)(D}1eiKpm zIBb1Sj+1g!yU*WqWwmpInCkL;^KJ{@Plv@MZa8|w`*U-10iXh0L43)y9v?=Oaw113V2Gh+=6*Y~1RJu4|ci)p7^7#rw8K$HdvFq#u=lxSLF@16d2E20c^4y*UFVAxr)brZzK=Ai5 zIC5FCq3>$*HrasfRmD3sidkR5FQ6KQ$7TG759Oxdhk5O{w-*ePr<>&IGuHd2SKjoc z(mRxFj5||Iptnpid7Zj-3%`EgCbhX|@sL(Xx%*Qt1@TOo{Up_hK$ zpLOqRDMF>cwq_0|KeQ0Pa`_msV%B5O_bvQQ< zPy7e1wn~uJC=9b$hG-rf{AL4-1|Ax2ssDV&68DFMghx|2eM-fM$>_M8lR-4(OSpps z*r(Esg~Vdd#1b~z{sVXDsaSY< z(K|B;cz9z0>j?+hF$u3-mG&i!-al%UC&LvSvHndPrl~H6<4=bCvgYyQ{3WdAH5s!cFfJy*O_z}zWhirswaBWmC!sMk)q7G z4ff#Ttnaq;o^WNjR<(jOl=UJRoEw25HK(wGL01uBMkRTQ=yQI-r9}ji=fw)NmoFNx zT`YK;(hPhyk{PTMvE1UQxbm4iFeR}?moX-m?JX2?fOpt2HZ=5+v^#g&0?RqV(lHor zWKReuFPPo+t~zpYavp$9UA_G-&m6sSD_xGNC@HBetMNbHn@ZS75#;IR`oN5-(wQsI zX+L4Mwf26Fxrp%ywD?U;FM*!9W9fxtBKf<_EYILpo@#;J(Uy?(i-)D4D~bzqhj@U{ z`IQx4I^XEfDtbR=FV8}qN&VCul)=URRo!NE4E#qrK$X|FQV z@yUi6s1z@f85kH`Tn`C%GUsk$v6wO)J_Mcc(Au|%$>R=x|0|8sL>9shjeBrs=(W89 zor6i-K%Pn^2pV;C)>#DE*+rT7-i+e!g}qQx>JPxx71}gh9eU4P#nS%kmxesC?+&nLhQSd{=DMd5%Ovm_ z-S4VQ_lO5ygc5S5j5CsZI+1*6a8*CdfM7{uZq?o(F8VNSi4PVHj$ftadY=-NA=V7$ zwqVxAu(l2g71X(3v^55jO)>W=1`NSKqc?gqrvS9QIRHLn@~c# zSrI}CaaPFv{rwz^;^1y8QqHyNjM0f@N*X@?R9AQ9WySU^H9S7h_^V+9J%t5RSbkaX z>LhU9qblAH4lcJ{%>pYUTtsZVyr;1DO;CD2e}}EXAsfSEA|n9{86@u)>=$n`|DK5T4lYU}ev*F&m68&h%yVmHUU;+{RcXUsauC*uqh+f% z_H)yEllObup4D2-N1TW$$Lc(jkZ2ga+5buZPM3q+CM2idEf1B?OdVyMg?;??Io= ztFuk_P;DBl55v%Gi2ZCdDmQA~fy){Y96x$`Fp*W_2H(GJN)wsz?U}crE=#oh zzh(iXO%+03zNCU~*Ba3Pk+3$H4=Fj=yLl?sgn6EsCmA4VB%;71Qj(LCJKb*0+^NWx zy%$R?he?i&^Xh(fH)xR;MWDkNsQC~Ra{yzI(cipaGM?vIyDO5IKv>V7ALmOxTtgt& z-CZCoEbK`m71qykwl(cX*48I^3qp^&uPeEM6c--(K~~Mv`JE|8YcFMV-olaAixlyZ z<_#t$CcwkfA4b>zR3!pJ2t0Flh^$`Ua2Uy{Gs!Xvfhi%tGFVfO&kv>0b`i}`0QIyZ z?Gw04(4BOKM_Qflp!v)|3XT{n_z2?Zr|J!mljZL%hQY$bA46VPxT@!v=-WWy8+eQz z9vlea&2ei6_)8MAP4vstMZ;r;4s6mu=>=~5xW!$^|*ObL_b#TqrgHL-D`nKBhM(tUpX{P~D7^QGKK z+fUUK%x3Dbt}Yu6uA-NE$>pWAgOw(3`1sXp5QlV+iD9Livj z==e*DZx5B}(?3TvQP4~)$0a1h2#+HaQ|Uhys%#5JP8?va6cYn`Eck5dhX3sruqcUU zW)ZDwmKav;?b&>k1CfpL4tBM?z35HEmhaDh>>d~!6UIZ$RJ8qp1a}_v+3B~vgyWKu zqGMv{B#B_zhZF6oUu4!i)ao@i_tHgxG#TC?N>r8WW!Le=qtHNfMNX$csW2SKHv7O8 zc1R;5Be1qikMN-L()0?XkVN_OEpj4q%$mOzWj*Y@ZeU@Nl9cou&A5z;JpD5=MH1{K zplZ#{rA@~JYZs1!1Wm@5| zQROPh(n4C`A&A|CV(?2@Sy^dm#G;>&*qn;5zyDG|TgH?;3T(3A@c>7#)N4YSB-lOr z=g>$N6hncXmq&)*5V5ebkgI{FqM-gAVv>c`Roc`B8 zvNh4n0BSFAOvA_jiMtI>4fyz+H$BR+V*ftmpk55dh#TlPJxxTFN1@(Se-sG`6@&&x|vi@Y#(25g`33q!`{E&%V#ZLK7amNZvRXT2pOVcM!6ur4udN=8RgFf`jk?0Sh~BLQM5$Z zr1Z-dISGmOt}ei0i#9*d>V~IWf2OYPpKs<6ZStIcA=pn(R=FNRC}Ek0_=R2MHGYVE zDKC$0W%K{pfVi(H)896L33}|Jr1)3=!t-iZ3XwQ>p0F+!o=B;&~|RMDAom9 z>;@t*5;`64ya6u9{^21QWRx2)?$5ez)$=I}algN90zbmQ;EUsK%R;x-L@o6q2Pe)v&9W1-A{kY> z=%wZAtp;=-7ayn_B_ee`0EKqKPg*)~@7A^J*8vIs_U!29BaXD(Zt z@QT?q8h*#@K~e1xa0p)L!=?h(!WC}(O=Dwg>*d+We2&^oUky7E42c{A4OtV$3spo@x*g_Wr`jI02jmV&~<$NO5EyTf#Y z1_Phzz_X;3b>NL1Jp|0c@~4AY5OWK>Lo726A-aL?&m)NnUXPGYSZ?&CI*)ER4={*dk%gf$q2Yk;v*vT&CGb+&Y*-( zngkY6f&#<=#bQ+-PTcKw$cD}Vp4L?I7Azh&s6!{eC`4kV*#Y9Ba~W_rPkkw?kUr1Y zPo9T`g}q}q1aUsIF17_7J=wQ!-}nx!ucM&AGAKq|b?zMF;NT$V2jm3^kIOYSr3xX_ ztK_l2sJq_7eBCxPCkgR@*0X4<7i7itgEbx7%-d4NY*m7@4V2dJCg^ z@eSMQh3*f+_qG;#T;+0{UDZjP9URV%7XzVRO5~ZRmI8%o2D@KwWo0DdB7w6hFULocu<{Xyu|-4>cu=uepYAd! z$HJmK513OR_*rrJ1?v{@Jh8r=3EJ5}XO?bS_e3F^;^gM8fElL42E>SpPQQp!w%Y<3 z$m$seutmhrMRG|w&0l6psz9`IAWGOxRwi(Z+iQe^kH9k{nO$}$QU~QT}p#WlRz--O4juFE9FF zi20?%w{?^-xs)_`k-Jz)6Mdqo<7vBi%-H+$F zq%#nklAMRcPg)wkQMrL7#OHf9;vob2?)3(rFULLv%cEsx;H%Wo*5~ zF;Km;UFMmd^GrA2yQjiiIu($XrsuE$Iu22fuzHvTfN3`utb;FySHYYyd`zrauBu|= z?tV!j9&*E$CmN;5xR-SN`|%CiV(qFUCF#5TPC%z$oN%=#$a@9PvN@^2f>l=Q1zw2S zj?h5w4GanrnJ37Lfb%cKH6ku{kc#=?7wS>(+tzTK@WXNWG@H^_+bVA%|u5zh(i70I*QR#LUz5 zdKWc`ggAV8wfQd3u0`d+%GG_h`}SCYdP%ww@m{MuGvL+dQq#utKECrLM-wt$PGr%W zfss6GwnSvhj*^(@n6QM?@jM|AewrKG=$1dx!h73+F*|ybXtW8Oh|%C4I%`139UC79 zU2M(y!3R=)r}@LEtu22~T{apTb=g!q2=JiAqr#_)ioHokL-S0)dnzh2QfaGX7B&k} zxKMw>!h`#}F^{0vSf@f|C+N}m&&Xls-J;bNn&xP7d4X(&grXvQwkX(NI*GRnSs#L@ zYeDuC@gb}Oiz#HGDXDY!m*&;=6-O9 z88LZ9hi!PV)yR7R8&Syxf{Vi0VIZ9FOP|C>y@v0fg$Ov~@ zu0~cC)h&`d$%r0L&oLTOapUEI#6S%UZC7xDGuWQ8^x8IS*e{sC{4U)Vre60UB-Q3e z+TDI&h;t6Z5jelxaj9jOChjGDAA%MXF7@pM4l}vL#~Rg6j4l?S)>!?S$#&fy@&&%y zZ`MH6uEJ7Ly4|`aqbB>X3+cKXw3K3RkG8_$Ox4iRDmUp9wr0EGpYc23uy{n{P{wk3nL4 zd#uk3!9YB%NyvEu6l(QSlQh1)y8suPCzcl$Y4l!^ei(1%)}Iwhmge&51D6WzQf91< zdhwK3uF~Xam7_s@C!~qE0fYF6_VHAJ-)yRmj!raP?m?;0NksK$WK$Hxx9!(-&nz*^BtUsPqY6vm7dj+p!K+O0*I zEx^E1xlmgoCu11E{0jHwXTj?ch)r|Y(%P2f(lOUwG^&*=_;SL#xqSSqoe|2}vah2)!hOScP_YxRq=Rs>IG#+jifl4HKH|x1`(PpKIPG(j zsE0wfhVKy;sb-CfkNjcl%q*^aul$Wkx6=YBMFYr*xUKd;0>-j(ypkX3eNv?tItBb0!fmv@%1NedR1yn*DJra31t zb`&SXoF0OXj19zL`1rQNzbny&$9>R^nV*Az>n#OH(~xIB0kEPm2o5{Ib_aowFe-&i zH?|5*Ob^%wm0DaW;IiA3k1Prn_y`3zxyXD1-|>5h6d=vtgLH2&{=fsa!|S_T(oQ5r z96+QSQzzigfq^dgtZ3zAj@zkI@XtD)g&yPMv5&nJJ6+1aTI`BH$IIV??cH(igtJAD z6m#wYQ~2WC7(6TtQedC@E94$;x;YA}E^NW(AlwQ8JtRVYycifNcL=wJK2jO1vpPO& zKk!a!y$2T$&g7ep8&{ZPc5 z3GgLIlj^3XK!=yBFUmUSH@UoxCHUolE2ck9WD)*2m^W4MBQ}vcG2Oe zzF056^!i9W$;zxuf$2G`#H!BO`cn)-5m~b~7$!Ajolt=N+tD?kM{8%yz(vL_SR3K@a)*OJm-bEo(W{un z0k<`pN6?L1T3Y&tgxsdg{U}9}ItbO6+=~}enNqQ~O!8lh-hcc!*Se7zFLf0qQwUk^ znqc=Y?c@+Xfh`Go3Q2Cs0KPjAUTj6%^4*AqxX6YL-Z=$+?aQ!{R`^K}F5<)IKegw72Mhn7jK1P- z&>?AQY1tp%Ngq%t(&>khXB0-&xA*V6arvRc{P^^}W>!{~kH!(LP71U(H}c|qeDYEs z^TtBTBrKIPX*`WT`?gXutM#DZ0^|+U58W0eo@9(OT4u2KMdQS3>bJ77v9hLzL3!Yl zNj9C;q1V*1iBYSqJv}vHauyR+DhfkiLRG4r3IO^as5o3^hUI+P;C53^G)>-SV{LXr zXEz`j{A>&BF2999^Gz0u2gJXTPEIU6R_>Omy@d*eNfm@0(1ubA-OA7&V ziNDLYpzPDrl!gr9vc{+LpkD;lt*_rCJFCK26|wKfymR!S{NYrnjst0%$8pER+&owwivYNo4P#?SwH6c1k^=M;6rkTR1+^Wh1_m>OuyhW< zLnh*PkTIiQOi;kdRlaj1(3^4yTp*-m4u>tcPquh$`CyNaqZ*#$!y|io3Vq#JQ-*^R z>@7`xOh?Yz7C-P@_X1f%=lT^F#@D~?>eQZZS+E;*?T%hzCk&St7Xw*~MCaNhb;MaT zeO~EU;d5~pqBE_L-@H@2d~auNxY0s@o;HZyIPhiYT)5lv?bg3LFb(ahul-1KbtnW= zM~VP4`2*eNo3PIHJc2=j( z#pSE;P*7DV{j;l`o#_EqzP|Vqpa-!gWXfr=-i@KFm@6~$pc#WY@QI3We2z*dGP=P@ z8_wj}?ayOGb?7sVko$nO8DURMPoEE}>9g}g(=)9X47UeguMXXco6PR5gOLw}w=ACP zTf35WOTn&u8Xb&)IiD^fdLCtexoc(wD~Ks9*x%v57a|QEF4micAP#b?`z9Q(e%@7K zp;{17odMtv2N#!kR)ZvNe$PGLuP^kxul>3?Q1=LtCBjGmOhUWzH`SjC3RubL^~c~i zG_bKz9xmjci%_3#X>A1x4Y5Q9B(vU-a=)xzfS@99#E5aTv$4gZlHRY?(+l?67%khM zsICN(QHylc3Mu~yyweX@7#Z=G>4$}EXz+nCD+o(%9WuGrRY8}2G|;4$FXYOFM`UbQ%o}Bv&SbE>L=I=AB4etP3*q{j) zIZ3?|AjP^h0;<_oSAB$4((uqCp>YIb0Bqvd>pb{(9yEnQhmVSaLSC90TJCyEh2ct2 zqcfNJTnJ5Fhd`$3BZ#x^<9;=nrj&XzQ63|uKZPYVQDKcPh6qMHJy_q_ANP87jXQ}9 znVZ1ttH0dQinjLH=-2feYgW<9wKax{UAhj+7R&9-2e&g8(xVo4n+0|p=T^asw!bNO zP0AFNFI+CZ|9BsE@=snjM-X9jZ^lA(%jd;cM8)Hv{uaDI`gc~9KHU-#DWVV#CSrY4 z_fC)IMUINYjd`^4XC_1ro%?7+2O{2eIi#vHG`FF~Y@r8GiDZto6zHwGL_M0Gi}F@| zb|H}blo;?Sf}Tqf1c6UkWu>K?#DzC8Go0GIi543foy<&5<~2z(A?``n+bL&rsNjbE z;fj?2PYhU?qAa}IfUw#$SxrJaiP!%6N9wfW4`I7jN~O(9J}Oij8wS z?WM}Axw5lpm6@)tX=83ODLXeN2LxBWPXUl?+SxUj=efyLWqeXM!64s+i%}cz*`meA zT{5JUYr4F*IWBH)ZL`D#p9IQ<`2E;zTB4=eqF2sW6}d3G4cJDE`{}Q!vT>}y=&RLc zb>EM6K7**c>EsKD<7oUrN6~uMyBy5^rcYQBhTZsqGo(H$%L>!@E+th`uHijRMA z>g`iR=&!iiVD#c&vjA09tOqP+ViO_Y?B z`bdLU1)mkhos_S%!DrKXY)02BC}^=xafU9D#olvnU98LzqLp&5Xqg9k?~o{JPX z911iY7Y>_E1w!RHkP8%|`VJ#0)hTKp#U9#=9zTs6W(crAu1y8yDUw&0!;;c8>ODSS z6d0so8h~}Ee&x1-L8z|>dy;_IQ=j)7-7p06~Q?TJQKSF{~zr_c0?e4o=8qx_+}(n;LkW5NvbgGJh9g*PgB_U2{c6 z&x1vrmGkFsoi}xC3e|dwy5`UC8Ad97c?k}xprDO4Nm1oz*W`L8>^AxglBb zXPQVX92@~i%}rzFWo3=dY0tDYHQOKN7TCSUsyGyk5sm!>1r+(DE_Z!>{q!{IS6sP5 zsn|C}r=sl5&(?Q)Yqi^q+iz127^=juKUAg9M?Jlq<3HEDTB}g#enW$KyRz+0=y2;M zjlX}#_28yge)-V#5q`eg)HHUw+iN&#Esxvh713{X*#)>+NA7E|wfwZZTR9|~DH+W{ zt=p%1w0V-Bjt1 zn(v3UqGQ$m0@unX$3fJp{a54P3*yFrActa2@mR5Rj?I7M@Q{ zf5_4woCJk2k`9{K4GJ)6RZ!@bnGGxOabsLDP$y&*Fz4s{+MeanLZ#_{(Z3qPfj>-9BBO_R&iAj+^+_vjqR79iS;Da6`k-=Co^H5)f zn)(qWgP9;hQ(nYk$h5iXe>C}emu2rbUo?fs#UO5o>lecrNWq=$-w$ zF~PxA&!2c=*vG9EfQ zA3kw-VjG!VAzTM*xw|ct+g=_D`<{6mE-$9=_riU#$g1tuB?qXg)FudBzB_F{=7|w+ zH6O^?j;lM-jd|Bvvk%;8n!B?t)$f{o8lomYO!IQPPt*VNyp%litw~rxJDyz(v z|CdN3E>z$&p%p)ln-@^|8=-u=-=~)$%%)@vk7C}9-{rWkB^Ux^2&*gto|=)&aANTl_ROPweXrWW!qvw+SJ_P(TiZH17`pbuw@zounYxM+C;MToiEbU{ z%Wk*U1e?GqWP^9ax={xr3kwTJb;pAjaQSLO$sF52*t1HD(-#TQgTMWHl$6$Vr-!V1 z(gGd!F}`G%jV)uS`~$hYh~dbgAZmcAak(6UMHujjN&2Pv5_%Kgw|u^BNfF%NBs~2n z*Q>Iyb9!$ZRo-fn8)vyz|C8BuN515!0@XOm-A*jW;g2ZUyt!M0NEI}Ytml6DAm^5P za|VA7cdu1F8HFIX>CCI?3ONP7fz9|+G+Pw4%CiZvML|&oG-~JxX=e^xCk9`+`P|Om z2VcNs^@q)vrvAa8?6C4Rf`tNfUH>4`Ez$5*<%#UYLtfAtE4*c$5|~h8Q(_$!pj8b( zP`|_@Nyf^m3JPA!Tc8nt1i@PT_WrhHWJQIK@PlL(Jw=-0W4*}4LWa)+B%R zYVX{nMxc1C%gCaVAAffQHBEwk317Hme-|CX1453w~yo z6V*Szp&=R&z2#B)RK6=+o{{X9J|ZPg6h3~@D{Xs4nU1XGFSUt|1b&nx)PR2Xs!-Ep z(`22gR(`vuq>I)mWq$Vy*r?xjT#3tW)1&|Uf-%6$Ll)?=@v3Z1`6`x2cavyZ^dV`o zjhHK@%wSJK9$$8s!nvBi-{PPP(V|r=&p$sUqaipumK}AJ|JmDwFQ4v@<+=?x_RiH8 zez?@x(#|!VrL~qG80WEVWfD)5-z+}1oRr!3jwlI141U_++E`|5ZoF(`$VRhWNn#)D zZ)oZ$(22$@suzr$IXfNsO3pS?Hf|plek? zR*B)WC)pHI8TzK_1OsyZ7f%4OAa_YadK}tigw<21pDxfF*q}@!{k&R7#gIFV z`yp0E??=_)*ul`eET1;#E}g=1C`8|8nR@16|EO2u(@eK4_t{+q9)$%F!oAdw8raHD zh1Kp{+?vI+5I-`wrJ5qPJM9*?@|^Ygs&hUei%PQ)u1sCO8{^j<|D00RS%nAf&)KbW zjrtMZHe4eqR03IJtv%7S7 zK7}(MUo^wtfj~w0Ni0#Xn`a3GWd&e|>M#o>OU>6uZgm-@nnGM6 z0P(H|UuDzDwX(I{J=lQR`uUMqnf<8Cx6aI&dP6*A@zLB$hlCBqSYiMi^YQU~LAz-nSDhT$*xNB>#~bMD|NJQN1+3t(x&us%(~z`Gw|p~#sE1%w z@2zsN)KZ@AEd%{ZDvb=xufQrF1*RYbDxnEHASGDs%P6<&hQrUiKa1bZY<&k@bBo`K z7I480GCXC09D@L9JPj>onLXWymAQ)BcKl^(LfAW)D7sPc_cAN-vWN2Tnb}QxcuO19H{Y=q(aCp^}!{;+E?ui4jY7B>j|aPU+B89O7e-@`M&W{CIo&> z7_>O+1KI+19Sx!|nuK5)?^jXfkh-W~<0$tn;xhf1ER>q`?$<+0O^#eVo(P?G@mlEU zf!GaC*pvO?$p(8Eo1VimdY5ToZZ7zp{i^CwiaS#Z=Xz34QYHDUqhpy!9k0s>*Zrvu zAP_=La6GJ=zXD*bh=`e$l{`KCFXzo>&Q{2M{u{Z3lai7+UUd7_<&m&u zP!Up6HQd@;UIXP2TM6*YuEYItzo%qhsR0`Yh3Nf(R`03bM{|)rMC#39d9Vo7R|NQ) zOpAz7zZ*zn1ZM(Iex;cXR8!P2X0~NvWfh9z zsmL~jY^t}vb0ZJ21YJl72qNrR@yyZ$FssalHv$3zK&2!r+XIdR;eE=1w@@!Sy?%W@ zW$6Sj+fQr0Mafr3zyk&s%Wx23Z)att-D};NvvN>r;dv!7YUW@{Y>xdRss6-Vdt!4w z2?T)iyW^;YE@wuH>R``bIUUB@uWmK+4N(kugsD}5~jjDKUmjw=#wJ=Z1VXhq~W2* z4*4zR6A4W2JG@#qTc*I+A4<$tBSn}3KrSC&o!+u54ZWTy)(B3nu@%dQF%2V7SmmoS zwt54tBPpnTr|-M17GU~r-ptd{RaG@ojWMN`1_}a#R1lDuh-#4Y331p0LQ+Ij^xMHW zoFj9hV2X&2=Ey_3j!r@TraiuX*vnEY!|A=Ju2QaxgJ^P|6MtOW<~M)i=E1?&z&yeE zU30u(y)JSevb9(~n?VTLW_l;}0Iyx4R{0$gRNyj)odD%7Bp$SRRr<{qOS2rnA*;0q z1p*4);r?p<(H)>6h{b5Z9Pm2|`a}ws#n&^TyuJzy$zGtgsjI8MK+4}nFxz5*Hk*vY zK4eLPXR|-bF?e-)zJvcUzfqwi9xkqzmP-!=3fWo8@9$wx&xIrb)1j|XG>Dm!lDpPk zj}ov;4Z&*WVK3RUkA?;G9~419+;I~~Zc);BVo|q@jj6eDD{hqAXKV%{&D*>3rOCx| zgEZzcwdcV&=(5}!ykkv=5B~5bLc@E&1=e1Rcwj7` z+h!86EjXs}%xu)>3RtUSQG`yd6|k_WIymTZzo0Ekr<4w(?| zpqc|8aMbv7V4>20@!bx_!Gy(gKhh{b8Uw5vNZ6AibqztALj=i9dJxgxdpXuAfnL^3<#163r&CR!K2OwMssB)z#J(55_3n1QHo~OflMg=}6QEIeZ9E8iMcQatP z@OiZHys#HSM%d7lz~Ror%L`&6-0H_RC2-PKew=$)x?p8xugRgt$2@i7aH%6a9shx2 zRgh4h09OjQ8aPG#>6f1!He^ zikpeWW>``Z<>BwB9aZvR&SU}CpY81$r&FGPY0$!y!cs~#cj$2d?U#5|JkGS3DLxe{o{n0yOIoUIOS5F{(G2a}T4!nJGx11uNySexDuIsY9M`vWuwoINx<{#P2~) z(YiXjOgv9<86Jrxl1TTJRs~1)oFdAxC^TC~ezjrgdq$FCD<8N9G6p_Go<}72JK9{u z;s$ugAE=ug84cNC+yXh@XI@lpiL@H5nZyP0+$utcC)H2Bo?K&+EI{66FKULdTTbaK zrsAi{#Uv6nj>f*(cfsUvBKjv5>Y{zT!+9iJDbMqR(<2hvjpWHo?=bi)2U=%y57sfb zw}MU-GFO~#VtGwwpw9B2jDpL7;g1_EcZt%mmui9R)C}X=zdIEsuPXMJn;IOaXEWOGKR;Nx41Ub~<8x_r2?ypjupX#R9m;W#k5z=PHtes4v&Tv@L` z)QZgq>t^X@DE~C}Y;dv3xE1hi-(Se}O5}SgcW-N_bm5}f=(_{<)=1OeK?CB8orJ>vd>Wq+9Wq2)hX`%t ztKK*1ige{^ZWPTHIF+PCmm^!`TG6y`Nt!Bc^ozJ4j3+*t%Ee0lII#a21czxKi}0nM z=BB;hFYSo_&cpL!l#BJ%^7s;c?yG;^DtIM~EA%HQZt-8M8m7Czrz zlf~aQ0thIpr#b2ArGPDoG-1^1l+ZCeIMMTa;?2>%JWL*bqC!Zo^tEM< zByO3({$1Kb--x=_3Pw=NDF09u`FTon^Q|qM()Odddvd}L2+ zhq3%8MEO3nMGs=Ym54Z0yor0KXo9M){C)WlxGB$$XnNx}ZLun46sns88k{+)5NXot_CC#HCM*$&FLs=PQ zc8}QCX7OtgM(VYZc?b*ylK|w9-V*Tw#hr+hWE4O6elHJxGea8hFMneIs6qnBa7A2X zBpcEMjJZLI3t-Ksi#^X-`#baRAx%K&E@YzfZ-8DER|XbDABGCo`?6$sp^^&zVk&Fu zjR?BUFwP6N zeL50|Uu)14O#B6tOn?SmD3<9tZ17zUeu)DyO4wqcK&!X^i{6VDPaq%OD#TCxIU#{SJ+GJUD?FPS--O92Xb+b`gB@g`nMaL4}YNz(S`YsR0T=N z9pF3IFoi`&`@DY#w3F(S6_RhK(9pjPN)PJM$ye1m5?T=vA82X0zU%6GnAT?xjb}?7 zLV&{!MQUC7c{??_lAQY|-{jX_ z`Z#HpR-)+@V7cUSx=sw34ka4t9}5x0SgWr~Yxn4;rgwu$xD>`e@AkjsLq#1#IRWL% z?24uUHFfLgsAdo1Pius#y9?^E)`A|2R(VWA;vCJvdm5O1DE+`_1FIp=B2b(eF?6Hp zRCL{rIn1vzJU`)B$?P%2p1|IMat3yn1Ls{r4oW z>UFvEm5G zN)1llIFllT2G31RIqb}pfZ~>yHx|}+Phu?qJtCZ915vX4?ZA>B0+~)uo(hM!c7y`HS^*fy4q zw1kIitqbog4rqoMpP!UL+K@PztMC4owFbQ|S!-63ZrKkJFiTnwSX!z?Btd%sF7Xcj z`o=g7ampU*b(kgbF)O3u4JI~f6xuDy>VcX^q7f|1g zL-Eu=zwvtY?+-X{Wj9`s7rlgxg_nwo89rZWWzPXz_x9bGL>?_5o?ls!i4l5LAK&F>7{f<6~j|ABe<^^?`D{d3YGu8TU5s5F37^Ba@^Y;7sqeOC2q*+Ol(O*1aD$9TA0HpM zuEqMHd{Lm)N9cRgX9!8%L{8NqjP*ZbV~3w`DNCj?%t-?mLdA7dBIGGx^RNWed({DTNTHHcYQ8J*rp^Cp9MM1giyT;NFz zuJcy3w7j)n9ydYY^Dxp80%I-znHOeeYPVwpQqLY5itEa3+p;ik>gznu;dLM}FcLSq z4u2&g#`Rw~@Tj$$BpRcV8a75lHAhE*1OyzP7<|5aaZ$_+MGrtG$VXHNNcl?#5Br-n zOZ6vf7EyLB1L*B)!Ozrxf8wNVwK8DtDt9cR|Fa{I`5X)U>|Dp=c5t>G9~Aj{kat9=4x|Tvz`o!_FzKs8lO@{=0EtU=O5=z*|I9{|Opsg$ybf zV8ZnUjD_}i2uL2xAFlJRy?g~**UaYsie`Z%Fs&#(o=&jSff|*@6o}G+oZeB2T z1QB(zpgSi_O9B5u4&2SX`B*8A%0;S&O`D@-vm+&qayv-rZ~}wP3cQ2-QYV?3QQ|ux|)Z zh?bovg>GSDhcHlC+All!rdKHzjyXW#hY@1uFM05_~i(dupBc# z|MM?U=zhsI7AZYtd6y^KCc}X>2yVvKY-cE?j;n#9NbIfX^ZPx?@aA|W>_zgSW&@8! zyM-PhA)zwmCs~x-c}9^R*>dhL!BToze;DxSCCqZ-;=b9gwt$!BsC-g+Htw@Gcsy$1 zu!sK&9~;nRBqde5@C4#X>Of(=4GbDOOsLYofWivwu~%?mOy3*FS}7J3GhB$$6Thnq zBV4euG@Zfu4#MyG4;D2~S}lI0>cLo!lZ~zNErmG{s5Hyp(4zxcl?>jERR}p~AOg!v zv9J%64XTaI$w^7?KW1B(jEtOqEBpGjp5_siqLRw6ctiRBpzW=rvfAH$Q3FL0L{Pc} zL_m;6q`RdAL_kU!5u`g7ARQtlUD8N*OLvDL-QAt{dG)vU+53!p?>OU*as9{PlBGD` zIp_CzKIt0|IjY((mFb3y4ENw{HsD3H@&XF(ax`%Syo8+P4)@tf?Au!BAFvsb=DmlI zz?ZhNikXL-6VOR9IgArO-DZT4@b)&rSBU^+5LX+%xQe#Ohd(GKq|dBLyu$D z_pEdXXbYL@ue$7v8~|y3aDI@|YY6+kJLDSwEWNBe4DzT7Vl*J=TZijak?B<1@83)H zpZA~`Pq5N-wlM;*?iBB=o%dSpdU8H5(H9k6Xc-HYSteg?!(fjPP5 zT~mEu-(9Kt-rg_A$7SFxg8mT-|6oGzSK{LQ#DM_;KUtj3PI|%p=p!e_a)gMMVn?<@ z)BXum4{o54;1Hy1PoXa6s+QV=p31ug9mF;6gW2hAq25{zd!4(PdtYeSuF*E zvr))8yOX`V)a`T>#OsW9|31o{GFk-(W|M{@2K76}5KAXO46LqzGiT}x=mG&zJ-odj z2j4(TYZM$RH`E^_rwI(5`U&I1X5}3eBw8mpp;q>3=Nn@Wh)v<_c^%SM52={o$pRZ^ zvMd!8M}9tmv9jSB-fgPBzy^lVwvd*&+1dFxYG8)Ulz52Lvf*9vH}5-3U9A|M@&@Uf z!fya8k3pwRQGR}gd|J-CV|cL{+(xi!xZNK{3yK*;(sj$=p-}Vv^aNjZc&Nf}RD zKdtL52@)z!SXI3-lEH`?oguHEkl@R7Q_gyC5e@S5f0Y>YCIrVAnr_KLrZ>Dr>`K8p zR;E_6z61%4g61&a=i6c$Op%IDLO-U|7r2$7)8o6IQRZ0uC-4meGGJtC>M~uYuAwoM zcpW7Ak5&h~ij0PCKLp?Ws$<2XV=L~f5O+(lr*Qh3rxGR%64u%*1sZAj+a`7CLa3fU zhUcTtr*5s_Z}iK`B1i?seqNALnozh=*aCfz^~~C+p%4NO3N2TZAU9O-k$01-qMo`_#FQmZIX-jeAWFCsuU?Cna z`M3wg#IWEanAvHnCfr9~EjmJ?ND#8V}8y-{Dh$nJTN2aB2dEGS*Zsq%Slu!f9 z794e6cZ(DO_<>@QWwYLP0><|&PZ4zvON~t#&*XlWYyi<>~GotAYd(h}1cS&61nW@=U@L3fZ z8M(_&3b=fmKP{OGknc>PJCunvkk&)Utan~ILGI5N6#EFc{9O^eg>HSFu%lfEA)Nwu-ir_~_J5 zu|(J)rWWJtLMQLlxuC^-g(uhb3@)5e(0(p-0_zR=oym!mGTJ~>27xcv43XOX5;z%Q zqDPiP4at*!^wOuZeoeD|jI8Vkx@do=!e;BU^rQAsIP$&1tS9}`SbW1_*05ZA(+KsW z?iig=al!t6ZOP^adD!xFN;=oCDIKWg9P6oPDic13Y)Ir9K0C2;#L?_KTVeU_59NZy zf5Nw5VQC3la?g;NfId3=lac#g(7XcCsO>k_W}2MoIF?Gsq7gkYuX5IWuUL}@_C=a!S%GVa+?mDa@@}B0FK}45R(vnqxQ`Kv? zx>O7L2zX;wF$YYefSp|DEx7A9H#XdFgxb)5bsLD)!;5SD<}D_}fROhEul8qaX_4~t zD_O;L_(BNYoj*xIKA?L(224@s_#33i{1N{k=l)}o9{LAEHKq=j^Y>NQwX6`o6#W+q z_#cz1pw)6tU{(fk;9{E!)Lpk0wmswy-?H1QVn$cbLzEBwB{33DH&rBZt|N_KP(Kkw z;J0?%22g<`dCAzr`ZBPR*BA;H{$RgR8Rd36s6nIu))O7|TR3j%g_kn@F!}Iokf^FW zqW{K25*Paq5QCQZ??z&jL#HMH9ZSlVG4HR~3sJh0L~bdv3nH5i_gG(Nv)jBz)_)Jc z=)bFo*q>Dd@1gq5*uVc^OHITw20#A41cRZzb;oBnMeEu(iJ~6F@x9P0WTk&3DRi3r z7is^hdo$kLbg+2SZ3PxKFHC3uPE??F>|vRJ$UKwu2iupC@kz4hoT3?zl5yCX7BPa= z`JY2KASQ(??14OYY;zTr(;i$6zeth2w6>uI0pbce)DAs$Lw(3$J9ZuY-=!n|8zO3( z4_StD`ue7|wJoB02<>k_kpyfWk`Yhw5bNT3I11Wz2=){a8b?K~BI{|`oSjvHJW$8#pFc;Sa0XN= zntFga$<@ynf^*~M4Z3@fGY%zecg)8KOsk#`DmAo184;)?3UqlPV!OxGjy|^p!pFc~ zT?b7@L23Fo^{Vtg{B&#DP51?R2$NrV#1G*e9LZtj>9`LOA60;Xfx4t6Lm^{VlD>L% zb@i#j5?T)=ezZVUFcNndsh}sKP)M4*0};9}F75-8tt|EG+l$A?mNHUOk-RQ+CET89 z1RFqjMl^P!^Vq^>k9C7toxih7ajFO43B+kR2?@bFfV@Ytez14$u-O=!`1w;#kE9Bm52bzr*N zl>%>BJj#P5;t?>#h(n0+b z!RJ}Bi2^!0PmJRE-z7(SOWbFkoYN{^+j2RDRU%oGcckRw87z_|h66dUkmA}la1272 z?%}!}EF>VU&v~2wRUq*jw5mbD3O|nxwA6t65v<8mFkzJ!&?)5*s<%v3+rNH|S>bqg zl!Y=6>)ZCb>#h-TC=+k*2)E zDl>A_fCS)@X6VR3%yzQ99StpquViEZRsIu>9Yd?&Zlu?04BI(`B1SXJP>e&7hj)&yVgP<|Z*jE_<9#gVBIPI+jBlCbb1Z1sZ=&?e}vcLyUKDSw#lWH4Z?{0fywuxgbf_ zmiUKqkxe|4bH&D^5YpY*kWBove_()|tUiB{h|R=8#iTz==~?zvKVYn|ua^4uygN&xr5fI;XeMdzCR77AL}AD_b6+miz-y_|R{lYkJ2@m_W zdng3N$pfPx=G3jfzW|vjBRLm)=O=|xC%>-Mp?iMqD}XozG^RMNOfHZuLdbr8Ie4_x za?`|Ky#f^lIz#Vxw(=eXjHP^kVsZ2^i760oYbTuk3hiUXJ7yq_z>b>@{R7z8*w7+& z@8mUXr--SU1XAJeQc^;vEb`K(sOKYvY*6yX!&Avc9+{?3Ui9{8ebf+?z&})F*o(#i z?||D1ST5nE0a%LtqF@}8ewFaUS8MQwbcTeGPNRUJAh1G^>XpX+hVUb-MQf%tC+D?S zu>mvP;Mz`m6?qC>Y2YaCFb|({bc3ZxNSm3J^;Rs~ITA855x4!F4D=N`YU+GpKWNG2 z@P7u2=Of3Z<>dE#c+Akz3g&u?NgSa=!3xNs=TdQlP_|%)fHmwi-qSL(!10rU6hIVGgg?HMApVS+y%wO!p)%A0uU4N##|Xt+btDvrT%56%S)A%zmgIqqsWP7q8lNi5?M^qq+7oI~-ctQ6)c}xHz7B%})9uJhihclKH0O?7flp ze}ojQsqD2Iy?*4sMrUrYs{)iV24Igr}V@(2a; z%p8F&bNi?B-F0+gg6(T)+yJBM$bKCHFn@(juPO~@&a2Z8iaPVLApHJoZ28XiZ6~hZ z=OLF83IgE9SRwiY`Xc~bEoenq;o5pC(=0Az?@fu4D)1UbmsB4UgIn-#n@!k%8uP3^S3n(qrGBUCU5K1av z^A_nd`ENVED3PGJ%`oSnq&$NRCy498Aa*9NU4RFai<3dIOV%l06}mkLqwcg7)-}ngknpU%TFJ}n1(8ju$%Nimk&Rw= zjHe2fcib>gmVglBJ$%UQc)Sd$W58;kLrouOjJIVW@Q=mse>I-}vaLS{njK+0uL%hF z2yZ)>hM}tm0~;GsU*mjJQYehZn*&^E!q^X1?cQ@UTdnTHys#}q?}lW)Gi#?;g$a?b zV<~}`{rmOjP=f<8QNN2;NXofxvm)wi!oyeBDz|z`r+}T%bUKA_d-!~!zkTbB0rSAuiM1yEnc0mQe791XLoc*9XC{Q#%5<)JBQ6Ic~ z`4WVl5cooL{;$nzG7Bc4Rg6$z(A6-(Rj>Q8a!+Aak@ESHc1zO z_kwj85sf9m%}vaUO9WF)W_J^y?;f&c6&20R%_u{Gif`WH-b9A%o1eY3_)UP48l*~J zprgn`+9qT$F|LMAz4vx}drK>HZ*{1ST=YKaF~uL2ts2N9Sf8QH0ZU;#?*xN@!2R7v zEv%rRvjHU(@pM}8|-w!O&%gsv)Y%8 z^Fi6C9)+Zf4Wx3=@E##5{a%2_1I-7*-k@ktEA{X)GRTSl%>%NzIv({MK1IQ$kH7MQ zK?Vlf^=>B~uktTKLc)a`ZiGSulydZdJsac#J{31(#O-IZOksI=T+go=6R^E;PyAZbtm-^G_N;M!X@ zbOKn-U#AB6M?g~R+mR>OmFMPjXdpW=iUMTc`KD9EsLEi>hS(EnfSTK!STRJ9u-H6S zLEWGz9Rx@zBcjL!iZ-80J1WO%r9eYzS%8041B!scFwi4uutg){lz-*P;oc63-9eh8rxq$<1HW<0#^KiF>534_el;} znVCoYn>hZ|p?@+P%E#XIzBir(#z$?OlWUjffN(yA)GA}>_A#wybpl@j2=A)7#K2*& zbhtIe3?<3suCcKfRd)B}>H!FLS!hpz*KA^vma~Y6J+#BFjvS$)qCQZ_-sbM`f#4Z` z3~kjLP71I#Dc{<@AmO&BdiJcpzWytgNBS$RW*TWcJUn0-S0^gtOKsRfxs4-|e;n<0 z4M)ZqG!0Ze5-;+~s zopjBod|`nu#ybaZ8*uQ& zUooM@u@?k$fV<2=wk>3DzVz~`cCdl>i27mi@205&s%*_RkSYEsk z>DGZW@`C~=n(yrN;u}&ZRqajgO@-`v=~sh0U2j!Yp;pL{btDZem zzjg3_ONqAwOBkuqjZ9*z`UvU9U5{)3@Zqm~tkqw7(on!zsVsn5(6JYx0hepC+pdR` zS+y7iD@ubjEX_Ju$nokz1_~vCIMfr&@!r`7UZ;S~3=d00sVpt9N>RNib)9#QZMSXuu z>Pr1cD+^dWm?47PMDqP--+oMj@-d4@J*t+Wfsv2|R zM|r4K%Z^7wIS4J%^?KJ1gi-h{Bb60W2f{o}IWR0TmN&cb*1B=plRi30^m!1m=POgB z%&m0Bbq~Zm#*iDzOEwRvMa1A)`_%qp;?+4BpTWHHgPGu!>Hf%Z$VoM=kkH}ZFAzFx zn!goUL375gwj(^c#(dyO=QO(Xl_u#Y&7@s|HcX=5-~Um%D0WG>E?JgGY_^kj>^e&1 zrQKJ4jeW!jUi1EDT*Qo+sla|cjx=;2Y$C-Fa}lqebAND6CymtC;ywH!Iz)dz0=m#^ zGD?TdVI5j=a{g&bleyGeoeDSj7W6Y>XwSm|qs3$RBPq(%ruI<^|4Db= z%a<7b^h_GRWr_G6`Fbw3yYgV6kHST80&-Y=%EvmP1eZk4h?8db)Of zDoiDf8;HQYq#ryo3jb`8szubmMOM+r`1dbK<1tXeZ^|%L=KrLBoYv?dolK=vm$qFL^b2xVF*y^x6*?VW7 zg2H}&XIthGjamG8q@~knK-5!4;+s5VnU2;9IqY`}WbSOABcIQzq{UaIHBl7hD&@w< zT&N&!tLs=cIG+;;Lc0ZOJA&rhqFFH}+Kl5wKkYrW={fqy!iS4#QQ?|;!O6unk|xb8 zcnpZf@q9q+uJH2|`MyyOpdCJFi!R`6b2)rQjZ?-;wfL|L0*y@Z7%VrFPZiK0u3gw) zn)Q-;OSzBzUaZBZCDBl0^%ZS<&-;{W3RhlXrWiLl?rL3j#j)=J~2*-;#>C$bQ1IPH9>(NOPpwu9Stuwv~lCZF~ngcJNw{ zt;(wf47p;s%@nDcHIT>;>0qc6aC7kO5*Ek#1mtLc$XM`Xui3eS&x;Jlz4fs?%}-7 zG0N6z@T8=SpD5IicwC_u7Z(TQI`}NvZRk#>f8PB3`Bp(~VWCAwcps$VN=j9i*=%hl zC6U2Z(B**nIxGz9p+)*Re}L&2ZAX3BMC{7L-`$V2TW1Z0EM z*F9)v1_Okl{K}uUr=15m<{9Yd=-}5miXCj!E1_c{-h^Qd&9*S1tW8#@iT40J-Wh$A zT=t@jM>=vB0_d$i@6zCbe&`JRw;u^@wx<)*166%?$jAwbVRdQy+SJsP9U%{yUd!A% ztqccqpb-8nf^1`R1X_ZVlan??S;Edv3y9Ga!J?^BX`L?KGg??1%;;{FEIJkW)qFA) zcT1w8pk>z|(|*jZs)M?chK&2RU5ZJ?$|7um;h~|e7YED9GOaiQ3Bxe|?M>KdBsw(v zD;F$504ahq8YD{|6J7)DB$Z{8eF&zO}fL~wx<;xfb23oMbYT2is8S<2je43iQzw(a~^ucu3pCSpn zX+Rv@mZB%6<_k=gEzQI=bAgqzrr7Oh<~CE@#5nSy4Cz`u@mJbrW*}6tA)kwgi<`_; zL{Lk1Yd@0n6byJnE?~m|PkkH+@*g~sBg3DuKVUj1tA|+VlJ`vokbHEHtm9K}9DB$k zJUlEyLjNEG(UvK|US~~T_h*3KoU{#*##wN+7%hq$R0f-jZpS6aRU+h=`RMSiD2jZ} zcROzI3$a0ENJ+>2M%z0ZI*C5!D4!Q&*ryVO?~Y}KVI{?7?pnAYor+Ewv;JJO z71fs67xEFWYhZ%cKHbOB?}{q_Fam8tmK0E#7wQkyZfyI}^F+e)2yLW0GE`=BN*WiX--VzkWB^vi84j9I+N4vyM>#*3^?Hk5 zB>am7RFPRYJ)Lwu>nbWzAc^OlvYp$2dpP;Wqdft}lOA5l?5Te7C!{t3PZm&6cIBFFR{GI04CSnQu$cOnw>CEsa-D1F2YHdq`V>#&_~b!uTcB*T zD*PyFRW#HL=m2{&GmQEwQDVC)spuCO8HQAXOPwdqBPN2LpB6iz{BZ>$!pC+gf5f)T z&Do57S`ZbkpW?m7&uYX99S87=!M*?|4rG(ad(0kb?e(c3ViQ?$ z4cgo!23sz?Y%G>yZOF@^b*@g$`V`eem)H=Ui>Yf0MLOO=ZNnmT=P>>Uw zW4?)zHk)7PJLz>Mt$O!DJ*Vx(=&jj)pL`EMABp-A$u++3|o}Y7dT|?(KQ+xB&_AW-OqtCrQMgFyyW7NHyk5SPrO5{&*H{19J zZmQ6DnCFJ3m<87eQe3lJfxMobxh)u~UxWmRX2KCXuRRY>1V)Ok!FL9(7mf<|KOJ_i zxVrL1KmE94$HnwH>h4$YUBmlqa$r@rtBfv~XKr!_Z1vFExEcJoTrWh2HP(-7 zqc^csNSGBIDXVkii{jQm&es0 z#P7%j5fe8TmuOIp(@0KiI^P_Jw+oaU++{LNr_y!>w!MQCBk$z+xD^?bmOmsqD~l#- z_|rckzo_T9#amS6T5oH&OoFn{H|h<*ejYOktXx4h$J>c;z6IbhOJC~TxGzUov^WRe zZ3yAtF8LS@=zQPBk<9BL7c#Oi<5Qt}4QHX0ss1m{&X)z`Pa0+Kr+)vgqodOrgcrHu zdHwumXlO7R?feezTI9gmfq_Z*MEdW?oo6#%SGSP8Ud0@Lacq!45NmAgC$u>yN0Xd# zlUm?7VmVy##gb%A8!g?srXUR&{@%8^R%;~&6M^7G@NS(P|HMd)kFTwbeze@DN;UeT z*aeOkI3|*>@0R>-z9T59W2~ixkrUV2_x$a)ysT_5k&XXEk>N#>aBT^s>EU!~I7hkw z8UZqkmz;#b)(cf$_wI?ikJix^E{VbclGem}`(($Ap=7(8{XBt-CK6e@< zznB83r7I#*@wfoBg&0Niyn!_0tGRj&3#hbof2as2-@Q2@CciaWsCwz`eX@rO$AIBj zQ6vM4`XjK*K<-gRKXiI*+rUtOL$7Y|yM$Zi$#b-ojSX(+^9!*{e?mI~9w--iOhZEu z5eqe~v~L;cnV2%IH+naIzxWW4S$nRep31UkYacR`F*wtJtcx7w-)HS_i+T{5kf3>j zQhI*wsG!#SXnAolM{n=^(XQeR6kX;u$twsmZ#TPs=Cn54<>tNqWv<26{<9g0hjm$h zN~$^wz@_|$Et-S0`KJ35bB)9-SNvyzXh9y63AC1?aM zsx)|ulFGL+s0&Os$G7^1r?*I4-@m;`+=J1?X40&A!FVMp z{bIcBWJpN^?y|b?;z41{2WFz$IOp$F(!;*-P$kiyrS*2tt$dE_i)A&f+R0q!J6KGK zh?94|E0{J(rXc&p^wRTH2np|ae#176tRD`&O38XW6yuznmpmpr`@XlkiycM^rNGS0 z^1p5(nTw5HT%Ofitx4?JLap--D5$Ttu6RVdj{7MFiuq*r^j967EaP z=F3-eX$1<-XJeTlS`CRvzadDhZ>EACc2snpZca1u|XqV8Yw`<&=V+pq0rj}$~9G7~@3 z{ko8ggj|p;6O*;-UFFQ?(D_KMdZq?1s$m7Zl_OoUlsyd(J3Cpb!h5TDllaY83kq4C zZKHTyVXtQuN~{kjW%ZM6=rK~YZ6*8}DNR4e@3g`lRc}tbwR;hhT51ZhLp}s_&8h|S zGFrBFdj5`~?@pnHpyWV#TcSYorwlPMF_1FL&1URL19(ZSDU~e!g0H3Z7woFO(j;?o z3j$_BV^W4I~ea(XMTK!@bk~ zT7hywd7_J5LRK7i?`09oh6^5q@JbE6`RMI4yYjCE4)g~ixoUlvPE`e5aiocqXPNX0 z+K&Ms@MVmmT=&x2^kt#t!eOGJX%WRy66@0L_($I!#cTQea}gY_EVp+Q_n~ zkOHfb^B?2{>*@39-5phwB*>w8$MV+^0=(vqjKA(=?%GbWYY_-AgNs>SWvB=Il}Z9t&UACgpFCnS}upZvZBJLeR^X<9*g%Oe|YG8%A**yxplnN z^@pUPsOpoGhD~^k2q1?+;dfTSV@n&Gp0+k@MIYhctOIJJ88l95zW&pe5m)rcwB6<3 z@M@cfI9q96_P2?DC(*E&KT;~_Az~fx@p%;2x!KloOotrsY8FF|H9y&WL*yI{Tvz7T z;3o%7;yG$FHN%)Rd1u@S@w9XKS#HW$>42LsQYP|vD{;g)X6UsoXegJ>w)Ey6*XC_U!X41?7aIx`|E4cC zNqzAp*rK1t^erRGuy3EB_yQi9#wmVx|55fb{Z)M&hiluqx3`JqpH+RiLbjf(x@KRT zyBZU1RbsA&Nrd-v4jT#pfUQi;(D@*wkV2cXD`faB4as~LkDH>7S+Z*-z-6v#-kdT7 zeqZG;Hl$ccym*(QpC+Jh5a*LR-k49v3g>P3?KgihAR|re#S9X9QTyF+saUzH)B}60 z>fY7^0G05XZ4Wo@l5A;hnvhCYJgRm!*VDqePMK8Q7`s{Eje zlh!thK=^L=u2{kfa;&PL@C3pzJ?xa8>_3=(PGZLH$CmEgR(E=C`=f(+;NeC&U*q{&}+L@q^>gpk5)E_3y`a`kI_g&F&_+ z15e-snBW7jUJeFucjTj`g#`c@N{#V0*479fT2L|WfOEg%YeYn;pPwXn6`tR$EH9t1 zTY`Y5Ttw$O&+IM$H=m&6oSJ&+mg2oRnyU4=`T4;dH80>nK{H8plO}vO{y38nP!I0yx+j)TDC4ChI9PSt2b`Uqk6&umO@rVVrVG#^ z-=f0_;$j+YXm~gQ>l)PgjaECP9muT?Wlfb^jAUm^!rJ+}?jLtif85ykc$?k%yN$Mj zUX$jl3bToV$ViHKA@FV44M?#at+D07IMZxfI>qxw{g9N64WN8=Y@zuC+&RU&Y-~1< zeBIKU`95rd>L%a}SQLsEhufd;QNtOp1F%hBjRP=uuw)@>iI$&JQEl(+uz%8wCoI!y zXiZNSfi^=hMYO|Ft+Y348^o9SZH zXrQGbCc0z{1}k{e+J9B z#iFCvSJT1t-?b-;SJJUe<3vZTtm{w8jtBF}_0WiA!gA@cp zVT7!{egvcrg8B-L1ez_lZMUVs*<7)bs)a@%(x$2qXFEGOJDYTMdGye74in>{g#m=C z1@T_8wC93nXNJQ)X#B zPhNqL)8_X_ZNLal0V7x+m81&dfOex*NQ%0%L2SP)_fDM6`0d)v3_I{OfH(3hhzkg= z?XQpOL&hKIX`gBU$M;>g!(i~{ed2NYGayCpU4q^JK3b`k)(qUqceZ=?8Jtf23iY}m z>OAc+nOFE%^P2D9zXLOGGj65?g1%l4dHY-?wet1w}l_fmd#LLBX;{Y4UR;*e&`o%#9J+Gj-%!A-+xa~k8 za-cux4`Sk4m^}=3{r&yh+bcYp3zwHmj!P_-a*iwYx-!SdV<2*>_F&kbLh>?n6e~2@ zE&Z!MkpsQN4wQV7sx*ie(I%XWh+dp(8N35#4?c$=23Nx4!3?2uDM?8hs#Y5tn~v62 zzUOGg6|izoI2~0Fn+}taA-iV%Op7Or3>nT+nrb0&S)8!p#b;f$-k+*=h+wFCl>$|@ zA2I5+@`gDSE(ZR^0)G5X3KqMN5RmoCV*oTbxAwBkc4xLg*M+=OOoHP8#^1Y{H=>km zpDcd~6l~pCT=c}A2l3`T!C0N2U`*Se@!CpILwC2c$B!$erJ|~qc35?Si*3HW~!qUX=t;rzqaGyDV`T)u|uH{WYX&_o1T8qXlpPgLV6X5$-2?{@^mX8zZnw|@q5@Z@}UlyB!ShrfI zvp{f8%(YBi{X{msM`msSNTPnH?JebU&@ZCmvpizwM!HsMy#ZQueq&drdp8q=J^=VD zP1(Ee?gC|uZaK(5XwGl%^ZOobR^fZqa%q);0f47Y z7Nc{a1_J~hhhXy*;DZYWP`LGHM)j1H$!f}|=L^v-y~g3eavp&`4@v5q9$77ep0;YY z9Tn9z4rBwBd)<|gqv}Q54)A4f8psKN^FKM;Y68WZ;baxX*gTTl!pTb77Divb;_O^*HFseieP9g)hmC_&ls@=#XL2@=K`X$9veO_Hc9lWHqV z@D3wkj1n~4hNX%IO}37$SaO^T``ZBC%`g}wm$?%BjNjo~P>_tZb-3)Lpwqqq?JNhX ze@vY0dx7)k&z?1g+5F1Ga7%xk*!IRcL7bS?_4ru(mvw&k6hnoXaK>X3O|f?FgFy=>6-gNx zP%5*1`ANW_`M{U2(Hx?5ApswP5a0#&sC{D>4q^)Gl|hHC3uF?E%fzt3si`ld;$q%G z{{Dy;Zl9%*t~I9s?IhL!O-OD@nuMqO!;f{trYlbh1SXqg%N?DZ5nr z0-$?-?q>RF?z?i-QBq47(reB(Tj0clGl9hsqzp$~+}!aFn{6;n)Ya8NHQgMbzsjX1 z2ZI^(w~>%)cH8&Njl_=7@R(5NcrNyJ*7l^F`8X$V7$dewvyjpQAV!qbgw1!eNNYq} zET&}PzPM#W<_xfERqY-UpWssmRzY4~ekiU?*52Xlmw8xY4HYeI-j3G+Oh&;(TK@V$ zU*<(I!ebnVxvDj;cX!h@H3<)UX#t&enB*Dpp-QQcDUtMH275>xh{j&LaNkKw@$pVy zd}%|ctPp1oLpfO{?VqU&s4YTdZ3M{;KU#kNnYm$3HrqZjT33|tIefTH9k!#xg&{}L zzgHnt()W8DlztqUCw~~g0~zOk^sZ8FIi|;Y8->r#zUS9*4@+aQ&yjeq_8I<5lt#`@ zg<&pFSNo)rj=k*$J>4bmy?hdkJ}bHG2SP7w^=*8_=U(zS*}7U`k!A9)C%v1q8VKYL z4c`$osi`t#7S4?i(Rj(u3BHmcRQu{0F;Z`1i|HiXUHvv+dC%VaS8txOqg}NuXl6nh z@6a`%K6lu9eY{)g{{9rajrbMfhuF>kn}6aTz8L=1=7j&RSAwCr(A0*-EUx#JW#-3Y zy#3fEE8b@I=Nvz$iYb%u`Ey5bXpn-tqAJT??e45Etom+t2{%nG;t)Qk16LWVD^VDN z;QX~V5kq2_p~P#Q^JD8>8reTJ!&`JO8;(&ybl)KvJBMf%DNZ+6cJE)e&?AOkjC- zdJ4J1iFtVj;Occtf4Z}iF$Bt+gSmWdGsvq2@bS5-2z*g^Yev3vo(l3uo23>{oaSoej&>CLAGfWv@wKX%7F|sBoxK>tP{#^b1EK`xD zm*hI}Y8S+p>iUbawgAl96Dujw;`8wCF~|qQfBkB}q5IBt_$vgQo*##^Ps%NpdWro# zXX@+g-P~?~5n&Y7pU2?#eKa&FX=xtwC!V#RrF3d#6%wfj+I(`$iX5PQPuLabp~W&x zAuTCqFjOwH8~^$=C@xD-ezFKPwkE2Of(B)em!}`(jE!Fba(ff~mB7JmMq)bPtZr#( zX?fiz;(PjOs-{|5If9`gBErVfk^mh&sIf7GQo8XTS;ZvooLUwLcBc!D2V{5?!2ZpU z<3n}F$>}0h+G(bT7YEXTh;V*ZZX%8h!go}amMhjB&r2_tc$q(;p`oLVb@2>c9=Jompa&rp{D0HUXT2d}Ik6e#OYyokVm7)lzZ;hSD z4izkezW`B&i1a`qHt7cnX!<>uw-r%DLz>fvGy8BEo1AD3t~h`n3tqvYmBb7={xof;b2w)hOv$p4)ejJ>=^ z&qeONY(KXJ*KMw4PSz#%#fIWL8o6o6hA=wWb&*}4F(1v>rbO*6{$K+GB^;W`mD(-c z1`F*S9a-*@dK0HG_fz42{z}Xr^7i%XBx7kX>9(v4q#?<5xqAAmni5?!6Kp-+$UX#1 z`Q0&kIAPHL$7*m=k(c)g2{wZ-y)Y0WOgnJAym=6OAt-*-D;ShC{X0+VhnLq9#H3@8 zKCXi?FE(p;%^LXTW(Kjv$8`->nmuu`t><&92XFW)Y9PbcW4BXmYw|+feq{U>=g&nZ&Wc5fXU?4kwetCUck3zijrRbskb1fO@hFL(v&+8HBW zm&+hGZ=-AQ>>IUx<+WM=g^l^z0jNN5TkD_h;}sCceECE;B4wFyml;w>z}dGP=xuH{@X*j3LV7CKeN#jGnTEWIBMm*MSUE*M2ui2G=17hEAz_8)Hh z@5Rn3sa`*QR-0r z^2sTjt>5c2E1d4XsvRgRhLAB5+JsaHJQ{ufv^A{Zt;q}P(?!antm*5aJ191)z(><3U66hqFMcia@5YCvVe51 z^v@K22n#x=YO$}#eNksib8{Z+b*chB7}No6$*!#A6yQm0L={^&IUfA#Zf9$IC-Zt< z8n6X(vw^Kg4YzLJ28H#V1+P@Wj7QSC)wf6M<9(qt+7>UEMqY>_ho;Wpl zuYn1UWaa|6wivF+I-I=+YBv5mIYYpdnwbfq;YLWFM{^u}h8uN%s?r9X$-!V4Zhzs2 zI4Ud)XLlDWsj>%llsYr?a2YgZ`L6k1y&C1J)oyun?lkM89~&DB?@{6x#$^oSG3DwO z?~3dnS2SD?dbvHUDmntbtOvnrckgBdShxWDLbYf##N=T_)nR|M37>j&?Ugz#@!@bD z1rj;r654_m8Qw_{IIjdTeXrJm!H|UvO?-EViP&qWVTSYQ*3dOTT~-IuNHVE*F+KHxSIu^(E)Spx4NH-spp*p~3qI>DH~2aN@M}-!`sUS!<9ufUyO- zn-b>>2EZ&wh_tId3?=Zt+_j2}Q!g`s3fn;tGvG*Q8Blg~x3(G#rZZwfp@!8%cVLQ& z)LYxa3!_acz*!fDhqvOkT=ANOLO7U&fPmcedAL-0Vxraj$2NEdXz5;z=TL85$(5It zS#FL?1;#~0P`Xfic(31)K8!Xc{-^%l5|8(& znB^5vG4F+SbjZ&INsvx}yLr9U*VK1EHBpZ5LEk~DV2!$h=a(i3w3iYuPa#}vRZ%Li(Eog>rQ=kIEr(UMpn zsqW@yd3YSRwx;*+oXLJo_B{O;3*Zq1wiXr18ED@ZQO+LbSX0m0!yF3t)zTe3rnJZKENxvsvr^}X84 z6RFu;!%Y*WM0{=P7v5A_Z{uynH#y?JU-CFdYdi+l1+|iDBnH$rLb}_BhZ(-{K96aJ zpRUO0uz$8)U8SxyE8DBujIHk0XnG00aeThq5hX+Z<;_iXYF=@pTYe=*)`{PB1o^eR z{p8CE6KMr+ZxxT9CpS?bpml^4c5A4}v~}?UQ-9n&U;&J9E${t&6E~*)!T-xwyJ3p}1*QjhKp31~aW0!|)G63KQ$;h}95!nCo{NTQet2V-7IL!1ko#7OD07RYtPOAuUu##L+ zx#tuN_&HvP^-o>OA%)9b=`-N`ir9bm4EQC*P55OJ{66ps8cGhv`GLZ)D~&5+DZ(G` zUG(2Kj!ap=BB52H+HOyTjjfb(s|r*ASO7~hl4OH80DP+_3lRWhpSXFqy1{u1u+B0Z zrD3NNOX4erL$zIQsp>#0-SY|L5r<=y*)8tfL!k(YYAz z;zWWU)oE9JC2Jm#;?!4mV@*xBq$E4Sck~+xTcAANWOA>3Def6v&2XtLXgpo!TYtyK zpqjo1pz;{-@vk90V@yfUg@3_;nXqcT+a=_py+9M%9t3k_^HdV`1@HicgHbg)ZSfGmDK5@V z@9j+sm|Q)$6_S#FmsM609BuWdaNghw`KKkY8s~c4DTZ>*AFU#TLP6i73ezcd7WPEm zcCl9vu9HRy%fxu&D9l|PjF21?U=LqI`vqnS2?-7g)Y>*xvK%SEWkDrsx7cYIDhhxy zAePWLf>%2;Mr8oaRsTS^_^V=b4YRfxh#-HoCSe$lnU!cAy#z zB=Q44@96Gtv$;({?sr|+Ul`-!VGU6+Gw*HB2tp9etO$@j{V_bT5D{hGl?$XlvCH`i zFbueag#BqUwIEpq;0Q2k%BUAtN42?t{egIAZo+0tAa*@cKd$AU$wWa)mSpGi9S878 z=G$zUoC}ug5<-^{R8tTW5DdWHTdY5j+NDxtpgV5yR}I+9_`cELoQTJ9P?DY-hj_Wi2fvR{%Kg?wV^jS*1#+34W;m52f({1Me;> zssQsqsuTg8T6+;oXM0$OjwqqOIAVwuje1q#I-gmg=HgY->zcZalecZZ;)gfvKZHv&qhq%@KO5_|H0 z-e=z3nVtQx`_AmneqzP}?wi}|IrK{t`oDyd1bl`DWfl|2E=rd;Bx?pI6P?>>N8Tx@nR02))wamKdgpCE8$t=UBg zRFi%CI9nwD2hc1>^G>glo!0&SG^pfo0Q|c8S-sZHP95C+JURw|0~X;Cl1Q-{;j-@i z3su2+A`7=^g@fbOE2wp(!h$lV^%sN4;6U9guYQb)N5m%US2fuLH8?`=_L$$kEqT2C z#rk}lO~gM_gdTJ_+JDwXz8H1A7!K1WB7j{1x*}b5^~*&oaudl#>>W}L#!>@vJ}kym zt0>H$`SeIy?fw1O;BLU@0TXoE?o>VuSiz{sbt(>LBRymw%01I4@_MFNz<-EMdRJG}=^%ReCQBriT*lfhfLcnN-AAl0t<=6O^j z4|swnZ)bK?k3C~R-Q=IhoOmfGlbS{6(&l;EVm?9+*lRFq2o8S-Ct@p5cjcSQs;Yjg z*7>Ts#u{jkBF90!m(F4k%&l}A37sVcxuF<~XdfL;Dx9E=yhL_o0_iG}FQT^5tK`9` ziC@OMkPr02QX7=(hOcI;NEz0{u?PsLw*p?;XRuiKUrx6ju+g=*zS{>08u996u7Q9T zK@M-jA97&k5pkT=x?rBjUHTsXWe`teZS0H>4Tnw@8X%hII6# zxAXJ!_+okY(1rG^tvcZDYI0NrK~I>wq(Ve?EAPA}g|Pi^E&Rca3+^}|HDyfor3dcn z=L$e@T2aZY0gY`)hS6h2M1)nY@IxY!VVei8t?mt;jI6l0VCyu=O(o$mo@?7J^?dYXsQG{g!pv=tDs)nA^C~O~!i`!ByydgUWw*gqSp!mU{ zGx{fXGMi^;UBnomti$U7%mvC(Y|~zlKFei|5l{IAP*MbNekw{E&Enc1`Sd$*dMJK4 z$FYod+&z~ix!)3p~rf;eHJ)CxU>a0YMLD88(2W+6u{ z$3QC%mkTK65B^CY{mb?r#%DPGlkg|N_J0IW_W`m}=tlgCLsS$k2wRbK2kie}0F+JU z2mznkp3gsyDf6MkBv8>&?yan6c%>he$Q&Efb`XI8Tv80b4F<2}jRwHDkE!vsoS{s+ z3KP@jnUF}nxK`i)Eh(8ca8JQbsW|Y(x7^y8vOw-X_=W?OrTcOsVVa9)Qbed!#7^Ga zz_3>X#~~~V85YVLpuAK`yMD-f{R4#jA4HP5ADgUX z4u;@4{DB#CR{q}`Vopj5a~W1ly#K}m{!e(Q|K%Q;@GrK;<-cYt{onp=N(ITO zsj1D)Y^Kzd1qXpx@H_FPrc6uV((qAtY|*rl(t2J*0sV*vx>ROWPHL<{AJ_q;Wkuu z-+i|wDqr#s3$N+u7+)&=&!5YnZqTAR!k)v{j!}#u^$JgmAWP&H3I@D?XdpBJ9+~=g znr?x#waR`^a%PM)6VxBdjFk@sm7zhTW`?S zG3yaiOoQhDUYEJ7Qop>6Kv~=+(p|I+<8W(R{lnxVz-0O)4p79@O-x>xmK_qW68`g1 z;i@_8=+6Edzfu2m!(sh=o&FE3@BihC=Ra?m|L<>;;K(wqZ0_U3d30>t?|EX9UoDyY zxe$<$z?P6$D#!n+3UrWwGYbf(3JaH!T77`FM^I1@R)ZZ(IRKO-hWBuBA>`P$RG$3) z@EzDOgj?I%kRyP|F*F4~z=3?ge2X^_Kq25V#X}pKoa_RTWxZsj78YPFov!4I-6Hjp zObhcCqznn?=~Am6lf>7u0#YtQek-euBXw8Xu_M2W+#e)l`D5E7${#KT{RK}R{yb5Q zx`Am0ggl%dRwMUoxO#Z_UoYZ=wMa}q1ne+JOD{;Re=Ousw2h+vDuG8%8pm5-28kW<$KG%cisipYsXzEA%zAJ;`svM=0z zoY`bD7Q1JW<6RS8-?Zo2ErAvG9PBCp_dz)=f|iIp46)H32q*_Ya0hUTMjl*gs&C&+ zsnD+O6t+lK=enJ&$e$smOB3>)Ujt%5%H!9sUqk&Q_n_K}`IxI;errgi2GG_J3%Kx( zX__0u6#4Z>WRWU4pMmdmx$n<$3%IV#^!1M-$U6(u(|`+v$7A#H!FODyey5wNl^pqp$k|Q>t)u`<7~4!2f;^{BT@6NYKyoJNu!$F5?N5 zyQOl^WuODd7CFsuT@RL}V}=@mCF4HUK0d7gU_|0#nBhL@@Z4N>n#92kgL2YOO?az3 zvumL|!8SQNi%WcvqP)gRHw>OuBmh~7VSW4?hM$9{1!HDmWu?(991T_N+ucDU1ldm# zVh-~LV^KEmnTdTpNHl3^ZZ-$}J|GA*nD~N)AXgYaS9i7?zGKgFE`D8Mo1Bp%?*xVHf4Qugx^bW>sH^fGlUm^sAfh$If0y%XPYy*VwD%j;7A7z5WA6$Bwms{ zG|vxOv3F*6g1a(SR(*bNmA>&?(S9C#r=-MBE5V?QKU1^1(Y3}-UQ}92$8|;d$Ynjn z24tNrT<;3+&hIaP_2`F6L^m6tQ1uSyj#V^(j50o+Q~;DWQw%oR56~NLoZ62dbgm?aX1IBvMVYX}n_?$s}me?n%VhjKc z0>#>#O47RM09wpar5-v?eJPh|CfMEK;NZYY=(gIbv7l^j8{`4BKNTJ<`leTGl(K*E$u_zp+V3f~5eO zh2|kC-s>Xi0~C(HR}|LOB_du7%HK@?nAFhbzDFBNcy9)@B<2>i&q<%rvpr<*chWup z!7ecJJ1(%f&R0cqqAT0~j#IK`^~|)o97WEx_7AJ4#j}~KAS|^m7%@JaQv~a`geVz# zfC5^eB#TFrF1D)9L4bxZDq3Alk(4}76YCpR9dgnSp((W3pnxRS)B-@1r=7mh=!$A#?KZ_cT|%&0}x>$J?j7`7z)lB^J0A?iWG8bjke0 zi<#g&-PZ>*L?EvJF@N&__UcwsTN}QqdoBab-+oPmheyjW_&NP(5GVBgo^(yLMS2DT zr(65|`C)S`5c0$0zi@IoeVbAY{!?gKS(SHZxs>vBVLX`^1m$;e6i@CuWBwuLT^{mOT62E_@gEpTicAR&T`TJN;;xoB>VY z5^4I&G|kPmE-Vb0i(5)mv>Uh@+km77aH1e0hrweYd$qmgTd!Q>+4UnE{o4yhk~mpD z7Hu~CA*e=fZVpYWf#?9|FHQ?=awio)-OS;q1^l%@Kjh*N0FE9P?;gBvn;7~`CybV@ zs=3nAC!N;IFrS9Ctox*0Er6_HxyckOk3V4l!~_h4$e>N1o^wlA`k5#loLp9L85`t`!kK_RTjG% z)Jy215B58?9ka}I13~^;oyB3m!i~L; zidqVonT?I`qTa+zs&pic7c;tSGWjcLci-i!xd0TQL8;zDtLQ_*ha^d)t()40pu#RF zH?`LBw@^^n`}}1Yw8JtHS&Ha{)Aq1{N`>Q6J=q(L3_?E@i>PQ@CWg_sT1nNhVc~OP zevGWF^T4`ccYLJp^8_7CDH*cYoi0Y|*Aq+*srI{Gv?&!bR-7hxkD173^pZ*Wc0M~h zs#O|)y3}Bx`4x)m6yQQg0?rX8_U0ra0A>dQX6cvy8{$auU=W%CW(E^4N!Zem=;&J4Pc2#U7>#eZ=}p=4 zXa4r}_9l?VAR{4(`;pe7a&x5nAi0Z0KtG)4=e#N#5W7%BIskuW6!ma!}6Y0(Q6Yik2Zm)`R*qU_B%JtrABGWZKc zo>ky8P=-(_@f*N)a$w@%1UF!W%OFt^b4Df#O(%yX)DcHq4ktBL8+F041N1W-LDMZH z66woAk+RN_vAy9f`f=KzkqPq@XJBIs$h+Ed?oISuVaJQ$$ud(w6Kr_Cm<89$-{3#OqiF`^-=XS$blZA&Hc!b`!g6!X5wphq5PcDtssV5iNMjC zFJ2v8HQ-?HIeh$Uef!oW2z3Dx;cNsil{O$_NNMV<(5}V#!uOd5a8y(fsL1sieHsIe=u{?`Q!+-IEB_V<{;bsSBO_VWHQW3~v@T4R@xBvs|-O##Ab_t_PBq#QvrZ zdqE)UA*md59{u~0$O9+wSYRP+5^+ITiM~?1TKYl(a7p`zrH`tsTPRS=Huak7Za{(1#WoiCuyP)M%y}CvjOo0rrB3D>UEW z%XN2=f*>O5RFoXL zv90}4S0Db@MOu7fWJG`CL<*+WJ*a7ja-PJHUvu~mQw;NduBn02KsEmmYbJ=ZA>JY; ze;iWtxne6)CCh$iRTI$AV~^HHphP9*D7`6BO4{&S2`Gnsk<d7oE!jk;RI&0GSf*JIiXLKIDV~Cx ziHQ!>&q*gjRQIAsL7~c^&Bp&`LvNx-lB-uId=K{PU=;WJ8`;k-7>yKLaX)GKTbaKj zAV^y1q6C2a_y`iGiGX@B$6uh5wGdKhF}8Pm@vHP<2X(u5iaIq=qwyUj%)#7EL_NBy zGr};r-(^&8Ku$!^Z1#Grkz){ZVGH#^H()qHe-7*VPs%FzkNA<^7={JJ#4mzg1QWia zDvwg2A<_gn{~I+x3gui=J@5}+mGmd6ahK)hzQzp)WD zXEb3lGv6>41i5H-rhu*NI)FlmR$x#*|ANk1J?2@HheacRF-DKgUTIIEfrW!(Dk!6% zrnoaOL@uGY144*YBV=MhXO}NE3S>E5xx%DEKZL|RGX@{E35o)~U@e`dT)_HwdJIKs zn`j(9Oj`JXw?!o4ORV7$<34aWk<>EsU;0Q?!hYblCp5uPe--i7<5+HHRyw{NwSi_Ln#l)phdDy{C2@J-(R9}0PJ1HXPf??4*n3Y*^2mSO1Q0Cq86lx;J3Vd0`< zyFrTxm7F%|K!(&!DyqKUjWy8@i3gq+6HS|u;>eIs{8uy0S&`!)0E9B1mA>&((D3cs zTUJya(8U~N4l7;=bjz;0Y`G(7|T$ z)+I71b8Vhap6d@Y^Yf>B(*)zNSGu9WU?=dunD_%8PqZk^AcMTvl)sdQa*@m9sH2qL z^cD

*xDzU{ZB8)cylc=B54#Ib19+79uI7^M!X&wM1MRnplR0ar;x%^DL-CG-F{A z`)hyy_VI77$ylK+2x78ymyDflb#Fs@I!ZQ7RR>8!-Bdm2fJ)Ba4ATalvyxW{sj2T2 zy97}O)yp0cw0R)X8=w#DgNj0y^c()UQ4Tz}1;n&VK&_EL7B}#L01wYmsDZJ3 zn3tUKQTB7^XG$ZRq-F3%X@NpQrEx|!g;pdirBy!T1Ob9kr^}xN6v^Q~$QPYL!$4Mt z7(AA6sVY@6A&(<;mQ#&|vrhBHHra9*6ra{fFViulLsY(ke8t63OM;TIfJYnf^w6Z;*b@?wPmF*6`**l?WY5*J z7jiH}=zJJ^;naFY!i0K#_H&CEL!N1daDKgT>ExEq5-goIW|;r^NE`3B-1n}wMM8>V z+mV?=R9N`e_bN??FuLA?m|gxJ-eyLSlY*VUwL>}Lq_Qnr>>7=R*-fXE?t$+y88RZ< z9{;WbuQOp%^*)8PskR$wNW2Xp$@oA z8XOa;Wh;#zj^IqR2NfPXaa~eE&$xnP+i?}U*-4Kpi{X8n{oKRlk3V2yzCUh4tv;7f zk`PX-1?1SXkx}I7)_61(e%<$!**wm2oK0{zLen8O67a?Mp#1%=^kl4dM%@s|pi23N zvkMt4{l(L7ooGvqqRx$%AFr1uOI->ILRxo0)H6IP(GoaIk4Qflie|Y23~&JrAR~C> zAYX_Q8)x{40`2|PEKiA|ucm%SrH#mkCy(H}w4z!x(xqoK@nzM6PDKYLs^(n~^3ym^Ig41L2j=bC>&1gYF6`bCVnjFmxSpDPzq)+s4BIR zeM}&7k9ePfj``uoNW}X{M`zTC4z;GSms(n%@!k!eH1kr8~=;i!FZJ1|7JksiAkF4+tAR^T{aoUO8`-qqo2o- zc*7ID%k|!6Ka`J2{|S`zTQVFq-JrPjfHg3GG}7POyJgVKM7npchb)udok;o?Oz#6+ z%}eW|O=y1!=7mz26B;Cl)HDy{B5>qV=@>qnb_KDF5WCD;m6j>O`ooZpFm8LKW*ixz zwYbbspxe|5O)Tk43>Q0fd*GaKsBAgzBbEkvgSHA3;>Nvov-J=a(_ja;S=e?J6!)L z5ebOnf}^aPUUmuyokRMJJ^Kf^zH&WVOajT)>TEQr2j{=6zd@AbC&w)RAhwu?dXx#Ga zAjkVZ}?;SrR#9$23&M%nv)8VY$tFl=B!stq97924#iULan)XJEN*GEQ0 z^GtvB28q2w2ZU!@m!^VzI;j? zyLyt#@A)rk>+hv)Wm;mJ0Ch=4LR|pY-2w`vwDeuW9=`e+&2VOAq#}vhV3Dq_O-@dJ z?$Y&G_;%ymvGnyVnYyE+~(bz5Q+tTdv|xb(xP+C z*nN4)>2k$Jg(D9DN6dNS<0T6%jiT$bFSxj~mp5mH+-M05uJ%VS-kF2-4E08adGo08r4cp6tn6hqxg*f;s6CFh^0r%4~@wbAGn501d&2i5)&#- zRZk?%IP^u_%>w47WtD2ogI&yQtzYz*|F zqF?kGG7H#Sy>B)=UJMOEg0ZqNQ2o32PAPKg`*+m=uF`)#8-lGQgWe*2x@jrhC8bU0rvN*H+J|KKR@w)^4DTX?io&4#2K zTVNYsDx1tzUTbp7Fbt5YLz_qeEIObCdYY}RB?ah$>FO5&g9HpPsA+J(kpNg7Baj%m zNXlQ2uO@BBe(-|;q@}V5uVH-7S3jJ}Kg{Bi55r;)kB~8~1IiNfk%R5+l{$-Te)j{4 z;kUB9;ksTSa7oO%l3V33V8TH(2cMA8?0Y6Awk6@(O)06cOdu`HuD~c14_}HM!An7I zaa=`eX{o_fDe8PjU+8`quWhfsRZ}{YK;EOJ-LN}RSR19P#tJC9%I0R-4@vvR3;^Tp z?SH22f{Q{HDfgyDXyM7Q#DZRiT$DIWcBZ+8K^ni z+B9Va8S6nn3{x?QnGM}W<_@i_JXt7%UT7QSVXaOC_J9w`$rUX~2-EChM>t0-{r#Z} z3n9Hz_axCmVLdgZAjJ-dN#bN-9xMW*4KRpwt$uxDX4b9m!Q24>m>nR)1nVLSdFN5v z{3D=vCqD_qtOil_dj-`toZ+TV{rCY)Ta`e&4Mf;xnwm86sudM=S+5sJhb>07Y?m8R z7jm?BxgGc^d}9&Ggxkp*=(cQ@nxzkZFTiHA zVg|(qVfL3UR>r9z97!I>=^R91(O^uaReU&9g_!Ah>gRVGoVH+#Y*b@^mtlnAM ze5{eP^RqfaBXN&FKNax3uHT&w9x*-PRjPSiJ7E!qh28%p6!1B1faW-v`SS7z`hD1r zi@Up!59e-wlv(;jy+n|y2)obx2hlX zy+Pqx{gK)Hsj|`EbA(Z!2ct&}4;L@352^J8wsy{WEjQ+IWl=pO8m5-p zGs}nM0<>4Z!lZiPrdLh}3yp33{9pilXY%EUZ9Um_>?g?(W=t0VZTrv6T${_RQh?t6 zn(Dg`!S2tVj%Fe|!{Qn$=c_k{Y(_>B5)^VKB;4AVB$k;O%tKMTn!T?bAMVl?u6UvQ zdWlSt3u!qb_V@8!II~CyXlOC3WHqaTB(KB%*d~)o^yH;41_gA^w%g5i@m;%}A2JVA*794WeOg9ao~t~3^X3QF5WsE)=5Pi!K)#-eqluqIOJr8nEjGTk3r=*M zPsA`%7$uzaZ3OIcy$x47-d9mH)NIT&;+c2sMl&fYt!7>qJ+UQ6r{tecU={dyd0*lp z&rqnukM3Z~2@x!lTfC7TvMm@4i3_)VeZVQJUeH@_G1mI|Gd7*)nL~@4WCP54L8$a< zz4yi3)j=2qiu8aiOXV&Di0$l?oPs|i*3E-7h)<%Cw&pDbp-}ghr86%xs8v-KGu3RJ zpPz5{ni;6fu66pQ?Dk`J7p8}>!NEiIe!IKsQzZHf%okBzr6?QSebSz@eh-tPwnxl}0#1}~@I*SH_TkudO* zIiuvA$8p2X6x6Wnd;>RPP2vI?D25q|?>h^R&JYxs`GMM2=ljRs`bAQjxH?^}Y}Qvc zJ~#z5-3x3yGZP#V@zPn7x-1!F+jQ`hmVgSe*#urzp&7(cgOE?>fx|Fy7;@G)(5aG>GPBX$4VQXY+M-UQMYh$_*4$iB9DhN{ z!-GGe+S`jOiWcPs_4WGtdXPy)ZO9#tR$N8J!2&Oc;AZ@TWIq`}k;RxtZ!f)N`yY0@ zlsBth$i``qTj>~-cn<|Rf4vi(;$DU=>mgIKzo5l>_&e_G_N(a?e1>sMT1(e+8~Z1a zRRtXJu-N^@qWgi1HY^Fdx|z9wJ9r8URJ*>7EAmJ*VK0)Cl}x*g)$*8X>es7R&Kq3n z5s)n+cGFY!;9g@|hQ1bSw{>lfuKS7@VGYbdn9Z9<(!?*8IBWMgS=BNdMhiuD*@8z^B6EQEOLo_M8?xhfBcYps8et69f^1i}Y8L?mr2vfd-_zU{%M95Hd zv~9ExH_{svKq4C(BeX*9haniG`OJh@FgW+xKf!1L@YPy28Kz1S|7)EOLs@oZ9ziK z&M>(1e&U43e#{pmqCiD5sIMuIpo3+zq)^N1O*H1@q!`oa;{d7F83HouL{m6%QO39` zr!ugl#4mg?aG^~i3-q$8dqH2-<|bNU_h59Pc;O6GHzH|i5QPqH>K#9W;lV_ZHmtqd zrDRgV9V$^Evjh3e3`8LH_@Q*vnBeP7+#AbicT72gsJoZ8;=Jm=G2Q)+?CSB%JgG7< zK_ z1F=i0-Lg1t9$ZV3hmWtn}TIa>c=cAhX*X)Wo654E+ zl6Bu!SJm{r=~X6+LE1Mvrup)3Tw&8uAmO+eOVvk7xhJTCk-Zxs5w@fy79JH+s;T5` z!rBdUh}5LuOfptv$IGf^)3GN3_s$J9!=EyFb0aWtc8K!@aY#(GzAEv&_p)Eq3MxfL zl3@E8@m-gc%$-Qb2xs)dx0EvPeXj;t8uKFdEa~EuH418z?F1Bd((6(VScwMoG?8&7 zW;hkuyqwQ0xY5zcSRMM*IJu=4CLVxh9}wXa1!|!R%P)n)VLisyLKDMmsoGg)`3HfJ znc5l_+wUkvuXHsgxaW8Nrp}bXeWyWq?kPy1CK$E6m#V~Xcv|a#AEkiQ*`gII;kgry zqg}yOKr>X(l}tCYG{Kl6-iC(X$)Z1g5+qFEc0G!Of%h15B|;9VTQRq(9LN0HHr-=J6N>0Rp5S6VK1sFO#2 zuryXAo&9zVu7i#UfuDWl-EGX3hASo^A#~`*X^4DeWUf-nk|~fb1_w7-LTp)*qsbxK zyJ2Nh#F_A70vpeSK@-CzNhMy$UbLd&b~;#UK=n%-VnnX$2jp(P7&R;iNpb9#*-Ec6m>z<^*P?%z>^*5olDq#aMg@m&yq|(bOfA5T4L+Rq=d|K2w&P+|> z>KXI)Se04wKeAfLd>OiZUf}v6~r@oD)@nt+*tb9h#AwF#uTR_ zc=tTBI&2qF+p))dU%SEot=Pw6;fs&CPQ2o&?lTLg3=NgcVB_{)%x`(Ulb-YQEP&Rx(J^GdH}-qIr@Hu%5#Rn=*zu%;H;_?D0@Pd(+U zL|>yz&cqF@Pq>FMEv>rM&Vw!?VYsWh8`JjN8A6jgFvM9Kv*@YSzopHrfRzIoZt2Y4 zrS{Sw%W|qQ2 zVxowyj2$RO3^G`su=UW>aBpRkHAE^K9obVw*Fx#49E1%lK1+KZJn_v%o~l&Z;YUaBe{ zT)=`b3_on$V)@0mY9InL27$+ct8s|7@#TnVvg5jzX4^Qag3e{rvB3&!goWOIY>SNx zatA`?d#nl06Ga@I=b4uxK9@E8uzVI1jDh^rEF8_zqeTNFF~@<0uXz_#h&9Vw8tmrN zkj?z@GLQW7-HXQ_dE0^LmI|-8Hk#_Q(>evm0o@;_H@aksq?isdxX1hv5!& zCYI9>e>SI!l#dn+8PAY$V-8(Jj4W<*MW&e$(mQz#3sX9PhvjJd*1U5)t})wWmW>*T zJs<$6t>w-!k7X@adY zEvab<8P(gouJ(LNOb)O02b1guTB5=vd02>eCoqR>$p{94Vsu`V@^A02vth&hoU*ye z?YPwD(Zc*Huo{co$Z9U*@37HD|7MmWN_BQvdMC*jq>CnJq`wEbehxga72h|#AB9KU zq1=6y%Ara?rsq^3=&ni3)!y8kS$8M4pZvMlP?CfI6En>8i$R-GsSqf&5yRCK{{4zE zDH9V(-Mr)Os_ku~wW;wo9@`f%V7s5LDbu;rn3UoXj2Zw0euOX$@i|k(>Z(y=965&8 ziJv2!-uMG#G7;T~o{*u7LP9!EJSvpbRD((5YS~b@{mV5SEXY(5VOYxCcygOnlFjy_ zCgRK7QfQdRPvO-lJ!ripQY(HT7WyQQhm95`+n6h0ywS*^_!8l5VzQ0u%RoH8H)`=a z{Y7l^Ii}p}Xf}18%1JQPZ@U(xho>MM#%$C|)07Z%Ko+jfUn6O=&3t65*<}Lfw8=*3 zfTh-nIZO9QJQLxZ_=R;(?7=K@-(b(tR3d8td+)svotCs_(dzD&DPk?ud6HD2!Oti) z8vr&%D(#4(5Ugwff#u!Fuv7YbVV%ECWGg^Jqa3PWP~?fJ%H(PE+8%WVyb zC(yCB{DP?DcJ1S6>F^~8KA{a3X0rZkP;bOh$QrV3$-rU4lB2ArH-?H0&Kv&j&ts_g zA?pW|0B6(Zv-G4t=A-+>Mo2T2saZZ^RS2EO>rchSih4GN#i;{q6K|vIi2u%BtLVjJ z%j+R7q#EQQvXEi>Pc|;d*gxbXopT# zEN@s+gP;Y|#0zHwTCULlA-qbr;ia9GFkTsEX4`vPlCbhSe>a@_o6-ApY8+Prf`K$9 zqgo3#%+TOc4Z6cj+FvpJjCXm_G$=d@IsK`%3%em-Qj1UO>rm4oG^CfA<>cyPDd#zj zyTN281s_INj7{u|K}>ET(h932EMB}*{yfRtRVPa+w#_gqIo;My3=B7HW6hZ_@>ym3>b#fqfuB$E?$yR96}4gN zDh+MwU)mk4ELL2Gvq3Gg!sFeNPXD(ZLdw<#FJdFqs~{f^D3WsgA*n*#;g_WGB;)d( zLK@beU`OZZR+F==T3fx&-!j*mkC?~ntAV)$@vPB>IwUI7==u70H_A&-K*_$}Crvy5 zY#Kh@T2iLRL9vlpw8L(MJb_)a68!c;oB2<2om1`xOaJqLnF9T_ugvDeb89v=#{3^J zXi!huFZ_AjPP9rHiPmp+CKJNU`CwE$PAnWdCPR2S(l_3?C27W(PwclXkUss=dtWu` zW8rO(i7S?kVIBqs(Yl|W%CJ%#7}56u96K0SK2D{< z$Hn#mb~onO@;n~z0(XX_LVv;W+@WC3w+wY{hb8lPTuaOizp$htKzrY8tUt{4M(5Us zW^vcAL1fr+-&q5HHl3^0HJB)S>6(wI8di7ZYe^z}_2RG2*Pw9wuUv>5Ln1ifeS$?7T^Y}Cp8576NGEb^VZdo88Cz{<@*FA!U7 zQ%lDQ-kp>@VX;leLft3kp|>r1Bbm$t(YstFSjqtfwp&c=?_{>2? z@rVfsT(9l1RFSMLWXgtoY#apfQ&E98VMr$&WBLZafYZuKSSMwcom`$U?;XKR%QUC& zhh4P@*!tHreS%>wPATTNoA~^NjCm6hjt4#{(NJ-mOyn~wY_d3 zqsH%-)0v5ePG3&KEJWV~6iQD7he+tvM|L_>41C7bx5M*U&KMdzu4XcUwX>Y(sA6XD zsMp_8)oF!JEjNX4-`#iOtm=e*(;(>V(o)E%HGD1mO 5` will divide by zero on the right side even when x is zero. - -**Sam:** Hmm. That's actually a correctness issue when combined with bug number one. - -**Alex:** Exactly. It's not just a nice-to-have semantic. It makes conditional guards unreliable. The fix is to move AND/OR out of `compile_binop` and into `compile_expr`, using branching -- similar to how we compile `if`. Evaluate left side, conditionally skip right side. - -**Sam:** How big is that? - -**Alex:** Medium. Maybe two to three hours. We need to create intermediate blocks, branch on the left operand, and phi the result. But we already have that pattern in `compile_if`. - -**Sam:** This is a DO. It directly interacts with the division by zero bug. If both are present, users literally cannot write safe guard clauses. - ---- - -**Alex:** Number four. Modulo operator doesn't work for floats. We emit `srem` which is integer-only. Cranelift doesn't have a native float remainder instruction, so we'd need to call out to `fmod` or implement it ourselves. - -**Sam:** Is anyone doing float modulo in Phase 1? - -**Alex:** Unlikely. Our test files don't use it. Float modulo is a niche operation. - -**Sam:** SKIP. We can add it in Phase 2 with a proper math stdlib. If someone hits it, they'll get a codegen error, not silent corruption, right? - -**Alex:** Currently it would try to use `srem` on float values and Cranelift would reject it at compile time with an error. So yes, it fails loudly. - -**Sam:** Even better. SKIP. - ---- - -**Alex:** Number five. String arithmetic. If someone writes `"hello" + "world"`, we don't have string concatenation. What actually happens is both operands are pointers, and `iadd` does pointer math. So you get some garbage address interpreted as a number, or worse, a valid address pointing at random memory. - -**Sam:** That's... terrifying. - -**Alex:** It is. Imagine someone trying `"hello" + " world"` because they come from JavaScript or Python. They expect concatenation, they get a segfault or garbage. This is tied to issue six -- no type checking -- but even without a full type checker, we should at minimum reject arithmetic on pointer-typed values in codegen. - -**Sam:** So what's the fix? - -**Alex:** Two options. The fast fix: in `compile_binop`, check if either operand is `ptr_type` and emit a codegen error. The real fix: type checking pass, which is issue seven. The fast fix is thirty minutes. - -**Sam:** Do the fast fix now. The type checker is a bigger piece. DO -- but the quick guard, not string concatenation. - -**Alex:** Agreed. We emit a clear error: "cannot perform arithmetic on strings". DO. - ---- - -**Alex:** Number six. The big one. No type checking at all. You can pass a string to a function expecting an integer. You can add a boolean to a float. You can return the wrong type from a function. Everything just... proceeds. Cranelift does whatever the bits tell it to do. - -**Sam:** Give me the honest assessment. How bad is this in practice right now? - -**Alex:** In practice, most of our test programs happen to be well-typed because we wrote them carefully. But the moment a real user makes a type error, they get garbage output with no explanation. That's the number one "this language is broken" experience. - -**Sam:** And the fix is issue seven -- the semantic analysis pass? - -**Alex:** Yes. Issues six and seven are really the same thing. You can't "fix" the lack of type checking without building the type checker. - -**Sam:** Let's talk about it as part of issue seven then. But the verdict on issue six as a standalone: it's not a separate fix item, it's the symptom. The cure is issue seven. - -**Alex:** Correct. I'll mark six as DO but note it's resolved by implementing seven. - ---- - -### MISSING - -**Alex:** Issue seven. Semantic analysis / type checking pass. This is the elephant in the room. Right now the pipeline is: lex -> parse -> codegen. We need: lex -> parse -> **check** -> codegen. The checker would verify types match in binary operations, function arguments match parameter types, return types match declarations, and variables are defined before use. - -**Sam:** How big is this? - -**Alex:** For Phase 1's limited type system -- i64, f64, bool, str, and unit -- it's manageable. We don't have generics, traits, or user-defined types yet. I'd estimate two to three days for a solid implementation. We walk the AST, infer types for expressions, check them against declarations. - -**Sam:** Two to three days is significant. But without it... - -**Alex:** Without it, every other fix we do is a band-aid. The division by zero guard? Pointless if someone passes a string where an integer is expected and we happily divide two pointers. The immutability enforcement? Useful, but still incomplete if types are unchecked. The type checker is the foundation. - -**Sam:** I hate to say it, but you're right. This is the one thing that separates "toy" from "real." DO. And it's priority one. - -**Alex:** Agreed. I'd argue we build a new crate -- `turbo-sema` or `turbo-checker` -- that takes the AST, annotates it with types, and returns errors. Then codegen can trust that everything is well-typed. - -**Sam:** DO, top priority. - ---- - -**Alex:** Issue eight. Immutability enforcement. We already said DO for issue two -- that's the codegen side. This is the semantic analysis side. The checker should reject `x = 5` when `x` was declared with `let` (not `let mut`). Since we're building the type checker anyway, we fold this in. - -**Sam:** Is this extra work on top of the type checker? - -**Alex:** Minimal. When we walk `Assign` and `CompoundAssign` nodes, we check the variable's mutability flag. Fifteen minutes of extra code in the checker. But we should also keep the codegen-level guard from issue two as a defense-in-depth measure. - -**Sam:** DO, bundled with the type checker. - ---- - -**Alex:** Issue nine. Variable scope tracking. Right now, variables declared inside a nested block are visible outside it. If you do `if true { let x = 5 }` and then `print(x)`, it works -- x leaked out of the if block. That's wrong. Blocks should introduce a new scope. - -**Sam:** Is this hard to fix? - -**Alex:** In the type checker, we use a scope stack -- push a new scope when entering a block, pop when leaving. In codegen, we already use a flat `HashMap`, so we'd need to save/restore the map around blocks. Or switch to a scope chain. - -**Sam:** Is this something users will actually hit? - -**Alex:** Absolutely. The moment someone accidentally uses a variable from an inner scope, they'll get either a wrong answer or a confusing error. And when we do add loops, loop variables leaking out would be a constant source of bugs. - -**Sam:** DO. This is basic scoping. Every language has it. - ---- - -**Alex:** Issue ten. Function redefinition not detected. If you define `fn foo()` twice, the second one silently overwrites the first in the `user_fns` HashMap. No error. - -**Sam:** How often will this happen in Phase 1? People are writing small single-file programs. - -**Alex:** It's an easy check -- when inserting into the functions map, check if the key already exists. Five lines of code. And it prevents a genuinely confusing experience where your function "isn't working" because you accidentally defined another one with the same name lower in the file. - -**Sam:** Five lines? DO. That's a no-brainer. - ---- - -### NEEDS (Code Quality) - -**Alex:** Issue eleven. Better error messages with ariadne. Right now our errors look like `error: expected expression, found end of file at test.tb:3:1`. With ariadne, we'd get pretty-printed errors with source context, underlines, colors. The crate is already in our workspace dependencies -- line 21 of `Cargo.toml`. We're paying for it in compile time but not using it. - -**Sam:** How much work? - -**Alex:** To wire up ariadne for lex errors and parse errors, maybe half a day. For type checking errors too, another few hours. But it's polish, not correctness. - -**Sam:** SKIP for this sprint. Our errors are functional -- they have file, line, column, and a message. Pretty errors are a Phase 2 polish item. We should either use ariadne or remove it from dependencies. - -**Alex:** Fair. I'd rather spend that half day on the type checker. - -**Sam:** SKIP. - ---- - -**Alex:** Issue twelve. Remove chumsky from workspace dependencies. We switched to a hand-written recursive descent parser but never cleaned up `Cargo.toml`. Chumsky is still listed on line 13. It's dead weight -- extra compile time for nothing. - -**Sam:** That's a one-line delete, right? - -**Alex:** One line. And it'll speed up clean builds by a few seconds since chumsky pulls in a dependency tree. - -**Sam:** DO. Takes ten seconds. No reason not to. - ---- - -**Alex:** Issue thirteen. Short-circuit evaluation for `&&` and `||`. This is the same as issue three. We already said DO. - -**Sam:** Duplicate. Already DO. Let's merge it with issue three. - ---- - -### DEVX - -**Alex:** Issue fourteen. No `turbo build` command. Right now we only have `turbo run`, which JIT-compiles and immediately executes. There's no way to compile to a binary. - -**Sam:** The JIT approach is actually great for the developer experience right now. `turbo run hello.tb` -- done. `turbo build` requires us to figure out object file emission, linking, output paths, all of that. That's a significant chunk of work. - -**Alex:** Agreed. Cranelift can emit object files, but wiring that up, calling a system linker, handling platform differences... that's a week of work minimum. - -**Sam:** Hard SKIP. Phase 2 or Phase 3. The JIT workflow is fine for now. Honestly, most modern language toolchains lead with `run` anyway -- Deno, Go, etc. - ---- - -**Alex:** Issue fifteen. No `--verbose` or `--debug` flag. Would be useful for us during development -- dump tokens, dump AST, show timing. - -**Sam:** How much work? - -**Alex:** Trivial. Add a `--verbose` flag to clap, conditionally print the token stream and AST. Maybe an hour. - -**Sam:** Actually... this helps *us* debug faster for everything else on this list. If we're building a type checker and debugging codegen, being able to `turbo run --verbose test.tb` and see the AST would save us time. - -**Alex:** Good point. It's a force multiplier. - -**Sam:** DO. But keep it simple -- just `--verbose` that dumps tokens and AST. No fancy debug infrastructure. - ---- - -**Alex:** Issue sixteen. REPL mode. Interactive Turbo shell. - -**Sam:** *laughs* No. SKIP. That's a Phase 3 feature. We don't even have loops yet. - -**Alex:** Yeah, a REPL without variables persisting across lines, without loops, without any interactive features... it'd be a bad first impression. - -**Sam:** SKIP. Hard skip. - ---- - -### WISH LIST - -**Alex:** Issue seventeen. String interpolation. `"Hello, {name}"` instead of string concatenation. This is actually in our design spec. - -**Sam:** It's in the spec, but it requires the lexer to parse interpolation segments, the parser to handle embedded expressions, and codegen to allocate and concatenate strings at runtime. That's a multi-day feature. - -**Alex:** And we don't even have string concatenation yet. We'd need a runtime string type with allocation. - -**Sam:** SKIP. Phase 2. This is a big feature that deserves proper attention, not a rush job. - ---- - -**Alex:** Issue eighteen. While and for loops. The lexer already has `While`, `For`, and `In` tokens. The parser and codegen don't handle them. - -**Sam:** How fundamental are loops? - -**Alex:** Pretty fundamental. Right now the only way to loop is recursion, which... works, but it's not what anyone expects. Our `recursion.tb` test proves recursion works, but you can't write a simple counter loop. - -**Sam:** Counter-argument: we're trying to ship a polished Phase 1, not expand scope. Loops touch the parser and codegen. How long? - -**Alex:** `while` is straightforward -- maybe three to four hours. It's just a conditional jump back to the top of a block. `for` with ranges is harder because we'd need range expressions and iterators. - -**Sam:** What if we do just `while` and skip `for`? - -**Alex:** `while` alone would be a huge usability win. The pattern `let mut i = 0; while i < 10 { ... i += 1 }` covers most use cases. - -**Sam:** Hmm. I'm torn. It's scope creep, but loops are so basic... - -**Alex:** Here's my argument: without loops, every demo program that needs iteration looks weird. Fibonacci via recursion is fine for a showcase, but "count from 1 to 10" should not require a recursive function. - -**Sam:** Fine. DO for `while` only. SKIP `for` until Phase 2. And only after the type checker is done -- I don't want `while` without type checking. - -**Alex:** Deal. `while` loops, type checker first. - ---- - -**Alex:** Issue nineteen. `assert` and `panic` built-ins. `assert(condition)` that aborts with an error, `panic("message")` that halts execution. - -**Sam:** How would we implement them? - -**Alex:** Same pattern as `print` -- built-in functions handled specially in codegen. `panic` calls a runtime function that prints and exits. `assert` checks a condition and calls panic if false. Maybe two hours. - -**Sam:** These would be really useful for testing. `assert(add(2, 3) == 5)` is a lot better than eyeballing print output. - -**Alex:** Exactly. And it makes our test files self-verifying instead of "look at the output and hope." - -**Sam:** DO. This is high leverage for low effort. We can write proper test files that actually fail when things are wrong. - ---- - -**Alex:** Issue twenty. Multiple print arguments. `print("x =", x)` instead of separate print calls. - -**Sam:** How hard? - -**Alex:** The codegen's `compile_print` currently only handles `args[0]`. We'd need to iterate, print each with a space separator, and print a newline at the end instead of after each argument. Maybe an hour. - -**Sam:** SKIP. It's nice but not critical. You can call print multiple times. Let's not touch the print infrastructure when we have bigger fish. Phase 2. - -**Alex:** Fair. SKIP. - ---- - -## Final Tally - -**Sam:** Let me read back the list. - -**Alex:** Go ahead. - -**Sam:** Here's what we've got: - -| # | Issue | Decision | Why | -|---|-------|----------|-----| -| 1 | Division by zero crashes process | **DO** | Process crash is unacceptable even in alpha. One-hour fix with a zero-check guard before `sdiv`. | -| 2 | Mutable bindings not enforced | **DO** | Core language promise (immutability by default) is broken. Parser already tracks `mutable` flag; codegen just ignores it. | -| 3 | Logical AND/OR don't short-circuit | **DO** | Correctness issue -- guard clauses like `x != 0 && 100/x > 5` will crash. Interacts directly with bug #1. | -| 4 | Modulo unsupported for floats | **SKIP** | Niche operation, fails loudly at compile time (Cranelift rejects it), not silent corruption. Phase 2 with math stdlib. | -| 5 | String arithmetic does pointer math | **DO** | Silent garbage/segfault when users try `"a" + "b"`. Quick guard: reject arithmetic on pointer-typed values in codegen. | -| 6 | No type checking (garbage on wrong types) | **DO** | Resolved by implementing #7. Not a separate work item -- this is the symptom, #7 is the cure. | -| 7 | Semantic analysis / type checking pass | **DO** | Foundation of correctness. Without it, every other fix is a band-aid. Covers type mismatches, return types, argument types. Top priority. | -| 8 | Immutability enforcement (sema side) | **DO** | Bundled with type checker (#7). Fifteen minutes of extra code -- check mutability flag on assignment nodes. | -| 9 | Variable scope tracking | **DO** | Nested block variables leak out. Basic scoping is expected by every programmer. Needed before loops (#18). | -| 10 | Function redefinition not detected | **DO** | Five-line check when inserting into function map. Prevents genuinely confusing silent overwrites. | -| 11 | Better error messages (ariadne) | **SKIP** | Polish, not correctness. Current errors are functional (file:line:col + message). Time better spent on type checker. | -| 12 | Remove chumsky from workspace deps | **DO** | One-line delete. Dead dependency adding compile time. No reason not to. | -| 13 | Short-circuit evaluation | **DO** | Duplicate of #3. Already decided DO. | -| 14 | `turbo build` command | **SKIP** | Requires object file emission, linking, platform handling. JIT via `turbo run` is the right UX for now. Phase 2/3. | -| 15 | `--verbose` / `--debug` flag | **DO** | Force multiplier for debugging everything else on this list. Trivial to add (clap flag + conditional AST dump). | -| 16 | REPL mode | **SKIP** | No loops, no persistent state, no interactive features. Would be a bad first impression. Phase 3. | -| 17 | String interpolation | **SKIP** | Multi-day feature requiring lexer, parser, and runtime string allocation work. Deserves proper attention in Phase 2. | -| 18 | While loops | **DO** (`while` only) | Loops are too fundamental to skip. `while` is straightforward (conditional back-jump). `for` deferred to Phase 2. | -| 19 | Assert/panic built-ins | **DO** | High leverage, low effort. Makes test files self-verifying. Same pattern as `print` -- built-in function in codegen. | -| 20 | Multiple print arguments | **SKIP** | Nice convenience but not critical. Call print multiple times. Phase 2. | - ---- - -## DO NOW: Implementation Priority Order - -**Alex:** Here's my proposed order, and the reasoning: - -**Sam:** Let's hear it. - -**Alex:** We build bottom-up. Foundation first, then the things that depend on it. - -### Priority 1: Foundations (Day 1) - -1. **#12 -- Remove chumsky from workspace deps** - Ten seconds. Clean up. Gets it off the table. - -2. **#15 -- Add `--verbose` flag** - One hour. We need this to debug everything that follows. Dump tokens, dump AST, show stage timings. - -3. **#10 -- Detect function redefinition** - Five-line guard. Quick win that prevents confusing behavior during all subsequent testing. - -### Priority 2: Safety Guards (Day 1-2) - -4. **#1 -- Division by zero guard** - One hour. Emit a check before `sdiv`/`srem`. Branch to trap on zero. Process-crash-level bugs ship first. - -5. **#5 -- String arithmetic guard** - Thirty minutes. Check for pointer-type operands in `compile_binop`, emit codegen error. Prevents silent garbage. - -6. **#2 -- Immutability enforcement (codegen side)** - Two hours. Store mutability flag in vars HashMap. Check on `Assign`/`CompoundAssign`. Defense in depth before the type checker exists. - -### Priority 3: The Type Checker (Day 2-4) - -7. **#7 + #6 + #8 -- Semantic analysis pass** - Two to three days. New `turbo-sema` crate. Walk the AST, infer expression types, check: - - Binary ops have matching/compatible types - - Function arguments match parameter types - - Return values match declared return type - - Variables defined before use - - Assignments only to mutable bindings (#8) - This is the big one. Everything else is better once this exists. - -8. **#9 -- Variable scope tracking** - Bundled with #7. Scope stack in the checker, scope-aware variable map in codegen. Half day. - -### Priority 4: Correctness (Day 4-5) - -9. **#3 + #13 -- Short-circuit evaluation for && / ||** - Three hours. Move AND/OR out of `compile_binop` into `compile_expr`. Use branching pattern from `compile_if`. Depends on the type checker being done so we can trust operand types. - -### Priority 5: Features (Day 5-6) - -10. **#18 -- While loops** (while only, no for) - Three to four hours. Parser: recognize `while condition { body }`. Codegen: loop header block, condition check, body block, back-edge jump. Depends on scope tracking (#9) being done. - -11. **#19 -- Assert/panic built-ins** - Two hours. Runtime functions `rt_panic(msg)` and `rt_assert(cond, msg)`. Wire into codegen like `print`. Last because it benefits from everything above being stable. - ---- - -**Sam:** That's a clean six-day sprint. Thirteen items get done, seven get deferred. The deferred items are either pure polish (#11 ariadne, #20 multi-print), large features that deserve their own sprint (#14 build command, #16 REPL, #17 string interpolation), or edge cases that fail loudly (#4 float modulo). - -**Alex:** And at the end of this sprint, the Turbo compiler will: type-check programs before running them, enforce immutability, handle scoping correctly, support while loops, have short-circuit logic, and never crash on division by zero. That's a real compiler. - -**Sam:** Ship it. Let's go. - ---- - -## Summary Statistics - -- **Total issues reviewed:** 20 -- **DO NOW:** 13 (items 1, 2, 3, 5, 6, 7, 8, 9, 10, 12, 13, 15, 18, 19) -- **SKIP (deferred):** 7 (items 4, 11, 14, 16, 17, 20) -- **Estimated sprint duration:** 6 working days -- **Unique work items:** 11 (after merging duplicates: 3+13, 6+7+8) diff --git a/turbo/crates/turbo-cli/Cargo.toml b/turbo/crates/turbo-cli/Cargo.toml index c4577ab..6394362 100644 --- a/turbo/crates/turbo-cli/Cargo.toml +++ b/turbo/crates/turbo-cli/Cargo.toml @@ -14,7 +14,6 @@ turbo-lexer = { path = "../turbo-lexer" } turbo-ast = { path = "../turbo-ast" } turbo-parser = { path = "../turbo-parser" } turbo-codegen-cranelift = { path = "../turbo-codegen-cranelift" } -turbo-codegen-llvm = { path = "../turbo-codegen-llvm", optional = true } turbo-sema = { path = "../turbo-sema" } clap = { workspace = true } ariadne = { workspace = true } @@ -28,4 +27,3 @@ tempfile = "3" [features] default = [] -llvm = ["dep:turbo-codegen-llvm"] diff --git a/turbo/crates/turbo-codegen-llvm/Cargo.toml b/turbo/crates/turbo-codegen-llvm/Cargo.toml deleted file mode 100644 index eae197b..0000000 --- a/turbo/crates/turbo-codegen-llvm/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "turbo-codegen-llvm" -version = "0.9.0" -edition = "2021" -rust-version.workspace = true - -[dependencies] -turbo-ast = { path = "../turbo-ast" } -inkwell = { version = "0.5", features = ["llvm18-0"] } diff --git a/turbo/crates/turbo-codegen-llvm/src/builtins.rs b/turbo/crates/turbo-codegen-llvm/src/builtins.rs deleted file mode 100644 index bd023e8..0000000 --- a/turbo/crates/turbo-codegen-llvm/src/builtins.rs +++ /dev/null @@ -1,1817 +0,0 @@ -//! Built-in function dispatch (compile_call) and all built-in -//! function implementations (print, assert, len, map, filter, etc.). - -use inkwell::types::{BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; -use inkwell::values::{BasicMetadataValueEnum, BasicValueEnum}; -use inkwell::{AddressSpace, FloatPredicate, IntPredicate}; -use turbo_ast::*; - -use crate::ctx::Ctx; -use crate::expr::{ - coerce_arg, compile_expr, convert_to_str, int_to_ptr_if_needed, widen_for_storage, -}; -use crate::types::{ - turbo_ty_from_type_expr, turbo_ty_to_llvm, turbo_ty_to_llvm_ctx, MaybeTyped, TurboTy, -}; -use crate::CodegenError; - -// ── Function calls ────────────────────────────────────────────────── - -pub(crate) fn compile_call<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - callee: &Spanned, - args: &[Spanned], -) -> Result, CodegenError> { - // Method calls: expr.method(args) - if let Expr::FieldAccess { - ref object, - ref field, - } = callee.node - { - let (obj_val, obj_tty) = compile_expr(cx, object)?.unwrap(); - if let TurboTy::Struct(ref type_name) = obj_tty { - let mangled = format!("{}__{}", type_name, field); - if let Some(&func) = cx.user_fns.get(&mangled) { - let mut arg_vals: Vec = vec![obj_val.into()]; - for arg in args { - if let Some((v, _)) = compile_expr(cx, arg)? { - arg_vals.push(v.into()); - } - } - let call = cx - .builder - .build_direct_call(func, &arg_vals, "") - .expect("build_direct_call failed"); - let ret_tty = cx - .fn_ret_types - .get(&mangled) - .cloned() - .unwrap_or(TurboTy::Unit); - return match call.try_as_basic_value().left() { - Some(val) => Ok(Some((val, ret_tty))), - None => Ok(None), - }; - } - } - // String/array method calls - return compile_method_call(cx, obj_val, &obj_tty, field, args); - } - - // Check if callee is a closure variable (TurboTy::Fn stored in vars) - if let Expr::Ident(callee_name) = &callee.node { - if let Some((alloca, TurboTy::Fn(ref param_tys, ref ret_ty))) = - cx.vars.get(callee_name.as_str()).cloned() - { - let param_tys = param_tys.clone(); - let ret_ty = *ret_ty.clone(); - let ptr_type = cx.context.ptr_type(AddressSpace::default()); - let i8_type = cx.context.i8_type(); - let i64_type = cx.context.i64_type(); - - // Load the closure pair struct pointer - let closure_ptr = cx - .builder - .build_load(ptr_type, alloca, "closure_ptr") - .expect("load") - .into_pointer_value(); - - // Load fn_ptr (as i64) from offset 0, convert to pointer - let fn_ptr_i64 = cx - .builder - .build_load(i64_type, closure_ptr, "fn_ptr_i64") - .expect("load") - .into_int_value(); - let fn_ptr = cx - .builder - .build_int_to_ptr(fn_ptr_i64, ptr_type, "fn_ptr") - .expect("itp"); - - // Load env_ptr (as i64) from offset 8, convert to pointer - let env_slot = unsafe { - cx.builder - .build_gep( - i8_type, - closure_ptr, - &[i64_type.const_int(8, false)], - "env_slot", - ) - .expect("gep") - }; - let env_ptr_i64 = cx - .builder - .build_load(i64_type, env_slot, "env_ptr_i64") - .expect("load") - .into_int_value(); - let env_ptr = cx - .builder - .build_int_to_ptr(env_ptr_i64, ptr_type, "env_ptr") - .expect("itp"); - - // Build LLVM function type: (ptr, ...params) -> ret - let mut llvm_param_types: Vec> = vec![ptr_type.into()]; // env_ptr - for pt in ¶m_tys { - llvm_param_types - .push(turbo_ty_to_llvm_ctx(pt, cx.context, cx.enum_max_slots).into()); - } - let fn_type = if ret_ty == TurboTy::Unit { - cx.context.void_type().fn_type(&llvm_param_types, false) - } else { - let ret_llvm = turbo_ty_to_llvm_ctx(&ret_ty, cx.context, cx.enum_max_slots); - ret_llvm.fn_type(&llvm_param_types, false) - }; - - // Compile arguments - let mut arg_vals: Vec> = vec![env_ptr.into()]; - for (i, arg) in args.iter().enumerate() { - if let Some((val, _)) = compile_expr(cx, arg)? { - if i < param_tys.len() { - let expected = - turbo_ty_to_llvm_ctx(¶m_tys[i], cx.context, cx.enum_max_slots); - let val = coerce_arg(cx, val, expected); - arg_vals.push(val.into()); - } else { - arg_vals.push(val.into()); - } - } - } - - let call = cx - .builder - .build_indirect_call(fn_type, fn_ptr, &arg_vals, "closure_call") - .expect("indirect call failed"); - - return match call.try_as_basic_value().left() { - Some(val) => Ok(Some((val, ret_ty))), - None => Ok(None), - }; - } - } - - let Expr::Ident(name) = &callee.node else { - return Err(CodegenError { - code: ErrorCode::E0400, - message: "indirect function calls not yet supported in LLVM backend".to_string(), - }); - }; - - match name.as_str() { - "print" => compile_print(cx, args), - "panic" => compile_panic(cx, args), - "assert" => compile_assert(cx, args), - "assert_eq" => compile_assert_eq(cx, args, false), - "assert_ne" => compile_assert_eq(cx, args, true), - "len" => compile_len(cx, args), - "abs" => compile_abs(cx, args), - "min" => compile_min_max(cx, args, true), - "max" => compile_min_max(cx, args, false), - "to_str" => compile_to_str_builtin(cx, args), - // Stdlib - "split" => compile_stdlib_2arg_rt( - cx, - args, - "rt_str_split", - TurboTy::Array(Box::new(TurboTy::Str)), - ), - "trim" => compile_stdlib_1arg_rt(cx, args, "rt_str_trim", TurboTy::Str), - "upper" => compile_stdlib_1arg_rt(cx, args, "rt_str_upper", TurboTy::Str), - "lower" => compile_stdlib_1arg_rt(cx, args, "rt_str_lower", TurboTy::Str), - "starts_with" => compile_stdlib_2arg_rt(cx, args, "rt_str_starts_with", TurboTy::Bool), - "ends_with" => compile_stdlib_2arg_rt(cx, args, "rt_str_ends_with", TurboTy::Bool), - "contains" => compile_stdlib_2arg_rt(cx, args, "rt_str_contains", TurboTy::Bool), - "index_of" => compile_stdlib_2arg_rt(cx, args, "rt_str_index_of", TurboTy::Int), - "replace" => { - if args.len() >= 3 { - let (a, _) = compile_expr(cx, &args[0])?.unwrap(); - let (b, _) = compile_expr(cx, &args[1])?.unwrap(); - let (c, _) = compile_expr(cx, &args[2])?.unwrap(); - let result = cx - .rt_call("rt_str_replace", &[a.into(), b.into(), c.into()]) - .unwrap(); - Ok(Some((result, TurboTy::Str))) - } else { - Ok(None) - } - } - "char_at" => compile_stdlib_2arg_rt(cx, args, "rt_str_char_at", TurboTy::Str), - "join" => compile_stdlib_2arg_rt(cx, args, "rt_str_join", TurboTy::Str), - "repeat" => compile_stdlib_2arg_rt(cx, args, "rt_str_repeat", TurboTy::Str), - "read_line" => { - let result = cx.rt_call("rt_read_line", &[]).unwrap(); - Ok(Some((result, TurboTy::Str))) - } - "read_file" => compile_stdlib_1arg_rt(cx, args, "rt_read_file", TurboTy::Str), - "write_file" => { - if args.len() >= 2 { - let (a, _) = compile_expr(cx, &args[0])?.unwrap(); - let (b, _) = compile_expr(cx, &args[1])?.unwrap(); - cx.rt_call("rt_write_file", &[a.into(), b.into()]); - } - Ok(None) - } - "pow" => { - if args.len() >= 2 { - let (a, _) = compile_expr(cx, &args[0])?.unwrap(); - let (b, _) = compile_expr(cx, &args[1])?.unwrap(); - let result = cx.rt_call("rt_pow", &[a.into(), b.into()]).unwrap(); - Ok(Some((result, TurboTy::Int))) - } else { - Ok(None) - } - } - "sqrt" => { - if !args.is_empty() { - let (a, _) = compile_expr(cx, &args[0])?.unwrap(); - let result = cx.rt_call("rt_sqrt", &[a.into()]).unwrap(); - Ok(Some((result, TurboTy::Float))) - } else { - Ok(None) - } - } - "sleep" => { - if !args.is_empty() { - let (a, _) = compile_expr(cx, &args[0])?.unwrap(); - cx.rt_call("rt_sleep_ms", &[a.into()]); - } - Ok(None) - } - "http_get" => compile_stdlib_1arg_rt(cx, args, "rt_http_get", TurboTy::Str), - "http_post" => compile_stdlib_2arg_rt(cx, args, "rt_http_post", TurboTy::Str), - "json_get" => compile_stdlib_2arg_rt(cx, args, "rt_json_get", TurboTy::Str), - "channel" => { - let result = cx.rt_call("rt_channel_create", &[]).unwrap(); - Ok(Some((result, TurboTy::Struct("Channel".to_string())))) - } - "send" => { - if args.len() >= 2 { - let (ch, _) = compile_expr(cx, &args[0])?.unwrap(); - let (val, _) = compile_expr(cx, &args[1])?.unwrap(); - let val_i64 = widen_for_storage(cx, val); - let ch_ptr = int_to_ptr_if_needed(cx, ch); - cx.rt_call("rt_channel_send", &[ch_ptr.into(), val_i64.into()]); - } - Ok(None) - } - "recv" => { - if !args.is_empty() { - let (ch, _) = compile_expr(cx, &args[0])?.unwrap(); - let ch_ptr = int_to_ptr_if_needed(cx, ch); - let result = cx.rt_call("rt_channel_recv", &[ch_ptr.into()]).unwrap(); - Ok(Some((result, TurboTy::Int))) - } else { - Ok(None) - } - } - "mutex" => { - if !args.is_empty() { - let (val, _) = compile_expr(cx, &args[0])?.unwrap(); - let val_i64 = widen_for_storage(cx, val); - let result = cx.rt_call("rt_mutex_create", &[val_i64.into()]).unwrap(); - Ok(Some((result, TurboTy::Struct("Mutex".to_string())))) - } else { - Ok(None) - } - } - "mutex_get" => { - if !args.is_empty() { - let (m, _) = compile_expr(cx, &args[0])?.unwrap(); - let m_ptr = int_to_ptr_if_needed(cx, m); - let result = cx.rt_call("rt_mutex_get", &[m_ptr.into()]).unwrap(); - Ok(Some((result, TurboTy::Int))) - } else { - Ok(None) - } - } - "mutex_set" => { - if args.len() >= 2 { - let (m, _) = compile_expr(cx, &args[0])?.unwrap(); - let (val, _) = compile_expr(cx, &args[1])?.unwrap(); - let m_ptr = int_to_ptr_if_needed(cx, m); - let val_i64 = widen_for_storage(cx, val); - cx.rt_call("rt_mutex_set", &[m_ptr.into(), val_i64.into()]); - } - Ok(None) - } - "hashmap" => { - let result = cx.rt_call("rt_hashmap_new", &[]).unwrap(); - Ok(Some((result, TurboTy::Struct("HashMap".to_string())))) - } - "hashmap_set" => { - if args.len() >= 3 { - let (m, _) = compile_expr(cx, &args[0])?.unwrap(); - let (k, _) = compile_expr(cx, &args[1])?.unwrap(); - let (v, _) = compile_expr(cx, &args[2])?.unwrap(); - cx.rt_call("rt_hashmap_set", &[m.into(), k.into(), v.into()]); - } - Ok(None) - } - "hashmap_get" => compile_stdlib_2arg_rt(cx, args, "rt_hashmap_get", TurboTy::Str), - "hashmap_has" => compile_stdlib_2arg_rt(cx, args, "rt_hashmap_has", TurboTy::Bool), - "hashmap_len" => compile_stdlib_1arg_rt(cx, args, "rt_hashmap_len", TurboTy::Int), - "hashmap_keys" => compile_stdlib_1arg_rt( - cx, - args, - "rt_hashmap_keys", - TurboTy::Array(Box::new(TurboTy::Str)), - ), - "hashmap_remove" => { - if args.len() >= 2 { - let (m, _) = compile_expr(cx, &args[0])?.unwrap(); - let (k, _) = compile_expr(cx, &args[1])?.unwrap(); - cx.rt_call("rt_hashmap_remove", &[m.into(), k.into()]); - } - Ok(None) - } - "map" => compile_builtin_map_llvm(cx, args), - "filter" => compile_builtin_filter_llvm(cx, args), - "reduce" => compile_builtin_reduce_llvm(cx, args), - "clone" => { - if !args.is_empty() { - let (val, tty) = compile_expr(cx, &args[0])?.unwrap(); - // For structs with clone derive, call StructName__clone - if let TurboTy::Struct(ref sname) = tty { - let clone_fn_name = format!("{sname}__clone"); - if let Some(&clone_fn) = cx.user_fns.get(&clone_fn_name) { - let call = cx - .builder - .build_direct_call(clone_fn, &[val.into()], "") - .expect("build_direct_call"); - return match call.try_as_basic_value().left() { - Some(v) => Ok(Some((v, tty))), - None => Ok(None), - }; - } - } - // Otherwise, shallow clone (return same value for primitives) - Ok(Some((val, tty))) - } else { - Ok(None) - } - } - "deref" => { - if !args.is_empty() { - let (val, _) = compile_expr(cx, &args[0])?.unwrap(); - // deref: load i64 from pointer - let i64_type = cx.context.i64_type(); - let ptr = cx - .builder - .build_int_to_ptr( - val.into_int_value(), - cx.context.ptr_type(AddressSpace::default()), - "deref_ptr", - ) - .expect("itp"); - let loaded = cx - .builder - .build_load(i64_type, ptr, "deref_val") - .expect("load"); - Ok(Some((loaded, TurboTy::Int))) - } else { - Ok(None) - } - } - "store" => { - if args.len() >= 2 { - let (ptr_val, _) = compile_expr(cx, &args[0])?.unwrap(); - let (val, _) = compile_expr(cx, &args[1])?.unwrap(); - let ptr = cx - .builder - .build_int_to_ptr( - ptr_val.into_int_value(), - cx.context.ptr_type(AddressSpace::default()), - "store_ptr", - ) - .expect("itp"); - cx.builder.build_store(ptr, val).expect("store"); - } - Ok(None) - } - "json_stringify" => { - // rt_json_stringify(key_str, value_str) -> json_str - if args.len() >= 2 { - let (a, _) = compile_expr(cx, &args[0])?.unwrap(); - let (b, _) = compile_expr(cx, &args[1])?.unwrap(); - let result = cx - .rt_call("rt_json_stringify", &[a.into(), b.into()]) - .unwrap(); - Ok(Some((result, TurboTy::Str))) - } else if args.len() == 1 { - let (a, _) = compile_expr(cx, &args[0])?.unwrap(); - let null_ptr = cx.context.ptr_type(AddressSpace::default()).const_null(); - let result = cx - .rt_call("rt_json_stringify", &[a.into(), null_ptr.into()]) - .unwrap(); - Ok(Some((result, TurboTy::Str))) - } else { - Ok(None) - } - } - "to_json" => { - if !args.is_empty() { - let (val, tty) = compile_expr(cx, &args[0])?.unwrap(); - if let TurboTy::Struct(ref sname) = tty { - compile_struct_to_json_llvm(cx, val, sname) - } else { - let str_val = convert_to_str(cx, val, &tty)?; - Ok(Some((str_val, TurboTy::Str))) - } - } else { - Ok(None) - } - } - "to_json_array" => { - if !args.is_empty() { - let (arr_val, arr_tty) = compile_expr(cx, &args[0])?.unwrap(); - let elem_sname = match &arr_tty { - TurboTy::Array(inner) => match inner.as_ref() { - TurboTy::Struct(s) => Some(s.clone()), - _ => None, - }, - _ => None, - }; - if let Some(sname) = elem_sname { - compile_array_to_json_llvm(cx, arr_val, &sname) - } else { - let str_val = convert_to_str(cx, arr_val, &arr_tty)?; - Ok(Some((str_val, TurboTy::Str))) - } - } else { - Ok(None) - } - } - "http_server" | "route" | "http_listen" | "respond" | "request_body" => { - // Stub: these are complex HTTP server operations; emit a no-op for now - for arg in args { - compile_expr(cx, arg)?; - } - Ok(None) - } - _ => { - // Check enum variant construction - if !args.is_empty() { - if let Expr::Ident(ref first_name) = args[0].node { - if let Some(variants) = cx.enum_variants.get(first_name.as_str()) { - if let Some(variant_index) = variants.iter().position(|v| v == name) { - let data_args = &args[1..]; - let enum_name = first_name; - - if let Some(&max_slots) = cx.enum_max_slots.get(enum_name.as_str()) { - let total_slots = 1 + max_slots; - let num_fields_val = - cx.context.i64_type().const_int(total_slots as u64, false); - let ptr = cx - .rt_call("rt_struct_alloc", &[num_fields_val.into()]) - .unwrap() - .into_pointer_value(); - - // Store tag at offset 0 - let tag_val = - cx.context.i64_type().const_int(variant_index as u64, false); - cx.builder - .build_store(ptr, tag_val) - .expect("build_store failed"); - - // Store fields at offsets 8, 16, ... - for (j, arg) in data_args.iter().enumerate() { - let (val, _) = compile_expr(cx, arg)?.unwrap(); - let offset = ((j + 1) * 8) as u64; - let field_ptr = unsafe { - cx.builder - .build_gep( - cx.context.i8_type(), - ptr, - &[cx.context.i64_type().const_int(offset, false)], - "var_field_ptr", - ) - .expect("build_gep failed") - }; - let store_val = widen_for_storage(cx, val); - cx.builder - .build_store(field_ptr, store_val) - .expect("build_store failed"); - } - - return Ok(Some((ptr.into(), TurboTy::Enum(enum_name.clone())))); - } else { - let val = - cx.context.i64_type().const_int(variant_index as u64, false); - return Ok(Some((val.into(), TurboTy::Enum(enum_name.clone())))); - } - } - } - } - } - - // UFCS method call: parser rewrites obj.method(args) -> method(obj, args) - if cx.user_fns.get(name.as_str()).is_none() && !args.is_empty() { - let (first_val, first_tty) = compile_expr(cx, &args[0])?.unwrap(); - if let TurboTy::Struct(ref type_name) = first_tty { - let mangled = format!("{}__{}", type_name, name); - if let Some(&func) = cx.user_fns.get(&mangled) { - let mut arg_vals: Vec = vec![first_val.into()]; - for arg in &args[1..] { - if let Some((v, _)) = compile_expr(cx, arg)? { - arg_vals.push(v.into()); - } - } - let call = cx - .builder - .build_direct_call(func, &arg_vals, "") - .expect("build_direct_call failed"); - let ret_tty = cx - .fn_ret_types - .get(&mangled) - .cloned() - .unwrap_or(TurboTy::Unit); - return match call.try_as_basic_value().left() { - Some(val) => Ok(Some((val, ret_tty))), - None => Ok(None), - }; - } - } - } - - // Regular user function call - let func = *cx.user_fns.get(name.as_str()).ok_or_else(|| CodegenError { - code: ErrorCode::E0402, - message: format!("undefined function: {name}"), - })?; - - let ret_tty = cx - .fn_ret_types - .get(name.as_str()) - .cloned() - .unwrap_or(TurboTy::Unit); - - let type_params = cx - .fn_type_params - .get(name.as_str()) - .cloned() - .unwrap_or_default(); - - let mut arg_vals: Vec = Vec::new(); - let mut arg_ttys: Vec = Vec::new(); - for (i, arg) in args.iter().enumerate() { - if let Some((val, tty)) = compile_expr(cx, arg)? { - // Type coercion: match parameter types - let expected_type = func.get_type().get_param_types(); - if i < expected_type.len() { - let val = coerce_arg(cx, val, expected_type[i]); - arg_vals.push(val.into()); - } else { - arg_vals.push(val.into()); - } - arg_ttys.push(tty); - } - } - - // For generic functions, infer the actual return TurboTy from args. - let actual_ret_tty = if !type_params.is_empty() { - if let Some(f_def) = cx.fn_asts.get(name.as_str()) { - if let Some(ret_ty) = &f_def.return_type { - if let TypeExpr::Named(ref ret_name) = ret_ty.node { - if type_params.contains(ret_name) { - let mut inferred = None; - for (i, param) in f_def.params.iter().enumerate() { - if let TypeExpr::Named(ref pname) = param.ty.node { - if pname == ret_name { - if i < arg_ttys.len() { - inferred = Some(arg_ttys[i].clone()); - } - break; - } - } - } - inferred.unwrap_or(ret_tty) - } else { - ret_tty - } - } else { - ret_tty - } - } else { - ret_tty - } - } else { - ret_tty - } - } else { - ret_tty - }; - - let call = cx - .builder - .build_direct_call(func, &arg_vals, "") - .expect("build_direct_call failed"); - - match call.try_as_basic_value().left() { - Some(val) => Ok(Some((val, actual_ret_tty))), - None => Ok(None), - } - } - } -} - -// ── Closure-based builtins (map, filter, reduce) ──────────────────── - -/// compile_builtin_map_llvm: map(arr, closure) -> [T] -fn compile_builtin_map_llvm<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], -) -> Result, CodegenError> { - if args.len() < 2 { - return Ok(None); - } - let (arr_ptr, _arr_tty) = compile_expr(cx, &args[0])?.unwrap(); - let (closure_ptr_val, fn_tty) = compile_expr(cx, &args[1])?.unwrap(); - - let (param_tty, ret_tty) = match &fn_tty { - TurboTy::Fn(params, ret) => (params[0].clone(), *ret.clone()), - _ => (TurboTy::Int, TurboTy::Int), - }; - - let ptr_type = cx.context.ptr_type(AddressSpace::default()); - let i8_type = cx.context.i8_type(); - let i64_type = cx.context.i64_type(); - - let closure_ptr = closure_ptr_val.into_pointer_value(); - - // Load fn_ptr (as i64) from offset 0 - let fn_ptr_i64 = cx - .builder - .build_load(i64_type, closure_ptr, "map_fn_i64") - .expect("load") - .into_int_value(); - let fn_ptr = cx - .builder - .build_int_to_ptr(fn_ptr_i64, ptr_type, "map_fn_ptr") - .expect("itp"); - - // Load env_ptr (as i64) from offset 8 - let env_slot = unsafe { - cx.builder - .build_gep( - i8_type, - closure_ptr, - &[i64_type.const_int(8, false)], - "env_slot", - ) - .expect("gep") - }; - let env_ptr_i64 = cx - .builder - .build_load(i64_type, env_slot, "map_env_i64") - .expect("load") - .into_int_value(); - let env_ptr = cx - .builder - .build_int_to_ptr(env_ptr_i64, ptr_type, "map_env_ptr") - .expect("itp"); - - // Get array length - let arr_len = cx - .rt_call("rt_array_len", &[arr_ptr.into()]) - .unwrap() - .into_int_value(); - - // Allocate result array - let result_ptr = cx - .rt_call("rt_array_alloc", &[arr_len.into()]) - .unwrap() - .into_pointer_value(); - - // Build function type for indirect call: (ptr, elem) -> ret - let param_llvm = turbo_ty_to_llvm_ctx(¶m_tty, cx.context, cx.enum_max_slots); - let fn_type = if ret_tty == TurboTy::Unit { - cx.context - .void_type() - .fn_type(&[ptr_type.into(), param_llvm.into()], false) - } else { - let ret_llvm = turbo_ty_to_llvm_ctx(&ret_tty, cx.context, cx.enum_max_slots); - ret_llvm.fn_type(&[ptr_type.into(), param_llvm.into()], false) - }; - - // Loop: for i in 0..arr_len - let current_fn = cx.current_fn; - let header_block = cx.context.append_basic_block(current_fn, "map_header"); - let body_block = cx.context.append_basic_block(current_fn, "map_body"); - let exit_block = cx.context.append_basic_block(current_fn, "map_exit"); - - // Allocate loop index on the stack - let idx_alloca = cx - .builder - .build_alloca(i64_type, "map_idx") - .expect("alloca"); - cx.builder - .build_store(idx_alloca, i64_type.const_int(0, false)) - .expect("store"); - - cx.builder - .build_unconditional_branch(header_block) - .expect("br"); - cx.builder.position_at_end(header_block); - - let idx = cx - .builder - .build_load(i64_type, idx_alloca, "idx") - .expect("load") - .into_int_value(); - let cond = cx - .builder - .build_int_compare(IntPredicate::SLT, idx, arr_len, "cond") - .expect("cmp"); - cx.builder - .build_conditional_branch(cond, body_block, exit_block) - .expect("br"); - - cx.builder.position_at_end(body_block); - let idx2 = cx - .builder - .build_load(i64_type, idx_alloca, "idx2") - .expect("load") - .into_int_value(); - - // Get element (as i64) - let raw_elem = cx - .rt_call("rt_array_get", &[arr_ptr.into(), idx2.into()]) - .unwrap() - .into_int_value(); - // Narrow to param type - let typed_elem: BasicValueEnum = match ¶m_tty { - TurboTy::Bool => cx - .builder - .build_int_truncate(raw_elem, cx.context.bool_type(), "trunc") - .expect("trunc") - .into(), - TurboTy::Float => cx - .builder - .build_bit_cast(raw_elem, cx.context.f64_type(), "f2i") - .expect("bc") - .into(), - _ => raw_elem.into(), - }; - - // Call closure - let mapped_val = cx - .builder - .build_indirect_call( - fn_type, - fn_ptr, - &[env_ptr.into(), typed_elem.into()], - "mapped", - ) - .expect("indirect_call"); - - // Store result - if let Some(mapped_basic) = mapped_val.try_as_basic_value().left() { - let store_val = widen_for_storage(cx, mapped_basic); - let idx3 = cx - .builder - .build_load(i64_type, idx_alloca, "idx3") - .expect("load") - .into_int_value(); - cx.rt_call( - "rt_array_set", - &[result_ptr.into(), idx3.into(), store_val.into()], - ); - } - - let idx4 = cx - .builder - .build_load(i64_type, idx_alloca, "idx4") - .expect("load") - .into_int_value(); - let one = i64_type.const_int(1, false); - let next_idx = cx - .builder - .build_int_add(idx4, one, "next_idx") - .expect("add"); - cx.builder.build_store(idx_alloca, next_idx).expect("store"); - cx.builder - .build_unconditional_branch(header_block) - .expect("br"); - - cx.builder.position_at_end(exit_block); - - Ok(Some((result_ptr.into(), TurboTy::Array(Box::new(ret_tty))))) -} - -/// compile_builtin_filter_llvm: filter(arr, closure) -> [T] -fn compile_builtin_filter_llvm<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], -) -> Result, CodegenError> { - if args.len() < 2 { - return Ok(None); - } - let (arr_ptr, arr_tty) = compile_expr(cx, &args[0])?.unwrap(); - let (closure_ptr_val, fn_tty) = compile_expr(cx, &args[1])?.unwrap(); - - let elem_tty = match &arr_tty { - TurboTy::Array(inner) => *inner.clone(), - _ => TurboTy::Int, - }; - let param_tty = match &fn_tty { - TurboTy::Fn(params, _) => params[0].clone(), - _ => TurboTy::Int, - }; - - let ptr_type = cx.context.ptr_type(AddressSpace::default()); - let i8_type = cx.context.i8_type(); - let i64_type = cx.context.i64_type(); - - let closure_ptr = closure_ptr_val.into_pointer_value(); - let fn_ptr_i64 = cx - .builder - .build_load(i64_type, closure_ptr, "filt_fn_i64") - .expect("load") - .into_int_value(); - let fn_ptr = cx - .builder - .build_int_to_ptr(fn_ptr_i64, ptr_type, "filt_fn_ptr") - .expect("itp"); - let env_slot = unsafe { - cx.builder - .build_gep( - i8_type, - closure_ptr, - &[i64_type.const_int(8, false)], - "env_slot", - ) - .expect("gep") - }; - let env_ptr_i64 = cx - .builder - .build_load(i64_type, env_slot, "filt_env_i64") - .expect("load") - .into_int_value(); - let env_ptr = cx - .builder - .build_int_to_ptr(env_ptr_i64, ptr_type, "filt_env_ptr") - .expect("itp"); - - let arr_len = cx - .rt_call("rt_array_len", &[arr_ptr.into()]) - .unwrap() - .into_int_value(); - let result_ptr = cx - .rt_call("rt_array_alloc", &[arr_len.into()]) - .unwrap() - .into_pointer_value(); - - let param_llvm = turbo_ty_to_llvm_ctx(¶m_tty, cx.context, cx.enum_max_slots); - let bool_type = cx.context.bool_type(); - let fn_type = bool_type.fn_type(&[ptr_type.into(), param_llvm.into()], false); - - let current_fn = cx.current_fn; - let header_block = cx.context.append_basic_block(current_fn, "filt_header"); - let body_block = cx.context.append_basic_block(current_fn, "filt_body"); - let store_block = cx.context.append_basic_block(current_fn, "filt_store"); - let inc_block = cx.context.append_basic_block(current_fn, "filt_inc"); - let exit_block = cx.context.append_basic_block(current_fn, "filt_exit"); - - let idx_alloca = cx - .builder - .build_alloca(i64_type, "filt_idx") - .expect("alloca"); - let out_idx_alloca = cx - .builder - .build_alloca(i64_type, "filt_out_idx") - .expect("alloca"); - cx.builder - .build_store(idx_alloca, i64_type.const_int(0, false)) - .expect("store"); - cx.builder - .build_store(out_idx_alloca, i64_type.const_int(0, false)) - .expect("store"); - - cx.builder - .build_unconditional_branch(header_block) - .expect("br"); - cx.builder.position_at_end(header_block); - let idx = cx - .builder - .build_load(i64_type, idx_alloca, "idx") - .expect("load") - .into_int_value(); - let cond = cx - .builder - .build_int_compare(IntPredicate::SLT, idx, arr_len, "cond") - .expect("cmp"); - cx.builder - .build_conditional_branch(cond, body_block, exit_block) - .expect("br"); - - cx.builder.position_at_end(body_block); - let idx2 = cx - .builder - .build_load(i64_type, idx_alloca, "idx2") - .expect("load") - .into_int_value(); - let raw_elem = cx - .rt_call("rt_array_get", &[arr_ptr.into(), idx2.into()]) - .unwrap() - .into_int_value(); - let typed_elem: BasicValueEnum = match ¶m_tty { - TurboTy::Bool => cx - .builder - .build_int_truncate(raw_elem, bool_type, "trunc") - .expect("trunc") - .into(), - TurboTy::Float => cx - .builder - .build_bit_cast(raw_elem, cx.context.f64_type(), "bc") - .expect("bc") - .into(), - _ => raw_elem.into(), - }; - let pred_val = cx - .builder - .build_indirect_call( - fn_type, - fn_ptr, - &[env_ptr.into(), typed_elem.into()], - "pred", - ) - .expect("indirect_call"); - let keep = match pred_val.try_as_basic_value().left() { - Some(BasicValueEnum::IntValue(v)) => v, - _ => i64_type.const_int(0, false), - }; - let zero8 = cx.context.bool_type().const_int(0, false); - let keep_bool = cx - .builder - .build_int_compare(IntPredicate::NE, keep, zero8, "keep") - .expect("cmp"); - cx.builder - .build_conditional_branch(keep_bool, store_block, inc_block) - .expect("br"); - - cx.builder.position_at_end(store_block); - let raw_elem2 = cx - .rt_call("rt_array_get", &[arr_ptr.into(), idx2.into()]) - .unwrap() - .into_int_value(); - let out_idx = cx - .builder - .build_load(i64_type, out_idx_alloca, "out_idx") - .expect("load") - .into_int_value(); - cx.rt_call( - "rt_array_set", - &[result_ptr.into(), out_idx.into(), raw_elem2.into()], - ); - let one = i64_type.const_int(1, false); - let next_out = cx - .builder - .build_int_add(out_idx, one, "next_out") - .expect("add"); - cx.builder - .build_store(out_idx_alloca, next_out) - .expect("store"); - cx.builder - .build_unconditional_branch(inc_block) - .expect("br"); - - cx.builder.position_at_end(inc_block); - let idx3 = cx - .builder - .build_load(i64_type, idx_alloca, "idx3") - .expect("load") - .into_int_value(); - let one2 = i64_type.const_int(1, false); - let next_idx = cx - .builder - .build_int_add(idx3, one2, "next_idx") - .expect("add"); - cx.builder.build_store(idx_alloca, next_idx).expect("store"); - cx.builder - .build_unconditional_branch(header_block) - .expect("br"); - - cx.builder.position_at_end(exit_block); - // Update result array length to actual number of kept elements - let final_out_idx = cx - .builder - .build_load(i64_type, out_idx_alloca, "final_out") - .expect("load"); - cx.builder - .build_store(result_ptr, final_out_idx) - .expect("store"); - - Ok(Some((result_ptr.into(), arr_tty))) -} - -/// compile_builtin_reduce_llvm: reduce(arr, init, closure) -> T -fn compile_builtin_reduce_llvm<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], -) -> Result, CodegenError> { - if args.len() < 3 { - return Ok(None); - } - let (arr_ptr, arr_tty) = compile_expr(cx, &args[0])?.unwrap(); - let (init_val, init_tty) = compile_expr(cx, &args[1])?.unwrap(); - let (closure_ptr_val, _fn_tty) = compile_expr(cx, &args[2])?.unwrap(); - - let elem_tty = match &arr_tty { - TurboTy::Array(inner) => *inner.clone(), - _ => TurboTy::Int, - }; - - let ptr_type = cx.context.ptr_type(AddressSpace::default()); - let i8_type = cx.context.i8_type(); - let i64_type = cx.context.i64_type(); - - let closure_ptr = closure_ptr_val.into_pointer_value(); - let fn_ptr_i64 = cx - .builder - .build_load(i64_type, closure_ptr, "red_fn_i64") - .expect("load") - .into_int_value(); - let fn_ptr = cx - .builder - .build_int_to_ptr(fn_ptr_i64, ptr_type, "red_fn_ptr") - .expect("itp"); - let env_slot = unsafe { - cx.builder - .build_gep( - i8_type, - closure_ptr, - &[i64_type.const_int(8, false)], - "env_slot", - ) - .expect("gep") - }; - let env_ptr_i64 = cx - .builder - .build_load(i64_type, env_slot, "red_env_i64") - .expect("load") - .into_int_value(); - let env_ptr = cx - .builder - .build_int_to_ptr(env_ptr_i64, ptr_type, "red_env_ptr") - .expect("itp"); - - let arr_len = cx - .rt_call("rt_array_len", &[arr_ptr.into()]) - .unwrap() - .into_int_value(); - - // Accumulator stored on stack (as i64) - let acc_alloca = cx.builder.build_alloca(i64_type, "acc").expect("alloca"); - let init_i64 = widen_for_storage(cx, init_val); - cx.builder.build_store(acc_alloca, init_i64).expect("store"); - - let elem_llvm = turbo_ty_to_llvm_ctx(&elem_tty, cx.context, cx.enum_max_slots); - let acc_llvm = turbo_ty_to_llvm_ctx(&init_tty, cx.context, cx.enum_max_slots); - // closure: (env, acc, elem) -> acc - let fn_type = acc_llvm.fn_type(&[ptr_type.into(), acc_llvm.into(), elem_llvm.into()], false); - - let current_fn = cx.current_fn; - let header_block = cx.context.append_basic_block(current_fn, "red_header"); - let body_block = cx.context.append_basic_block(current_fn, "red_body"); - let exit_block = cx.context.append_basic_block(current_fn, "red_exit"); - - let idx_alloca = cx - .builder - .build_alloca(i64_type, "red_idx") - .expect("alloca"); - cx.builder - .build_store(idx_alloca, i64_type.const_int(0, false)) - .expect("store"); - - cx.builder - .build_unconditional_branch(header_block) - .expect("br"); - cx.builder.position_at_end(header_block); - let idx = cx - .builder - .build_load(i64_type, idx_alloca, "idx") - .expect("load") - .into_int_value(); - let cond = cx - .builder - .build_int_compare(IntPredicate::SLT, idx, arr_len, "cond") - .expect("cmp"); - cx.builder - .build_conditional_branch(cond, body_block, exit_block) - .expect("br"); - - cx.builder.position_at_end(body_block); - let idx2 = cx - .builder - .build_load(i64_type, idx_alloca, "idx2") - .expect("load") - .into_int_value(); - let raw_elem = cx - .rt_call("rt_array_get", &[arr_ptr.into(), idx2.into()]) - .unwrap() - .into_int_value(); - let acc_i64 = cx - .builder - .build_load(i64_type, acc_alloca, "acc_i64") - .expect("load") - .into_int_value(); - - // Narrow both for the call - let typed_elem: BasicValueEnum = match &elem_tty { - TurboTy::Float => cx - .builder - .build_bit_cast(raw_elem, cx.context.f64_type(), "bc") - .expect("bc") - .into(), - _ => raw_elem.into(), - }; - let typed_acc: BasicValueEnum = match &init_tty { - TurboTy::Float => cx - .builder - .build_bit_cast(acc_i64, cx.context.f64_type(), "bc_acc") - .expect("bc") - .into(), - _ => acc_i64.into(), - }; - - let new_acc = cx - .builder - .build_indirect_call( - fn_type, - fn_ptr, - &[env_ptr.into(), typed_acc.into(), typed_elem.into()], - "new_acc", - ) - .expect("indirect_call"); - - if let Some(new_acc_val) = new_acc.try_as_basic_value().left() { - let new_acc_i64 = widen_for_storage(cx, new_acc_val); - cx.builder - .build_store(acc_alloca, new_acc_i64) - .expect("store"); - } - - let one = i64_type.const_int(1, false); - let idx3 = cx - .builder - .build_load(i64_type, idx_alloca, "idx3") - .expect("load") - .into_int_value(); - let next_idx = cx - .builder - .build_int_add(idx3, one, "next_idx") - .expect("add"); - cx.builder.build_store(idx_alloca, next_idx).expect("store"); - cx.builder - .build_unconditional_branch(header_block) - .expect("br"); - - cx.builder.position_at_end(exit_block); - let final_acc_i64 = cx - .builder - .build_load(i64_type, acc_alloca, "final_acc") - .expect("load") - .into_int_value(); - let final_acc: BasicValueEnum = match &init_tty { - TurboTy::Float => cx - .builder - .build_bit_cast(final_acc_i64, cx.context.f64_type(), "bc_final") - .expect("bc") - .into(), - _ => final_acc_i64.into(), - }; - - Ok(Some((final_acc, init_tty))) -} - -// ── Built-in functions ────────────────────────────────────────────── - -/// Serialize a struct to JSON: {"field1":val1,"field2":val2} -fn compile_struct_to_json_llvm<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - struct_ptr: BasicValueEnum<'ctx>, - struct_name: &str, -) -> Result, CodegenError> { - let struct_layout = cx - .struct_fields - .get(struct_name) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("undefined struct: {struct_name}"), - })? - .clone(); - - let i8_type = cx.context.i8_type(); - let i64_type = cx.context.i64_type(); - let ptr = struct_ptr.into_pointer_value(); - - let mut result: BasicValueEnum = cx.create_string("{")?.into(); - - for (i, (field_name, field_ty)) in struct_layout.iter().enumerate() { - // Add key prefix - let prefix = if i > 0 { - format!(",\"{}\":", field_name) - } else { - format!("\"{}\":", field_name) - }; - let prefix_ptr = cx.create_string(&prefix)?; - result = cx - .rt_call("rt_str_concat", &[result.into(), prefix_ptr.into()]) - .unwrap(); - - // Load field value - let offset = (i * 8) as u64; - let field_ptr = if offset == 0 { - ptr - } else { - unsafe { - cx.builder - .build_gep( - i8_type, - ptr, - &[i64_type.const_int(offset, false)], - "json_field_ptr", - ) - .expect("gep") - } - }; - let raw_val = cx - .builder - .build_load(i64_type, field_ptr, "json_field_val") - .expect("load"); - - // Convert field to JSON string representation - let field_str = match field_ty { - TurboTy::Str => { - let str_ptr = cx - .builder - .build_int_to_ptr( - raw_val.into_int_value(), - cx.context.ptr_type(AddressSpace::default()), - "str_ptr", - ) - .expect("itp"); - let quote = cx.create_string("\"")?; - let tmp = cx - .rt_call("rt_str_concat", &[quote.into(), str_ptr.into()]) - .unwrap(); - let quote2 = cx.create_string("\"")?; - cx.rt_call("rt_str_concat", &[tmp.into(), quote2.into()]) - .unwrap() - } - TurboTy::Int => cx.rt_call("rt_i64_to_str", &[raw_val.into()]).unwrap(), - TurboTy::Bool => { - let bool_val = cx - .builder - .build_int_truncate(raw_val.into_int_value(), cx.context.i8_type(), "trunc") - .expect("trunc"); - cx.rt_call("rt_bool_to_str", &[bool_val.into()]).unwrap() - } - TurboTy::Float => { - let fval = cx - .builder - .build_bit_cast(raw_val.into_int_value(), cx.context.f64_type(), "i2f") - .expect("bc"); - cx.rt_call("rt_f64_to_str", &[fval.into()]).unwrap() - } - _ => cx.rt_call("rt_i64_to_str", &[raw_val.into()]).unwrap(), - }; - - result = cx - .rt_call("rt_str_concat", &[result.into(), field_str.into()]) - .unwrap(); - } - - let suffix = cx.create_string("}")?; - result = cx - .rt_call("rt_str_concat", &[result.into(), suffix.into()]) - .unwrap(); - - Ok(Some((result, TurboTy::Str))) -} - -/// Serialize an array of structs to JSON: [item1,item2,...] -fn compile_array_to_json_llvm<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - arr_val: BasicValueEnum<'ctx>, - struct_name: &str, -) -> Result, CodegenError> { - let i64_type = cx.context.i64_type(); - let arr_len = cx - .rt_call("rt_array_len", &[arr_val.into()]) - .unwrap() - .into_int_value(); - - let mut result: BasicValueEnum = cx.create_string("[")?.into(); - - let current_fn = cx.current_fn; - let header = cx.context.append_basic_block(current_fn, "json_arr_header"); - let body = cx.context.append_basic_block(current_fn, "json_arr_body"); - let exit = cx.context.append_basic_block(current_fn, "json_arr_exit"); - - let idx_alloca = cx - .builder - .build_alloca(i64_type, "json_idx") - .expect("alloca"); - let result_alloca = cx - .builder - .build_alloca(cx.context.ptr_type(AddressSpace::default()), "json_result") - .expect("alloca"); - cx.builder - .build_store(idx_alloca, i64_type.const_int(0, false)) - .expect("store"); - cx.builder - .build_store(result_alloca, result.into_pointer_value()) - .expect("store"); - cx.builder.build_unconditional_branch(header).expect("br"); - - cx.builder.position_at_end(header); - let idx = cx - .builder - .build_load(i64_type, idx_alloca, "idx") - .expect("load") - .into_int_value(); - let cond = cx - .builder - .build_int_compare(IntPredicate::SLT, idx, arr_len, "cond") - .expect("cmp"); - cx.builder - .build_conditional_branch(cond, body, exit) - .expect("br"); - - cx.builder.position_at_end(body); - let idx2 = cx - .builder - .build_load(i64_type, idx_alloca, "idx2") - .expect("load") - .into_int_value(); - let cur_result = cx - .builder - .build_load( - cx.context.ptr_type(AddressSpace::default()), - result_alloca, - "cur", - ) - .expect("load"); - - // Add comma if not first - let zero = i64_type.const_int(0, false); - let is_first = cx - .builder - .build_int_compare(IntPredicate::EQ, idx2, zero, "is_first") - .expect("cmp"); - let comma_ptr = cx.create_string(",")?; - let empty_ptr = cx.create_string("")?; - let sep = cx - .builder - .build_select(is_first, empty_ptr, comma_ptr, "sep") - .expect("select"); - let with_sep = cx - .rt_call("rt_str_concat", &[cur_result.into(), sep.into()]) - .unwrap(); - - // Get element and serialize - let elem = cx - .rt_call("rt_array_get", &[arr_val.into(), idx2.into()]) - .unwrap(); - let elem_ptr = cx - .builder - .build_int_to_ptr( - elem.into_int_value(), - cx.context.ptr_type(AddressSpace::default()), - "elem_ptr", - ) - .expect("itp"); - let sname = struct_name.to_string(); - let (elem_json, _) = compile_struct_to_json_llvm(cx, elem_ptr.into(), &sname)?.unwrap(); - let new_result = cx - .rt_call("rt_str_concat", &[with_sep.into(), elem_json.into()]) - .unwrap(); - cx.builder - .build_store(result_alloca, new_result.into_pointer_value()) - .expect("store"); - - let one = i64_type.const_int(1, false); - let next = cx.builder.build_int_add(idx2, one, "next").expect("add"); - cx.builder.build_store(idx_alloca, next).expect("store"); - cx.builder.build_unconditional_branch(header).expect("br"); - - cx.builder.position_at_end(exit); - let final_result = cx - .builder - .build_load( - cx.context.ptr_type(AddressSpace::default()), - result_alloca, - "final", - ) - .expect("load"); - let suffix = cx.create_string("]")?; - let done = cx - .rt_call("rt_str_concat", &[final_result.into(), suffix.into()]) - .unwrap(); - - Ok(Some((done, TurboTy::Str))) -} - -fn compile_print<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], -) -> Result, CodegenError> { - if args.is_empty() { - let ptr = cx.create_string("")?; - cx.rt_call("rt_print_str", &[ptr.into()]); - return Ok(None); - } - - let result = compile_expr(cx, &args[0])?; - if let Some((v, tty)) = result { - match tty { - TurboTy::Str => { - // Generic functions return i64 even for Str; convert to ptr if needed - let ptr_val: BasicValueEnum = match v { - BasicValueEnum::PointerValue(_) => v, - BasicValueEnum::IntValue(iv) => cx - .builder - .build_int_to_ptr( - iv, - cx.context.ptr_type(AddressSpace::default()), - "str_ptr", - ) - .expect("itp") - .into(), - _ => v, - }; - cx.rt_call("rt_print_str", &[ptr_val.into()]); - } - TurboTy::Float => { - cx.rt_call("rt_print_f64", &[v.into()]); - } - TurboTy::Bool => { - let iv = v.into_int_value(); - let iv = if iv.get_type().get_bit_width() > 8 { - cx.builder - .build_int_truncate(iv, cx.context.i8_type(), "tobool") - .expect("build_int_truncate failed") - } else { - iv - }; - cx.rt_call("rt_print_bool", &[iv.into()]); - } - TurboTy::Int => { - let iv = v.into_int_value(); - let iv = if iv.get_type().get_bit_width() < 64 { - cx.builder - .build_int_s_extend(iv, cx.context.i64_type(), "ext") - .expect("build_int_s_extend failed") - } else { - iv - }; - cx.rt_call("rt_print_i64", &[iv.into()]); - } - TurboTy::Unit => { - let ptr = cx.create_string("()")?; - cx.rt_call("rt_print_str", &[ptr.into()]); - } - TurboTy::Struct(ref sname) => { - // If Display is derived, call StructName__to_string - let sname = sname.clone(); - let to_str_fn = format!("{sname}__to_string"); - if let Some(&ts_fn) = cx.user_fns.get(&to_str_fn) { - let s = cx - .builder - .build_direct_call(ts_fn, &[v.into()], "to_str") - .expect("call") - .try_as_basic_value() - .left() - .unwrap(); - cx.rt_call("rt_print_str", &[s.into()]); - } else { - // No Display, print struct name as placeholder - let ptr = cx.create_string(&format!("<{sname}>"))?; - cx.rt_call("rt_print_str", &[ptr.into()]); - } - } - TurboTy::Array(_) => { - // Print array as a bracketed list via rt_array_print_str if available - let ptr = cx.create_string("")?; - cx.rt_call("rt_print_str", &[ptr.into()]); - } - TurboTy::Enum(_) => { - // Enums: print the integer tag value - let iv = match v { - BasicValueEnum::IntValue(i) => { - if i.get_type().get_bit_width() < 64 { - cx.builder - .build_int_s_extend(i, cx.context.i64_type(), "ext") - .expect("extend") - } else { - i - } - } - BasicValueEnum::PointerValue(p) => cx - .builder - .build_ptr_to_int(p, cx.context.i64_type(), "p2i") - .expect("p2i"), - _ => cx.context.i64_type().const_int(0, false), - }; - cx.rt_call("rt_print_i64", &[iv.into()]); - } - TurboTy::Result(_, _) | TurboTy::Optional(_) => { - let ptr = cx.rt_call("rt_result_to_str", &[v.into()]); - if let Some(s) = ptr { - cx.rt_call("rt_print_str", &[s.into()]); - } else { - let ptr = cx.create_string("")?; - cx.rt_call("rt_print_str", &[ptr.into()]); - } - } - _ => { - // For other types, print as integer (best-effort) - let iv = match v { - BasicValueEnum::IntValue(i) => i, - BasicValueEnum::PointerValue(p) => cx - .builder - .build_ptr_to_int(p, cx.context.i64_type(), "p2i") - .expect("p2i"), - _ => cx.context.i64_type().const_int(0, false), - }; - cx.rt_call("rt_print_i64", &[iv.into()]); - } - } - } - Ok(None) -} - -fn compile_panic<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], -) -> Result, CodegenError> { - if args.is_empty() { - let ptr = cx.create_string("explicit panic")?; - cx.rt_call("rt_panic", &[ptr.into()]); - } else { - let (val, _) = compile_expr(cx, &args[0])?.unwrap(); - cx.rt_call("rt_panic", &[val.into()]); - } - cx.builder - .build_unreachable() - .expect("build_unreachable failed"); - let dead = cx.context.append_basic_block(cx.current_fn, "after_panic"); - cx.builder.position_at_end(dead); - Ok(None) -} - -fn compile_assert<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], -) -> Result, CodegenError> { - if args.is_empty() { - return Ok(None); - } - let (cond, _) = compile_expr(cx, &args[0])?.unwrap(); - let cond_bool = cx.to_bool(cond); - - let fail_block = cx.context.append_basic_block(cx.current_fn, "assert_fail"); - let ok_block = cx.context.append_basic_block(cx.current_fn, "assert_ok"); - - cx.builder - .build_conditional_branch(cond_bool, ok_block, fail_block) - .expect("build_conditional_branch failed"); - - cx.builder.position_at_end(fail_block); - if args.len() > 1 { - let (msg, _) = compile_expr(cx, &args[1])?.unwrap(); - cx.rt_call("rt_assert_fail", &[msg.into()]); - } else { - let ptr = cx.create_string("assertion failed")?; - cx.rt_call("rt_assert_fail", &[ptr.into()]); - } - cx.builder - .build_unreachable() - .expect("build_unreachable failed"); - - cx.builder.position_at_end(ok_block); - Ok(None) -} - -fn compile_assert_eq<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], - negate: bool, -) -> Result, CodegenError> { - if args.len() < 2 { - return Ok(None); - } - let (a, a_tty) = compile_expr(cx, &args[0])?.unwrap(); - let (b, _) = compile_expr(cx, &args[1])?.unwrap(); - - let eq = if matches!(a_tty, TurboTy::Str) || a.is_pointer_value() && b.is_pointer_value() { - // String comparison via rt_str_eq - let eq_val = cx - .rt_call("rt_str_eq", &[a.into(), b.into()]) - .unwrap() - .into_int_value(); - let zero = cx.context.i8_type().const_int(0, false); - let cmp = cx - .builder - .build_int_compare(IntPredicate::NE, eq_val, zero, "str_eq") - .expect("icmp"); - if negate { - cx.builder.build_not(cmp, "not_eq").expect("not") - } else { - cmp - } - } else { - let ai = a.into_int_value(); - let bi = b.into_int_value(); - let pred = if negate { - IntPredicate::NE - } else { - IntPredicate::EQ - }; - cx.builder - .build_int_compare(pred, ai, bi, "assert_eq") - .expect("build_int_compare failed") - }; - - let fail_block = cx - .context - .append_basic_block(cx.current_fn, "assert_eq_fail"); - let ok_block = cx.context.append_basic_block(cx.current_fn, "assert_eq_ok"); - - cx.builder - .build_conditional_branch(eq, ok_block, fail_block) - .expect("build_conditional_branch failed"); - - cx.builder.position_at_end(fail_block); - // Simple assert fail for now - let ptr = cx.create_string("assertion failed: values not equal")?; - cx.rt_call("rt_assert_fail", &[ptr.into()]); - cx.builder - .build_unreachable() - .expect("build_unreachable failed"); - - cx.builder.position_at_end(ok_block); - Ok(None) -} - -fn compile_len<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], -) -> Result, CodegenError> { - if args.is_empty() { - return Ok(None); - } - let (val, tty) = compile_expr(cx, &args[0])?.unwrap(); - let result = match tty { - TurboTy::Str => cx.rt_call("rt_str_len", &[val.into()]).unwrap(), - TurboTy::Array(_) => cx.rt_call("rt_array_len", &[val.into()]).unwrap(), - _ => cx.context.i64_type().const_int(0, false).into(), - }; - Ok(Some((result, TurboTy::Int))) -} - -fn compile_abs<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], -) -> Result, CodegenError> { - if args.is_empty() { - return Ok(None); - } - let (val, tty) = compile_expr(cx, &args[0])?.unwrap(); - match tty { - TurboTy::Int => { - let iv = val.into_int_value(); - let zero = cx.context.i64_type().const_int(0, false); - let neg = cx - .builder - .build_int_neg(iv, "neg") - .expect("build_int_neg failed"); - let is_neg = cx - .builder - .build_int_compare(IntPredicate::SLT, iv, zero, "is_neg") - .expect("build_int_compare failed"); - let result: BasicValueEnum = cx - .builder - .build_select( - is_neg, - BasicValueEnum::IntValue(neg), - BasicValueEnum::IntValue(iv), - "abs", - ) - .expect("build_select failed"); - Ok(Some((result, TurboTy::Int))) - } - TurboTy::Float => { - // Use llvm.fabs intrinsic via negation + select - let fv = val.into_float_value(); - let zero = cx.context.f64_type().const_float(0.0); - let neg = cx - .builder - .build_float_neg(fv, "fneg") - .expect("build_float_neg failed"); - let is_neg = cx - .builder - .build_float_compare(FloatPredicate::OLT, fv, zero, "is_neg") - .expect("build_float_compare failed"); - let result: BasicValueEnum = cx - .builder - .build_select( - is_neg, - BasicValueEnum::FloatValue(neg), - BasicValueEnum::FloatValue(fv), - "fabs", - ) - .expect("build_select failed"); - Ok(Some((result, TurboTy::Float))) - } - _ => Ok(Some((val, tty))), - } -} - -fn compile_min_max<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], - is_min: bool, -) -> Result, CodegenError> { - if args.len() < 2 { - return Ok(None); - } - let (a, tty) = compile_expr(cx, &args[0])?.unwrap(); - let (b, _) = compile_expr(cx, &args[1])?.unwrap(); - - let cmp_pred = if is_min { - IntPredicate::SLT - } else { - IntPredicate::SGT - }; - - let ai = a.into_int_value(); - let bi = b.into_int_value(); - let cond = cx - .builder - .build_int_compare(cmp_pred, ai, bi, "cmp") - .expect("build_int_compare failed"); - let result: BasicValueEnum = cx - .builder - .build_select( - cond, - BasicValueEnum::IntValue(ai), - BasicValueEnum::IntValue(bi), - if is_min { "min" } else { "max" }, - ) - .expect("build_select failed"); - Ok(Some((result, tty))) -} - -fn compile_to_str_builtin<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], -) -> Result, CodegenError> { - if args.is_empty() { - return Ok(None); - } - let (val, tty) = compile_expr(cx, &args[0])?.unwrap(); - let str_val = convert_to_str(cx, val, &tty)?; - Ok(Some((str_val, TurboTy::Str))) -} - -fn compile_stdlib_1arg_rt<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], - rt_name: &str, - ret_tty: TurboTy, -) -> Result, CodegenError> { - if args.is_empty() { - return Ok(None); - } - let (a, _) = compile_expr(cx, &args[0])?.unwrap(); - let result = cx.rt_call(rt_name, &[a.into()]).unwrap(); - Ok(Some((result, ret_tty))) -} - -fn compile_stdlib_2arg_rt<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - args: &[Spanned], - rt_name: &str, - ret_tty: TurboTy, -) -> Result, CodegenError> { - if args.len() < 2 { - return Ok(None); - } - let (a, _) = compile_expr(cx, &args[0])?.unwrap(); - let (b, _) = compile_expr(cx, &args[1])?.unwrap(); - let result = cx.rt_call(rt_name, &[a.into(), b.into()]).unwrap(); - Ok(Some((result, ret_tty))) -} - -#[allow(unused_variables)] -fn compile_method_call<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - obj: BasicValueEnum<'ctx>, - obj_tty: &TurboTy, - field: &str, - args: &[Spanned], -) -> Result, CodegenError> { - Err(CodegenError { - code: ErrorCode::E0400, - message: format!("LLVM: method call `{field}` not yet implemented"), - }) -} diff --git a/turbo/crates/turbo-codegen-llvm/src/ctx.rs b/turbo/crates/turbo-codegen-llvm/src/ctx.rs deleted file mode 100644 index ba97184..0000000 --- a/turbo/crates/turbo-codegen-llvm/src/ctx.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Codegen context struct and its impl block. - -use inkwell::builder::Builder; -use inkwell::context::Context; -use inkwell::module::Module; -use inkwell::values::{ - BasicMetadataValueEnum, BasicValueEnum, FunctionValue, IntValue, PointerValue, -}; -use inkwell::{AddressSpace, IntPredicate}; -use std::collections::HashMap; -use turbo_ast::*; - -use crate::types::TurboTy; -use crate::CodegenError; - -// ── Codegen context ───────────────────────────────────────────────── - -#[allow(dead_code)] -pub(crate) struct Ctx<'a, 'ctx> { - pub(crate) context: &'ctx Context, - pub(crate) module: &'a Module<'ctx>, - pub(crate) builder: &'a Builder<'ctx>, - /// Currently compiled function - pub(crate) current_fn: FunctionValue<'ctx>, - /// User-defined functions - pub(crate) user_fns: &'a HashMap>, - /// Function return types - pub(crate) fn_ret_types: &'a HashMap, - /// Function ASTs (for inlining) - pub(crate) fn_asts: &'a HashMap, - /// Function type params - pub(crate) fn_type_params: &'a HashMap>, - /// Runtime functions - pub(crate) rt_fns: &'a HashMap>, - /// Variable allocas: name -> (alloca ptr, turbo type) - pub(crate) vars: HashMap, TurboTy)>, - /// String literal counter for unique names - pub(crate) string_counter: &'a mut usize, - /// Struct field layouts - pub(crate) struct_fields: &'a HashMap>, - /// Enum variant lists - pub(crate) enum_variants: &'a HashMap>, - /// Data-carrying enum variant fields - pub(crate) enum_variant_fields: &'a HashMap<(String, String), Vec>, - /// Max slots per data enum - pub(crate) enum_max_slots: &'a HashMap, - /// Module-level constants - pub(crate) constants: &'a HashMap>, - /// Loop stack for break/continue: (header_block, exit_block) - pub(crate) loop_stack: Vec<( - inkwell::basic_block::BasicBlock<'ctx>, - inkwell::basic_block::BasicBlock<'ctx>, - )>, - /// Closure functions: span_start -> (fn_name, TurboTy::Fn, free_var_names) - pub(crate) closure_fns: &'a HashMap)>, - /// Spawn thunks: span_start -> thunk_fn_name - pub(crate) spawn_thunks: &'a HashMap, - /// Struct derives: struct_name -> vec of trait names - pub(crate) struct_derives: &'a HashMap>, - /// Trait impls: type_name -> vec of trait names - pub(crate) trait_impls: &'a HashMap>, - /// Concrete field types for generic struct instances: var_name -> [(field, type)] - pub(crate) concrete_struct_fields: HashMap>, -} - -impl<'a, 'ctx> Ctx<'a, 'ctx> { - pub(crate) fn create_string(&mut self, s: &str) -> Result, CodegenError> { - let name = format!(".str.{}", *self.string_counter); - *self.string_counter += 1; - let val = self - .builder - .build_global_string_ptr(s, &name) - .map_err(|e| CodegenError { - code: ErrorCode::E0405, - message: e.to_string(), - })?; - Ok(val.as_pointer_value()) - } - - pub(crate) fn rt_call( - &self, - name: &str, - args: &[BasicMetadataValueEnum<'ctx>], - ) -> Option> { - let func = self.rt_fns[name]; - let call = self - .builder - .build_direct_call(func, args, "") - .expect("build_direct_call failed"); - call.try_as_basic_value().left() - } - - /// Convert a value to i1 boolean for use in conditional branches. - /// LLVM requires `i1` for branch conditions, unlike Cranelift which uses `i8`. - pub(crate) fn to_bool(&self, val: BasicValueEnum<'ctx>) -> IntValue<'ctx> { - match val { - BasicValueEnum::IntValue(iv) => { - let ty = iv.get_type(); - let zero = ty.const_int(0, false); - self.builder - .build_int_compare(IntPredicate::NE, iv, zero, "tobool") - .expect("build_int_compare failed") - } - _ => { - // For non-int types, assume truthy - self.context.bool_type().const_int(1, false) - } - } - } - - /// Create an alloca in the entry block of the current function. - pub(crate) fn create_entry_block_alloca( - &self, - ty: inkwell::types::BasicTypeEnum<'ctx>, - name: &str, - ) -> PointerValue<'ctx> { - let entry = self.current_fn.get_first_basic_block().unwrap(); - let builder = self.context.create_builder(); - match entry.get_first_instruction() { - Some(first_instr) => builder.position_before(&first_instr), - None => builder.position_at_end(entry), - } - builder.build_alloca(ty, name).expect("build_alloca failed") - } -} diff --git a/turbo/crates/turbo-codegen-llvm/src/expr.rs b/turbo/crates/turbo-codegen-llvm/src/expr.rs deleted file mode 100644 index 70d7487..0000000 --- a/turbo/crates/turbo-codegen-llvm/src/expr.rs +++ /dev/null @@ -1,2615 +0,0 @@ -//! Expression compilation: compile_expr, compile_binop, control flow, -//! and value conversion helpers. - -use inkwell::types::{BasicType, BasicTypeEnum}; -use inkwell::values::{BasicValueEnum, IntValue, PointerValue}; -use inkwell::{AddressSpace, FloatPredicate, IntPredicate}; -use std::collections::HashMap; -use turbo_ast::*; - -use crate::builtins::compile_call; -use crate::ctx::Ctx; -use crate::helpers::{collect_free_vars_llvm, lookup_variant_tag}; -use crate::stmt::compile_stmt; -use crate::types::{ - turbo_ty_from_type_expr, turbo_ty_to_llvm, turbo_ty_to_llvm_ctx, MaybeTyped, TurboTy, -}; -use crate::CodegenError; - -// ── Expression compilation ────────────────────────────────────────── - -pub(crate) fn compile_expr<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - expr: &Spanned, -) -> Result, CodegenError> { - match &expr.node { - Expr::IntLit(n) => { - let val = cx.context.i64_type().const_int(*n as u64, true); - Ok(Some((val.into(), TurboTy::Int))) - } - - Expr::FloatLit(f) => { - let val = cx.context.f64_type().const_float(*f); - Ok(Some((val.into(), TurboTy::Float))) - } - - Expr::BoolLit(b) => { - let val = cx.context.i8_type().const_int(*b as u64, false); - Ok(Some((val.into(), TurboTy::Bool))) - } - - Expr::StringLit(s) => { - let ptr = cx.create_string(s)?; - Ok(Some((ptr.into(), TurboTy::Str))) - } - - Expr::Unit => Ok(None), - - Expr::Ident(name) => { - // Check constants first - if let Some(const_expr) = cx.constants.get(name.as_str()) { - let const_expr = const_expr.clone(); - return compile_expr(cx, &const_expr); - } - let (alloca, turbo_ty) = cx.vars.get(name).ok_or_else(|| CodegenError { - code: ErrorCode::E0401, - message: format!("undefined variable: {name}"), - })?; - let turbo_ty = turbo_ty.clone(); - let llvm_ty = turbo_ty_to_llvm_ctx(&turbo_ty, cx.context, cx.enum_max_slots); - let val = cx - .builder - .build_load(llvm_ty, *alloca, name) - .expect("build_load failed"); - Ok(Some((val, turbo_ty))) - } - - Expr::BinaryOp { left, op, right } => { - // Short-circuit for && and || - if *op == BinOp::And || *op == BinOp::Or { - return compile_short_circuit(cx, left, *op, right); - } - - let (lhs, lhs_tty) = compile_expr(cx, left)?.unwrap(); - let (rhs, rhs_tty) = compile_expr(cx, right)?.unwrap(); - - // String operations - if lhs_tty == TurboTy::Str && rhs_tty == TurboTy::Str { - match op { - BinOp::Add => { - let result = cx - .rt_call("rt_str_concat", &[lhs.into(), rhs.into()]) - .unwrap(); - return Ok(Some((result, TurboTy::Str))); - } - BinOp::Eq | BinOp::NotEq => { - let result = cx.rt_call("rt_str_eq", &[lhs.into(), rhs.into()]).unwrap(); - let result_int = result.into_int_value(); - if *op == BinOp::NotEq { - let one = cx.context.i8_type().const_int(1, false); - let flipped = cx - .builder - .build_xor(result_int, one, "neq") - .expect("build_xor failed"); - return Ok(Some((flipped.into(), TurboTy::Bool))); - } - return Ok(Some((result, TurboTy::Bool))); - } - _ => {} - } - } - - // Struct equality: use derived __eq method if available - if let TurboTy::Struct(ref sname) = lhs_tty { - if *op == BinOp::Eq || *op == BinOp::NotEq { - let eq_fn_name = format!("{sname}__eq"); - if let Some(&eq_fn) = cx.user_fns.get(&eq_fn_name) { - let result = cx - .builder - .build_direct_call(eq_fn, &[lhs.into(), rhs.into()], "struct_eq") - .expect("build_direct_call") - .try_as_basic_value() - .left() - .unwrap(); - if *op == BinOp::NotEq { - let one = cx.context.i8_type().const_int(1, false); - let flipped = cx - .builder - .build_xor(result.into_int_value(), one, "neq") - .expect("xor"); - return Ok(Some((flipped.into(), TurboTy::Bool))); - } - return Ok(Some((result, TurboTy::Bool))); - } - // Fallback: pointer comparison - let lp = cx - .builder - .build_ptr_to_int(lhs.into_pointer_value(), cx.context.i64_type(), "lp") - .expect("p2i"); - let rp = cx - .builder - .build_ptr_to_int(rhs.into_pointer_value(), cx.context.i64_type(), "rp") - .expect("p2i"); - let pred = if *op == BinOp::Eq { - IntPredicate::EQ - } else { - IntPredicate::NE - }; - let cmp = cx - .builder - .build_int_compare(pred, lp, rp, "ptr_eq") - .expect("cmp"); - return Ok(Some((cmp.into(), TurboTy::Bool))); - } - } - - // String coercion: str + non-str or non-str + str - if *op == BinOp::Add { - if lhs_tty == TurboTy::Str && rhs_tty != TurboTy::Str { - let rhs_str = convert_to_str(cx, rhs, &rhs_tty)?; - let result = cx - .rt_call("rt_str_concat", &[lhs.into(), rhs_str.into()]) - .unwrap(); - return Ok(Some((result, TurboTy::Str))); - } - if rhs_tty == TurboTy::Str && lhs_tty != TurboTy::Str { - let lhs_str = convert_to_str(cx, lhs, &lhs_tty)?; - let result = cx - .rt_call("rt_str_concat", &[lhs_str.into(), rhs.into()]) - .unwrap(); - return Ok(Some((result, TurboTy::Str))); - } - } - - let result = compile_binop(cx, lhs, *op, rhs)?; - let result_tty = match op { - BinOp::Eq - | BinOp::NotEq - | BinOp::Less - | BinOp::LessEq - | BinOp::Greater - | BinOp::GreaterEq - | BinOp::And - | BinOp::Or => TurboTy::Bool, - _ => lhs_tty, - }; - Ok(Some((result, result_tty))) - } - - Expr::UnaryOp { op, expr: inner } => { - let (val, tty) = compile_expr(cx, inner)?.unwrap(); - let result = match op { - UnaryOp::Neg => match val { - BasicValueEnum::FloatValue(fv) => cx - .builder - .build_float_neg(fv, "fneg") - .expect("build_float_neg failed") - .into(), - BasicValueEnum::IntValue(iv) => cx - .builder - .build_int_neg(iv, "ineg") - .expect("build_int_neg failed") - .into(), - _ => { - return Err(CodegenError { - code: ErrorCode::E0403, - message: "cannot negate this type".to_string(), - }) - } - }, - UnaryOp::Not => { - let iv = val.into_int_value(); - let one = cx.context.i8_type().const_int(1, false); - cx.builder - .build_xor(iv, one, "not") - .expect("build_xor failed") - .into() - } - }; - let result_tty = match op { - UnaryOp::Not => TurboTy::Bool, - UnaryOp::Neg => tty, - }; - Ok(Some((result, result_tty))) - } - - Expr::Call { callee, args } => compile_call(cx, callee, args), - - Expr::If { - condition, - then_branch, - else_branch, - } => compile_if(cx, condition, then_branch, else_branch.as_deref()), - - Expr::IfLet { - pattern, - value, - then_branch, - else_branch, - } => compile_if_let(cx, pattern, value, then_branch, else_branch.as_deref()), - - Expr::Block { stmts, tail_expr } => { - let saved_vars = cx.vars.clone(); - - let mut deferred: Vec<&Spanned> = Vec::new(); - for stmt in stmts { - if let Stmt::Defer(ref defer_expr) = stmt.node { - deferred.push(defer_expr); - } - compile_stmt(cx, stmt)?; - } - let result = if let Some(tail) = tail_expr { - compile_expr(cx, tail) - } else { - Ok(None) - }; - - // Emit deferred expressions in LIFO order - for defer_expr in deferred.iter().rev() { - let block = cx.builder.get_insert_block().unwrap(); - if block.get_terminator().is_none() { - compile_expr(cx, defer_expr)?; - } - } - - cx.vars = saved_vars; - result - } - - Expr::Assign { target, value } => { - let (val, tty) = compile_expr(cx, value)?.unwrap(); - let (alloca, _) = cx.vars.get(target).ok_or_else(|| CodegenError { - code: ErrorCode::E0401, - message: format!("undefined variable: {target}"), - })?; - let alloca = *alloca; - cx.builder - .build_store(alloca, val) - .expect("build_store failed"); - // Update type - if let Some(entry) = cx.vars.get_mut(target) { - entry.1 = tty; - } - Ok(None) - } - - Expr::CompoundAssign { target, op, value } => { - let (rhs, _) = compile_expr(cx, value)?.unwrap(); - let (alloca, turbo_ty) = cx.vars.get(target).ok_or_else(|| CodegenError { - code: ErrorCode::E0401, - message: format!("undefined variable: {target}"), - })?; - let alloca = *alloca; - let turbo_ty = turbo_ty.clone(); - let llvm_ty = turbo_ty_to_llvm_ctx(&turbo_ty, cx.context, cx.enum_max_slots); - let lhs = cx - .builder - .build_load(llvm_ty, alloca, target) - .expect("build_load failed"); - let result = compile_binop(cx, lhs, *op, rhs)?; - cx.builder - .build_store(alloca, result) - .expect("build_store failed"); - Ok(None) - } - - Expr::FieldAssign { - object, - field, - value, - } => { - let (obj_ptr, obj_tty) = compile_expr(cx, object)?.unwrap(); - let (val, _) = compile_expr(cx, value)?.unwrap(); - - let struct_name = match &obj_tty { - TurboTy::Struct(name) => name.clone(), - _ => { - return Err(CodegenError { - code: ErrorCode::E0400, - message: "field assignment on non-struct type".to_string(), - }) - } - }; - - let struct_layout = cx - .struct_fields - .get(&struct_name) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("undefined struct: {struct_name}"), - })? - .clone(); - - let field_index = struct_layout - .iter() - .position(|(n, _)| n == field) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("struct `{struct_name}` has no field `{field}`"), - })?; - - let offset = field_index as u64 * 8; - let obj_ptr_val = obj_ptr.into_pointer_value(); - - // GEP to field offset - let field_ptr = unsafe { - cx.builder - .build_gep( - cx.context.i8_type(), - obj_ptr_val, - &[cx.context.i64_type().const_int(offset, false)], - "field_ptr", - ) - .expect("build_gep failed") - }; - - // Widen to i64 for uniform storage - let store_val = widen_for_storage(cx, val); - cx.builder - .build_store(field_ptr, store_val) - .expect("build_store failed"); - Ok(None) - } - - Expr::IndexAssign { - object, - index, - value, - } => { - let (arr, _) = compile_expr(cx, object)?.unwrap(); - let (idx, _) = compile_expr(cx, index)?.unwrap(); - let (val, _) = compile_expr(cx, value)?.unwrap(); - - let store_val = widen_for_storage(cx, val); - let new_arr = cx - .rt_call("rt_array_set", &[arr.into(), idx.into(), store_val.into()]) - .unwrap(); - - // Update the variable to point to the (possibly new) array - if let Expr::Ident(name) = &object.node { - if let Some((alloca, _)) = cx.vars.get(name) { - cx.builder - .build_store(*alloca, new_arr) - .expect("build_store failed"); - } - } - - Ok(None) - } - - Expr::While { condition, body } => compile_while(cx, condition, body), - - Expr::ForIn { - var_name, - iterable, - body, - } => compile_for_in(cx, var_name, iterable, body), - - Expr::ArrayLit(elems) => { - let len = elems.len() as u64; - let len_val = cx.context.i64_type().const_int(len, false); - let arr = cx.rt_call("rt_array_alloc", &[len_val.into()]).unwrap(); - - let mut elem_tty = TurboTy::Int; - for (i, elem) in elems.iter().enumerate() { - let (val, tty) = compile_expr(cx, elem)?.unwrap(); - if i == 0 { - elem_tty = tty; - } - let idx = cx.context.i64_type().const_int(i as u64, false); - let store_val = widen_for_storage(cx, val); - cx.rt_call("rt_array_set", &[arr.into(), idx.into(), store_val.into()]); - } - - Ok(Some((arr, TurboTy::Array(Box::new(elem_tty))))) - } - - Expr::Index { object, index } => { - let (obj, obj_tty) = compile_expr(cx, object)?.unwrap(); - let (idx, _) = compile_expr(cx, index)?.unwrap(); - - let elem_tty = match &obj_tty { - TurboTy::Array(inner) => *inner.clone(), - _ => TurboTy::Int, - }; - - let raw = cx - .rt_call("rt_array_get", &[obj.into(), idx.into()]) - .unwrap(); - - // Narrow the result back from i64 to the element type - let result = narrow_from_storage(cx, raw, &elem_tty); - Ok(Some((result, elem_tty))) - } - - Expr::StructLit { name, fields } => { - let struct_layout = cx - .struct_fields - .get(name) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("undefined struct: {name}"), - })? - .clone(); - - let num_fields = struct_layout.len() as u64; - let num_fields_val = cx.context.i64_type().const_int(num_fields, false); - let ptr = cx - .rt_call("rt_struct_alloc", &[num_fields_val.into()]) - .unwrap() - .into_pointer_value(); - - let mut concrete_fields: Vec<(String, TurboTy)> = Vec::new(); - for (field_name, field_expr) in fields { - let (val, val_tty) = compile_expr(cx, field_expr)?.unwrap(); - concrete_fields.push((field_name.clone(), val_tty)); - let field_index = struct_layout - .iter() - .position(|(n, _)| n == field_name) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("struct `{name}` has no field `{field_name}`"), - })?; - - let offset = field_index as u64 * 8; - let field_ptr = unsafe { - cx.builder - .build_gep( - cx.context.i8_type(), - ptr, - &[cx.context.i64_type().const_int(offset, false)], - "field_ptr", - ) - .expect("build_gep failed") - }; - - let store_val = widen_for_storage(cx, val); - cx.builder - .build_store(field_ptr, store_val) - .expect("build_store failed"); - } - - let result_tty = TurboTy::Struct(name.clone()); - // Store concrete field types for generic struct tracking - // Use a temp key "__last_struct_lit" that Let binding will pick up - if !concrete_fields.is_empty() { - cx.concrete_struct_fields - .insert("__last_struct_lit".to_string(), concrete_fields); - } - Ok(Some((ptr.into(), result_tty))) - } - - Expr::FieldAccess { object, field } => { - // Check if this is an enum variant access: EnumName.VariantName - if let Expr::Ident(ref name) = object.node { - if let Some(variants) = cx.enum_variants.get(name.as_str()) { - let index = - variants - .iter() - .position(|v| v == field) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("enum `{name}` has no variant `{field}`"), - })?; - - if let Some(&max_slots) = cx.enum_max_slots.get(name.as_str()) { - // Data-carrying enum: allocate tagged union - let total_slots = 1 + max_slots; - let num_fields_val = - cx.context.i64_type().const_int(total_slots as u64, false); - let ptr = cx - .rt_call("rt_struct_alloc", &[num_fields_val.into()]) - .unwrap() - .into_pointer_value(); - let tag_val = cx.context.i64_type().const_int(index as u64, false); - cx.builder - .build_store(ptr, tag_val) - .expect("build_store failed"); - return Ok(Some((ptr.into(), TurboTy::Enum(name.clone())))); - } else { - let val = cx.context.i64_type().const_int(index as u64, false); - return Ok(Some((val.into(), TurboTy::Enum(name.clone())))); - } - } - } - - let (obj, obj_tty) = compile_expr(cx, object)?.unwrap(); - - let struct_name = match &obj_tty { - TurboTy::Struct(name) => name.clone(), - _ => { - return Err(CodegenError { - code: ErrorCode::E0400, - message: format!("field access on non-struct type: {field}"), - }) - } - }; - - let struct_layout = cx - .struct_fields - .get(&struct_name) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("undefined struct: {struct_name}"), - })? - .clone(); - - let (field_index, (_, field_tty)) = struct_layout - .iter() - .enumerate() - .find(|(_, (n, _))| n == field) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("struct `{struct_name}` has no field `{field}`"), - })?; - - // Check if we have concrete field types (from generic struct instantiation) - let concrete_tty = if let Expr::Ident(ref var_name) = object.node { - cx.concrete_struct_fields.get(var_name).and_then(|fields| { - fields - .iter() - .find(|(n, _)| n == field) - .map(|(_, t)| t.clone()) - }) - } else { - None - }; - let field_tty = concrete_tty.unwrap_or_else(|| field_tty.clone()); - - let offset = field_index as u64 * 8; - let obj_ptr = obj.into_pointer_value(); - - let field_ptr = unsafe { - cx.builder - .build_gep( - cx.context.i8_type(), - obj_ptr, - &[cx.context.i64_type().const_int(offset, false)], - "field_ptr", - ) - .expect("build_gep failed") - }; - - // Load as i64 then narrow to the field type - let raw = cx - .builder - .build_load(cx.context.i64_type(), field_ptr, field) - .expect("build_load failed"); - let result = narrow_from_storage(cx, raw, &field_tty); - Ok(Some((result, field_tty))) - } - - Expr::EnumVariant { enum_name, variant } => { - let variants = cx - .enum_variants - .get(enum_name) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("undefined enum: {enum_name}"), - })?; - let variant_index = - variants - .iter() - .position(|v| v == variant) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("enum `{enum_name}` has no variant `{variant}`"), - })?; - let val = cx.context.i64_type().const_int(variant_index as u64, false); - Ok(Some((val.into(), TurboTy::Enum(enum_name.clone())))) - } - - Expr::Match { subject, arms } => compile_match(cx, subject, arms), - - Expr::Interpolation(parts) => { - let empty_str = cx.create_string("")?; - let mut result: BasicValueEnum<'ctx> = empty_str.into(); - - for part in parts { - match part { - InterpolPart::Lit(s) => { - let lit_ptr = cx.create_string(s)?; - result = cx - .rt_call("rt_str_concat", &[result.into(), lit_ptr.into()]) - .unwrap(); - } - InterpolPart::Expr(e) => { - let (val, tty) = compile_expr(cx, e)?.unwrap(); - let str_val = convert_to_str(cx, val, &tty)?; - result = cx - .rt_call("rt_str_concat", &[result.into(), str_val.into()]) - .unwrap(); - } - } - } - - Ok(Some((result, TurboTy::Str))) - } - - Expr::Range { start, end } => { - // Ranges are only used inside for-in, but if used standalone, return a tuple-like thing - let (start_val, _) = compile_expr(cx, start)?.unwrap(); - let (end_val, _) = compile_expr(cx, end)?.unwrap(); - // Store as array [start, end] - let len_val = cx.context.i64_type().const_int(2, false); - let arr = cx.rt_call("rt_array_alloc", &[len_val.into()]).unwrap(); - let idx0 = cx.context.i64_type().const_int(0, false); - let idx1 = cx.context.i64_type().const_int(1, false); - cx.rt_call("rt_array_set", &[arr.into(), idx0.into(), start_val.into()]); - cx.rt_call("rt_array_set", &[arr.into(), idx1.into(), end_val.into()]); - Ok(Some((arr, TurboTy::Array(Box::new(TurboTy::Int))))) - } - - Expr::OkExpr(inner) => { - let (val, _) = compile_expr(cx, inner)?.unwrap(); - let val_i64 = widen_for_storage(cx, val); - let result = cx.rt_call("rt_result_ok", &[val_i64.into()]).unwrap(); - Ok(Some(( - result, - TurboTy::Result(Box::new(TurboTy::Int), Box::new(TurboTy::Str)), - ))) - } - - Expr::ErrExpr(inner) => { - let (val, _) = compile_expr(cx, inner)?.unwrap(); - let val_i64 = widen_for_storage(cx, val); - let result = cx.rt_call("rt_result_err", &[val_i64.into()]).unwrap(); - Ok(Some(( - result, - TurboTy::Result(Box::new(TurboTy::Int), Box::new(TurboTy::Str)), - ))) - } - - Expr::SomeExpr(inner) => { - let (val, _) = compile_expr(cx, inner)?.unwrap(); - let val_i64 = widen_for_storage(cx, val); - let result = cx.rt_call("rt_option_some", &[val_i64.into()]).unwrap(); - Ok(Some((result, TurboTy::Optional(Box::new(TurboTy::Int))))) - } - - Expr::NoneExpr => { - let result = cx.rt_call("rt_option_none", &[]).unwrap(); - Ok(Some((result, TurboTy::Optional(Box::new(TurboTy::Int))))) - } - - Expr::NullCoalesce { value, default } => { - let (val, val_tty) = compile_expr(cx, value)?.unwrap(); - // Get the tag - let tag = cx - .rt_call("rt_option_tag", &[val.into()]) - .unwrap() - .into_int_value(); - let zero = cx.context.i64_type().const_int(0, false); - let is_none = cx - .builder - .build_int_compare(IntPredicate::EQ, tag, zero, "is_none") - .expect("build_int_compare failed"); - - let then_block = cx - .context - .append_basic_block(cx.current_fn, "coalesce_none"); - let else_block = cx - .context - .append_basic_block(cx.current_fn, "coalesce_some"); - let merge_block = cx - .context - .append_basic_block(cx.current_fn, "coalesce_merge"); - - cx.builder - .build_conditional_branch(is_none, then_block, else_block) - .expect("build_conditional_branch failed"); - - // None case: use default - cx.builder.position_at_end(then_block); - let (default_val, default_tty) = compile_expr(cx, default)?.unwrap(); - let then_end_block = cx.builder.get_insert_block().unwrap(); - cx.builder - .build_unconditional_branch(merge_block) - .expect("build_unconditional_branch failed"); - - // Some case: unwrap - cx.builder.position_at_end(else_block); - let unwrapped = cx.rt_call("rt_option_value", &[val.into()]).unwrap(); - let inner_tty = match &val_tty { - TurboTy::Optional(inner) => *inner.clone(), - _ => TurboTy::Int, - }; - let unwrapped = narrow_from_storage(cx, unwrapped, &inner_tty); - let else_end_block = cx.builder.get_insert_block().unwrap(); - cx.builder - .build_unconditional_branch(merge_block) - .expect("build_unconditional_branch failed"); - - cx.builder.position_at_end(merge_block); - let phi = cx - .builder - .build_phi(default_val.get_type(), "coalesce") - .expect("build_phi failed"); - phi.add_incoming(&[(&default_val, then_end_block), (&unwrapped, else_end_block)]); - - Ok(Some((phi.as_basic_value(), default_tty))) - } - - Expr::OptionalChain { object, field } => compile_optional_chain(cx, object, field), - - Expr::Closure { params, .. } => { - // Look up the pre-extracted closure function by span start - let span_start = expr.span.start; - let (closure_name, closure_ty, free_vars) = cx - .closure_fns - .get(&span_start) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: "internal error: closure not found in pre-compiled map".to_string(), - })? - .clone(); - - let func = *cx - .user_fns - .get(closure_name.as_str()) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!( - "internal error: closure function {} not found", - closure_name - ), - })?; - - // Get the function pointer as an i64 (pointer-sized integer) - let fn_ptr = func.as_global_value().as_pointer_value(); - - // Determine captures: free variables that actually exist in scope - let mut bound_params: Vec = params.iter().map(|p| p.name.clone()).collect(); - let mut all_free: Vec = Vec::new(); - collect_free_vars_llvm(&expr.node, &mut bound_params, &mut all_free); - let capture_names: Vec = free_vars - .iter() - .filter(|n| cx.vars.contains_key(*n)) - .cloned() - .collect(); - - let ptr_type = cx.context.ptr_type(AddressSpace::default()); - let i8_type = cx.context.i8_type(); - let i64_type = cx.context.i64_type(); - - // Allocate environment struct for captured variables - let env_ptr = if !capture_names.is_empty() { - let num_captures = i64_type.const_int(capture_names.len() as u64, false); - let env_ptr = cx - .rt_call("rt_struct_alloc", &[num_captures.into()]) - .unwrap() - .into_pointer_value(); - - // Store each captured variable into the env struct - for (cap_idx, cap_name) in capture_names.iter().enumerate() { - let (alloca, cap_tty) = cx - .vars - .get(cap_name) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("capture variable {} not found", cap_name), - })? - .clone(); - let val = cx - .builder - .build_load( - turbo_ty_to_llvm_ctx(&cap_tty, cx.context, cx.enum_max_slots), - alloca, - cap_name, - ) - .expect("build_load failed"); - let val_i64 = widen_for_storage(cx, val); - let offset = (cap_idx as u64) * 8; - let field_ptr = unsafe { - cx.builder - .build_gep( - i8_type, - env_ptr, - &[i64_type.const_int(offset, false)], - "cap_ptr", - ) - .expect("build_gep failed") - }; - cx.builder - .build_store(field_ptr, val_i64) - .expect("build_store failed"); - } - env_ptr.into() - } else { - // No captures: null pointer - ptr_type.const_null().into() - }; - - // Allocate closure pair: [fn_ptr_as_i64, env_ptr_as_i64] - let two = i64_type.const_int(2, false); - let closure_ptr = cx - .rt_call("rt_struct_alloc", &[two.into()]) - .unwrap() - .into_pointer_value(); - - // Store fn_ptr at slot 0 (as i64) - let fn_ptr_i64 = cx - .builder - .build_ptr_to_int(fn_ptr, i64_type, "fn_ptr_i64") - .expect("build_ptr_to_int failed"); - cx.builder - .build_store(closure_ptr, fn_ptr_i64) - .expect("build_store failed"); - - // Store env_ptr at slot 1 (offset 8) - let env_slot = unsafe { - cx.builder - .build_gep( - i8_type, - closure_ptr, - &[i64_type.const_int(8, false)], - "env_slot", - ) - .expect("build_gep failed") - }; - let env_i64: BasicValueEnum = match env_ptr { - BasicValueEnum::PointerValue(pv) => cx - .builder - .build_ptr_to_int(pv, i64_type, "env_i64") - .expect("pti") - .into(), - other => other, - }; - cx.builder - .build_store(env_slot, env_i64) - .expect("build_store failed"); - - Ok(Some((closure_ptr.into(), closure_ty))) - } - - Expr::Await(inner) => { - let result = compile_expr(cx, inner)?; - if let Some((val, tty)) = result { - match tty { - TurboTy::Future(inner_tty) => { - let joined = cx.rt_call("rt_await_handle", &[val.into()]).unwrap(); - let narrowed = narrow_from_storage(cx, joined, &inner_tty); - Ok(Some((narrowed, *inner_tty))) - } - _ => Ok(Some((val, tty))), - } - } else { - Ok(None) - } - } - - Expr::Spawn(inner) => { - let span_start = expr.span.start; - if let Some(thunk_name) = cx.spawn_thunks.get(&span_start).cloned() { - if let Expr::Call { callee, args } = &inner.node { - if let Expr::Ident(callee_name) = &callee.node { - let inner_ret_tty = cx - .fn_ret_types - .get(callee_name.as_str()) - .cloned() - .unwrap_or(TurboTy::Unit); - - let target_func = - *cx.user_fns - .get(callee_name.as_str()) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0402, - message: format!("spawn: unknown function `{}`", callee_name), - })?; - let target_fn_ptr = target_func.as_global_value().as_pointer_value(); - - // Compile all arguments - let mut arg_vals: Vec = Vec::new(); - for arg in args { - if let Some((val, _tty)) = compile_expr(cx, arg)? { - let val_i64 = widen_for_storage(cx, val); - arg_vals.push(val_i64.into()); - } - } - - let i8_type = cx.context.i8_type(); - let i64_type = cx.context.i64_type(); - let ptr_type = cx.context.ptr_type(AddressSpace::default()); - - // Allocate args struct: [fn_ptr, arg0, arg1, ...] - let num_slots = i64_type.const_int((1 + arg_vals.len()) as u64, false); - let args_ptr = cx - .rt_call("rt_struct_alloc", &[num_slots.into()]) - .unwrap() - .into_pointer_value(); - - // Store fn_ptr at offset 0 - let fn_ptr_i64 = cx - .builder - .build_ptr_to_int(target_fn_ptr, i64_type, "spawn_fn_i64") - .expect("pti"); - cx.builder.build_store(args_ptr, fn_ptr_i64).expect("store"); - - // Store args at offsets 8, 16, 24, ... - for (i, val) in arg_vals.iter().enumerate() { - let offset = ((i + 1) * 8) as u64; - let slot = unsafe { - cx.builder - .build_gep( - i8_type, - args_ptr, - &[i64_type.const_int(offset, false)], - "arg_slot", - ) - .expect("gep") - }; - cx.builder.build_store(slot, *val).expect("store"); - } - - // Get the thunk function address - let thunk_func = - *cx.user_fns - .get(thunk_name.as_str()) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0405, - message: format!("spawn: thunk `{}` not found", thunk_name), - })?; - let thunk_fn_ptr = thunk_func.as_global_value().as_pointer_value(); - - // rt_spawn_with_args(thunk_ptr: ptr, args_ptr: ptr) -> ptr (handle) - let handle = cx - .rt_call( - "rt_spawn_with_args", - &[thunk_fn_ptr.into(), args_ptr.into()], - ) - .unwrap(); - - return Ok(Some((handle, TurboTy::Future(Box::new(inner_ret_tty))))); - } - } - } - // Fallback: compile inner expression synchronously - compile_expr(cx, inner) - } - - Expr::Try(inner) => { - let (val, val_tty) = compile_expr(cx, inner)?.unwrap(); - // Check tag: 0 = Ok, 1 = Err - let tag = cx - .rt_call("rt_result_tag", &[val.into()]) - .unwrap() - .into_int_value(); - let one = cx.context.i64_type().const_int(1, false); - let is_err = cx - .builder - .build_int_compare(IntPredicate::EQ, tag, one, "is_err") - .expect("build_int_compare failed"); - - let err_block = cx.context.append_basic_block(cx.current_fn, "try_err"); - let ok_block = cx.context.append_basic_block(cx.current_fn, "try_ok"); - - cx.builder - .build_conditional_branch(is_err, err_block, ok_block) - .expect("build_conditional_branch failed"); - - // Error path: propagate - cx.builder.position_at_end(err_block); - let err_val = cx.rt_call("rt_result_value", &[val.into()]).unwrap(); - let err_result = cx.rt_call("rt_result_err", &[err_val.into()]).unwrap(); - cx.builder - .build_return(Some(&err_result)) - .expect("build_return failed"); - - // Ok path: unwrap - cx.builder.position_at_end(ok_block); - let ok_val = cx.rt_call("rt_result_value", &[val.into()]).unwrap(); - let inner_tty = match &val_tty { - TurboTy::Result(ok, _) => *ok.clone(), - _ => TurboTy::Int, - }; - let narrowed = narrow_from_storage(cx, ok_val, &inner_tty); - Ok(Some((narrowed, inner_tty))) - } - - Expr::Break => { - if let Some((_, exit_block)) = cx.loop_stack.last() { - cx.builder - .build_unconditional_branch(*exit_block) - .expect("build_unconditional_branch failed"); - // Create unreachable block for subsequent code - let dead_block = cx.context.append_basic_block(cx.current_fn, "after_break"); - cx.builder.position_at_end(dead_block); - } - Ok(None) - } - - Expr::Continue => { - if let Some((header_block, _)) = cx.loop_stack.last() { - cx.builder - .build_unconditional_branch(*header_block) - .expect("build_unconditional_branch failed"); - let dead_block = cx - .context - .append_basic_block(cx.current_fn, "after_continue"); - cx.builder.position_at_end(dead_block); - } - Ok(None) - } - - Expr::MapLit(entries) => { - let map = cx.rt_call("rt_hashmap_new", &[]).unwrap(); - for (key, value) in entries { - let (k, _) = compile_expr(cx, key)?.unwrap(); - let (v, _) = compile_expr(cx, value)?.unwrap(); - cx.rt_call("rt_hashmap_set", &[map.into(), k.into(), v.into()]); - } - Ok(Some((map, TurboTy::Int))) - } - } -} - -// ── Binary operations ─────────────────────────────────────────────── - -pub(crate) fn compile_binop<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - lhs: BasicValueEnum<'ctx>, - op: BinOp, - rhs: BasicValueEnum<'ctx>, -) -> Result, CodegenError> { - // Float operations - if let (BasicValueEnum::FloatValue(lf), BasicValueEnum::FloatValue(rf)) = (lhs, rhs) { - let result = match op { - BinOp::Add => cx - .builder - .build_float_add(lf, rf, "fadd") - .expect("build_float_add failed") - .into(), - BinOp::Sub => cx - .builder - .build_float_sub(lf, rf, "fsub") - .expect("build_float_sub failed") - .into(), - BinOp::Mul => cx - .builder - .build_float_mul(lf, rf, "fmul") - .expect("build_float_mul failed") - .into(), - BinOp::Div => cx - .builder - .build_float_div(lf, rf, "fdiv") - .expect("build_float_div failed") - .into(), - BinOp::Mod => cx - .builder - .build_float_rem(lf, rf, "fmod") - .expect("build_float_rem failed") - .into(), - BinOp::Eq => cx - .builder - .build_float_compare(FloatPredicate::OEQ, lf, rf, "feq") - .expect("build_float_compare failed") - .into(), - BinOp::NotEq => cx - .builder - .build_float_compare(FloatPredicate::ONE, lf, rf, "fneq") - .expect("build_float_compare failed") - .into(), - BinOp::Less => cx - .builder - .build_float_compare(FloatPredicate::OLT, lf, rf, "flt") - .expect("build_float_compare failed") - .into(), - BinOp::LessEq => cx - .builder - .build_float_compare(FloatPredicate::OLE, lf, rf, "fle") - .expect("build_float_compare failed") - .into(), - BinOp::Greater => cx - .builder - .build_float_compare(FloatPredicate::OGT, lf, rf, "fgt") - .expect("build_float_compare failed") - .into(), - BinOp::GreaterEq => cx - .builder - .build_float_compare(FloatPredicate::OGE, lf, rf, "fge") - .expect("build_float_compare failed") - .into(), - _ => { - return Err(CodegenError { - code: ErrorCode::E0403, - message: format!("unsupported float op: {op:?}"), - }) - } - }; - // Widen i1 comparison results to i8 for consistent Bool representation - let result = widen_i1_to_i8(cx, result); - return Ok(result); - } - - // Integer operations - let li = lhs.into_int_value(); - let ri = rhs.into_int_value(); - - // Widen mismatched widths - let (li, ri) = if li.get_type().get_bit_width() != ri.get_type().get_bit_width() { - let target_bits = li - .get_type() - .get_bit_width() - .max(ri.get_type().get_bit_width()); - let target_type = cx.context.custom_width_int_type(target_bits); - let li = if li.get_type().get_bit_width() < target_bits { - cx.builder - .build_int_s_extend(li, target_type, "sext") - .expect("build_int_s_extend failed") - } else { - li - }; - let ri = if ri.get_type().get_bit_width() < target_bits { - cx.builder - .build_int_s_extend(ri, target_type, "sext") - .expect("build_int_s_extend failed") - } else { - ri - }; - (li, ri) - } else { - (li, ri) - }; - - let result: BasicValueEnum = match op { - BinOp::Add => cx - .builder - .build_int_add(li, ri, "iadd") - .expect("build_int_add failed") - .into(), - BinOp::Sub => cx - .builder - .build_int_sub(li, ri, "isub") - .expect("build_int_sub failed") - .into(), - BinOp::Mul => cx - .builder - .build_int_mul(li, ri, "imul") - .expect("build_int_mul failed") - .into(), - BinOp::Div => { - emit_div_zero_check(cx, ri); - cx.builder - .build_int_signed_div(li, ri, "sdiv") - .expect("build_int_signed_div failed") - .into() - } - BinOp::Mod => { - emit_div_zero_check(cx, ri); - cx.builder - .build_int_signed_rem(li, ri, "srem") - .expect("build_int_signed_rem failed") - .into() - } - BinOp::Eq => cx - .builder - .build_int_compare(IntPredicate::EQ, li, ri, "ieq") - .expect("build_int_compare failed") - .into(), - BinOp::NotEq => cx - .builder - .build_int_compare(IntPredicate::NE, li, ri, "ineq") - .expect("build_int_compare failed") - .into(), - BinOp::Less => cx - .builder - .build_int_compare(IntPredicate::SLT, li, ri, "ilt") - .expect("build_int_compare failed") - .into(), - BinOp::LessEq => cx - .builder - .build_int_compare(IntPredicate::SLE, li, ri, "ile") - .expect("build_int_compare failed") - .into(), - BinOp::Greater => cx - .builder - .build_int_compare(IntPredicate::SGT, li, ri, "igt") - .expect("build_int_compare failed") - .into(), - BinOp::GreaterEq => cx - .builder - .build_int_compare(IntPredicate::SGE, li, ri, "ige") - .expect("build_int_compare failed") - .into(), - BinOp::And => cx - .builder - .build_and(li, ri, "and") - .expect("build_and failed") - .into(), - BinOp::Or => cx - .builder - .build_or(li, ri, "or") - .expect("build_or failed") - .into(), - }; - // Widen i1 comparison results to i8 for consistent Bool representation - let result = widen_i1_to_i8(cx, result); - Ok(result) -} - -/// If a value is i1 (LLVM comparison result), widen it to i8 (Turbo Bool type). -pub(crate) fn widen_i1_to_i8<'a, 'ctx>( - cx: &Ctx<'a, 'ctx>, - val: BasicValueEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - if let BasicValueEnum::IntValue(iv) = val { - if iv.get_type().get_bit_width() == 1 { - return cx - .builder - .build_int_z_extend(iv, cx.context.i8_type(), "i1_to_i8") - .expect("build_int_z_extend failed") - .into(); - } - } - val -} - -fn emit_div_zero_check<'a, 'ctx>(cx: &mut Ctx<'a, 'ctx>, divisor: IntValue<'ctx>) { - let zero = divisor.get_type().const_int(0, false); - let is_zero = cx - .builder - .build_int_compare(IntPredicate::EQ, divisor, zero, "divzero") - .expect("build_int_compare failed"); - - let trap_block = cx.context.append_basic_block(cx.current_fn, "div_trap"); - let ok_block = cx.context.append_basic_block(cx.current_fn, "div_ok"); - - cx.builder - .build_conditional_branch(is_zero, trap_block, ok_block) - .expect("build_conditional_branch failed"); - - cx.builder.position_at_end(trap_block); - cx.rt_call("rt_div_by_zero", &[]); - cx.builder - .build_unreachable() - .expect("build_unreachable failed"); - - cx.builder.position_at_end(ok_block); -} - -// ── Short-circuit && / || ─────────────────────────────────────────── - -fn compile_short_circuit<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - left: &Spanned, - op: BinOp, - right: &Spanned, -) -> Result, CodegenError> { - let (lhs, _) = compile_expr(cx, left)?.unwrap(); - let lhs_bool = cx.to_bool(lhs); - - let eval_rhs_block = cx.context.append_basic_block(cx.current_fn, "sc_rhs"); - let merge_block = cx.context.append_basic_block(cx.current_fn, "sc_merge"); - - let current_block = cx.builder.get_insert_block().unwrap(); - - match op { - BinOp::And => { - cx.builder - .build_conditional_branch(lhs_bool, eval_rhs_block, merge_block) - .expect("build_conditional_branch failed"); - } - BinOp::Or => { - cx.builder - .build_conditional_branch(lhs_bool, merge_block, eval_rhs_block) - .expect("build_conditional_branch failed"); - } - _ => unreachable!(), - } - - cx.builder.position_at_end(eval_rhs_block); - let (rhs, _) = compile_expr(cx, right)?.unwrap(); - let rhs_bool = cx.to_bool(rhs); - let rhs_end_block = cx.builder.get_insert_block().unwrap(); - cx.builder - .build_unconditional_branch(merge_block) - .expect("build_unconditional_branch failed"); - - cx.builder.position_at_end(merge_block); - let phi = cx - .builder - .build_phi(cx.context.bool_type(), "sc_result") - .expect("build_phi failed"); - - match op { - BinOp::And => { - let false_val = cx.context.bool_type().const_int(0, false); - phi.add_incoming(&[(&false_val, current_block), (&rhs_bool, rhs_end_block)]); - } - BinOp::Or => { - let true_val = cx.context.bool_type().const_int(1, false); - phi.add_incoming(&[(&true_val, current_block), (&rhs_bool, rhs_end_block)]); - } - _ => unreachable!(), - } - - // Widen i1 back to i8 for consistent Bool representation - let result = cx - .builder - .build_int_z_extend( - phi.as_basic_value().into_int_value(), - cx.context.i8_type(), - "sc_zext", - ) - .expect("build_int_z_extend failed"); - Ok(Some((result.into(), TurboTy::Bool))) -} - -// ── If/else ───────────────────────────────────────────────────────── - -fn compile_if<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - condition: &Spanned, - then_branch: &Spanned, - else_branch: Option<&Spanned>, -) -> Result, CodegenError> { - let (cond, _) = compile_expr(cx, condition)?.unwrap(); - let cond_bool = cx.to_bool(cond); - - let then_block = cx.context.append_basic_block(cx.current_fn, "then"); - let else_block = cx.context.append_basic_block(cx.current_fn, "else"); - let merge_block = cx.context.append_basic_block(cx.current_fn, "ifmerge"); - - cx.builder - .build_conditional_branch(cond_bool, then_block, else_block) - .expect("build_conditional_branch failed"); - - // Then branch - cx.builder.position_at_end(then_block); - let then_result = compile_expr(cx, then_branch)?; - let then_end_block = cx.builder.get_insert_block().unwrap(); - let then_needs_jump = then_end_block.get_terminator().is_none(); - if then_needs_jump { - cx.builder - .build_unconditional_branch(merge_block) - .expect("build_unconditional_branch failed"); - } - - // Else branch - cx.builder.position_at_end(else_block); - let else_result = if let Some(else_expr) = else_branch { - compile_expr(cx, else_expr)? - } else { - None - }; - let else_end_block = cx.builder.get_insert_block().unwrap(); - let else_needs_jump = else_end_block.get_terminator().is_none(); - if else_needs_jump { - cx.builder - .build_unconditional_branch(merge_block) - .expect("build_unconditional_branch failed"); - } - - // Merge block - cx.builder.position_at_end(merge_block); - - if let (Some((then_val, then_tty)), Some((else_val, _))) = (then_result, else_result) { - if then_needs_jump && else_needs_jump { - let phi = cx - .builder - .build_phi(then_val.get_type(), "ifphi") - .expect("build_phi failed"); - phi.add_incoming(&[(&then_val, then_end_block), (&else_val, else_end_block)]); - Ok(Some((phi.as_basic_value(), then_tty))) - } else if then_needs_jump { - // Only then branch reaches merge - Ok(Some((then_val, then_tty))) - } else if else_needs_jump { - // Only else branch reaches merge - Ok(Some((else_val, then_tty))) - } else { - // Neither branch reaches merge (both return/break) - Ok(None) - } - } else { - Ok(None) - } -} - -// ── While loop ────────────────────────────────────────────────────── - -fn compile_while<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - condition: &Spanned, - body: &Spanned, -) -> Result, CodegenError> { - let header_block = cx.context.append_basic_block(cx.current_fn, "while_header"); - let body_block = cx.context.append_basic_block(cx.current_fn, "while_body"); - let exit_block = cx.context.append_basic_block(cx.current_fn, "while_exit"); - - cx.builder - .build_unconditional_branch(header_block) - .expect("build_unconditional_branch failed"); - - cx.builder.position_at_end(header_block); - let (cond, _) = compile_expr(cx, condition)?.unwrap(); - let cond_bool = cx.to_bool(cond); - cx.builder - .build_conditional_branch(cond_bool, body_block, exit_block) - .expect("build_conditional_branch failed"); - - cx.builder.position_at_end(body_block); - cx.loop_stack.push((header_block, exit_block)); - compile_expr(cx, body)?; - cx.loop_stack.pop(); - - let body_end = cx.builder.get_insert_block().unwrap(); - if body_end.get_terminator().is_none() { - cx.builder - .build_unconditional_branch(header_block) - .expect("build_unconditional_branch failed"); - } - - cx.builder.position_at_end(exit_block); - Ok(None) -} - -// ── For-in loop ───────────────────────────────────────────────────── - -fn compile_for_in<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - var_name: &str, - iterable: &Spanned, - body: &Spanned, -) -> Result, CodegenError> { - match &iterable.node { - Expr::Range { start, end } => compile_for_in_range(cx, var_name, start, end, body), - _ => compile_for_in_array(cx, var_name, iterable, body), - } -} - -fn compile_for_in_range<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - var_name: &str, - start: &Spanned, - end: &Spanned, - body: &Spanned, -) -> Result, CodegenError> { - let (range_start, _) = compile_expr(cx, start)?.unwrap(); - let (range_end, _) = compile_expr(cx, end)?.unwrap(); - - let alloca = cx.create_entry_block_alloca(cx.context.i64_type().into(), var_name); - cx.builder - .build_store(alloca, range_start) - .expect("build_store failed"); - cx.vars.insert(var_name.to_string(), (alloca, TurboTy::Int)); - - let header_block = cx.context.append_basic_block(cx.current_fn, "forin_header"); - let body_block = cx.context.append_basic_block(cx.current_fn, "forin_body"); - let continue_block = cx - .context - .append_basic_block(cx.current_fn, "forin_continue"); - let exit_block = cx.context.append_basic_block(cx.current_fn, "forin_exit"); - - cx.builder - .build_unconditional_branch(header_block) - .expect("build_unconditional_branch failed"); - - cx.builder.position_at_end(header_block); - let current_i = cx - .builder - .build_load(cx.context.i64_type(), alloca, "i") - .expect("build_load failed") - .into_int_value(); - let range_end_i = range_end.into_int_value(); - let cond = cx - .builder - .build_int_compare(IntPredicate::SLT, current_i, range_end_i, "forin_cond") - .expect("build_int_compare failed"); - cx.builder - .build_conditional_branch(cond, body_block, exit_block) - .expect("build_conditional_branch failed"); - - cx.builder.position_at_end(body_block); - cx.loop_stack.push((continue_block, exit_block)); - compile_expr(cx, body)?; - cx.loop_stack.pop(); - - let body_end = cx.builder.get_insert_block().unwrap(); - if body_end.get_terminator().is_none() { - cx.builder - .build_unconditional_branch(continue_block) - .expect("build_unconditional_branch failed"); - } - - cx.builder.position_at_end(continue_block); - let updated_i = cx - .builder - .build_load(cx.context.i64_type(), alloca, "i_cur") - .expect("build_load failed") - .into_int_value(); - let one = cx.context.i64_type().const_int(1, false); - let next_i = cx - .builder - .build_int_add(updated_i, one, "next_i") - .expect("build_int_add failed"); - cx.builder - .build_store(alloca, next_i) - .expect("build_store failed"); - cx.builder - .build_unconditional_branch(header_block) - .expect("build_unconditional_branch failed"); - - cx.builder.position_at_end(exit_block); - Ok(None) -} - -fn compile_for_in_array<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - var_name: &str, - iterable: &Spanned, - body: &Spanned, -) -> Result, CodegenError> { - let (arr, arr_tty) = compile_expr(cx, iterable)?.unwrap(); - let elem_tty = match &arr_tty { - TurboTy::Array(inner) => *inner.clone(), - _ => TurboTy::Int, - }; - - let arr_len = cx - .rt_call("rt_array_len", &[arr.into()]) - .unwrap() - .into_int_value(); - - // Index counter - let idx_alloca = cx.create_entry_block_alloca(cx.context.i64_type().into(), "__forin_idx"); - cx.builder - .build_store(idx_alloca, cx.context.i64_type().const_int(0, false)) - .expect("build_store failed"); - - // Loop variable - let elem_llvm_ty = turbo_ty_to_llvm_ctx(&elem_tty, cx.context, cx.enum_max_slots); - let var_alloca = cx.create_entry_block_alloca(elem_llvm_ty, var_name); - cx.vars - .insert(var_name.to_string(), (var_alloca, elem_tty.clone())); - - let header_block = cx - .context - .append_basic_block(cx.current_fn, "forin_arr_header"); - let body_block = cx - .context - .append_basic_block(cx.current_fn, "forin_arr_body"); - let continue_block = cx - .context - .append_basic_block(cx.current_fn, "forin_arr_continue"); - let exit_block = cx - .context - .append_basic_block(cx.current_fn, "forin_arr_exit"); - - cx.builder - .build_unconditional_branch(header_block) - .expect("build_unconditional_branch failed"); - - cx.builder.position_at_end(header_block); - let idx = cx - .builder - .build_load(cx.context.i64_type(), idx_alloca, "idx") - .expect("build_load failed") - .into_int_value(); - let cond = cx - .builder - .build_int_compare(IntPredicate::SLT, idx, arr_len, "forin_arr_cond") - .expect("build_int_compare failed"); - cx.builder - .build_conditional_branch(cond, body_block, exit_block) - .expect("build_conditional_branch failed"); - - cx.builder.position_at_end(body_block); - // Load element - let idx2 = cx - .builder - .build_load(cx.context.i64_type(), idx_alloca, "idx") - .expect("build_load failed"); - let raw_elem = cx - .rt_call("rt_array_get", &[arr.into(), idx2.into()]) - .unwrap(); - let elem = narrow_from_storage(cx, raw_elem, &elem_tty); - cx.builder - .build_store(var_alloca, elem) - .expect("build_store failed"); - - cx.loop_stack.push((continue_block, exit_block)); - compile_expr(cx, body)?; - cx.loop_stack.pop(); - - let body_end = cx.builder.get_insert_block().unwrap(); - if body_end.get_terminator().is_none() { - cx.builder - .build_unconditional_branch(continue_block) - .expect("build_unconditional_branch failed"); - } - - cx.builder.position_at_end(continue_block); - let idx3 = cx - .builder - .build_load(cx.context.i64_type(), idx_alloca, "idx") - .expect("build_load failed") - .into_int_value(); - let one = cx.context.i64_type().const_int(1, false); - let next = cx - .builder - .build_int_add(idx3, one, "next_idx") - .expect("build_int_add failed"); - cx.builder - .build_store(idx_alloca, next) - .expect("build_store failed"); - cx.builder - .build_unconditional_branch(header_block) - .expect("build_unconditional_branch failed"); - - cx.builder.position_at_end(exit_block); - Ok(None) -} - -// ── Match expression ──────────────────────────────────────────────── - -fn compile_match<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - subject: &Spanned, - arms: &[MatchArm], -) -> Result, CodegenError> { - let (subject_val, subject_tty) = compile_expr(cx, subject)?.unwrap(); - - let merge_block = cx.context.append_basic_block(cx.current_fn, "match_merge"); - - let mut arm_blocks: Vec> = Vec::new(); - for i in 0..arms.len() { - arm_blocks.push( - cx.context - .append_basic_block(cx.current_fn, &format!("match_arm_{i}")), - ); - } - let default_block = cx - .context - .append_basic_block(cx.current_fn, "match_default"); - - // Build chain of comparisons - let mut phi_incoming: Vec<(BasicValueEnum<'ctx>, inkwell::basic_block::BasicBlock<'ctx>)> = - Vec::new(); - let mut first_arm_tty: Option = None; - - for (i, arm) in arms.iter().enumerate() { - let arm_block = arm_blocks[i]; - let next_block = if i + 1 < arms.len() { - cx.context - .append_basic_block(cx.current_fn, &format!("match_test_{}", i + 1)) - } else { - default_block - }; - - // Only branch from the first test block for the first arm - if i == 0 { - // We're still at the end of the block before the match - } - - // Build test - let matches = match &arm.pattern.node { - Pattern::Wildcard => None, - Pattern::Ident(name) => { - // Check if this ident is an enum variant name - let variant_tag = lookup_variant_tag(cx.enum_variants, name); - if let Some(tag_val) = variant_tag { - let pat_val = cx.context.i64_type().const_int(tag_val as u64, false); - if let TurboTy::Enum(ref enum_name) = subject_tty { - if cx.enum_max_slots.contains_key(enum_name) { - // Data enum: load tag from ptr - let ptr = subject_val.into_pointer_value(); - let tag = cx - .builder - .build_load(cx.context.i64_type(), ptr, "tag") - .expect("build_load failed") - .into_int_value(); - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, tag, pat_val, "var_eq") - .expect("build_int_compare failed"), - ) - } else { - // Unit enum: direct tag compare - let tag = subject_val.into_int_value(); - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, tag, pat_val, "var_eq") - .expect("build_int_compare failed"), - ) - } - } else { - // Subject is int, compare directly - let tag = subject_val.into_int_value(); - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, tag, pat_val, "var_eq") - .expect("build_int_compare failed"), - ) - } - } else { - None // catch-all bind - } - } - Pattern::IntLit(n) => { - let pat_val = cx.context.i64_type().const_int(*n as u64, true); - let subject_int = subject_val.into_int_value(); - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, subject_int, pat_val, "pat_eq") - .expect("build_int_compare failed"), - ) - } - Pattern::BoolLit(b) => { - let pat_val = cx.context.i8_type().const_int(*b as u64, false); - let subject_int = subject_val.into_int_value(); - let subject_i8 = if subject_int.get_type().get_bit_width() > 8 { - cx.builder - .build_int_truncate(subject_int, cx.context.i8_type(), "trunc") - .expect("build_int_truncate failed") - } else { - subject_int - }; - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, subject_i8, pat_val, "pat_eq") - .expect("build_int_compare failed"), - ) - } - Pattern::StringLit(s) => { - let pat_ptr = cx.create_string(s)?; - let eq = cx - .rt_call("rt_str_eq", &[subject_val.into(), pat_ptr.into()]) - .unwrap() - .into_int_value(); - let zero = cx.context.i8_type().const_int(0, false); - Some( - cx.builder - .build_int_compare(IntPredicate::NE, eq, zero, "pat_eq") - .expect("build_int_compare failed"), - ) - } - Pattern::Ok(_) => { - // Result tag 0 = Ok - let tag = cx - .rt_call("rt_result_tag", &[subject_val.into()]) - .unwrap() - .into_int_value(); - let zero = cx.context.i64_type().const_int(0, false); - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, tag, zero, "is_ok") - .expect("build_int_compare failed"), - ) - } - Pattern::Err(_) => { - let tag = cx - .rt_call("rt_result_tag", &[subject_val.into()]) - .unwrap() - .into_int_value(); - let one = cx.context.i64_type().const_int(1, false); - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, tag, one, "is_err") - .expect("build_int_compare failed"), - ) - } - Pattern::Some(_) => { - let tag = cx - .rt_call("rt_option_tag", &[subject_val.into()]) - .unwrap() - .into_int_value(); - let one = cx.context.i64_type().const_int(1, false); - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, tag, one, "is_some") - .expect("build_int_compare failed"), - ) - } - Pattern::None => { - let tag = cx - .rt_call("rt_option_tag", &[subject_val.into()]) - .unwrap() - .into_int_value(); - let zero = cx.context.i64_type().const_int(0, false); - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, tag, zero, "is_none") - .expect("build_int_compare failed"), - ) - } - Pattern::VariantDestructure { variant, .. } => { - // Match on enum tag - if let TurboTy::Enum(ref enum_name) = subject_tty { - if let Some(variants) = cx.enum_variants.get(enum_name) { - if let Some(idx) = variants.iter().position(|v| v == variant) { - let tag_val = cx.context.i64_type().const_int(idx as u64, false); - // For data enums, load tag from heap - if cx.enum_max_slots.contains_key(enum_name) { - let ptr = subject_val.into_pointer_value(); - let tag = cx - .builder - .build_load(cx.context.i64_type(), ptr, "tag") - .expect("build_load failed") - .into_int_value(); - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, tag, tag_val, "var_eq") - .expect("build_int_compare failed"), - ) - } else { - let tag = subject_val.into_int_value(); - Some( - cx.builder - .build_int_compare(IntPredicate::EQ, tag, tag_val, "var_eq") - .expect("build_int_compare failed"), - ) - } - } else { - None - } - } else { - None - } - } else { - None - } - } - }; - - let has_pattern_test = matches.is_some(); - if let Some(cond) = matches { - if arm.guard.is_some() { - // Pattern matched -- jump to a guard-check block - let guard_block = cx - .context - .append_basic_block(cx.current_fn, &format!("match_guard_{i}")); - cx.builder - .build_conditional_branch(cond, guard_block, next_block) - .expect("build_conditional_branch failed"); - cx.builder.position_at_end(guard_block); - } else { - cx.builder - .build_conditional_branch(cond, arm_block, next_block) - .expect("build_conditional_branch failed"); - } - } else { - // Wildcard or Ident: always matches - if arm.guard.is_some() { - // Need a guard block for wildcard + guard - let guard_block = cx - .context - .append_basic_block(cx.current_fn, &format!("match_guard_{i}")); - cx.builder - .build_unconditional_branch(guard_block) - .expect("br"); - cx.builder.position_at_end(guard_block); - } else { - cx.builder - .build_unconditional_branch(arm_block) - .expect("build_unconditional_branch failed"); - } - } - - // If there's a guard, we're now in the guard block. - // Bind pattern variables first (guard may reference them), then evaluate guard. - if arm.guard.is_some() { - // We're in the guard_block; bind vars, eval guard, branch - // Bind variables needed by guard - // (For simplicity, bind subject for ident patterns) - let saved_guard_vars = cx.vars.clone(); - match &arm.pattern.node { - Pattern::Ident(name) - if name != "_" && lookup_variant_tag(cx.enum_variants, name).is_none() => - { - let llvm_ty = subject_val.get_type(); - let alloca = cx.create_entry_block_alloca(llvm_ty, name); - cx.builder.build_store(alloca, subject_val).expect("store"); - cx.vars.insert(name.clone(), (alloca, subject_tty.clone())); - } - Pattern::VariantDestructure { variant, bindings } => { - if let TurboTy::Enum(ref enum_name) = subject_tty { - if cx.enum_max_slots.contains_key(enum_name) { - let ptr = subject_val.into_pointer_value(); - for (j, bname) in bindings.iter().enumerate() { - if bname == "_" { - continue; - } - let offset = ((j + 1) * 8) as u64; - let field_ptr = unsafe { - cx.builder - .build_gep( - cx.context.i8_type(), - ptr, - &[cx.context.i64_type().const_int(offset, false)], - "guard_bind_ptr", - ) - .expect("gep") - }; - let val = cx - .builder - .build_load(cx.context.i64_type(), field_ptr, "guard_bind_val") - .expect("load"); - let field_tty = cx - .enum_variant_fields - .get(&(enum_name.clone(), variant.clone())) - .and_then(|fs| fs.get(j)) - .cloned() - .unwrap_or(TurboTy::Int); - let alloca = cx.create_entry_block_alloca(val.get_type(), bname); - cx.builder.build_store(alloca, val).expect("store"); - cx.vars.insert(bname.clone(), (alloca, field_tty)); - } - } - } - } - _ => {} - } - let guard_expr = arm.guard.as_ref().unwrap(); - let (guard_val, _) = compile_expr(cx, guard_expr)?.unwrap(); - let guard_bool = guard_val.into_int_value(); - // Normalize to i1 for the branch - let guard_cond = if guard_bool.get_type().get_bit_width() == 1 { - guard_bool - } else { - let zero = guard_bool.get_type().const_int(0, false); - cx.builder - .build_int_compare(IntPredicate::NE, guard_bool, zero, "guard_cond") - .expect("icmp") - }; - cx.builder - .build_conditional_branch(guard_cond, arm_block, next_block) - .expect("cond_br"); - cx.vars = saved_guard_vars; - } - - // Compile arm body - cx.builder.position_at_end(arm_block); - - // Bind pattern variables - let saved_vars = cx.vars.clone(); - match &arm.pattern.node { - Pattern::Ident(name) - if name != "_" && lookup_variant_tag(cx.enum_variants, name).is_none() => - { - // Catch-all bind: bind subject to name - let llvm_ty = subject_val.get_type(); - let alloca = cx.create_entry_block_alloca(llvm_ty, name); - cx.builder - .build_store(alloca, subject_val) - .expect("build_store failed"); - cx.vars.insert(name.clone(), (alloca, subject_tty.clone())); - } - Pattern::Ok(name) | Pattern::Some(name) => { - let is_ok = matches!(arm.pattern.node, Pattern::Ok(_)); - let inner_tty = match &subject_tty { - TurboTy::Result(ok_ty, _) if is_ok => *ok_ty.clone(), - TurboTy::Optional(inner_ty) if !is_ok => *inner_ty.clone(), - _ => TurboTy::Int, - }; - let inner_raw = if is_ok { - cx.rt_call("rt_result_value", &[subject_val.into()]) - .unwrap() - } else { - cx.rt_call("rt_option_value", &[subject_val.into()]) - .unwrap() - }; - // inner_raw is i64; narrow to the inner type for storage - let inner_narrowed = narrow_from_storage(cx, inner_raw, &inner_tty); - let inner_llvm_ty = turbo_ty_to_llvm_ctx(&inner_tty, cx.context, cx.enum_max_slots); - let alloca = cx.create_entry_block_alloca(inner_llvm_ty, name); - cx.builder - .build_store(alloca, inner_narrowed) - .expect("build_store failed"); - cx.vars.insert(name.clone(), (alloca, inner_tty)); - } - Pattern::Err(name) => { - let inner_tty = match &subject_tty { - TurboTy::Result(_, err_ty) => *err_ty.clone(), - _ => TurboTy::Int, - }; - let inner_raw = cx - .rt_call("rt_result_value", &[subject_val.into()]) - .unwrap(); - let inner_narrowed = narrow_from_storage(cx, inner_raw, &inner_tty); - let inner_llvm_ty = turbo_ty_to_llvm_ctx(&inner_tty, cx.context, cx.enum_max_slots); - let alloca = cx.create_entry_block_alloca(inner_llvm_ty, name); - cx.builder - .build_store(alloca, inner_narrowed) - .expect("build_store failed"); - cx.vars.insert(name.clone(), (alloca, inner_tty)); - } - Pattern::VariantDestructure { variant, bindings } => { - // Bind destructured fields - if let TurboTy::Enum(ref enum_name) = subject_tty { - if cx.enum_max_slots.contains_key(enum_name) { - let ptr = subject_val.into_pointer_value(); - for (j, binding_name) in bindings.iter().enumerate() { - if binding_name == "_" { - continue; - } - let offset = ((j + 1) * 8) as u64; - let field_ptr = unsafe { - cx.builder - .build_gep( - cx.context.i8_type(), - ptr, - &[cx.context.i64_type().const_int(offset, false)], - "vf_ptr", - ) - .expect("build_gep failed") - }; - let field_val = cx - .builder - .build_load(cx.context.i64_type(), field_ptr, binding_name) - .expect("build_load failed"); - // Determine field type - let field_tty = cx - .enum_variant_fields - .get(&(enum_name.clone(), variant.clone())) - .and_then(|tys| tys.get(j).cloned()) - .unwrap_or(TurboTy::Int); - let field_val = narrow_from_storage(cx, field_val, &field_tty); - let alloca = - cx.create_entry_block_alloca(field_val.get_type(), binding_name); - cx.builder - .build_store(alloca, field_val) - .expect("build_store failed"); - cx.vars.insert(binding_name.clone(), (alloca, field_tty)); - } - } - } - } - _ => {} - } - - let arm_result = compile_expr(cx, &arm.body)?; - cx.vars = saved_vars; - - let arm_end_block = cx.builder.get_insert_block().unwrap(); - if arm_end_block.get_terminator().is_none() { - if let Some((val, ref tty)) = arm_result { - if first_arm_tty.is_none() { - first_arm_tty = Some(tty.clone()); - } - phi_incoming.push((val, arm_end_block)); - } - cx.builder - .build_unconditional_branch(merge_block) - .expect("build_unconditional_branch failed"); - } - - // Position at next test block if needed - if i + 1 < arms.len() { - cx.builder.position_at_end(next_block); - } - } - - // Default block (unreachable) - cx.builder.position_at_end(default_block); - cx.builder - .build_unreachable() - .expect("build_unreachable failed"); - - // Merge - cx.builder.position_at_end(merge_block); - - if !phi_incoming.is_empty() && first_arm_tty.is_some() { - let first_type = phi_incoming[0].0.get_type(); - let phi = cx - .builder - .build_phi(first_type, "match_result") - .expect("build_phi failed"); - for (val, block) in &phi_incoming { - phi.add_incoming(&[(val, *block)]); - } - Ok(Some((phi.as_basic_value(), first_arm_tty.unwrap()))) - } else { - Ok(None) - } -} - -// ── Value conversion helpers ──────────────────────────────────────── - -pub(crate) fn convert_to_str<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - val: BasicValueEnum<'ctx>, - tty: &TurboTy, -) -> Result, CodegenError> { - match tty { - TurboTy::Str => Ok(val), - TurboTy::Int => { - let iv = val.into_int_value(); - let iv = if iv.get_type().get_bit_width() < 64 { - cx.builder - .build_int_s_extend(iv, cx.context.i64_type(), "ext") - .expect("build_int_s_extend failed") - } else { - iv - }; - Ok(cx.rt_call("rt_i64_to_str", &[iv.into()]).unwrap()) - } - TurboTy::Float => Ok(cx.rt_call("rt_f64_to_str", &[val.into()]).unwrap()), - TurboTy::Bool => { - let iv = val.into_int_value(); - let iv = if iv.get_type().get_bit_width() > 8 { - cx.builder - .build_int_truncate(iv, cx.context.i8_type(), "trunc") - .expect("build_int_truncate failed") - } else { - iv - }; - Ok(cx.rt_call("rt_bool_to_str", &[iv.into()]).unwrap()) - } - TurboTy::Struct(ref sname) => { - let sname = sname.clone(); - let to_str_fn = format!("{sname}__to_string"); - if let Some(&ts_fn) = cx.user_fns.get(&to_str_fn) { - let s = cx - .builder - .build_direct_call(ts_fn, &[val.into()], "to_str") - .expect("call") - .try_as_basic_value() - .left() - .unwrap(); - Ok(s) - } else { - let ptr = cx.create_string(&format!("<{sname}>"))?; - Ok(ptr.into()) - } - } - _ => { - let ptr = cx.create_string("")?; - Ok(ptr.into()) - } - } -} - -/// Widen a value to i64 for uniform heap storage. -pub(crate) fn widen_for_storage<'a, 'ctx>( - cx: &Ctx<'a, 'ctx>, - val: BasicValueEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - match val { - BasicValueEnum::IntValue(iv) => { - if iv.get_type().get_bit_width() < 64 { - cx.builder - .build_int_s_extend(iv, cx.context.i64_type(), "widen") - .expect("build_int_s_extend failed") - .into() - } else { - val - } - } - BasicValueEnum::FloatValue(fv) => { - // bitcast f64 -> i64 for storage - cx.builder - .build_bit_cast(fv, cx.context.i64_type(), "f2i") - .expect("build_bitcast failed") - } - BasicValueEnum::PointerValue(pv) => { - // ptr -> i64 for storage in uniform-width arrays/structs - cx.builder - .build_ptr_to_int(pv, cx.context.i64_type(), "ptr2i") - .expect("build_ptr_to_int failed") - .into() - } - _ => val, - } -} - -/// Convert an integer value to a pointer if it's an i64 (for channel/mutex/hashmap operations). -pub(crate) fn int_to_ptr_if_needed<'a, 'ctx>( - cx: &Ctx<'a, 'ctx>, - val: BasicValueEnum<'ctx>, -) -> PointerValue<'ctx> { - match val { - BasicValueEnum::PointerValue(pv) => pv, - BasicValueEnum::IntValue(iv) => cx - .builder - .build_int_to_ptr(iv, cx.context.ptr_type(AddressSpace::default()), "i2ptr") - .expect("int_to_ptr failed"), - _ => cx.context.ptr_type(AddressSpace::default()).const_null(), - } -} - -/// Narrow a value from i64 storage back to its actual type. -pub(crate) fn narrow_from_storage<'a, 'ctx>( - cx: &Ctx<'a, 'ctx>, - val: BasicValueEnum<'ctx>, - tty: &TurboTy, -) -> BasicValueEnum<'ctx> { - match tty { - TurboTy::Bool => { - let iv = val.into_int_value(); - cx.builder - .build_int_truncate(iv, cx.context.i8_type(), "narrow_bool") - .expect("build_int_truncate failed") - .into() - } - TurboTy::Float => { - let iv = val.into_int_value(); - cx.builder - .build_bit_cast(iv, cx.context.f64_type(), "i2f") - .expect("build_bitcast failed") - } - TurboTy::Str - | TurboTy::Array(_) - | TurboTy::Struct(_) - | TurboTy::Result(_, _) - | TurboTy::Optional(_) - | TurboTy::Future(_) => { - // i64 -> ptr via inttoptr - let iv = val.into_int_value(); - cx.builder - .build_int_to_ptr(iv, cx.context.ptr_type(AddressSpace::default()), "i2ptr") - .expect("build_int_to_pointer failed") - .into() - } - _ => val, // Int, Enum, etc. stay as i64 - } -} - -/// Coerce argument value to match the expected LLVM type. -pub(crate) fn coerce_arg<'a, 'ctx>( - cx: &Ctx<'a, 'ctx>, - val: BasicValueEnum<'ctx>, - expected: BasicTypeEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - let actual = val.get_type(); - if actual == expected { - return val; - } - - // Int width mismatch - if let (BasicTypeEnum::IntType(actual_int), BasicTypeEnum::IntType(expected_int)) = - (actual, expected) - { - if actual_int.get_bit_width() < expected_int.get_bit_width() { - return cx - .builder - .build_int_s_extend(val.into_int_value(), expected_int, "coerce_ext") - .expect("build_int_s_extend failed") - .into(); - } - if actual_int.get_bit_width() > expected_int.get_bit_width() { - return cx - .builder - .build_int_truncate(val.into_int_value(), expected_int, "coerce_trunc") - .expect("build_int_truncate failed") - .into(); - } - } - - // Pointer to int coercion - if let (BasicTypeEnum::PointerType(_), BasicTypeEnum::IntType(expected_int)) = - (actual, expected) - { - return cx - .builder - .build_ptr_to_int(val.into_pointer_value(), expected_int, "coerce_ptr2int") - .expect("build_ptr_to_int failed") - .into(); - } - - // Int to pointer coercion - if let (BasicTypeEnum::IntType(_), BasicTypeEnum::PointerType(expected_ptr)) = - (actual, expected) - { - return cx - .builder - .build_int_to_ptr(val.into_int_value(), expected_ptr, "coerce_int2ptr") - .expect("build_int_to_ptr failed") - .into(); - } - - val -} - -// ── if-let ────────────────────────────────────────────────────────── -// -// Mirrors `compile_if_let` in the Cranelift backend -// (turbo-codegen-cranelift/src/builtins.rs). We tag-check the scrutinee -// against the pattern, branch into a bind-and-evaluate then-block, and -// phi the two arms at a merge block. Only `some`/`none`/`ok`/`err` are -// supported today — any richer pattern lands in the error arm below. -fn compile_if_let<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - pattern: &Spanned, - value: &Spanned, - then_branch: &Spanned, - else_branch: Option<&Spanned>, -) -> Result, CodegenError> { - let (val, val_tty) = compile_expr(cx, value)?.unwrap(); - - let i64_ty = cx.context.i64_type(); - - // Determine the tag check based on the pattern. - let matches_cond = match &pattern.node { - Pattern::Some(_) => { - let tag = cx - .rt_call("rt_option_tag", &[val.into()]) - .unwrap() - .into_int_value(); - let one = i64_ty.const_int(1, false); - cx.builder - .build_int_compare(IntPredicate::EQ, tag, one, "iflet_is_some") - .expect("build_int_compare failed") - } - Pattern::None => { - let tag = cx - .rt_call("rt_option_tag", &[val.into()]) - .unwrap() - .into_int_value(); - let zero = i64_ty.const_int(0, false); - cx.builder - .build_int_compare(IntPredicate::EQ, tag, zero, "iflet_is_none") - .expect("build_int_compare failed") - } - Pattern::Ok(_) => { - let tag = cx - .rt_call("rt_result_tag", &[val.into()]) - .unwrap() - .into_int_value(); - let zero = i64_ty.const_int(0, false); - cx.builder - .build_int_compare(IntPredicate::EQ, tag, zero, "iflet_is_ok") - .expect("build_int_compare failed") - } - Pattern::Err(_) => { - let tag = cx - .rt_call("rt_result_tag", &[val.into()]) - .unwrap() - .into_int_value(); - let one = i64_ty.const_int(1, false); - cx.builder - .build_int_compare(IntPredicate::EQ, tag, one, "iflet_is_err") - .expect("build_int_compare failed") - } - _ => { - return Err(CodegenError { - code: ErrorCode::E0400, - message: "unsupported pattern in `if let`".to_string(), - }); - } - }; - - let then_block = cx.context.append_basic_block(cx.current_fn, "iflet_then"); - let else_block = cx.context.append_basic_block(cx.current_fn, "iflet_else"); - let merge_block = cx.context.append_basic_block(cx.current_fn, "iflet_merge"); - - cx.builder - .build_conditional_branch(matches_cond, then_block, else_block) - .expect("build_conditional_branch failed"); - - // ── Then block: bind the pattern variable, compile the then branch ── - cx.builder.position_at_end(then_block); - let saved_vars = cx.vars.clone(); - - match &pattern.node { - Pattern::Some(binding) => { - let raw = cx.rt_call("rt_option_value", &[val.into()]).unwrap(); - let inner_tty = match &val_tty { - TurboTy::Optional(inner) => *inner.clone(), - _ => TurboTy::Int, - }; - let narrowed = narrow_from_storage(cx, raw, &inner_tty); - let alloca = cx.create_entry_block_alloca(narrowed.get_type(), binding); - cx.builder - .build_store(alloca, narrowed) - .expect("build_store failed"); - cx.vars.insert(binding.clone(), (alloca, inner_tty)); - } - Pattern::Ok(binding) | Pattern::Err(binding) => { - let raw = cx.rt_call("rt_result_value", &[val.into()]).unwrap(); - let inner_tty = match &val_tty { - TurboTy::Result(ok_tty, err_tty) => { - if matches!(&pattern.node, Pattern::Ok(_)) { - *ok_tty.clone() - } else { - *err_tty.clone() - } - } - _ => TurboTy::Int, - }; - let narrowed = narrow_from_storage(cx, raw, &inner_tty); - let alloca = cx.create_entry_block_alloca(narrowed.get_type(), binding); - cx.builder - .build_store(alloca, narrowed) - .expect("build_store failed"); - cx.vars.insert(binding.clone(), (alloca, inner_tty)); - } - Pattern::None => { - // No binding. - } - _ => {} - } - - let then_result = compile_expr(cx, then_branch)?; - let then_end_block = cx.builder.get_insert_block().unwrap(); - let then_needs_jump = then_end_block.get_terminator().is_none(); - if then_needs_jump { - cx.builder - .build_unconditional_branch(merge_block) - .expect("build_unconditional_branch failed"); - } - - // Restore variable scope (matches Cranelift's behaviour). - cx.vars = saved_vars; - - // ── Else block ── - cx.builder.position_at_end(else_block); - let else_result = if let Some(else_expr) = else_branch { - compile_expr(cx, else_expr)? - } else { - None - }; - let else_end_block = cx.builder.get_insert_block().unwrap(); - let else_needs_jump = else_end_block.get_terminator().is_none(); - if else_needs_jump { - cx.builder - .build_unconditional_branch(merge_block) - .expect("build_unconditional_branch failed"); - } - - // ── Merge block ── - cx.builder.position_at_end(merge_block); - - if let (Some((then_val, then_tty)), Some((else_val, _))) = (then_result, else_result) { - if then_needs_jump && else_needs_jump { - let phi = cx - .builder - .build_phi(then_val.get_type(), "iflet_phi") - .expect("build_phi failed"); - phi.add_incoming(&[(&then_val, then_end_block), (&else_val, else_end_block)]); - Ok(Some((phi.as_basic_value(), then_tty))) - } else if then_needs_jump { - Ok(Some((then_val, then_tty))) - } else if else_needs_jump { - Ok(Some((else_val, then_tty))) - } else { - Ok(None) - } - } else { - Ok(None) - } -} - -// ── Optional chaining (`x?.field`) ────────────────────────────────── -// -// Mirrors `compile_optional_chain` in the Cranelift backend. The -// semantics are "map over Option": if `x` is `some(v)`, yield -// `some(v.field)`; otherwise yield `none`. This is NOT the Rust `?` -// operator (early-return on None) — it always produces an Optional. -fn compile_optional_chain<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - object: &Spanned, - field: &str, -) -> Result, CodegenError> { - let (opt_val, val_tty) = compile_expr(cx, object)?.unwrap(); - - let inner_tty = match &val_tty { - TurboTy::Optional(inner) => inner.as_ref().clone(), - _ => { - return Err(CodegenError { - code: ErrorCode::E0400, - message: "optional chaining `?.` requires an optional type".to_string(), - }) - } - }; - - let struct_name = match &inner_tty { - TurboTy::Struct(name) => name.clone(), - _ => { - return Err(CodegenError { - code: ErrorCode::E0400, - message: "optional chaining `?.` requires an optional struct type".to_string(), - }) - } - }; - - let fields = cx - .struct_fields - .get(&struct_name) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("undefined struct: {struct_name}"), - })? - .clone(); - - let (field_idx, field_tty) = fields - .iter() - .enumerate() - .find(|(_, (name, _))| name == field) - .map(|(idx, (_, tty))| (idx, tty.clone())) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("struct `{struct_name}` has no field `{field}`"), - })?; - - let i64_ty = cx.context.i64_type(); - let i8_ty = cx.context.i8_type(); - let offset = (field_idx as u64) * 8; - - // Extract the tag: 0 = none, 1 = some. - let tag = cx - .rt_call("rt_option_tag", &[opt_val.into()]) - .unwrap() - .into_int_value(); - let one = i64_ty.const_int(1, false); - let is_some = cx - .builder - .build_int_compare(IntPredicate::EQ, tag, one, "optchain_is_some") - .expect("build_int_compare failed"); - - let some_block = cx - .context - .append_basic_block(cx.current_fn, "optchain_some"); - let none_block = cx - .context - .append_basic_block(cx.current_fn, "optchain_none"); - let merge_block = cx - .context - .append_basic_block(cx.current_fn, "optchain_merge"); - - cx.builder - .build_conditional_branch(is_some, some_block, none_block) - .expect("build_conditional_branch failed"); - - // ── Some branch: unwrap, read field, wrap back in some ── - cx.builder.position_at_end(some_block); - let inner_raw = cx - .rt_call("rt_option_value", &[opt_val.into()]) - .unwrap() - .into_int_value(); - let inner_ptr = cx - .builder - .build_int_to_ptr( - inner_raw, - cx.context.ptr_type(AddressSpace::default()), - "optchain_inner_ptr", - ) - .expect("build_int_to_ptr failed"); - - let field_ptr = unsafe { - cx.builder - .build_gep( - i8_ty, - inner_ptr, - &[i64_ty.const_int(offset, false)], - "optchain_field_ptr", - ) - .expect("build_gep failed") - }; - let field_raw = cx - .builder - .build_load(i64_ty, field_ptr, "optchain_field") - .expect("build_load failed"); - - // rt_option_some takes an i64 — pass the raw storage word. For - // pointer/float fields this is already the correct bit-pattern. - let some_result = cx.rt_call("rt_option_some", &[field_raw.into()]).unwrap(); - let some_end_block = cx.builder.get_insert_block().unwrap(); - cx.builder - .build_unconditional_branch(merge_block) - .expect("build_unconditional_branch failed"); - - // ── None branch: produce a fresh none ── - cx.builder.position_at_end(none_block); - let none_result = cx.rt_call("rt_option_none", &[]).unwrap(); - let none_end_block = cx.builder.get_insert_block().unwrap(); - cx.builder - .build_unconditional_branch(merge_block) - .expect("build_unconditional_branch failed"); - - // ── Merge block ── - cx.builder.position_at_end(merge_block); - let phi = cx - .builder - .build_phi(some_result.get_type(), "optchain_phi") - .expect("build_phi failed"); - phi.add_incoming(&[ - (&some_result, some_end_block), - (&none_result, none_end_block), - ]); - - Ok(Some(( - phi.as_basic_value(), - TurboTy::Optional(Box::new(field_tty)), - ))) -} diff --git a/turbo/crates/turbo-codegen-llvm/src/helpers.rs b/turbo/crates/turbo-codegen-llvm/src/helpers.rs deleted file mode 100644 index 5e785ec..0000000 --- a/turbo/crates/turbo-codegen-llvm/src/helpers.rs +++ /dev/null @@ -1,592 +0,0 @@ -//! Free helper functions: closure extraction, spawn-site extraction, -//! variant tag lookup. - -use std::collections::HashMap; -use turbo_ast::*; - -use crate::types::TurboTy; - -// ── Closure extraction ────────────────────────────────────────────── - -pub(crate) struct ExtractedClosure<'a> { - pub(crate) span_start: usize, - pub(crate) name: String, - pub(crate) params: &'a [Param], - pub(crate) return_type: &'a Option>, - pub(crate) body: &'a Spanned, - pub(crate) free_vars: Vec, - /// Types of captured (free) variables, inferred from enclosing scope - pub(crate) capture_types: Vec, -} - -/// Infer the type of a captured variable from how it's used in the closure body. -/// Checks if the variable appears in string interpolation (-> Str) or string concat (-> Str). -pub(crate) fn infer_capture_type_from_body(body: &Expr, var_name: &str) -> TurboTy { - match body { - Expr::Interpolation(parts) => { - for part in parts { - if let InterpolPart::Expr(e) = part { - if let Expr::Ident(name) = &e.node { - if name == var_name { - return TurboTy::Str; - } - } - } - } - // Recurse into sub-expressions - for part in parts { - if let InterpolPart::Expr(e) = part { - let t = infer_capture_type_from_body(&e.node, var_name); - if t != TurboTy::Int { - return t; - } - } - } - TurboTy::Int - } - Expr::Block { stmts, tail_expr } => { - for stmt in stmts { - match &stmt.node { - Stmt::Let { value, .. } => { - let t = infer_capture_type_from_body(&value.node, var_name); - if t != TurboTy::Int { - return t; - } - } - Stmt::Expr(e) => { - let t = infer_capture_type_from_body(&e.node, var_name); - if t != TurboTy::Int { - return t; - } - } - Stmt::Return(Some(e)) => { - let t = infer_capture_type_from_body(&e.node, var_name); - if t != TurboTy::Int { - return t; - } - } - _ => {} - } - } - if let Some(tail) = tail_expr { - return infer_capture_type_from_body(&tail.node, var_name); - } - TurboTy::Int - } - Expr::Call { callee, args } => { - // If passed to rt_str_concat or similar, it's a string - for arg in args { - let t = infer_capture_type_from_body(&arg.node, var_name); - if t != TurboTy::Int { - return t; - } - } - infer_capture_type_from_body(&callee.node, var_name) - } - Expr::BinaryOp { left, right, .. } => { - let t = infer_capture_type_from_body(&left.node, var_name); - if t != TurboTy::Int { - return t; - } - infer_capture_type_from_body(&right.node, var_name) - } - Expr::If { - condition, - then_branch, - else_branch, - } => { - let t = infer_capture_type_from_body(&condition.node, var_name); - if t != TurboTy::Int { - return t; - } - let t = infer_capture_type_from_body(&then_branch.node, var_name); - if t != TurboTy::Int { - return t; - } - if let Some(e) = else_branch { - return infer_capture_type_from_body(&e.node, var_name); - } - TurboTy::Int - } - _ => TurboTy::Int, - } -} - -pub(crate) fn collect_free_vars_llvm(expr: &Expr, bound: &mut Vec, free: &mut Vec) { - match expr { - Expr::Ident(name) => { - if !bound.contains(name) && !free.contains(name) { - free.push(name.clone()); - } - } - // Other nodes don't bind names; handled by sub-expression walk below - _ => {} - } - // Walk sub-expressions - match expr { - Expr::BinaryOp { left, right, .. } => { - collect_free_vars_llvm(&left.node, bound, free); - collect_free_vars_llvm(&right.node, bound, free); - } - Expr::UnaryOp { expr: e, .. } => collect_free_vars_llvm(&e.node, bound, free), - Expr::Call { callee, args } => { - collect_free_vars_llvm(&callee.node, bound, free); - for arg in args { - collect_free_vars_llvm(&arg.node, bound, free); - } - } - Expr::Block { stmts, tail_expr } => { - let prev_len = bound.len(); - for stmt in stmts { - match &stmt.node { - Stmt::Let { name, value, .. } => { - collect_free_vars_llvm(&value.node, bound, free); - bound.push(name.clone()); - } - Stmt::LetDestructure { fields, value, .. } => { - collect_free_vars_llvm(&value.node, bound, free); - for field_name in fields { - bound.push(field_name.clone()); - } - } - Stmt::Expr(e) | Stmt::Return(Some(e)) | Stmt::Defer(e) => { - collect_free_vars_llvm(&e.node, bound, free); - } - Stmt::Return(None) => {} - } - } - if let Some(tail) = tail_expr { - collect_free_vars_llvm(&tail.node, bound, free); - } - bound.truncate(prev_len); - } - Expr::If { - condition, - then_branch, - else_branch, - } => { - collect_free_vars_llvm(&condition.node, bound, free); - collect_free_vars_llvm(&then_branch.node, bound, free); - if let Some(e) = else_branch { - collect_free_vars_llvm(&e.node, bound, free); - } - } - Expr::While { condition, body } => { - collect_free_vars_llvm(&condition.node, bound, free); - collect_free_vars_llvm(&body.node, bound, free); - } - Expr::ForIn { - var_name, - iterable, - body, - } => { - collect_free_vars_llvm(&iterable.node, bound, free); - let prev = bound.len(); - bound.push(var_name.clone()); - collect_free_vars_llvm(&body.node, bound, free); - bound.truncate(prev); - } - Expr::Assign { target, value } | Expr::CompoundAssign { target, value, .. } => { - if !bound.contains(target) && !free.contains(target) { - free.push(target.clone()); - } - collect_free_vars_llvm(&value.node, bound, free); - } - Expr::FieldAssign { object, value, .. } => { - collect_free_vars_llvm(&object.node, bound, free); - collect_free_vars_llvm(&value.node, bound, free); - } - Expr::IndexAssign { - object, - index, - value, - } => { - collect_free_vars_llvm(&object.node, bound, free); - collect_free_vars_llvm(&index.node, bound, free); - collect_free_vars_llvm(&value.node, bound, free); - } - Expr::FieldAccess { object, .. } | Expr::OptionalChain { object, .. } => { - collect_free_vars_llvm(&object.node, bound, free) - } - Expr::Index { object, index } => { - collect_free_vars_llvm(&object.node, bound, free); - collect_free_vars_llvm(&index.node, bound, free); - } - Expr::ArrayLit(elems) => { - for e in elems { - collect_free_vars_llvm(&e.node, bound, free); - } - } - Expr::StructLit { fields, .. } => { - for (_, e) in fields { - collect_free_vars_llvm(&e.node, bound, free); - } - } - Expr::MapLit(entries) => { - for (k, v) in entries { - collect_free_vars_llvm(&k.node, bound, free); - collect_free_vars_llvm(&v.node, bound, free); - } - } - Expr::Match { subject, arms } => { - collect_free_vars_llvm(&subject.node, bound, free); - for arm in arms { - if let Some(ref g) = arm.guard { - collect_free_vars_llvm(&g.node, bound, free); - } - collect_free_vars_llvm(&arm.body.node, bound, free); - } - } - Expr::Closure { params, body, .. } => { - let prev = bound.len(); - for p in params { - bound.push(p.name.clone()); - } - collect_free_vars_llvm(&body.node, bound, free); - bound.truncate(prev); - } - Expr::OkExpr(v) - | Expr::ErrExpr(v) - | Expr::SomeExpr(v) - | Expr::Await(v) - | Expr::Spawn(v) - | Expr::Try(v) => { - collect_free_vars_llvm(&v.node, bound, free); - } - Expr::NullCoalesce { value, default } => { - collect_free_vars_llvm(&value.node, bound, free); - collect_free_vars_llvm(&default.node, bound, free); - } - Expr::Interpolation(parts) => { - for part in parts { - if let InterpolPart::Expr(e) = part { - collect_free_vars_llvm(&e.node, bound, free); - } - } - } - Expr::Range { start, end } => { - collect_free_vars_llvm(&start.node, bound, free); - collect_free_vars_llvm(&end.node, bound, free); - } - _ => {} - } -} - -pub(crate) fn extract_closures_from_expr_llvm<'a>( - expr: &'a Spanned, - out: &mut Vec>, - counter: &mut usize, -) { - match &expr.node { - Expr::Closure { - params, - return_type, - body, - } => { - let name = format!("__closure_{}", *counter); - *counter += 1; - let mut bound: Vec = params.iter().map(|p| p.name.clone()).collect(); - let mut free_vars = Vec::new(); - collect_free_vars_llvm(&body.node, &mut bound, &mut free_vars); - // Infer capture types from body usage: scan for string interpolation/concat - let capture_types: Vec = free_vars - .iter() - .map(|var_name| infer_capture_type_from_body(&body.node, var_name)) - .collect(); - out.push(ExtractedClosure { - span_start: expr.span.start, - name, - params, - return_type, - body, - free_vars, - capture_types, - }); - extract_closures_from_expr_llvm(body, out, counter); - } - Expr::Block { stmts, tail_expr } => { - for stmt in stmts { - match &stmt.node { - Stmt::Let { value, .. } - | Stmt::LetDestructure { value, .. } - | Stmt::Expr(value) => { - extract_closures_from_expr_llvm(value, out, counter); - } - Stmt::Return(Some(e)) | Stmt::Defer(e) => { - extract_closures_from_expr_llvm(e, out, counter); - } - _ => {} - } - } - if let Some(tail) = tail_expr { - extract_closures_from_expr_llvm(tail, out, counter); - } - } - Expr::If { - condition, - then_branch, - else_branch, - } => { - extract_closures_from_expr_llvm(condition, out, counter); - extract_closures_from_expr_llvm(then_branch, out, counter); - if let Some(e) = else_branch { - extract_closures_from_expr_llvm(e, out, counter); - } - } - Expr::While { condition, body } - | Expr::ForIn { - iterable: condition, - body, - .. - } => { - extract_closures_from_expr_llvm(condition, out, counter); - extract_closures_from_expr_llvm(body, out, counter); - } - Expr::BinaryOp { left, right, .. } => { - extract_closures_from_expr_llvm(left, out, counter); - extract_closures_from_expr_llvm(right, out, counter); - } - Expr::UnaryOp { expr: e, .. } => extract_closures_from_expr_llvm(e, out, counter), - Expr::Call { callee, args } => { - extract_closures_from_expr_llvm(callee, out, counter); - for arg in args { - extract_closures_from_expr_llvm(arg, out, counter); - } - } - Expr::Assign { value, .. } | Expr::CompoundAssign { value, .. } => { - extract_closures_from_expr_llvm(value, out, counter); - } - Expr::FieldAssign { object, value, .. } => { - extract_closures_from_expr_llvm(object, out, counter); - extract_closures_from_expr_llvm(value, out, counter); - } - Expr::IndexAssign { - object, - index, - value, - } => { - extract_closures_from_expr_llvm(object, out, counter); - extract_closures_from_expr_llvm(index, out, counter); - extract_closures_from_expr_llvm(value, out, counter); - } - Expr::OkExpr(v) - | Expr::ErrExpr(v) - | Expr::SomeExpr(v) - | Expr::Await(v) - | Expr::Spawn(v) - | Expr::Try(v) => { - extract_closures_from_expr_llvm(v, out, counter); - } - Expr::NullCoalesce { value, default } => { - extract_closures_from_expr_llvm(value, out, counter); - extract_closures_from_expr_llvm(default, out, counter); - } - Expr::OptionalChain { object, .. } => { - extract_closures_from_expr_llvm(object, out, counter); - } - Expr::Interpolation(parts) => { - for part in parts { - if let InterpolPart::Expr(e) = part { - extract_closures_from_expr_llvm(e, out, counter); - } - } - } - Expr::ArrayLit(elems) => { - for e in elems { - extract_closures_from_expr_llvm(e, out, counter); - } - } - Expr::StructLit { fields, .. } => { - for (_, e) in fields { - extract_closures_from_expr_llvm(e, out, counter); - } - } - Expr::MapLit(entries) => { - for (k, v) in entries { - extract_closures_from_expr_llvm(k, out, counter); - extract_closures_from_expr_llvm(v, out, counter); - } - } - Expr::Match { subject, arms } => { - extract_closures_from_expr_llvm(subject, out, counter); - for arm in arms { - if let Some(ref g) = arm.guard { - extract_closures_from_expr_llvm(g, out, counter); - } - extract_closures_from_expr_llvm(&arm.body, out, counter); - } - } - _ => {} - } -} - -pub(crate) fn extract_all_closures_llvm( - ast_module: &turbo_ast::Module, -) -> Vec> { - let mut closures = Vec::new(); - let mut counter = 0; - for item in &ast_module.items { - match &item.node { - Item::Function(f) => { - extract_closures_from_expr_llvm(&f.body, &mut closures, &mut counter) - } - Item::Impl(imp) => { - for method in &imp.methods { - extract_closures_from_expr_llvm(&method.node.body, &mut closures, &mut counter); - } - } - _ => {} - } - } - closures -} - -// ── Spawn extraction ──────────────────────────────────────────────── - -pub(crate) struct SpawnSite { - pub(crate) span_start: usize, - pub(crate) thunk_name: String, - pub(crate) callee_name: String, - pub(crate) num_args: usize, -} - -fn extract_spawn_sites_from_expr_llvm( - expr: &Spanned, - out: &mut Vec, - counter: &mut usize, -) { - match &expr.node { - Expr::Spawn(inner) => { - if let Expr::Call { callee, args } = &inner.node { - if let Expr::Ident(name) = &callee.node { - out.push(SpawnSite { - span_start: expr.span.start, - thunk_name: format!("__spawn_thunk_{}", *counter), - callee_name: name.clone(), - num_args: args.len(), - }); - *counter += 1; - for arg in args { - extract_spawn_sites_from_expr_llvm(arg, out, counter); - } - return; - } - } - extract_spawn_sites_from_expr_llvm(inner, out, counter); - } - Expr::Block { stmts, tail_expr } => { - for stmt in stmts { - match &stmt.node { - Stmt::Let { value, .. } - | Stmt::LetDestructure { value, .. } - | Stmt::Expr(value) => { - extract_spawn_sites_from_expr_llvm(value, out, counter); - } - Stmt::Return(Some(e)) | Stmt::Defer(e) => { - extract_spawn_sites_from_expr_llvm(e, out, counter); - } - _ => {} - } - } - if let Some(tail) = tail_expr { - extract_spawn_sites_from_expr_llvm(tail, out, counter); - } - } - Expr::If { - condition, - then_branch, - else_branch, - } => { - extract_spawn_sites_from_expr_llvm(condition, out, counter); - extract_spawn_sites_from_expr_llvm(then_branch, out, counter); - if let Some(e) = else_branch { - extract_spawn_sites_from_expr_llvm(e, out, counter); - } - } - Expr::While { condition, body } - | Expr::ForIn { - iterable: condition, - body, - .. - } => { - extract_spawn_sites_from_expr_llvm(condition, out, counter); - extract_spawn_sites_from_expr_llvm(body, out, counter); - } - Expr::BinaryOp { left, right, .. } => { - extract_spawn_sites_from_expr_llvm(left, out, counter); - extract_spawn_sites_from_expr_llvm(right, out, counter); - } - Expr::UnaryOp { expr: e, .. } => extract_spawn_sites_from_expr_llvm(e, out, counter), - Expr::Call { callee, args } => { - extract_spawn_sites_from_expr_llvm(callee, out, counter); - for arg in args { - extract_spawn_sites_from_expr_llvm(arg, out, counter); - } - } - Expr::Assign { value, .. } | Expr::CompoundAssign { value, .. } => { - extract_spawn_sites_from_expr_llvm(value, out, counter); - } - Expr::OkExpr(v) | Expr::ErrExpr(v) | Expr::SomeExpr(v) | Expr::Await(v) | Expr::Try(v) => { - extract_spawn_sites_from_expr_llvm(v, out, counter); - } - Expr::NullCoalesce { value, default } => { - extract_spawn_sites_from_expr_llvm(value, out, counter); - extract_spawn_sites_from_expr_llvm(default, out, counter); - } - Expr::OptionalChain { object, .. } => { - extract_spawn_sites_from_expr_llvm(object, out, counter); - } - Expr::ArrayLit(elems) => { - for e in elems { - extract_spawn_sites_from_expr_llvm(e, out, counter); - } - } - Expr::Match { subject, arms } => { - extract_spawn_sites_from_expr_llvm(subject, out, counter); - for arm in arms { - extract_spawn_sites_from_expr_llvm(&arm.body, out, counter); - } - } - Expr::MapLit(entries) => { - for (k, v) in entries { - extract_spawn_sites_from_expr_llvm(k, out, counter); - extract_spawn_sites_from_expr_llvm(v, out, counter); - } - } - _ => {} - } -} - -pub(crate) fn extract_all_spawn_sites_llvm(ast_module: &turbo_ast::Module) -> Vec { - let mut sites = Vec::new(); - let mut counter = 0; - for item in &ast_module.items { - match &item.node { - Item::Function(f) => { - extract_spawn_sites_from_expr_llvm(&f.body, &mut sites, &mut counter) - } - Item::Impl(imp) => { - for method in &imp.methods { - extract_spawn_sites_from_expr_llvm(&method.node.body, &mut sites, &mut counter); - } - } - _ => {} - } - } - sites -} - -// ── Variant tag lookup ────────────────────────────────────────────── - -/// Look up a variant name across all enums and return its tag index. -pub(crate) fn lookup_variant_tag( - enum_variants: &HashMap>, - name: &str, -) -> Option { - for variants in enum_variants.values() { - if let Some(idx) = variants.iter().position(|v| v == name) { - return Some(idx); - } - } - None -} diff --git a/turbo/crates/turbo-codegen-llvm/src/lib.rs b/turbo/crates/turbo-codegen-llvm/src/lib.rs deleted file mode 100644 index de7215d..0000000 --- a/turbo/crates/turbo-codegen-llvm/src/lib.rs +++ /dev/null @@ -1,1355 +0,0 @@ -//! LLVM backend for the Turbo language compiler. -//! -//! Uses inkwell (LLVM 18) to compile Turbo AST to native code via LLVM IR. -//! Mirrors the Cranelift backend's semantics for all supported AST nodes. - -mod builtins; -mod ctx; -mod expr; -mod helpers; -mod stmt; -mod types; - -use inkwell::builder::Builder; -use inkwell::context::Context; -use inkwell::module::Module; -use inkwell::passes::PassBuilderOptions; -use inkwell::targets::{ - CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetMachine, -}; -#[allow(unused_imports)] -use inkwell::types::{BasicMetadataTypeEnum, BasicType, BasicTypeEnum, FunctionType}; -use inkwell::values::{BasicMetadataValueEnum, BasicValueEnum, FunctionValue, PointerValue}; -use inkwell::{AddressSpace, IntPredicate, OptimizationLevel}; - -use std::collections::HashMap; -use std::path::Path; -use turbo_ast::*; - -use ctx::Ctx; -use expr::{compile_expr, convert_to_str, narrow_from_storage}; -use helpers::{extract_all_closures_llvm, extract_all_spawn_sites_llvm}; -use types::{ - resolve_llvm_type_ctx, turbo_ty_from_type_expr, turbo_ty_from_type_expr_with_params, - turbo_ty_to_llvm, turbo_ty_to_llvm_ctx, TurboTy, -}; - -// ── Runtime C source for AOT linking ──────────────────────────────── - -const RUNTIME_C: &str = include_str!("../../turbo-codegen-cranelift/runtime/turbo_rt.c"); - -struct TempBuildDir { - path: std::path::PathBuf, -} - -impl TempBuildDir { - fn path(&self) -> &std::path::Path { - &self.path - } -} - -impl Drop for TempBuildDir { - fn drop(&mut self) { - let _ = std::fs::remove_dir_all(&self.path); - } -} - -fn create_exclusive_temp_dir(prefix: &str) -> Result { - for attempt in 0..100u32 { - let nanos = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - let path = std::env::temp_dir().join(format!( - "{prefix}_{}_{}_{}", - std::process::id(), - nanos, - attempt - )); - match std::fs::create_dir(&path) { - Ok(()) => return Ok(TempBuildDir { path }), - Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, - Err(e) => { - return Err(CodegenError { - code: ErrorCode::E0404, - message: format!("failed to create temp dir: {e}"), - }) - } - } - } - - Err(CodegenError { - code: ErrorCode::E0404, - message: "failed to create unique temp dir after 100 attempts".to_string(), - }) -} - -// ── Error type ────────────────────────────────────────────────────── - -#[derive(Debug)] -pub struct CodegenError { - pub code: ErrorCode, - pub message: String, -} - -impl std::fmt::Display for CodegenError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "codegen error: {}", self.message) - } -} - -impl std::error::Error for CodegenError {} - -// ── Public entry point ────────────────────────────────────────────── - -pub fn aot_compile(ast_module: &turbo_ast::Module, output_path: &Path) -> Result<(), CodegenError> { - let context = Context::create(); - let module = context.create_module("turbo_module"); - let builder = context.create_builder(); - - // Initialize target - Target::initialize_all(&InitializationConfig::default()); - - let target_triple = TargetMachine::get_default_triple(); - let target = Target::from_triple(&target_triple).map_err(|e| CodegenError { - code: ErrorCode::E0405, - message: format!("failed to get target: {}", e.to_string_lossy()), - })?; - let target_machine = target - .create_target_machine( - &target_triple, - "generic", - "", - OptimizationLevel::Aggressive, - RelocMode::PIC, - CodeModel::Default, - ) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0405, - message: "failed to create target machine".to_string(), - })?; - - module.set_triple(&target_triple); - module.set_data_layout(&target_machine.get_target_data().get_data_layout()); - - compile_module(&context, &module, &builder, ast_module)?; - - // Run optimization passes - module - .run_passes("default", &target_machine, PassBuilderOptions::create()) - .map_err(|e| CodegenError { - code: ErrorCode::E0405, - message: format!("LLVM optimization failed: {}", e.to_string_lossy()), - })?; - - // Emit object file - let tmp_dir = create_exclusive_temp_dir("turbo_llvm_aot")?; - - let obj_path = tmp_dir.path().join("turbo.o"); - let rt_path = tmp_dir.path().join("turbo_rt.c"); - - target_machine - .write_to_file(&module, FileType::Object, &obj_path) - .map_err(|e| CodegenError { - code: ErrorCode::E0404, - message: format!("failed to emit object: {}", e.to_string_lossy()), - })?; - - std::fs::write(&rt_path, RUNTIME_C).map_err(|e| CodegenError { - code: ErrorCode::E0400, - message: format!("failed to write runtime: {e}"), - })?; - - // Link with cc - let output = std::process::Command::new("cc") - .arg(&rt_path) - .arg(&obj_path) - .arg("-lm") - .arg("-o") - .arg(output_path) - .output() - .map_err(|e| CodegenError { - code: ErrorCode::E0404, - message: format!("failed to run linker: {e}"), - })?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(CodegenError { - code: ErrorCode::E0404, - message: format!("linker failed: {stderr}"), - }); - } - - Ok(()) -} - -// ── Module compilation ────────────────────────────────────────────── - -fn compile_module<'ctx>( - context: &'ctx Context, - module: &Module<'ctx>, - builder: &Builder<'ctx>, - ast_module: &turbo_ast::Module, -) -> Result<(), CodegenError> { - let ptr_type = context.ptr_type(AddressSpace::default()); - let i64_type = context.i64_type(); - let i8_type = context.i8_type(); - let f64_type = context.f64_type(); - let void_type = context.void_type(); - - // ── Declare runtime functions ─────────────────────────────────── - - let mut rt_fns: HashMap> = HashMap::new(); - - macro_rules! declare_rt { - ($name:expr, ($($param:expr),*) -> void) => { - let fn_type = void_type.fn_type(&[$($param.into()),*], false); - let func = module.add_function($name, fn_type, Some(inkwell::module::Linkage::External)); - rt_fns.insert($name.to_string(), func); - }; - ($name:expr, ($($param:expr),*) -> $ret:expr) => { - let fn_type: FunctionType = $ret.fn_type(&[$($param.into()),*], false); - let func = module.add_function($name, fn_type, Some(inkwell::module::Linkage::External)); - rt_fns.insert($name.to_string(), func); - }; - } - - // Core runtime - declare_rt!("rt_print_str", (ptr_type) -> void); - declare_rt!("rt_print_i64", (i64_type) -> void); - declare_rt!("rt_print_f64", (f64_type) -> void); - declare_rt!("rt_print_bool", (i8_type) -> void); - declare_rt!("rt_panic", (ptr_type) -> void); - declare_rt!("rt_assert_fail", (ptr_type) -> void); - declare_rt!("rt_assert_eq_fail", (i64_type, ptr_type, ptr_type) -> void); - declare_rt!("rt_div_by_zero", () -> void); - declare_rt!("rt_int_overflow", () -> void); - declare_rt!("rt_str_concat", (ptr_type, ptr_type) -> ptr_type); - declare_rt!("rt_str_eq", (ptr_type, ptr_type) -> i8_type); - declare_rt!("rt_array_alloc", (i64_type) -> ptr_type); - declare_rt!("rt_array_get", (ptr_type, i64_type) -> i64_type); - declare_rt!("rt_array_set", (ptr_type, i64_type, i64_type) -> ptr_type); - declare_rt!("rt_array_len", (ptr_type) -> i64_type); - declare_rt!("rt_str_len", (ptr_type) -> i64_type); - declare_rt!("rt_struct_alloc", (i64_type) -> ptr_type); - declare_rt!("rt_i64_to_str", (i64_type) -> ptr_type); - declare_rt!("rt_f64_to_str", (f64_type) -> ptr_type); - declare_rt!("rt_bool_to_str", (i8_type) -> ptr_type); - declare_rt!("rt_result_ok", (i64_type) -> ptr_type); - declare_rt!("rt_result_err", (i64_type) -> ptr_type); - declare_rt!("rt_result_tag", (ptr_type) -> i64_type); - declare_rt!("rt_result_value", (ptr_type) -> i64_type); - declare_rt!("rt_option_some", (i64_type) -> ptr_type); - declare_rt!("rt_option_none", () -> ptr_type); - declare_rt!("rt_option_tag", (ptr_type) -> i64_type); - declare_rt!("rt_option_value", (ptr_type) -> i64_type); - // Stdlib - declare_rt!("rt_str_split", (ptr_type, ptr_type) -> ptr_type); - declare_rt!("rt_str_trim", (ptr_type) -> ptr_type); - declare_rt!("rt_str_upper", (ptr_type) -> ptr_type); - declare_rt!("rt_str_lower", (ptr_type) -> ptr_type); - declare_rt!("rt_str_starts_with", (ptr_type, ptr_type) -> i8_type); - declare_rt!("rt_str_ends_with", (ptr_type, ptr_type) -> i8_type); - declare_rt!("rt_str_replace", (ptr_type, ptr_type, ptr_type) -> ptr_type); - declare_rt!("rt_str_char_at", (ptr_type, i64_type) -> ptr_type); - declare_rt!("rt_str_contains", (ptr_type, ptr_type) -> i8_type); - declare_rt!("rt_str_index_of", (ptr_type, ptr_type) -> i64_type); - declare_rt!("rt_str_join", (ptr_type, ptr_type) -> ptr_type); - declare_rt!("rt_str_repeat", (ptr_type, i64_type) -> ptr_type); - declare_rt!("rt_read_line", () -> ptr_type); - declare_rt!("rt_read_file", (ptr_type) -> ptr_type); - declare_rt!("rt_write_file", (ptr_type, ptr_type) -> void); - declare_rt!("rt_pow", (i64_type, i64_type) -> i64_type); - declare_rt!("rt_sqrt", (f64_type) -> f64_type); - // Async - declare_rt!("rt_sleep_ms", (i64_type) -> void); - declare_rt!("rt_spawn_with_args", (ptr_type, ptr_type) -> ptr_type); - declare_rt!("rt_await_handle", (ptr_type) -> i64_type); - // HTTP + JSON - declare_rt!("rt_http_get", (ptr_type) -> ptr_type); - declare_rt!("rt_http_post", (ptr_type, ptr_type) -> ptr_type); - declare_rt!("rt_json_get", (ptr_type, ptr_type) -> ptr_type); - declare_rt!("rt_json_stringify", (ptr_type, ptr_type) -> ptr_type); - // HTTP server - declare_rt!("rt_http_server", (i64_type) -> i64_type); - declare_rt!("rt_http_route", (i64_type, ptr_type, ptr_type, ptr_type, ptr_type) -> void); - declare_rt!("rt_http_listen", (i64_type) -> void); - declare_rt!("rt_respond", (i64_type, ptr_type) -> ptr_type); - declare_rt!("rt_request_body", (ptr_type) -> ptr_type); - // Channels - declare_rt!("rt_channel_create", () -> ptr_type); - declare_rt!("rt_channel_send", (ptr_type, i64_type) -> void); - declare_rt!("rt_channel_recv", (ptr_type) -> i64_type); - declare_rt!("rt_channel_clone_sender", (ptr_type) -> ptr_type); - // Mutex - declare_rt!("rt_mutex_create", (i64_type) -> ptr_type); - declare_rt!("rt_mutex_get", (ptr_type) -> i64_type); - declare_rt!("rt_mutex_set", (ptr_type, i64_type) -> void); - declare_rt!("rt_mutex_clone", (ptr_type) -> ptr_type); - // HashMap - declare_rt!("rt_hashmap_new", () -> ptr_type); - declare_rt!("rt_hashmap_set", (ptr_type, ptr_type, ptr_type) -> void); - declare_rt!("rt_hashmap_get", (ptr_type, ptr_type) -> ptr_type); - declare_rt!("rt_hashmap_has", (ptr_type, ptr_type) -> i8_type); - declare_rt!("rt_hashmap_len", (ptr_type) -> i64_type); - declare_rt!("rt_hashmap_keys", (ptr_type) -> ptr_type); - declare_rt!("rt_hashmap_remove", (ptr_type, ptr_type) -> void); - // ARC - declare_rt!("rt_retain", (ptr_type) -> void); - declare_rt!("rt_release", (ptr_type) -> void); - - // ── Build enum/struct metadata ────────────────────────────────── - - let mut enum_variants: HashMap> = HashMap::new(); - let mut enum_variant_fields: HashMap<(String, String), Vec> = HashMap::new(); - let mut enum_max_slots: HashMap = HashMap::new(); - for item in &ast_module.items { - if let Item::Enum(e) = &item.node { - let variant_names: Vec = e.variants.iter().map(|v| v.name.clone()).collect(); - let tp_names = e.type_param_names(); - let mut max_fields: usize = 0; - for v in &e.variants { - let field_tys: Vec = v - .fields - .iter() - .map(|f| { - turbo_ty_from_type_expr_with_params(&f.node, &enum_variants, &tp_names) - }) - .collect(); - if !field_tys.is_empty() { - max_fields = max_fields.max(field_tys.len()); - enum_variant_fields.insert((e.name.clone(), v.name.clone()), field_tys); - } - } - if max_fields > 0 { - enum_max_slots.insert(e.name.clone(), max_fields); - } - enum_variants.insert(e.name.clone(), variant_names); - } - } - - let mut struct_fields: HashMap> = HashMap::new(); - for item in &ast_module.items { - if let Item::Struct(s) = &item.node { - let tp_names = s.type_param_names(); - let fields: Vec<(String, TurboTy)> = s - .fields - .iter() - .map(|f| { - ( - f.name.clone(), - turbo_ty_from_type_expr_with_params(&f.ty.node, &enum_variants, &tp_names), - ) - }) - .collect(); - struct_fields.insert(s.name.clone(), fields); - } - } - - // Build constants map - let mut constants_map: HashMap> = HashMap::new(); - for item in &ast_module.items { - if let Item::Const(c) = &item.node { - constants_map.insert(c.name.clone(), c.value.clone()); - } - } - - // ── Build struct derives map ──────────────────────────────────── - let mut struct_derives: HashMap> = HashMap::new(); - for item in &ast_module.items { - if let Item::Struct(s) = &item.node { - if !s.derives.is_empty() { - struct_derives.insert(s.name.clone(), s.derives.clone()); - } - } - } - - // ── Build trait impls map ─────────────────────────────────────── - let mut trait_impls: HashMap> = HashMap::new(); - for item in &ast_module.items { - if let Item::Impl(imp) = &item.node { - if let Some(ref trait_name) = imp.trait_name { - trait_impls - .entry(imp.type_name.clone()) - .or_default() - .push(trait_name.clone()); - } - } - } - // @derive(Display) counts as implementing Display - for (sname, derives) in &struct_derives { - if derives.contains(&"Display".to_string()) { - trait_impls - .entry(sname.clone()) - .or_default() - .push("Display".to_string()); - } - } - - // ── Extract closures and spawn sites ─────────────────────────── - let all_closures = extract_all_closures_llvm(ast_module); - let all_spawn_sites = extract_all_spawn_sites_llvm(ast_module); - - // Build lookup maps - let mut closure_fns: HashMap)> = HashMap::new(); - let mut spawn_thunks_map: HashMap = HashMap::new(); - - for site in &all_spawn_sites { - spawn_thunks_map.insert(site.span_start, site.thunk_name.clone()); - } - - // ── Declare all user functions ────────────────────────────────── - - let mut user_fns: HashMap> = HashMap::new(); - let mut fn_ret_types: HashMap = HashMap::new(); - let mut fn_asts: HashMap = HashMap::new(); - let mut fn_type_params: HashMap> = HashMap::new(); - - for item in &ast_module.items { - let Item::Function(f) = &item.node else { - continue; - }; - - let tp_names = f.type_param_names(); - let param_types: Vec> = f - .params - .iter() - .map(|p| { - resolve_llvm_type_ctx( - &p.ty.node, - context, - &enum_variants, - &enum_max_slots, - &tp_names, - ) - .into() - }) - .collect(); - - let ret_turbo = if let Some(ret_ty) = &f.return_type { - turbo_ty_from_type_expr_with_params(&ret_ty.node, &enum_variants, &tp_names) - } else { - TurboTy::Unit - }; - - let fn_type = if ret_turbo == TurboTy::Unit { - void_type.fn_type(¶m_types, false) - } else { - let ret_llvm = turbo_ty_to_llvm_ctx(&ret_turbo, context, &enum_max_slots); - ret_llvm.fn_type(¶m_types, false) - }; - - // For AOT, rename main -> turbo_main (the C runtime provides the real main) - let sym_name = if f.name == "main" { - "turbo_main" - } else { - &f.name - }; - - let func = module.add_function(sym_name, fn_type, None); - user_fns.insert(f.name.clone(), func); - fn_ret_types.insert(f.name.clone(), ret_turbo); - fn_asts.insert(f.name.clone(), f); - fn_type_params.insert(f.name.clone(), tp_names); - } - - // Declare methods from impl blocks (including trait impls + default trait methods) - for item in &ast_module.items { - let Item::Impl(imp) = &item.node else { - continue; - }; - for method_spanned in &imp.methods { - let method = &method_spanned.node; - let mangled = format!("{}__{}", imp.type_name, method.name); - - let param_types: Vec> = method - .params - .iter() - .map(|p| { - if p.name == "self" { - ptr_type.into() - } else { - resolve_llvm_type_ctx( - &p.ty.node, - context, - &enum_variants, - &enum_max_slots, - &[], - ) - .into() - } - }) - .collect(); - - let ret_turbo = if let Some(ret_ty) = &method.return_type { - turbo_ty_from_type_expr(&ret_ty.node, &enum_variants) - } else { - TurboTy::Unit - }; - - let fn_type = if ret_turbo == TurboTy::Unit { - void_type.fn_type(¶m_types, false) - } else { - let ret_llvm = turbo_ty_to_llvm_ctx(&ret_turbo, context, &enum_max_slots); - ret_llvm.fn_type(¶m_types, false) - }; - - let func = module.add_function(&mangled, fn_type, None); - user_fns.insert(mangled.clone(), func); - fn_ret_types.insert(mangled, ret_turbo); - } - } - - // Declare default trait methods (methods defined in trait body that aren't overridden by impl) - let mut trait_defs: HashMap = HashMap::new(); - for item in &ast_module.items { - if let Item::Trait(t) = &item.node { - trait_defs.insert(t.name.clone(), t); - } - } - for item in &ast_module.items { - if let Item::Impl(imp) = &item.node { - let trait_name = match &imp.trait_name { - Some(t) => t.clone(), - None => continue, - }; - let trait_def = match trait_defs.get(&trait_name) { - Some(t) => *t, - None => continue, - }; - let implemented: std::collections::HashSet = - imp.methods.iter().map(|m| m.node.name.clone()).collect(); - for trait_method in &trait_def.methods { - if implemented.contains(&trait_method.name) { - continue; - } - let body_expr = match &trait_method.default_body { - Some(b) => b, - None => continue, - }; - let mangled = format!("{}__{}", imp.type_name, trait_method.name); - let param_types: Vec> = trait_method - .params - .iter() - .map(|p| { - if p.name == "self" { - ptr_type.into() - } else { - resolve_llvm_type_ctx( - &p.ty.node, - context, - &enum_variants, - &enum_max_slots, - &[], - ) - .into() - } - }) - .collect(); - let ret_turbo = if let Some(ref rt) = trait_method.return_type { - turbo_ty_from_type_expr(&rt.node, &enum_variants) - } else { - TurboTy::Unit - }; - let fn_llvm = if ret_turbo == TurboTy::Unit { - void_type.fn_type(¶m_types, false) - } else { - let rl = turbo_ty_to_llvm_ctx(&ret_turbo, context, &enum_max_slots); - rl.fn_type(¶m_types, false) - }; - let func = module.add_function(&mangled, fn_llvm, None); - user_fns.insert(mangled.clone(), func); - fn_ret_types.insert(mangled.clone(), ret_turbo); - // Store the body for compilation later; we use fn_asts to point to a trait method - // We'll compile the body inline below using fn_asts for the type_name context - let _ = body_expr; // will compile body in "define bodies" loop - } - } - } - - // Declare derived methods (Display to_string, Eq ==, Clone clone) - for (struct_name, derives) in &struct_derives { - for derive_name in derives { - match derive_name.as_str() { - "Display" => { - let mangled = format!("{struct_name}__to_string"); - if !user_fns.contains_key(&mangled) { - let fn_type = ptr_type.fn_type(&[ptr_type.into()], false); - let func = module.add_function(&mangled, fn_type, None); - user_fns.insert(mangled.clone(), func); - fn_ret_types.insert(mangled, TurboTy::Str); - } - } - "Eq" => { - let mangled = format!("{struct_name}__eq"); - if !user_fns.contains_key(&mangled) { - let fn_type = i8_type.fn_type(&[ptr_type.into(), ptr_type.into()], false); - let func = module.add_function(&mangled, fn_type, None); - user_fns.insert(mangled.clone(), func); - fn_ret_types.insert(mangled, TurboTy::Bool); - } - } - "Clone" => { - let mangled = format!("{struct_name}__clone"); - if !user_fns.contains_key(&mangled) { - let fn_type = ptr_type.fn_type(&[ptr_type.into()], false); - let func = module.add_function(&mangled, fn_type, None); - user_fns.insert(mangled.clone(), func); - fn_ret_types.insert(mangled, TurboTy::Struct(struct_name.clone())); - } - } - _ => {} - } - } - } - - // Declare pre-extracted closures - for cl in &all_closures { - let mut param_tys: Vec> = vec![ptr_type.into()]; // env_ptr first - let mut turbo_param_tys: Vec = Vec::new(); - for p in cl.params { - let tty = turbo_ty_from_type_expr(&p.ty.node, &enum_variants); - param_tys.push(turbo_ty_to_llvm(&tty, context).into()); - turbo_param_tys.push(tty); - } - let ret_turbo = if let Some(ref rt) = cl.return_type { - turbo_ty_from_type_expr(&rt.node, &enum_variants) - } else { - TurboTy::Int - }; - let fn_llvm = if ret_turbo == TurboTy::Unit { - void_type.fn_type(¶m_tys, false) - } else { - let rl = turbo_ty_to_llvm(&ret_turbo, context); - rl.fn_type(¶m_tys, false) - }; - let func = module.add_function(&cl.name, fn_llvm, None); - user_fns.insert(cl.name.clone(), func); - let closure_turbo_ty = TurboTy::Fn(turbo_param_tys, Box::new(ret_turbo)); - fn_ret_types.insert( - cl.name.clone(), - match &closure_turbo_ty { - TurboTy::Fn(_, r) => *r.clone(), - _ => TurboTy::Int, - }, - ); - closure_fns.insert( - cl.span_start, - (cl.name.clone(), closure_turbo_ty, cl.free_vars.clone()), - ); - } - - // Declare spawn thunks - for site in &all_spawn_sites { - // Thunk signature: fn(__spawn_thunk_N)(args_ptr: *) -> i64 - let fn_type = i64_type.fn_type(&[ptr_type.into()], false); - let func = module.add_function(&site.thunk_name, fn_type, None); - user_fns.insert(site.thunk_name.clone(), func); - fn_ret_types.insert(site.thunk_name.clone(), TurboTy::Unit); - } - - // ── Define all function bodies ────────────────────────────────── - - let mut string_counter: usize = 0; - - for item in &ast_module.items { - let Item::Function(f) = &item.node else { - continue; - }; - let func = user_fns[&f.name]; - let tp_names = f.type_param_names(); - - let entry = context.append_basic_block(func, "entry"); - builder.position_at_end(entry); - - let mut vars: HashMap, TurboTy)> = HashMap::new(); - - // Create allocas for parameters - for (i, param) in f.params.iter().enumerate() { - let llvm_ty = resolve_llvm_type_ctx( - ¶m.ty.node, - context, - &enum_variants, - &enum_max_slots, - &tp_names, - ); - let turbo_ty = - turbo_ty_from_type_expr_with_params(¶m.ty.node, &enum_variants, &tp_names); - let alloca = builder - .build_alloca(llvm_ty, ¶m.name) - .expect("build_alloca failed"); - let param_val = func.get_nth_param(i as u32).unwrap(); - builder - .build_store(alloca, param_val) - .expect("build_store failed"); - vars.insert(param.name.clone(), (alloca, turbo_ty)); - } - - macro_rules! make_ctx { - ($vars:expr, $current_fn:expr) => { - Ctx { - context, - module, - builder, - current_fn: $current_fn, - user_fns: &user_fns, - fn_ret_types: &fn_ret_types, - fn_asts: &fn_asts, - fn_type_params: &fn_type_params, - rt_fns: &rt_fns, - vars: $vars, - string_counter: &mut string_counter, - struct_fields: &struct_fields, - enum_variants: &enum_variants, - enum_variant_fields: &enum_variant_fields, - enum_max_slots: &enum_max_slots, - constants: &constants_map, - loop_stack: Vec::new(), - closure_fns: &closure_fns, - spawn_thunks: &spawn_thunks_map, - struct_derives: &struct_derives, - trait_impls: &trait_impls, - concrete_struct_fields: std::collections::HashMap::new(), - } - }; - } - - let mut cx = make_ctx!(vars, func); - - let result = compile_expr(&mut cx, &f.body)?; - - // Only emit a terminator if the current block doesn't have one - let current_block = builder.get_insert_block().unwrap(); - if current_block.get_terminator().is_none() { - if f.return_type.is_some() { - if let Some((val, _)) = result { - builder - .build_return(Some(&val)) - .expect("build_return failed"); - } else { - builder.build_return(None).expect("build_return failed"); - } - } else { - builder.build_return(None).expect("build_return failed"); - } - } - } - - macro_rules! make_ctx_global { - ($vars:expr, $current_fn:expr) => { - Ctx { - context, - module, - builder, - current_fn: $current_fn, - user_fns: &user_fns, - fn_ret_types: &fn_ret_types, - fn_asts: &fn_asts, - fn_type_params: &fn_type_params, - rt_fns: &rt_fns, - vars: $vars, - string_counter: &mut string_counter, - struct_fields: &struct_fields, - enum_variants: &enum_variants, - enum_variant_fields: &enum_variant_fields, - enum_max_slots: &enum_max_slots, - constants: &constants_map, - loop_stack: Vec::new(), - closure_fns: &closure_fns, - spawn_thunks: &spawn_thunks_map, - struct_derives: &struct_derives, - trait_impls: &trait_impls, - concrete_struct_fields: HashMap::new(), - } - }; - } - - // Define method bodies from impl blocks - for item in &ast_module.items { - let Item::Impl(imp) = &item.node else { - continue; - }; - for method_spanned in &imp.methods { - let method = &method_spanned.node; - let mangled = format!("{}__{}", imp.type_name, method.name); - let func = user_fns[&mangled]; - - let entry = context.append_basic_block(func, "entry"); - builder.position_at_end(entry); - - let mut vars: HashMap, TurboTy)> = HashMap::new(); - - for (i, param) in method.params.iter().enumerate() { - let (llvm_ty, turbo_ty) = if param.name == "self" { - ( - ptr_type.as_basic_type_enum(), - TurboTy::Struct(imp.type_name.clone()), - ) - } else { - let llvm_ty = resolve_llvm_type_ctx( - ¶m.ty.node, - context, - &enum_variants, - &enum_max_slots, - &[], - ); - let turbo_ty = turbo_ty_from_type_expr(¶m.ty.node, &enum_variants); - (llvm_ty, turbo_ty) - }; - let alloca = builder - .build_alloca(llvm_ty, ¶m.name) - .expect("build_alloca failed"); - let param_val = func.get_nth_param(i as u32).unwrap(); - builder - .build_store(alloca, param_val) - .expect("build_store failed"); - vars.insert(param.name.clone(), (alloca, turbo_ty)); - } - - let mut cx = make_ctx_global!(vars, func); - - let result = compile_expr(&mut cx, &method.body)?; - - let current_block = builder.get_insert_block().unwrap(); - if current_block.get_terminator().is_none() { - if method.return_type.is_some() { - if let Some((val, _)) = result { - builder - .build_return(Some(&val)) - .expect("build_return failed"); - } else { - builder.build_return(None).expect("build_return failed"); - } - } else { - builder.build_return(None).expect("build_return failed"); - } - } - } - } - - // Define default trait method bodies - for item in &ast_module.items { - if let Item::Impl(imp) = &item.node { - let trait_name = match &imp.trait_name { - Some(t) => t.clone(), - None => continue, - }; - let trait_def = match trait_defs.get(&trait_name) { - Some(t) => *t, - None => continue, - }; - let implemented: std::collections::HashSet = - imp.methods.iter().map(|m| m.node.name.clone()).collect(); - for trait_method in &trait_def.methods { - if implemented.contains(&trait_method.name) { - continue; - } - let body_expr = match &trait_method.default_body { - Some(b) => b, - None => continue, - }; - let mangled = format!("{}__{}", imp.type_name, trait_method.name); - let func = match user_fns.get(&mangled) { - Some(f) => *f, - None => continue, - }; - - let entry = context.append_basic_block(func, "entry"); - builder.position_at_end(entry); - - let mut vars: HashMap, TurboTy)> = HashMap::new(); - for (i, param) in trait_method.params.iter().enumerate() { - let (llvm_ty, turbo_ty) = if param.name == "self" { - ( - ptr_type.as_basic_type_enum(), - TurboTy::Struct(imp.type_name.clone()), - ) - } else { - let lt = resolve_llvm_type_ctx( - ¶m.ty.node, - context, - &enum_variants, - &enum_max_slots, - &[], - ); - let tt = turbo_ty_from_type_expr(¶m.ty.node, &enum_variants); - (lt, tt) - }; - let alloca = builder - .build_alloca(llvm_ty, ¶m.name) - .expect("alloca failed"); - builder - .build_store(alloca, func.get_nth_param(i as u32).unwrap()) - .expect("store failed"); - vars.insert(param.name.clone(), (alloca, turbo_ty)); - } - - let mut cx = make_ctx_global!(vars, func); - let result = compile_expr(&mut cx, body_expr)?; - let cur = builder.get_insert_block().unwrap(); - if cur.get_terminator().is_none() { - if trait_method.return_type.is_some() { - if let Some((val, _)) = result { - builder.build_return(Some(&val)).expect("return failed"); - } else { - builder.build_return(None).expect("return failed"); - } - } else { - builder.build_return(None).expect("return failed"); - } - } - } - } - } - - // Define derived method bodies - for (struct_name, derives) in &struct_derives { - let fields = struct_fields.get(struct_name).cloned().unwrap_or_default(); - - for derive_name in derives { - match derive_name.as_str() { - "Display" => { - let mangled = format!("{struct_name}__to_string"); - let func = match user_fns.get(&mangled) { - Some(f) => *f, - None => continue, - }; - // Already declared. Define: concatenate all fields as "StructName { f1: v1, f2: v2 }" - let entry = context.append_basic_block(func, "entry"); - builder.position_at_end(entry); - let vars: HashMap, TurboTy)> = { - let mut m = HashMap::new(); - let self_alloca = builder.build_alloca(ptr_type, "self").expect("alloca"); - builder - .build_store(self_alloca, func.get_nth_param(0).unwrap()) - .expect("store"); - m.insert( - "self".to_string(), - (self_alloca, TurboTy::Struct(struct_name.clone())), - ); - m - }; - let mut cx = make_ctx_global!(vars, func); - - // Build display string: "StructName { field1: val1, field2: val2 }" - let mut parts: Vec> = Vec::new(); - let prefix = cx - .create_string(&format!("{struct_name} {{ ")) - .expect("str"); - parts.push(prefix.into()); - - let self_ptr = cx - .builder - .build_load(ptr_type, cx.vars["self"].0, "self_ptr") - .expect("load self") - .into_pointer_value(); - - for (fi, (fname, ftty)) in fields.iter().enumerate() { - if fi > 0 { - let sep = cx.create_string(", ").expect("str"); - parts.push(sep.into()); - } - let flabel = cx.create_string(&format!("{fname}: ")).expect("str"); - parts.push(flabel.into()); - - let offset = fi as u64 * 8; - let field_ptr = unsafe { - cx.builder - .build_gep( - i8_type, - self_ptr, - &[i64_type.const_int(offset, false)], - "fp", - ) - .expect("gep") - }; - let raw = cx - .builder - .build_load(i64_type, field_ptr, "fv") - .expect("load"); - let fval = narrow_from_storage(&cx, raw.into(), ftty); - let fstr = - convert_to_str(&mut cx, fval, ftty).unwrap_or_else(|_| raw.into()); - parts.push(fstr); - } - - let suffix = cx.create_string(" }").expect("str"); - parts.push(suffix.into()); - - // Concatenate all parts - let mut result_str: BasicValueEnum<'ctx> = - cx.create_string("").expect("str").into(); - for part in parts { - result_str = cx - .rt_call("rt_str_concat", &[result_str.into(), part.into()]) - .unwrap(); - } - cx.builder.build_return(Some(&result_str)).expect("return"); - } - "Eq" => { - let mangled = format!("{struct_name}__eq"); - let func = match user_fns.get(&mangled) { - Some(f) => *f, - None => continue, - }; - let entry = context.append_basic_block(func, "entry"); - builder.position_at_end(entry); - // eq(self, other) -> bool: compare each field - let self_ptr = func.get_nth_param(0).unwrap().into_pointer_value(); - let other_ptr = func.get_nth_param(1).unwrap().into_pointer_value(); - - // Compare all fields; return false at first mismatch - let merge_block = context.append_basic_block(func, "eq_merge"); - let mut phi_sources: Vec<( - BasicValueEnum<'ctx>, - inkwell::basic_block::BasicBlock<'ctx>, - )> = Vec::new(); - - let mut cur_block = entry; - for (fi, (_, ftty)) in fields.iter().enumerate() { - let offset = fi as u64 * 8; - let fp1 = unsafe { - builder - .build_gep( - i8_type, - self_ptr, - &[i64_type.const_int(offset, false)], - "fp1", - ) - .expect("gep") - }; - let fp2 = unsafe { - builder - .build_gep( - i8_type, - other_ptr, - &[i64_type.const_int(offset, false)], - "fp2", - ) - .expect("gep") - }; - let v1 = builder - .build_load(i64_type, fp1, "v1") - .expect("load") - .into_int_value(); - let v2 = builder - .build_load(i64_type, fp2, "v2") - .expect("load") - .into_int_value(); - - let cmp = if *ftty == TurboTy::Str { - let pv1 = builder.build_int_to_ptr(v1, ptr_type, "p1").expect("itp"); - let pv2 = builder.build_int_to_ptr(v2, ptr_type, "p2").expect("itp"); - let eq = builder - .build_direct_call( - rt_fns["rt_str_eq"], - &[pv1.into(), pv2.into()], - "seq", - ) - .expect("call"); - let eq_i = eq.try_as_basic_value().left().unwrap().into_int_value(); - let zero = i8_type.const_int(0, false); - builder - .build_int_compare(IntPredicate::NE, eq_i, zero, "cmp") - .expect("cmp") - } else { - builder - .build_int_compare(IntPredicate::EQ, v1, v2, "cmp") - .expect("cmp") - }; - - let next_block = context.append_basic_block(func, "eq_next"); - let false_val = i8_type.const_int(0, false); - phi_sources.push((false_val.into(), cur_block)); - builder - .build_conditional_branch(cmp, next_block, merge_block) - .expect("br"); - builder.position_at_end(next_block); - cur_block = next_block; - } - let true_val = i8_type.const_int(1, false); - phi_sources.push((true_val.into(), cur_block)); - builder.build_unconditional_branch(merge_block).expect("br"); - builder.position_at_end(merge_block); - let phi = builder.build_phi(i8_type, "eq_result").expect("phi"); - for (val, block) in &phi_sources { - phi.add_incoming(&[(val, *block)]); - } - builder - .build_return(Some(&phi.as_basic_value())) - .expect("return"); - } - "Clone" => { - let mangled = format!("{struct_name}__clone"); - let func = match user_fns.get(&mangled) { - Some(f) => *f, - None => continue, - }; - let entry = context.append_basic_block(func, "entry"); - builder.position_at_end(entry); - // clone(self) -> Self: alloc new struct, copy all fields - let self_ptr = func.get_nth_param(0).unwrap().into_pointer_value(); - let nf = fields.len() as u64; - let new_ptr = builder - .build_direct_call( - rt_fns["rt_struct_alloc"], - &[i64_type.const_int(nf, false).into()], - "clone_ptr", - ) - .expect("call") - .try_as_basic_value() - .left() - .unwrap() - .into_pointer_value(); - for fi in 0..fields.len() { - let offset = fi as u64 * 8; - let sp = unsafe { - builder - .build_gep( - i8_type, - self_ptr, - &[i64_type.const_int(offset, false)], - "sp", - ) - .expect("gep") - }; - let dp = unsafe { - builder - .build_gep( - i8_type, - new_ptr, - &[i64_type.const_int(offset, false)], - "dp", - ) - .expect("gep") - }; - let val = builder.build_load(i64_type, sp, "val").expect("load"); - builder.build_store(dp, val).expect("store"); - } - builder - .build_return(Some(&BasicValueEnum::PointerValue(new_ptr))) - .expect("return"); - } - _ => {} - } - } - } - - // Define closure bodies - for cl in &all_closures { - let func = match user_fns.get(&cl.name) { - Some(f) => *f, - None => continue, - }; - let entry = context.append_basic_block(func, "entry"); - builder.position_at_end(entry); - - let mut vars: HashMap, TurboTy)> = HashMap::new(); - - // First param is env_ptr - let env_ptr_val = func.get_nth_param(0).unwrap().into_pointer_value(); - - // Closure params start at index 1 - for (i, param) in cl.params.iter().enumerate() { - let tty = turbo_ty_from_type_expr(¶m.ty.node, &enum_variants); - let llvm_ty = turbo_ty_to_llvm(&tty, context); - let alloca = builder.build_alloca(llvm_ty, ¶m.name).expect("alloca"); - let param_val = func.get_nth_param((i + 1) as u32).unwrap(); - builder.build_store(alloca, param_val).expect("store"); - vars.insert(param.name.clone(), (alloca, tty)); - } - - // Load captured variables from env_ptr - // We need to find what was captured -- we'll populate during compile by scanning free_vars - // For now, load each captured var from env struct at compile time - // The free_vars list tells us which outer vars are captured - for (cap_idx, cap_name) in cl.free_vars.iter().enumerate() { - let cap_tty = cl - .capture_types - .get(cap_idx) - .cloned() - .unwrap_or(TurboTy::Int); - let offset = cap_idx as u64 * 8; - let field_ptr = unsafe { - builder - .build_gep( - i8_type, - env_ptr_val, - &[i64_type.const_int(offset, false)], - "cap_ptr", - ) - .expect("gep") - }; - let raw = builder - .build_load(i64_type, field_ptr, cap_name) - .expect("load"); - // If the captured variable is a string (ptr), convert i64 -> ptr - if matches!(cap_tty, TurboTy::Str | TurboTy::Array(_)) { - let ptr_val = builder - .build_int_to_ptr( - raw.into_int_value(), - ptr_type, - &format!("cap_ptr_{cap_name}"), - ) - .expect("itp"); - let alloca = builder - .build_alloca(ptr_type, &format!("cap_{cap_name}")) - .expect("alloca"); - builder.build_store(alloca, ptr_val).expect("store"); - vars.insert(cap_name.clone(), (alloca, cap_tty)); - } else { - let alloca = builder - .build_alloca(i64_type, &format!("cap_{cap_name}")) - .expect("alloca"); - builder.build_store(alloca, raw).expect("store"); - vars.insert(cap_name.clone(), (alloca, cap_tty)); - } - } - - let mut cx = make_ctx_global!(vars, func); - let result = compile_expr(&mut cx, cl.body)?; - let cur = builder.get_insert_block().unwrap(); - if cur.get_terminator().is_none() { - let ret_turbo = if let Some(ref rt) = cl.return_type { - turbo_ty_from_type_expr(&rt.node, &enum_variants) - } else { - TurboTy::Int - }; // Default to Int to match function declaration - if ret_turbo != TurboTy::Unit { - if let Some((val, _)) = result { - builder.build_return(Some(&val)).expect("return"); - } else { - // No value from body -- return a zero/null of the expected type - let dummy: BasicValueEnum = match &ret_turbo { - TurboTy::Int => i64_type.const_int(0, false).into(), - TurboTy::Bool => i8_type.const_int(0, false).into(), - TurboTy::Float => context.f64_type().const_float(0.0).into(), - _ => ptr_type.const_null().into(), - }; - builder.build_return(Some(&dummy)).expect("return"); - } - } else { - builder.build_return(None).expect("return"); - } - } - } - - // Define spawn thunk bodies - for site in &all_spawn_sites { - let func = match user_fns.get(&site.thunk_name) { - Some(f) => *f, - None => continue, - }; - let entry = context.append_basic_block(func, "entry"); - builder.position_at_end(entry); - // args_ptr points to [fn_ptr, arg0, arg1, ...] - let args_ptr = func.get_nth_param(0).unwrap().into_pointer_value(); - - // Load fn_ptr from offset 0 - let fn_ptr_i64 = builder - .build_load(i64_type, args_ptr, "fn_ptr_i64") - .expect("load"); - let fn_ptr = builder - .build_int_to_ptr(fn_ptr_i64.into_int_value(), ptr_type, "fn_ptr") - .expect("itp"); - - // Load each arg from offsets 8, 16, ... - let target_fn = user_fns.get(&site.callee_name); - let mut arg_vals: Vec> = Vec::new(); - for i in 0..site.num_args { - let offset = (i + 1) as u64 * 8; - let ap = unsafe { - builder - .build_gep( - i8_type, - args_ptr, - &[i64_type.const_int(offset, false)], - "ap", - ) - .expect("gep") - }; - let av = builder - .build_load(i64_type, ap, &format!("arg{i}")) - .expect("load"); - - // If we know the target function's parameter types, coerce appropriately - let val: BasicValueEnum = if let Some(tf) = target_fn { - let param_types = tf.get_type().get_param_types(); - if i < param_types.len() { - let av_val = av.into_int_value(); - match param_types[i] { - BasicTypeEnum::IntType(it) if it.get_bit_width() < 64 => builder - .build_int_truncate(av_val, it, "trunc") - .expect("trunc") - .into(), - BasicTypeEnum::FloatType(ft) => builder - .build_bit_cast(av.into_int_value(), ft, "f2i") - .expect("bc") - .into(), - BasicTypeEnum::PointerType(_) => builder - .build_int_to_ptr(av_val, ptr_type, "itp") - .expect("itp") - .into(), - _ => av.into(), - } - } else { - av.into() - } - } else { - av.into() - }; - arg_vals.push(val.into()); - } - - // Call the target function via fn_ptr and return the result - let result = if let Some(tf) = target_fn { - let call = builder - .build_direct_call(*tf, &arg_vals, "spawn_result") - .expect("call"); - call.try_as_basic_value().left() - } else { - let fn_type = i64_type.fn_type(&vec![i64_type.into(); site.num_args], false); - let call = builder - .build_indirect_call(fn_type, fn_ptr, &arg_vals, "spawn_result") - .expect("indirect_call"); - call.try_as_basic_value().left() - }; - if let Some(val) = result { - // Widen result to i64 for return - let ret_val: BasicValueEnum = match val { - BasicValueEnum::IntValue(iv) => { - if iv.get_type().get_bit_width() < 64 { - builder - .build_int_s_extend(iv, i64_type, "widen") - .expect("ext") - .into() - } else { - iv.into() - } - } - BasicValueEnum::FloatValue(fv) => { - builder.build_bit_cast(fv, i64_type, "f2i").expect("bc") - } - BasicValueEnum::PointerValue(pv) => builder - .build_ptr_to_int(pv, i64_type, "p2i") - .expect("pti") - .into(), - other => other, - }; - builder.build_return(Some(&ret_val)).expect("return"); - } else { - builder - .build_return(Some(&i64_type.const_int(0, false))) - .expect("return"); - } - } - - // Verify the module - module.verify().map_err(|e| CodegenError { - code: ErrorCode::E0405, - message: format!("LLVM module verification failed: {}", e.to_string_lossy()), - })?; - - Ok(()) -} diff --git a/turbo/crates/turbo-codegen-llvm/src/stmt.rs b/turbo/crates/turbo-codegen-llvm/src/stmt.rs deleted file mode 100644 index 49f7dc4..0000000 --- a/turbo/crates/turbo-codegen-llvm/src/stmt.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! Statement compilation: compile_stmt. - -use inkwell::types::BasicType; -use turbo_ast::*; - -use crate::ctx::Ctx; -use crate::expr::{compile_expr, narrow_from_storage}; -use crate::types::TurboTy; -use crate::CodegenError; - -// ── Statement compilation ────────────────────────────────────────── - -pub(crate) fn compile_stmt<'a, 'ctx>( - cx: &mut Ctx<'a, 'ctx>, - stmt: &Spanned, -) -> Result<(), CodegenError> { - match &stmt.node { - Stmt::Let { name, value, .. } => { - let rhs_is_ident = matches!(&value.node, Expr::Ident(_)); - let result = compile_expr(cx, value)?; - let (llvm_ty, turbo_ty, val) = if let Some((v, tty)) = result { - (v.get_type(), tty, Some(v)) - } else { - ( - cx.context.i64_type().as_basic_type_enum(), - TurboTy::Unit, - None, - ) - }; - // COW: if RHS is another variable with a heap type, increment refcount - if rhs_is_ident { - if let Some(v) = val { - let needs_retain = matches!( - &turbo_ty, - TurboTy::Array(_) - | TurboTy::Struct(_) - | TurboTy::Result(_, _) - | TurboTy::Optional(_) - ); - if needs_retain && v.is_pointer_value() { - cx.rt_call("rt_retain", &[v.into()]); - } - } - } - let alloca = cx.create_entry_block_alloca(llvm_ty, name); - if let Some(v) = val { - cx.builder - .build_store(alloca, v) - .expect("build_store failed"); - } - cx.vars.insert(name.clone(), (alloca, turbo_ty)); - // Transfer concrete struct field types from StructLit - if let Some(fields) = cx.concrete_struct_fields.remove("__last_struct_lit") { - cx.concrete_struct_fields.insert(name.clone(), fields); - } - Ok(()) - } - Stmt::Expr(e) => { - compile_expr(cx, e)?; - Ok(()) - } - Stmt::Return(value) => { - if let Some(val_expr) = value { - let result = compile_expr(cx, val_expr)?; - if let Some((v, _)) = result { - cx.builder - .build_return(Some(&v)) - .expect("build_return failed"); - } else { - cx.builder.build_return(None).expect("build_return failed"); - } - } else { - cx.builder.build_return(None).expect("build_return failed"); - } - // Create dead block for subsequent code - let dead_block = cx.context.append_basic_block(cx.current_fn, "after_return"); - cx.builder.position_at_end(dead_block); - Ok(()) - } - Stmt::Defer(_) => { - // Handled at block level - Ok(()) - } - Stmt::LetDestructure { fields, value, .. } => { - // Compile the value expression (should produce a struct pointer) - let (struct_val, struct_tty) = - compile_expr(cx, value)?.ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: "destructured value produced no result".to_string(), - })?; - - let struct_name = match &struct_tty { - TurboTy::Struct(name) => name.clone(), - _ => { - return Err(CodegenError { - code: ErrorCode::E0400, - message: "cannot destructure non-struct type".to_string(), - }) - } - }; - - let struct_layout = cx - .struct_fields - .get(&struct_name) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("undefined struct: {struct_name}"), - })? - .clone(); - - // Structs are heap-allocated: values are pointers. Fields are - // stored uniformly as i64 and narrowed to their real type on load, - // mirroring the Cranelift backend. - let struct_ptr = struct_val.into_pointer_value(); - let i64_ty = cx.context.i64_type(); - let i8_ty = cx.context.i8_type(); - - for field_name in fields { - let field_index = struct_layout - .iter() - .position(|(n, _)| n == field_name) - .ok_or_else(|| CodegenError { - code: ErrorCode::E0400, - message: format!("struct `{struct_name}` has no field `{field_name}`"), - })?; - - let field_tty = struct_layout[field_index].1.clone(); - let offset = (field_index as u64) * 8; - - let field_ptr = unsafe { - cx.builder - .build_gep( - i8_ty, - struct_ptr, - &[i64_ty.const_int(offset, false)], - "destructure_field_ptr", - ) - .expect("build_gep failed") - }; - - let raw = cx - .builder - .build_load(i64_ty, field_ptr, field_name) - .expect("build_load failed"); - let narrowed = narrow_from_storage(cx, raw, &field_tty); - - let alloca = cx.create_entry_block_alloca(narrowed.get_type(), field_name); - cx.builder - .build_store(alloca, narrowed) - .expect("build_store failed"); - cx.vars.insert(field_name.clone(), (alloca, field_tty)); - } - Ok(()) - } - } -} diff --git a/turbo/crates/turbo-codegen-llvm/src/types.rs b/turbo/crates/turbo-codegen-llvm/src/types.rs deleted file mode 100644 index f8cef7b..0000000 --- a/turbo/crates/turbo-codegen-llvm/src/types.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! Turbo-level type tags and LLVM type conversion helpers. - -use inkwell::context::Context; -use inkwell::types::BasicTypeEnum; -use inkwell::AddressSpace; -use std::collections::HashMap; -use turbo_ast::*; - -// ── Turbo-level type tag ──────────────────────────────────────────── - -#[derive(Debug, Clone, PartialEq)] -#[allow(dead_code)] -pub(crate) enum TurboTy { - Int, - Float, - Bool, - Str, - Unit, - Array(Box), - Struct(String), - Enum(String), - Fn(Vec, Box), - Result(Box, Box), - Optional(Box), - Future(Box), -} - -pub(crate) type Typed<'ctx> = (inkwell::values::BasicValueEnum<'ctx>, TurboTy); -pub(crate) type MaybeTyped<'ctx> = Option>; - -// ── Type conversion helpers ───────────────────────────────────────── - -pub(crate) fn turbo_ty_from_type_expr( - te: &TypeExpr, - enum_variants: &HashMap>, -) -> TurboTy { - turbo_ty_from_type_expr_with_params(te, enum_variants, &[]) -} - -pub(crate) fn turbo_ty_from_type_expr_with_params( - te: &TypeExpr, - enum_variants: &HashMap>, - type_params: &[String], -) -> TurboTy { - match te { - TypeExpr::Named(name) => { - if type_params.contains(name) { - return TurboTy::Int; - } - match name.as_str() { - "i32" | "i64" | "u32" | "u64" => TurboTy::Int, - "f32" | "f64" => TurboTy::Float, - "bool" => TurboTy::Bool, - "str" => TurboTy::Str, - _ => { - if enum_variants.contains_key(name.as_str()) { - TurboTy::Enum(name.clone()) - } else { - TurboTy::Struct(name.clone()) - } - } - } - } - TypeExpr::Unit => TurboTy::Unit, - TypeExpr::Array(inner) => TurboTy::Array(Box::new(turbo_ty_from_type_expr( - &inner.node, - enum_variants, - ))), - TypeExpr::FnType { params, ret } => { - let param_tys: Vec = params - .iter() - .map(|p| turbo_ty_from_type_expr(&p.node, enum_variants)) - .collect(); - let ret_ty = turbo_ty_from_type_expr(&ret.node, enum_variants); - TurboTy::Fn(param_tys, Box::new(ret_ty)) - } - TypeExpr::Result { ok_type, err_type } => { - let ok_tty = turbo_ty_from_type_expr(&ok_type.node, enum_variants); - let err_tty = turbo_ty_from_type_expr(&err_type.node, enum_variants); - TurboTy::Result(Box::new(ok_tty), Box::new(err_tty)) - } - TypeExpr::Optional(inner) => TurboTy::Optional(Box::new(turbo_ty_from_type_expr( - &inner.node, - enum_variants, - ))), - TypeExpr::Future(inner) => TurboTy::Future(Box::new(turbo_ty_from_type_expr_with_params( - &inner.node, - enum_variants, - type_params, - ))), - _ => TurboTy::Int, - } -} - -/// Convert a TurboTy to an LLVM BasicTypeEnum. -pub(crate) fn turbo_ty_to_llvm<'ctx>(tty: &TurboTy, context: &'ctx Context) -> BasicTypeEnum<'ctx> { - match tty { - TurboTy::Int => context.i64_type().into(), - TurboTy::Float => context.f64_type().into(), - TurboTy::Bool => context.i8_type().into(), - TurboTy::Str => context.ptr_type(AddressSpace::default()).into(), - TurboTy::Unit => context.i64_type().into(), - TurboTy::Fn(_, _) => context.ptr_type(AddressSpace::default()).into(), - TurboTy::Array(_) => context.ptr_type(AddressSpace::default()).into(), - TurboTy::Struct(_) => context.ptr_type(AddressSpace::default()).into(), - // NOTE: Enum types are i64 for unit-only enums. For data-carrying enums, - // use turbo_ty_to_llvm_with_enums() which checks enum_max_slots. - TurboTy::Enum(_) => context.i64_type().into(), - TurboTy::Result(_, _) => context.ptr_type(AddressSpace::default()).into(), - TurboTy::Optional(_) => context.ptr_type(AddressSpace::default()).into(), - TurboTy::Future(_) => context.ptr_type(AddressSpace::default()).into(), - } -} - -/// Resolve a TypeExpr to a TurboTy, then to an LLVM type. -#[allow(dead_code)] -pub(crate) fn resolve_llvm_type<'ctx>( - ty: &TypeExpr, - context: &'ctx Context, - enum_variants: &HashMap>, - type_params: &[String], -) -> BasicTypeEnum<'ctx> { - let tty = turbo_ty_from_type_expr_with_params(ty, enum_variants, type_params); - turbo_ty_to_llvm(&tty, context) -} - -/// Like turbo_ty_to_llvm but correctly handles data-carrying enums as pointers. -pub(crate) fn turbo_ty_to_llvm_ctx<'ctx>( - tty: &TurboTy, - context: &'ctx Context, - enum_max_slots: &HashMap, -) -> BasicTypeEnum<'ctx> { - if let TurboTy::Enum(ref name) = tty { - if enum_max_slots.contains_key(name) { - return context.ptr_type(AddressSpace::default()).into(); - } - } - turbo_ty_to_llvm(tty, context) -} - -/// Resolve a TypeExpr to an LLVM type, data-enum-aware. -pub(crate) fn resolve_llvm_type_ctx<'ctx>( - ty: &TypeExpr, - context: &'ctx Context, - enum_variants: &HashMap>, - enum_max_slots: &HashMap, - type_params: &[String], -) -> BasicTypeEnum<'ctx> { - let tty = turbo_ty_from_type_expr_with_params(ty, enum_variants, type_params); - turbo_ty_to_llvm_ctx(&tty, context, enum_max_slots) -} From 6735bbbdb4cb4c6f76a371ef0804e4ae42e195dd Mon Sep 17 00:00:00 2001 From: ZVN DEV <78920650+zvndev@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:38:11 -0400 Subject: [PATCH 2/7] fix(generics): type-check generic bodies and fix [T] indexing segfault MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two correctness holes in generic functions: 1. Generic function bodies were never type-checked — check_function returned early for any function with type params, so concrete errors inside them (let y: i64 = "str"; undefined names) were silently accepted. Now the body is checked with each type parameter erased to the Error poison type, so genuine bugs fire while operations over a type param (x + 1) don't false-positive. 2. Indexing a generic array param ([T]) segfaulted: turbo_ty_from_type_expr dropped type_params when recursing into Array/Optional/Result/Fn, so [T]'s element resolved to Struct("T"); indexing then returned an integer typed as a struct pointer and the refcount path dereferenced it. The call site now also recovers the concrete element type so a [str] arg prints correctly. Also: undefined-function errors now suggest a close match ("did you mean `print`?"), backed by a single-source BUILTIN_FNS list. Adds regression tests generic_body_checked and generic_array_index. --- .../turbo-codegen-cranelift/src/expr.rs | 51 +++- .../src/turbo_types.rs | 21 +- turbo/crates/turbo-sema/src/lib.rs | 27 +- .../crates/turbo-sema/src/type_check/expr.rs | 54 +++- turbo/crates/turbo-sema/src/type_check/mod.rs | 272 ++++++++++-------- .../tests/phase1/generic_array_index.expected | 2 + turbo/tests/phase1/generic_array_index.tb | 14 + .../phase1/generic_body_checked.expected | 1 + turbo/tests/phase1/generic_body_checked.tb | 13 + 9 files changed, 299 insertions(+), 156 deletions(-) create mode 100644 turbo/tests/phase1/generic_array_index.expected create mode 100644 turbo/tests/phase1/generic_array_index.tb create mode 100644 turbo/tests/phase1/generic_body_checked.expected create mode 100644 turbo/tests/phase1/generic_body_checked.tb diff --git a/turbo/crates/turbo-codegen-cranelift/src/expr.rs b/turbo/crates/turbo-codegen-cranelift/src/expr.rs index 4dbe830..fee7650 100644 --- a/turbo/crates/turbo-codegen-cranelift/src/expr.rs +++ b/turbo/crates/turbo-codegen-cranelift/src/expr.rs @@ -216,7 +216,12 @@ fn compile_expr_inner( Expr::Assign { target, value } => { // Optimize s = s + expr → rt_str_concat_inplace(s, expr) - if let Expr::BinaryOp { left, op: BinOp::Add, right } = &value.node { + if let Expr::BinaryOp { + left, + op: BinOp::Add, + right, + } = &value.node + { if let Expr::Ident(name) = &left.node { if name == target { if let Some((var, _, TurboTy::Str)) = cx.vars.get(target) { @@ -414,7 +419,10 @@ fn compile_expr_inner( cx.builder.ins().store(trusted, val, elem_ptr, 0i32); } else { // COW check: if refcount > 1, call rt_array_set (slow/copy path) - let rc = cx.builder.ins().load(types::I64, MemFlags::new(), arr, -8i32); + let rc = cx + .builder + .ins() + .load(types::I64, MemFlags::new(), arr, -8i32); let shared = cx.builder.ins().icmp_imm(IntCC::SignedGreaterThan, rc, 1); let slow_block = cx.builder.create_block(); @@ -422,7 +430,9 @@ fn compile_expr_inner( let merge_block = cx.builder.create_block(); cx.builder.append_block_param(merge_block, types::I64); - cx.builder.ins().brif(shared, slow_block, &[], fast_block, &[]); + cx.builder + .ins() + .brif(shared, slow_block, &[], fast_block, &[]); // Slow path: call rt_array_set (handles COW copy) cx.builder.switch_to_block(slow_block); @@ -437,7 +447,10 @@ fn compile_expr_inner( cx.builder.switch_to_block(fast_block); cx.builder.seal_block(fast_block); let len = cx.builder.ins().load(types::I64, trusted, arr, 0i32); - let oob = cx.builder.ins().icmp(IntCC::UnsignedGreaterThanOrEqual, idx, len); + let oob = cx + .builder + .ins() + .icmp(IntCC::UnsignedGreaterThanOrEqual, idx, len); let oob_block = cx.builder.create_block(); let store_block = cx.builder.create_block(); @@ -698,7 +711,10 @@ fn compile_expr_inner( if !cx.is_unsafe { // Bounds check: load length, compare, branch to OOB handler let len = cx.builder.ins().load(types::I64, trusted, arr, 0i32); - let oob = cx.builder.ins().icmp(IntCC::UnsignedGreaterThanOrEqual, idx, len); + let oob = cx + .builder + .ins() + .icmp(IntCC::UnsignedGreaterThanOrEqual, idx, len); let oob_block = cx.builder.create_block(); let ok_block = cx.builder.create_block(); @@ -1907,16 +1923,29 @@ fn compile_call( if let Some(ret_ty) = &f_def.return_type { if let TypeExpr::Named(ref ret_name) = ret_ty.node { if type_params.contains(ret_name) { - // Find which param has this type parameter + // Find which param carries this type parameter and + // recover the concrete TurboTy from the matching + // argument. We handle two shapes: + // fn f(x: T) -> T — infer T from the arg + // fn f(xs: [T]) -> T — infer T from the arg's + // array element type let mut inferred = None; for (i, param) in f_def.params.iter().enumerate() { - if let TypeExpr::Named(ref pname) = param.ty.node { - if pname == ret_name { - if i < arg_ttys.len() { - inferred = Some(arg_ttys[i].clone()); - } + if i >= arg_ttys.len() { + continue; + } + match ¶m.ty.node { + TypeExpr::Named(pname) if pname == ret_name => { + inferred = Some(arg_ttys[i].clone()); break; } + TypeExpr::Array(elem) if matches!(&elem.node, TypeExpr::Named(en) if en == ret_name) => { + if let TurboTy::Array(inner) = &arg_ttys[i] { + inferred = Some((**inner).clone()); + break; + } + } + _ => {} } } inferred.unwrap_or(ret_tty) diff --git a/turbo/crates/turbo-codegen-cranelift/src/turbo_types.rs b/turbo/crates/turbo-codegen-cranelift/src/turbo_types.rs index 3453a6b..a1cc5bf 100644 --- a/turbo/crates/turbo-codegen-cranelift/src/turbo_types.rs +++ b/turbo/crates/turbo-codegen-cranelift/src/turbo_types.rs @@ -86,24 +86,33 @@ pub(crate) fn turbo_ty_from_type_expr_with_params( } TypeExpr::Unit => TurboTy::Unit, TypeExpr::Array(inner) => { - let inner_tty = turbo_ty_from_type_expr(&inner.node, enum_variants); + // Thread type_params so the element of a generic array like `[T]` + // resolves to `Int` (the uniform type-param representation) rather + // than being misread as `Struct("T")`. Getting this wrong makes + // indexing return an integer typed as a struct pointer, which the + // refcount/retain path then dereferences — a segfault. + let inner_tty = + turbo_ty_from_type_expr_with_params(&inner.node, enum_variants, type_params); TurboTy::Array(Box::new(inner_tty)) } TypeExpr::FnType { params, ret } => { let param_tys: Vec = params .iter() - .map(|p| turbo_ty_from_type_expr(&p.node, enum_variants)) + .map(|p| turbo_ty_from_type_expr_with_params(&p.node, enum_variants, type_params)) .collect(); - let ret_ty = turbo_ty_from_type_expr(&ret.node, enum_variants); + let ret_ty = turbo_ty_from_type_expr_with_params(&ret.node, enum_variants, type_params); TurboTy::Fn(param_tys, Box::new(ret_ty)) } TypeExpr::Result { ok_type, err_type } => { - let ok_tty = turbo_ty_from_type_expr(&ok_type.node, enum_variants); - let err_tty = turbo_ty_from_type_expr(&err_type.node, enum_variants); + let ok_tty = + turbo_ty_from_type_expr_with_params(&ok_type.node, enum_variants, type_params); + let err_tty = + turbo_ty_from_type_expr_with_params(&err_type.node, enum_variants, type_params); TurboTy::Result(Box::new(ok_tty), Box::new(err_tty)) } TypeExpr::Optional(inner) => { - let inner_tty = turbo_ty_from_type_expr(&inner.node, enum_variants); + let inner_tty = + turbo_ty_from_type_expr_with_params(&inner.node, enum_variants, type_params); TurboTy::Optional(Box::new(inner_tty)) } // Future is a thread handle pointer (underlying value is i64/ptr) diff --git a/turbo/crates/turbo-sema/src/lib.rs b/turbo/crates/turbo-sema/src/lib.rs index f4de60c..419e293 100644 --- a/turbo/crates/turbo-sema/src/lib.rs +++ b/turbo/crates/turbo-sema/src/lib.rs @@ -191,7 +191,7 @@ pub(crate) fn int_literal_fits_in_type(n: i64, target: &Ty) -> bool { match target { Ty::U8 => (0..=255).contains(&n), Ty::U16 => (0..=65535).contains(&n), - Ty::U32 => n >= 0 && n <= 4_294_967_295, + Ty::U32 => (0..=4_294_967_295).contains(&n), Ty::U64 => n >= 0, Ty::I8 => (-128..=127).contains(&n), Ty::I16 => (-32768..=32767).contains(&n), @@ -221,6 +221,31 @@ pub(crate) fn types_compatible(expected: &Ty, actual: &Ty) -> bool { } } +/// Replace every generic type parameter inside a type with `Ty::Error`. +/// +/// `Ty::Error` is the checker's poison type — expressions whose type touches it +/// are exempt from downstream checks. Erasing a generic function's parameter and +/// return types to `Error` lets us type-check the *body* (catching undefined +/// names and concrete-vs-concrete mismatches) without emitting false positives +/// on legitimate operations over a type parameter (e.g. `x + 1` where `x: T`). +pub(crate) fn erase_type_params(ty: &Ty) -> Ty { + match ty { + Ty::TypeParam(_) => Ty::Error, + Ty::Array(inner) => Ty::Array(Box::new(erase_type_params(inner))), + Ty::Optional(inner) => Ty::Optional(Box::new(erase_type_params(inner))), + Ty::Future(inner) => Ty::Future(Box::new(erase_type_params(inner))), + Ty::Result(ok, err) => Ty::Result( + Box::new(erase_type_params(ok)), + Box::new(erase_type_params(err)), + ), + Ty::Fn(params, ret) => Ty::Fn( + params.iter().map(erase_type_params).collect(), + Box::new(erase_type_params(ret)), + ), + other => other.clone(), + } +} + /// Check if a type is safe to use across the C FFI boundary. pub(crate) fn is_ffi_safe_type(ty: &Ty) -> bool { matches!( diff --git a/turbo/crates/turbo-sema/src/type_check/expr.rs b/turbo/crates/turbo-sema/src/type_check/expr.rs index c8547f1..513e407 100644 --- a/turbo/crates/turbo-sema/src/type_check/expr.rs +++ b/turbo/crates/turbo-sema/src/type_check/expr.rs @@ -354,7 +354,9 @@ impl Checker { args[0].span.clone(), ); } - if arg_ty.is_float() { return Ty::F64; } + if arg_ty.is_float() { + return Ty::F64; + } return Ty::I64; } if name == "min" || name == "max" { @@ -382,7 +384,9 @@ impl Checker { args[1].span.clone(), ); } - if a_ty.is_float() || b_ty.is_float() { return Ty::F64; } + if a_ty.is_float() || b_ty.is_float() { + return Ty::F64; + } return Ty::I64; } if name == "to_str" { @@ -1238,7 +1242,10 @@ impl Checker { if args.len() != 1 { self.error( ErrorCode::E0513, - format!("float_to_int() takes exactly 1 argument, got {}", args.len()), + format!( + "float_to_int() takes exactly 1 argument, got {}", + args.len() + ), callee.span.clone(), ); return Ty::Error; @@ -1259,7 +1266,10 @@ impl Checker { if args.len() != 1 { self.error( ErrorCode::E0513, - format!("int_to_float() takes exactly 1 argument, got {}", args.len()), + format!( + "int_to_float() takes exactly 1 argument, got {}", + args.len() + ), callee.span.clone(), ); return Ty::Error; @@ -1280,7 +1290,10 @@ impl Checker { if args.len() != 1 { self.error( ErrorCode::E0513, - format!("str_from_char() takes exactly 1 argument, got {}", args.len()), + format!( + "str_from_char() takes exactly 1 argument, got {}", + args.len() + ), callee.span.clone(), ); return Ty::Error; @@ -1854,7 +1867,10 @@ impl Checker { if args.len() != 3 { self.error( ErrorCode::E0513, - format!("http_post_with_headers() takes exactly 3 arguments, got {}", args.len()), + format!( + "http_post_with_headers() takes exactly 3 arguments, got {}", + args.len() + ), callee.span.clone(), ); return Ty::Error; @@ -2139,7 +2155,10 @@ impl Checker { if args.len() != 1 { self.error( ErrorCode::E0513, - format!("json_build() takes exactly 1 argument, got {}", args.len()), + format!( + "json_build() takes exactly 1 argument, got {}", + args.len() + ), callee.span.clone(), ); return Ty::Error; @@ -3114,11 +3133,22 @@ impl Checker { } } } - self.error( - ErrorCode::E0301, - format!("undefined function `{name}`"), - callee.span.clone(), - ); + // Offer a "did you mean" against user-defined functions + // and builtins — typos on common builtins like `print` + // are the most frequent beginner error. + let candidates: Vec<&str> = self + .functions + .keys() + .map(String::as_str) + .chain(crate::type_check::BUILTIN_FNS.iter().copied()) + .collect(); + let msg = match suggest::suggest_for(name, candidates.iter().copied()) { + Some(hit) => { + format!("undefined function `{name}`. did you mean `{hit}`?") + } + None => format!("undefined function `{name}`"), + }; + self.error(ErrorCode::E0301, msg, callee.span.clone()); Ty::Error } } else if let Expr::FieldAccess { object, field } = &callee.node { diff --git a/turbo/crates/turbo-sema/src/type_check/mod.rs b/turbo/crates/turbo-sema/src/type_check/mod.rs index ef5f7b4..e2a6289 100644 --- a/turbo/crates/turbo-sema/src/type_check/mod.rs +++ b/turbo/crates/turbo-sema/src/type_check/mod.rs @@ -31,125 +31,129 @@ use turbo_ast::*; use crate::scope::VarInfo; use crate::{ - extract_int_literal, int_literal_fits_in_type, is_ffi_safe_type, resolve_type_expr, - resolve_type_expr_with_params, types_compatible, Checker, EnumInfo, FnSig, StructInfo, - TraitInfo, TraitMethodInfo, Ty, MAX_EXPR_DEPTH, + erase_type_params, extract_int_literal, int_literal_fits_in_type, is_ffi_safe_type, + resolve_type_expr, resolve_type_expr_with_params, types_compatible, Checker, EnumInfo, FnSig, + StructInfo, TraitInfo, TraitMethodInfo, Ty, MAX_EXPR_DEPTH, }; mod expr; mod stmt; +/// Names recognized as built-in functions. Single source of truth for both the +/// `is_builtin_function` membership check and "did you mean" suggestions on an +/// undefined call. +pub(crate) const BUILTIN_FNS: &[&str] = &[ + "print", + "panic", + "assert", + "assert_eq", + "assert_ne", + "len", + "push", + "abs", + "min", + "max", + "to_str", + "map", + "filter", + "reduce", + "split", + "trim", + "upper", + "lower", + "starts_with", + "ends_with", + "replace", + "char_at", + "contains", + "index_of", + "join", + "repeat", + "read_line", + "read_file", + "write_file", + "try_read_file", + "try_write_file", + "shell_exec", + "exec", + "env_get", + "pow", + "sqrt", + "sleep", + "http_get", + "http_post", + "http_post_with_headers", + "json_get", + "json_stringify", + "json_build", + "float_to_int", + "int_to_float", + "str_from_char", + "http_server", + "http_server_public", + "route", + "http_listen", + "respond", + "respond_text", + "respond_html", + "respond_json", + "request_body", + "request_method", + "request_path", + "request_query", + "request_header", + "channel", + "send", + "recv", + "mutex", + "mutex_get", + "mutex_set", + "clone", + "hashmap", + "hashmap_set", + "hashmap_get", + "hashmap_has", + "hashmap_len", + "hashmap_size", + "hashmap_keys", + "hashmap_remove", + "to_json", + "to_json_array", + "deref", + "store", + "args", + "type_of", + "random", + "random_range", + "substring", + "pad_left", + "pad_right", + "str_to_int", + "str_to_float", + "file_exists", + "delete_file", + "list_dir", + "mkdir", + "path_join", + "path_dir", + "path_base", + "path_ext", + "sort", + "reverse", + "array_contains", + "slice", + "any", + "all", + "time_now", + "time_ms", + "format_time", +]; + impl Checker { // === Check module === pub(crate) fn is_builtin_function(name: &str) -> bool { - matches!( - name, - "print" - | "panic" - | "assert" - | "assert_eq" - | "assert_ne" - | "len" - | "push" - | "abs" - | "min" - | "max" - | "to_str" - | "map" - | "filter" - | "reduce" - | "split" - | "trim" - | "upper" - | "lower" - | "starts_with" - | "ends_with" - | "replace" - | "char_at" - | "contains" - | "index_of" - | "join" - | "repeat" - | "read_line" - | "read_file" - | "write_file" - | "try_read_file" - | "try_write_file" - | "shell_exec" - | "exec" - | "env_get" - | "pow" - | "sqrt" - | "sleep" - | "http_get" - | "http_post" - | "http_post_with_headers" - | "json_get" - | "json_stringify" - | "json_build" - | "float_to_int" - | "int_to_float" - | "str_from_char" - | "http_server" - | "http_server_public" - | "route" - | "http_listen" - | "respond" - | "respond_text" - | "respond_html" - | "respond_json" - | "request_body" - | "request_method" - | "request_path" - | "request_query" - | "request_header" - | "channel" - | "send" - | "recv" - | "mutex" - | "mutex_get" - | "mutex_set" - | "clone" - | "hashmap" - | "hashmap_set" - | "hashmap_get" - | "hashmap_has" - | "hashmap_len" - | "hashmap_size" - | "hashmap_keys" - | "hashmap_remove" - | "to_json" - | "to_json_array" - | "deref" - | "store" - | "args" - | "type_of" - | "random" - | "random_range" - | "substring" - | "pad_left" - | "pad_right" - | "str_to_int" - | "str_to_float" - | "file_exists" - | "delete_file" - | "list_dir" - | "mkdir" - | "path_join" - | "path_dir" - | "path_base" - | "path_ext" - | "sort" - | "reverse" - | "array_contains" - | "slice" - | "any" - | "all" - | "time_now" - | "time_ms" - | "format_time" - ) + BUILTIN_FNS.contains(&name) } /// Walk a chain of FieldAccess / Index expressions to find the root variable name. @@ -895,14 +899,30 @@ impl Checker { } }; - // Skip body checking for generic functions — their bodies - // contain type parameters that aren't concrete types. - // Type safety is enforced at the call site via inference. - if !sig.type_params.is_empty() { - return; - } + // Generic functions are still body-checked, but each type parameter is + // erased to `Ty::Error` first. `Error` is the checker's poison type: any + // expression that touches it is exempt from further checks, so we don't + // emit false positives on legitimate generic operations (`x + 1` where + // `x: T`). Concrete bugs that don't involve a type parameter — undefined + // names, `let y: i64 = "string"`, a concrete-typed tail that disagrees + // with the return type — still surface normally. Call-site inference + // (which uses the un-erased signature) is unaffected. + let is_generic = !sig.type_params.is_empty(); + let param_tys: Vec = if is_generic { + sig.params + .iter() + .map(|(_, t)| erase_type_params(t)) + .collect() + } else { + sig.params.iter().map(|(_, t)| t.clone()).collect() + }; + let ret_ty = if is_generic { + erase_type_params(&sig.ret) + } else { + sig.ret.clone() + }; - self.current_return_type = sig.ret.clone(); + self.current_return_type = ret_ty.clone(); let prev_unsafe = self.in_unsafe_context; self.in_unsafe_context = f.is_unsafe; @@ -911,8 +931,8 @@ impl Checker { // Inject module-level constants self.inject_constants(); - // Define parameters - for (i, (name, ty)) in sig.params.iter().enumerate() { + // Define parameters (type-param-erased for generic functions, see above) + for (i, (name, _)) in sig.params.iter().enumerate() { let (param_span, param_mutable) = if i < f.params.len() { (f.params[i].span.clone(), f.params[i].mutable) } else { @@ -921,7 +941,7 @@ impl Checker { self.define_var( name, VarInfo { - ty: ty.clone(), + ty: param_tys[i].clone(), mutable: param_mutable, span: param_span, from_let: false, @@ -944,10 +964,10 @@ impl Checker { } else { false }; - if !sig.ret.is_error() + if !ret_ty.is_error() && !body_ty.is_error() - && sig.ret != Ty::Unit - && !types_compatible(&sig.ret, &body_ty) + && ret_ty != Ty::Unit + && !types_compatible(&ret_ty, &body_ty) && !body_has_return { // Allow integer literal coercion for tail expression (e.g., fn foo() -> u8 { 42 }) @@ -958,18 +978,18 @@ impl Checker { }; let is_return_coercion = body_ty == Ty::I64 && matches!( - sig.ret, + ret_ty, Ty::I8 | Ty::I16 | Ty::I32 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::U64 ) && tail_expr .and_then(extract_int_literal) - .is_some_and(|n| int_literal_fits_in_type(n, &sig.ret)); + .is_some_and(|n| int_literal_fits_in_type(n, &ret_ty)); if !is_return_coercion { self.error( ErrorCode::E0109, format!( "function `{}` should return `{}` but body returns `{}`", - f.name, sig.ret, body_ty + f.name, ret_ty, body_ty ), f.body.span.clone(), ); diff --git a/turbo/tests/phase1/generic_array_index.expected b/turbo/tests/phase1/generic_array_index.expected new file mode 100644 index 0000000..c117988 --- /dev/null +++ b/turbo/tests/phase1/generic_array_index.expected @@ -0,0 +1,2 @@ +10 +alpha diff --git a/turbo/tests/phase1/generic_array_index.tb b/turbo/tests/phase1/generic_array_index.tb new file mode 100644 index 0000000..d54747f --- /dev/null +++ b/turbo/tests/phase1/generic_array_index.tb @@ -0,0 +1,14 @@ +// Regression: indexing a generic array parameter `[T]` used to segfault +// (the element type was misread as a struct pointer and the refcount path +// dereferenced an integer). The call site must also recover the concrete +// element type so a `[str]` argument prints as a string, not a pointer. +fn firstof(xs: [T]) -> T { + xs[0] +} + +fn main() { + let nums = [10, 20, 30] + let words = ["alpha", "beta"] + print(firstof(nums)) + print(firstof(words)) +} diff --git a/turbo/tests/phase1/generic_body_checked.expected b/turbo/tests/phase1/generic_body_checked.expected new file mode 100644 index 0000000..29ed32e --- /dev/null +++ b/turbo/tests/phase1/generic_body_checked.expected @@ -0,0 +1 @@ +ERROR:doesn't match value type `str` diff --git a/turbo/tests/phase1/generic_body_checked.tb b/turbo/tests/phase1/generic_body_checked.tb new file mode 100644 index 0000000..db842d3 --- /dev/null +++ b/turbo/tests/phase1/generic_body_checked.tb @@ -0,0 +1,13 @@ +// Regression: generic function bodies must be type-checked. +// Previously the checker skipped all generic bodies, so concrete type +// errors inside them were silently accepted. Type parameters are erased +// to the Error poison type, so operations over `T` are exempt while a +// concrete mismatch like this still fires. +fn bad(x: T) -> T { + let y: i64 = "a string" + x +} + +fn main() { + print("unreachable") +} From c84560e982a3bacb54574a7332b7ffe153a1932c Mon Sep 17 00:00:00 2001 From: ZVN DEV <78920650+zvndev@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:38:21 -0400 Subject: [PATCH 3/7] fix(dx): range bounds, int-overflow message, fmt safety, LSP codes - parser: range upper bound now parses a full binary expression, so `0..len + 1` and `1..n - 1` work (previously only a unary was parsed) - lexer/cli: an integer literal too large for i64 now reports "integer literal ... is too large for i64" instead of "unexpected character" - cli: `explain` accepts lowercase codes (`explain e0100`) - cli: drop the now-dead `--llvm` build flag - formatter: refuse to reformat files that don't lex/parse (no more silently reindenting broken code) - lsp: diagnostics now carry the E0NNN error code Adds regression test range_arith_bound. --- turbo/crates/turbo-cli/src/formatter.rs | 22 ++++ turbo/crates/turbo-cli/src/main.rs | 114 ++++++------------ turbo/crates/turbo-lsp/src/main.rs | 3 + turbo/crates/turbo-parser/src/lib.rs | 7 +- turbo/tests/phase1/range_arith_bound.expected | 1 + turbo/tests/phase1/range_arith_bound.tb | 10 ++ 6 files changed, 81 insertions(+), 76 deletions(-) create mode 100644 turbo/tests/phase1/range_arith_bound.expected create mode 100644 turbo/tests/phase1/range_arith_bound.tb diff --git a/turbo/crates/turbo-cli/src/formatter.rs b/turbo/crates/turbo-cli/src/formatter.rs index ddf3dc7..3364a68 100644 --- a/turbo/crates/turbo-cli/src/formatter.rs +++ b/turbo/crates/turbo-cli/src/formatter.rs @@ -15,6 +15,28 @@ pub fn format_file(path: &Path, check: bool) { } }; + // Refuse to reformat source that doesn't lex/parse. A formatter that + // silently reindents broken code can mask or compound the underlying error; + // gofmt/rustfmt both decline unparseable input. Leave the file untouched. + let (tokens, lex_errors) = turbo_lexer::tokenize(&source); + if !lex_errors.is_empty() { + eprintln!( + "error: {} has syntax errors and was not formatted (run `turbolang check {}`)", + path.display(), + path.display() + ); + std::process::exit(1); + } + let (_, parse_errors) = turbo_parser::parse(tokens); + if !parse_errors.is_empty() { + eprintln!( + "error: {} has syntax errors and was not formatted (run `turbolang check {}`)", + path.display(), + path.display() + ); + std::process::exit(1); + } + let formatted = format_source(&source); if check { diff --git a/turbo/crates/turbo-cli/src/main.rs b/turbo/crates/turbo-cli/src/main.rs index 3efcb47..2f8fd60 100644 --- a/turbo/crates/turbo-cli/src/main.rs +++ b/turbo/crates/turbo-cli/src/main.rs @@ -74,10 +74,6 @@ enum Commands { #[arg(long, short)] verbose: bool, - /// Use LLVM backend instead of Cranelift - #[arg(long)] - llvm: bool, - /// Compilation target (e.g. "wasm", "linux-arm64", "linux-x86") #[arg(long)] target: Option, @@ -177,19 +173,11 @@ fn main() { file, output, verbose, - llvm, target, link, } => { let path = resolve_entry_file(file); - build_file( - &path, - output.as_deref(), - verbose, - llvm, - target.as_deref(), - &link, - ); + build_file(&path, output.as_deref(), verbose, target.as_deref(), &link); } Commands::Init { name } => init_project(&name), Commands::Repl => repl::run_repl(), @@ -415,12 +403,10 @@ fn area(shape: Shape) -> f64 { }); // .gitignore - std::fs::write(dir.join(".gitignore"), "turbo_modules/\ntarget/\n*.o\n").unwrap_or_else( - |e| { - eprintln!("\x1b[1;31merror\x1b[0m: failed to write .gitignore: {e}"); - std::process::exit(1); - }, - ); + std::fs::write(dir.join(".gitignore"), "turbo_modules/\ntarget/\n*.o\n").unwrap_or_else(|e| { + eprintln!("\x1b[1;31merror\x1b[0m: failed to write .gitignore: {e}"); + std::process::exit(1); + }); eprintln!("\x1b[32m\u{2713}\x1b[0m Created project `{name}`"); eprintln!(" cd {name} && turbolang run"); @@ -1523,6 +1509,26 @@ fn update_deps() { } /// Print a rich error diagnostic using ariadne. +/// Produce a (message, help) pair for a lexer error span. A bare "unexpected +/// character" is unhelpful when the real problem is a numeric literal the lexer +/// matched but couldn't fit into `i64` (it returns `None`, which surfaces as a +/// lex error). Detect the all-digits case and say so precisely. +fn lex_error_message(snippet: &str) -> (String, &'static str) { + let is_int_literal = + !snippet.is_empty() && snippet.chars().all(|c| c.is_ascii_digit() || c == '_'); + if is_int_literal { + ( + format!("integer literal `{snippet}` is too large for `i64` (max 9223372036854775807)"), + "use a smaller value, or split the computation to stay within i64 range", + ) + } else { + ( + format!("unexpected character `{snippet}`"), + "remove this character or check for typos", + ) + } +} + fn report_error( source: &str, filename: &str, @@ -1673,14 +1679,8 @@ fn run_file(path: &std::path::Path, verbose: bool) { if !lex_errors.is_empty() { for span in &lex_errors { let snippet = &source[span.clone()]; - report_error( - &source, - &filename, - &format!("unexpected character `{snippet}`"), - span, - Some("remove this character or check for typos"), - None, - ); + let (msg, help) = lex_error_message(snippet); + report_error(&source, &filename, &msg, span, Some(help), None); } std::process::exit(1); } @@ -1829,14 +1829,8 @@ fn check_file(path: &std::path::Path) { if !lex_errors.is_empty() { for span in &lex_errors { let snippet = &source[span.clone()]; - report_error( - &source, - &filename, - &format!("unexpected character `{snippet}`"), - span, - Some("remove this character or check for typos"), - None, - ); + let (msg, help) = lex_error_message(snippet); + report_error(&source, &filename, &msg, span, Some(help), None); } std::process::exit(1); } @@ -1945,14 +1939,8 @@ fn test_file(file: Option) { if !lex_errors.is_empty() { for span in &lex_errors { let snippet = &source[span.clone()]; - report_error( - &source, - &filename, - &format!("unexpected character `{snippet}`"), - span, - Some("remove this character or check for typos"), - None, - ); + let (msg, help) = lex_error_message(snippet); + report_error(&source, &filename, &msg, span, Some(help), None); } std::process::exit(1); } @@ -2306,14 +2294,8 @@ fn test_run_fn(path: &std::path::Path, fn_name: &str) { if !lex_errors.is_empty() { for span in &lex_errors { let snippet = &source[span.clone()]; - report_error( - &source, - &filename, - &format!("unexpected character `{snippet}`"), - span, - Some("remove this character or check for typos"), - None, - ); + let (msg, help) = lex_error_message(snippet); + report_error(&source, &filename, &msg, span, Some(help), None); } std::process::exit(1); } @@ -2406,7 +2388,6 @@ fn build_file( path: &std::path::Path, output: Option<&std::path::Path>, verbose: bool, - use_llvm: bool, target: Option<&str>, link_libs: &[String], ) { @@ -2457,14 +2438,8 @@ fn build_file( if !lex_errors.is_empty() { for span in &lex_errors { let snippet = &source[span.clone()]; - report_error( - &source, - &filename, - &format!("unexpected character `{snippet}`"), - span, - Some("remove this character or check for typos"), - None, - ); + let (msg, help) = lex_error_message(snippet); + report_error(&source, &filename, &msg, span, Some(help), None); } std::process::exit(1); } @@ -2553,20 +2528,6 @@ fn build_file( let r = turbo_codegen_cranelift::wasm_compile(&module, output_path, use_wasi) .map_err(|e| e.to_string()); (r, "Cranelift/WASM") - } else if use_llvm { - #[cfg(feature = "llvm")] - { - let r = - turbo_codegen_llvm::aot_compile(&module, output_path).map_err(|e| e.to_string()); - (r, "LLVM") - } - #[cfg(not(feature = "llvm"))] - { - ( - Err("LLVM backend not available — rebuild with --features llvm".to_string()), - "LLVM", - ) - } } else { let cross_target = resolve_target_triple(target); let r = turbo_codegen_cranelift::aot_compile( @@ -3380,7 +3341,10 @@ fn detailed_explanation(code: ErrorCode) -> Option<&'static str> { } fn explain_error(code_str: &str) { - if let Some(code) = ErrorCode::parse(code_str) { + // Accept lowercase input (`e0100`) — the codes are conventionally uppercase + // but making users match case is needless friction. + let normalized = code_str.to_uppercase(); + if let Some(code) = ErrorCode::parse(&normalized) { println!( "\x1b[1;33m{}\x1b[0m: \x1b[1m{}\x1b[0m\n", code.as_str(), diff --git a/turbo/crates/turbo-lsp/src/main.rs b/turbo/crates/turbo-lsp/src/main.rs index a63e7e6..2c5a34d 100644 --- a/turbo/crates/turbo-lsp/src/main.rs +++ b/turbo/crates/turbo-lsp/src/main.rs @@ -405,6 +405,7 @@ fn publish_diagnostics(connection: &Connection, uri: &Uri, source: &str) { diagnostics.push(Diagnostic { range: span_to_range(source, &err.span), severity: Some(DiagnosticSeverity::ERROR), + code: Some(NumberOrString::String(err.code.as_str().to_string())), message: err.message.clone(), ..Default::default() }); @@ -416,6 +417,7 @@ fn publish_diagnostics(connection: &Connection, uri: &Uri, source: &str) { diagnostics.push(Diagnostic { range: span_to_range(source, &err.span), severity: Some(DiagnosticSeverity::ERROR), + code: Some(NumberOrString::String(err.code.as_str().to_string())), message: err.message.clone(), ..Default::default() }); @@ -424,6 +426,7 @@ fn publish_diagnostics(connection: &Connection, uri: &Uri, source: &str) { diagnostics.push(Diagnostic { range: span_to_range(source, &w.span), severity: Some(DiagnosticSeverity::WARNING), + code: Some(NumberOrString::String(w.code.as_str().to_string())), message: w.message.clone(), ..Default::default() }); diff --git a/turbo/crates/turbo-parser/src/lib.rs b/turbo/crates/turbo-parser/src/lib.rs index cb3442d..377510e 100644 --- a/turbo/crates/turbo-parser/src/lib.rs +++ b/turbo/crates/turbo-parser/src/lib.rs @@ -1145,7 +1145,12 @@ impl Parser { // Check for range operator (..) if matches!(self.peek(), Some(Token::DotDot)) { self.advance(); - let rhs = self.parse_unary()?; + // Parse the upper bound as a full binary expression so common + // idioms like `0..len + 1` and `1..n - 1` work. Without folding + // binary operators here the `+ 1` would be left dangling and the + // parse would fail on the trailing operator. + let rhs_atom = self.parse_unary()?; + let rhs = self.parse_binary(rhs_atom, 0)?; let span = lhs.span.start..rhs.span.end; return Ok(Spanned::new( Expr::Range { diff --git a/turbo/tests/phase1/range_arith_bound.expected b/turbo/tests/phase1/range_arith_bound.expected new file mode 100644 index 0000000..c3f407c --- /dev/null +++ b/turbo/tests/phase1/range_arith_bound.expected @@ -0,0 +1 @@ +55 diff --git a/turbo/tests/phase1/range_arith_bound.tb b/turbo/tests/phase1/range_arith_bound.tb new file mode 100644 index 0000000..e1e058c --- /dev/null +++ b/turbo/tests/phase1/range_arith_bound.tb @@ -0,0 +1,10 @@ +// Regression: the range upper bound must accept a full binary expression +// (`1..10 + 1`). Previously only a unary was parsed, leaving `+ 1` dangling +// and failing the parse. +fn main() { + let mut sum = 0 + for i in 1..10 + 1 { + sum = sum + i + } + print(sum) +} From e9fc9fa3ccbda042d8c2e8f178dca36ccf2054f0 Mon Sep 17 00:00:00 2001 From: ZVN DEV <78920650+zvndev@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:38:28 -0400 Subject: [PATCH 4/7] chore: remove dead code and clear clippy warnings - delete unused resolve_cl_type_with_data (was kept alive by #[allow(dead_code)]) - clippy: use std::ptr::eq for raw-pointer comparison; const thread-local Cell; collapse a nested if; use RangeInclusive::contains - rustfmt reflow cargo clippy --workspace is now warning-clean. --- .../turbo-codegen-cranelift/src/builtins.rs | 32 +++-- .../turbo-codegen-cranelift/src/compile.rs | 2 +- .../crates/turbo-codegen-cranelift/src/jit.rs | 10 +- .../turbo-codegen-cranelift/src/runtime.rs | 112 +++++++++--------- .../turbo-codegen-cranelift/src/type_conv.rs | 12 -- 5 files changed, 86 insertions(+), 82 deletions(-) diff --git a/turbo/crates/turbo-codegen-cranelift/src/builtins.rs b/turbo/crates/turbo-codegen-cranelift/src/builtins.rs index f4575db..2cca7c6 100644 --- a/turbo/crates/turbo-codegen-cranelift/src/builtins.rs +++ b/turbo/crates/turbo-codegen-cranelift/src/builtins.rs @@ -963,7 +963,10 @@ pub(crate) fn compile_builtin_http_post_with_headers( let (headers_val, _) = compile_expr(cx, &args[2])?.unwrap(); let fid = cx.rt_fns["rt_http_post_with_headers"]; let fref = cx.module.declare_func_in_func(fid, cx.builder.func); - let call = cx.builder.ins().call(fref, &[url_val, body_val, headers_val]); + let call = cx + .builder + .ins() + .call(fref, &[url_val, body_val, headers_val]); let result = cx.builder.inst_results(call)[0]; Ok(Some((result, TurboTy::Str))) } @@ -1897,10 +1900,12 @@ fn is_pure_expr(expr: &Expr) -> bool { match expr { Expr::IntLit(_) | Expr::FloatLit(_) | Expr::BoolLit(_) | Expr::Ident(_) => true, Expr::BinaryOp { left, op, right } => { - if matches!(op, BinOp::Div | BinOp::Mod) { - if !matches!(right.node, Expr::IntLit(n) if n != 0) { - return false; - } + // Division/modulo by a non-literal (or literal zero) can trap, so + // it isn't pure. + if matches!(op, BinOp::Div | BinOp::Mod) + && !matches!(right.node, Expr::IntLit(n) if n != 0) + { + return false; } is_pure_expr(&left.node) && is_pure_expr(&right.node) } @@ -1918,9 +1923,10 @@ pub(crate) fn compile_if( // Select optimization: if cond { x = a } else { x = b } // → compute both a and b, then x = select(cond, a, b) if let Some(else_br) = else_branch { - if let (Some((then_target, then_val)), Some((else_target, else_val))) = - (extract_single_assign(then_branch), extract_single_assign(else_br)) - { + if let (Some((then_target, then_val)), Some((else_target, else_val))) = ( + extract_single_assign(then_branch), + extract_single_assign(else_br), + ) { if then_target == else_target && is_pure_expr(&then_val.node) && is_pure_expr(&else_val.node) @@ -2665,7 +2671,10 @@ pub(crate) fn compile_for_in_array( let (arr_ptr, arr_tty) = compile_expr(cx, iterable)?.unwrap(); // Inline array length load (first i64 at arr_ptr) - let arr_len = cx.builder.ins().load(types::I64, MemFlags::trusted(), arr_ptr, 0i32); + let arr_len = cx + .builder + .ins() + .load(types::I64, MemFlags::trusted(), arr_ptr, 0i32); // Determine element TurboTy from the array type let elem_tty = match arr_tty { @@ -2727,7 +2736,10 @@ pub(crate) fn compile_for_in_array( let data_base = cx.builder.ins().iadd_imm(arr_ptr, 8); let byte_offset = cx.builder.ins().ishl_imm(idx_val, 3); let elem_ptr = cx.builder.ins().iadd(data_base, byte_offset); - let raw_elem = cx.builder.ins().load(types::I64, MemFlags::trusted(), elem_ptr, 0i32); + let raw_elem = cx + .builder + .ins() + .load(types::I64, MemFlags::trusted(), elem_ptr, 0i32); // Raw i64 bits; convert to the correct type let typed_elem = match &elem_tty { diff --git a/turbo/crates/turbo-codegen-cranelift/src/compile.rs b/turbo/crates/turbo-codegen-cranelift/src/compile.rs index fe5f239..be8725e 100644 --- a/turbo/crates/turbo-codegen-cranelift/src/compile.rs +++ b/turbo/crates/turbo-codegen-cranelift/src/compile.rs @@ -1516,7 +1516,7 @@ pub(crate) fn compile_module( constants: &constants_map, struct_derives: &struct_derives, loop_stack: Vec::new(), - is_unsafe: method.is_unsafe, + is_unsafe: method.is_unsafe, }; let entry = cx.builder.create_block(); diff --git a/turbo/crates/turbo-codegen-cranelift/src/jit.rs b/turbo/crates/turbo-codegen-cranelift/src/jit.rs index 88819f2..ef455df 100644 --- a/turbo/crates/turbo-codegen-cranelift/src/jit.rs +++ b/turbo/crates/turbo-codegen-cranelift/src/jit.rs @@ -165,7 +165,10 @@ pub fn jit_run(ast_module: &turbo_ast::Module) -> Result<(), CodegenError> { // HTTP + JSON builtins jit_builder.symbol("rt_http_get", rt_http_get as *const u8); jit_builder.symbol("rt_http_post", rt_http_post as *const u8); - jit_builder.symbol("rt_http_post_with_headers", rt_http_post_with_headers as *const u8); + jit_builder.symbol( + "rt_http_post_with_headers", + rt_http_post_with_headers as *const u8, + ); jit_builder.symbol("rt_json_get", rt_json_get as *const u8); jit_builder.symbol("rt_json_stringify", rt_json_stringify as *const u8); jit_builder.symbol("rt_json_build", rt_json_build as *const u8); @@ -369,7 +372,10 @@ pub fn jit_run_function(ast_module: &turbo_ast::Module, fn_name: &str) -> Result jit_builder.symbol("rt_await_handle", rt_await_handle as *const u8); jit_builder.symbol("rt_http_get", rt_http_get as *const u8); jit_builder.symbol("rt_http_post", rt_http_post as *const u8); - jit_builder.symbol("rt_http_post_with_headers", rt_http_post_with_headers as *const u8); + jit_builder.symbol( + "rt_http_post_with_headers", + rt_http_post_with_headers as *const u8, + ); jit_builder.symbol("rt_json_get", rt_json_get as *const u8); jit_builder.symbol("rt_json_stringify", rt_json_stringify as *const u8); jit_builder.symbol("rt_json_build", rt_json_build as *const u8); diff --git a/turbo/crates/turbo-codegen-cranelift/src/runtime.rs b/turbo/crates/turbo-codegen-cranelift/src/runtime.rs index 78afcf9..1496463 100644 --- a/turbo/crates/turbo-codegen-cranelift/src/runtime.rs +++ b/turbo/crates/turbo-codegen-cranelift/src/runtime.rs @@ -67,7 +67,11 @@ fn arena_str(cs: std::ffi::CString) -> *const u8 { let cap = cs.as_bytes_with_nul().len(); let ptr = cs.into_raw() as *mut u8; STRING_ARENA.with(|arena| { - arena.borrow_mut().push(ArenaEntry { ptr, cap, is_raw: false }); + arena.borrow_mut().push(ArenaEntry { + ptr, + cap, + is_raw: false, + }); }); ptr as *const u8 } @@ -82,7 +86,9 @@ pub(crate) extern "C" fn rt_arena_reset() { let layout = std::alloc::Layout::from_size_align(entry.cap, 1).unwrap(); std::alloc::dealloc(entry.ptr, layout); } else { - drop(std::ffi::CString::from_raw(entry.ptr as *mut std::ffi::c_char)); + drop(std::ffi::CString::from_raw( + entry.ptr as *mut std::ffi::c_char, + )); } } } @@ -203,11 +209,11 @@ pub(crate) extern "C" fn rt_array_alloc(len: i64) -> *mut u8 { // Layout: [cap: 8][refcount: 8][length: 8][data...] unsafe { *(ptr as *mut i64) = cap as i64; // capacity - *(ptr.add(8) as *mut i64) = 1; // refcount = 1 + *(ptr.add(8) as *mut i64) = 1; // refcount = 1 } let data_ptr = unsafe { ptr.add(16) }; // pointer past cap+refcount header unsafe { - *(data_ptr as *mut i64) = len; // length + *(data_ptr as *mut i64) = len; // length } data_ptr } @@ -248,7 +254,7 @@ pub(crate) extern "C" fn rt_array_set(arr: *mut u8, index: i64, value: i64) -> * register_alloc(new_alloc, layout); unsafe { *(new_alloc as *mut i64) = cap as i64; // capacity - *(new_alloc.add(8) as *mut i64) = 1; // refcount = 1 + *(new_alloc.add(8) as *mut i64) = 1; // refcount = 1 } let new_data = unsafe { new_alloc.add(16) }; unsafe { @@ -312,7 +318,10 @@ pub(crate) extern "C" fn rt_array_push(arr: *const u8, value: i64) -> *mut u8 { None => match checked_array_layout(new_len) { Some(l) => l, None => { - eprintln!("runtime error: array allocation overflow (length {})", new_len); + eprintln!( + "runtime error: array allocation overflow (length {})", + new_len + ); std::process::exit(1); } }, @@ -325,7 +334,7 @@ pub(crate) extern "C" fn rt_array_push(arr: *const u8, value: i64) -> *mut u8 { register_alloc(new_alloc, layout); unsafe { *(new_alloc as *mut i64) = new_cap as i64; // capacity - *(new_alloc.add(8) as *mut i64) = 1; // refcount = 1 + *(new_alloc.add(8) as *mut i64) = 1; // refcount = 1 } let new_data = unsafe { new_alloc.add(16) }; unsafe { @@ -336,7 +345,9 @@ pub(crate) extern "C" fn rt_array_push(arr: *const u8, value: i64) -> *mut u8 { // Decrement old refcount (don't free — JIT may still hold stale register refs // to the old data_ptr between the push call returning and the variable reassignment) let rc_ptr = unsafe { (arr as *mut u8).sub(8) as *mut std::sync::atomic::AtomicI64 }; - unsafe { (*rc_ptr).fetch_sub(1, std::sync::atomic::Ordering::Release); } + unsafe { + (*rc_ptr).fetch_sub(1, std::sync::atomic::Ordering::Release); + } new_data } @@ -366,8 +377,7 @@ pub(crate) extern "C" fn rt_str_concat(a: *const u8, b: *const u8) -> *const u8 let mut result = String::with_capacity(a_str.len() + b_str.len()); result.push_str(a_str); result.push_str(b_str); - let c_string = - cstring_or_empty(result); + let c_string = cstring_or_empty(result); arena_str(c_string) } @@ -382,7 +392,7 @@ pub(crate) extern "C" fn rt_str_concat_inplace(a: *const u8, b: *const u8) -> *c STRING_ARENA.with(|arena| { let mut entries = arena.borrow_mut(); if let Some(last) = entries.last_mut() { - if last.ptr as *const u8 == a { + if std::ptr::eq(last.ptr as *const u8, a) { let needed = new_len + 1; if needed <= last.cap { unsafe { @@ -409,7 +419,9 @@ pub(crate) extern "C" fn rt_str_concat_inplace(a: *const u8, b: *const u8) -> *c let old_layout = std::alloc::Layout::from_size_align(last.cap, 1).unwrap(); std::alloc::dealloc(last.ptr, old_layout); } else { - drop(std::ffi::CString::from_raw(last.ptr as *mut std::ffi::c_char)); + drop(std::ffi::CString::from_raw( + last.ptr as *mut std::ffi::c_char, + )); } } last.ptr = new_ptr; @@ -475,8 +487,8 @@ pub(crate) extern "C" fn rt_struct_alloc(num_fields: i64) -> *mut u8 { register_alloc(ptr, layout); // Layout: [cap: 8 (unused for structs)][refcount: 8][data...] unsafe { - *(ptr as *mut i64) = 0; // cap = 0 (not an array) - *(ptr.add(8) as *mut i64) = 1; // refcount = 1 + *(ptr as *mut i64) = 0; // cap = 0 (not an array) + *(ptr.add(8) as *mut i64) = 1; // refcount = 1 } unsafe { ptr.add(16) } // return pointer past cap+refcount header } @@ -492,7 +504,11 @@ pub(crate) extern "C" fn rt_array_oob_exit(index: i64, len: i64) { fn fast_i64_to_string(n: i64) -> String { let mut buf = [0u8; 20]; let neg = n < 0; - let mut v = if neg { (n as i128).unsigned_abs() as u64 } else { n as u64 }; + let mut v = if neg { + (n as i128).unsigned_abs() as u64 + } else { + n as u64 + }; let mut pos = 20; if v == 0 { pos -= 1; @@ -662,8 +678,7 @@ pub(crate) extern "C" fn rt_str_split(s: *const u8, sep: *const u8) -> *mut u8 { *(data_ptr as *mut i64) = len; } for (i, part) in parts.iter().enumerate() { - let cs = - cstring_or_empty(*part); + let cs = cstring_or_empty(*part); let p = arena_str(cs) as i64; unsafe { *((data_ptr as *mut i64).add(1 + i)) = p; @@ -677,8 +692,7 @@ pub(crate) extern "C" fn rt_str_trim(s: *const u8) -> *const u8 { .to_str() .unwrap_or(""); let trimmed = s.trim(); - let cs = - cstring_or_empty(trimmed); + let cs = cstring_or_empty(trimmed); arena_str(cs) } @@ -845,17 +859,14 @@ pub(crate) extern "C" fn rt_str_repeat(s: *const u8, n: i64) -> *const u8 { return rt_empty_cstr(); } let repeated = s.repeat(count); - arena_str( - cstring_or_empty(repeated), - ) + arena_str(cstring_or_empty(repeated)) } pub(crate) extern "C" fn rt_read_line() -> *const u8 { let mut line = String::new(); std::io::stdin().read_line(&mut line).unwrap_or(0); let trimmed = line.trim_end_matches('\n').trim_end_matches('\r'); - let cs = - cstring_or_empty(trimmed); + let cs = cstring_or_empty(trimmed); arena_str(cs) } @@ -972,8 +983,7 @@ pub(crate) extern "C" fn rt_exec(cmd: *const u8) -> *const u8 { } Err(e) => { let msg = format!("error: exec failed: {}", e); - let cs = - cstring_or_empty(msg); + let cs = cstring_or_empty(msg); arena_str(cs) } } @@ -1055,7 +1065,7 @@ pub(crate) extern "C" fn rt_exp(x: f64) -> f64 { } thread_local! { - static XORSHIFT_STATE: Cell = Cell::new(0); + static XORSHIFT_STATE: Cell = const { Cell::new(0) }; } fn xorshift64_next() -> u64 { @@ -1093,7 +1103,9 @@ pub(crate) extern "C" fn rt_random_range(min_val: i64, max_val: i64) -> i64 { if max_val < min_val { return min_val; } - let range = (max_val as u64).wrapping_sub(min_val as u64).wrapping_add(1); + let range = (max_val as u64) + .wrapping_sub(min_val as u64) + .wrapping_add(1); min_val + (xorshift64_next() % range) as i64 } @@ -1152,9 +1164,7 @@ pub(crate) extern "C" fn rt_pad_left(s: *const u8, width: i64, pad_char: *const let c = pad_str.chars().next().unwrap_or(' '); let slen = s_str.len() as i64; if slen >= width { - return arena_str( - cstring_or_empty(s_str), - ); + return arena_str(cstring_or_empty(s_str)); } let pad_count = (width - slen) as usize; let mut result = String::with_capacity(width as usize); @@ -1162,9 +1172,7 @@ pub(crate) extern "C" fn rt_pad_left(s: *const u8, width: i64, pad_char: *const result.push(c); } result.push_str(s_str); - arena_str( - cstring_or_empty(result), - ) + arena_str(cstring_or_empty(result)) } pub(crate) extern "C" fn rt_pad_right(s: *const u8, width: i64, pad_char: *const u8) -> *const u8 { @@ -1189,9 +1197,7 @@ pub(crate) extern "C" fn rt_pad_right(s: *const u8, width: i64, pad_char: *const let c = pad_str.chars().next().unwrap_or(' '); let slen = s_str.len() as i64; if slen >= width { - return arena_str( - cstring_or_empty(s_str), - ); + return arena_str(cstring_or_empty(s_str)); } let pad_count = (width - slen) as usize; let mut result = String::with_capacity(width as usize); @@ -1199,9 +1205,7 @@ pub(crate) extern "C" fn rt_pad_right(s: *const u8, width: i64, pad_char: *const for _ in 0..pad_count { result.push(c); } - arena_str( - cstring_or_empty(result), - ) + arena_str(cstring_or_empty(result)) } pub(crate) extern "C" fn rt_str_to_int(s: *const u8) -> *mut u8 { @@ -1374,7 +1378,11 @@ pub(crate) extern "C" fn rt_http_post(url: *const u8, body: *const u8) -> *const } /// HTTP POST with custom headers. `headers` is a newline-separated string of headers. -pub(crate) extern "C" fn rt_http_post_with_headers(url: *const u8, body: *const u8, headers: *const u8) -> *const u8 { +pub(crate) extern "C" fn rt_http_post_with_headers( + url: *const u8, + body: *const u8, + headers: *const u8, +) -> *const u8 { if url.is_null() { eprintln!("[rt_http] blocked non-http(s) URL: (null)"); return rt_empty_cstr(); @@ -1417,12 +1425,7 @@ pub(crate) extern "C" fn rt_http_post_with_headers(url: *const u8, body: *const cmd.arg("-H").arg(h); } } - let output = cmd - .arg("-d") - .arg(body_str) - .arg("--") - .arg(url) - .output(); + let output = cmd.arg("-d").arg(body_str).arg("--").arg(url).output(); match output { Ok(out) => { let resp = String::from_utf8_lossy(&out.stdout).to_string(); @@ -1479,8 +1482,7 @@ pub(crate) extern "C" fn rt_json_stringify(key: *const u8, value: *const u8) -> serde_json::Value::String(value_str.to_string()), ); let result = serde_json::Value::Object(map).to_string(); - let cs = - cstring_or_empty(result); + let cs = cstring_or_empty(result); arena_str(cs) } @@ -1517,8 +1519,7 @@ pub(crate) extern "C" fn rt_json_root(json: *const u8) -> *const u8 { let parsed: Result = serde_json::from_str(json_str); match parsed { Ok(serde_json::Value::String(s)) => { - let cs = - cstring_or_empty(s); + let cs = cstring_or_empty(s); arena_str(cs) } Ok(other) => { @@ -2287,8 +2288,7 @@ pub(crate) extern "C" fn rt_hashmap_keys(map_ptr: *const u8) -> *mut u8 { *(data_ptr as *mut i64) = len; } for (i, key) in keys.iter().enumerate() { - let cs = - cstring_or_empty(*key); + let cs = cstring_or_empty(*key); unsafe { *((data_ptr as *mut i64).add(1 + i)) = arena_str(cs) as i64; } @@ -2503,8 +2503,7 @@ pub(crate) extern "C" fn rt_path_dir(path: *const u8) -> *const u8 { Some(p) if !p.as_os_str().is_empty() => p.to_str().unwrap_or(".").to_string(), _ => ".".to_string(), }; - let cs = - cstring_or_empty(result); + let cs = cstring_or_empty(result); arena_str(cs) } @@ -2775,8 +2774,7 @@ fn chrono_like_format(epoch_secs: i64, fmt: &str) -> String { if tm.is_null() { return String::new(); } - let fmt_c = - cstring_or_empty(fmt); + let fmt_c = cstring_or_empty(fmt); let mut buf = [0u8; 256]; let n = libc::strftime( buf.as_mut_ptr() as *mut libc::c_char, diff --git a/turbo/crates/turbo-codegen-cranelift/src/type_conv.rs b/turbo/crates/turbo-codegen-cranelift/src/type_conv.rs index 9c59a8a..cc6f173 100644 --- a/turbo/crates/turbo-codegen-cranelift/src/type_conv.rs +++ b/turbo/crates/turbo-codegen-cranelift/src/type_conv.rs @@ -84,18 +84,6 @@ pub(crate) fn resolve_cl_type( resolve_cl_type_inner(ty, ptr_type, enum_variants, type_params, &HashMap::new()) } -/// Resolve Cranelift type, accounting for data-carrying enums that need pointer types. -#[allow(dead_code)] -pub(crate) fn resolve_cl_type_with_data( - ty: &TypeExpr, - ptr_type: types::Type, - enum_variants: &HashMap>, - type_params: &[String], - enum_max_slots: &HashMap, -) -> Result { - resolve_cl_type_inner(ty, ptr_type, enum_variants, type_params, enum_max_slots) -} - fn resolve_cl_type_inner( ty: &TypeExpr, ptr_type: types::Type, From ac1eb55aafa3f1576f2d4a925e7bd11092a56ee1 Mon Sep 17 00:00:00 2001 From: ZVN DEV <78920650+zvndev@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:38:39 -0400 Subject: [PATCH 5/7] feat(vscode): wire up the LSP client and add snippets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The extension shipped syntax highlighting only — the 2,140-LOC language server (turbolang lsp) was never reachable from VS Code. Add: - extension.js: a vscode-languageclient that launches `turbolang lsp` over stdio, enabling diagnostics, hover, go-to-definition, completions, references, rename, and document symbols in the editor - package.json: main/activationEvents/deps, plus turbo.lsp.path and turbo.lsp.enable settings - snippets/turbo.json: 25 idiomatic snippets (fn, struct, type, match, for, test, server, …) - README with build/install instructions --- editors/vscode/turbo-lang/README.md | 41 ++++++ editors/vscode/turbo-lang/extension.js | 64 +++++++++ editors/vscode/turbo-lang/package.json | 37 ++++- editors/vscode/turbo-lang/snippets/turbo.json | 133 ++++++++++++++++++ 4 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 editors/vscode/turbo-lang/README.md create mode 100644 editors/vscode/turbo-lang/extension.js create mode 100644 editors/vscode/turbo-lang/snippets/turbo.json diff --git a/editors/vscode/turbo-lang/README.md b/editors/vscode/turbo-lang/README.md new file mode 100644 index 0000000..9bd2931 --- /dev/null +++ b/editors/vscode/turbo-lang/README.md @@ -0,0 +1,41 @@ +# Turbo for VS Code + +Syntax highlighting, snippets, and full language support for the +[Turbo](https://github.com/ZVN-DEV/Turbo-Language) programming language. + +## Features + +- Syntax highlighting (`.tb` files) +- 25 snippets for common patterns (`fn`, `struct`, `type`, `match`, `for`, `test`, …) +- Language server integration via `turbolang lsp`: + - Diagnostics (errors and warnings with `E0NNN` codes) + - Hover + - Go-to-definition + - Completions + - References, rename, document symbols + +## Requirements + +The language-server features require the `turbolang` binary on your `PATH` +(install Turbo via Homebrew or build from source). If it lives elsewhere, set +`turbo.lsp.path` in your settings. To disable the server and keep only syntax +highlighting, set `turbo.lsp.enable` to `false`. + +## Building / packaging locally + +The client depends on `vscode-languageclient`, so install dependencies before +packaging: + +```bash +cd editors/vscode/turbo-lang +npm install +npx @vscode/vsce package # produces turbo-lang-.vsix +code --install-extension turbo-lang-*.vsix +``` + +## Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| `turbo.lsp.path` | `turbolang` | Path to the `turbolang` executable used for `turbolang lsp`. | +| `turbo.lsp.enable` | `true` | Enable the language server. | diff --git a/editors/vscode/turbo-lang/extension.js b/editors/vscode/turbo-lang/extension.js new file mode 100644 index 0000000..866af8c --- /dev/null +++ b/editors/vscode/turbo-lang/extension.js @@ -0,0 +1,64 @@ +// Turbo VS Code extension entry point. +// +// Boots the Turbo language server (`turbolang lsp`, implemented in the +// turbo-lsp crate) and wires it to VS Code over stdio so diagnostics, hover, +// go-to-definition, completions, references, rename, and document symbols all +// work in the editor. Without this client the bundled LSP never runs and the +// extension is syntax highlighting only. + +const vscode = require("vscode"); +const { LanguageClient, TransportKind } = require("vscode-languageclient/node"); + +let client; + +function startClient() { + const config = vscode.workspace.getConfiguration("turbo"); + if (!config.get("lsp.enable", true)) { + return; + } + + const command = config.get("lsp.path", "turbolang"); + + // `turbolang lsp` speaks LSP over stdio. The same command is used for the + // initial run and for restarts (debug uses the same server today). + const serverOptions = { + run: { command, args: ["lsp"], transport: TransportKind.stdio }, + debug: { command, args: ["lsp"], transport: TransportKind.stdio }, + }; + + const clientOptions = { + documentSelector: [{ scheme: "file", language: "turbo" }], + synchronize: { + fileEvents: vscode.workspace.createFileSystemWatcher("**/*.tb"), + }, + }; + + client = new LanguageClient( + "turbo", + "Turbo Language Server", + serverOptions, + clientOptions + ); + + // start() rejects if the `turbolang` binary can't be spawned — surface a + // clear, actionable message instead of failing silently. + client.start().catch((err) => { + vscode.window.showErrorMessage( + `Turbo: could not start the language server using \`${command} lsp\`. ` + + `Install Turbo or set "turbo.lsp.path" in settings. (${err.message})` + ); + }); +} + +function activate(_context) { + startClient(); +} + +function deactivate() { + if (!client) { + return undefined; + } + return client.stop(); +} + +module.exports = { activate, deactivate }; diff --git a/editors/vscode/turbo-lang/package.json b/editors/vscode/turbo-lang/package.json index 2434d96..8023ba0 100644 --- a/editors/vscode/turbo-lang/package.json +++ b/editors/vscode/turbo-lang/package.json @@ -1,11 +1,17 @@ { "name": "turbo-lang", "displayName": "Turbo Language", - "description": "Syntax highlighting and language support for the Turbo programming language", - "version": "0.3.0", + "description": "Syntax highlighting, snippets, and full language support (diagnostics, hover, go-to-definition, completions) for the Turbo programming language", + "version": "0.9.0", "publisher": "zvn-dev", + "repository": { + "type": "git", + "url": "https://github.com/ZVN-DEV/Turbo-Language.git" + }, "engines": { "vscode": "^1.80.0" }, "categories": ["Programming Languages"], + "main": "./extension.js", + "activationEvents": ["onLanguage:turbo"], "contributes": { "languages": [{ "id": "turbo", @@ -17,6 +23,31 @@ "language": "turbo", "scopeName": "source.turbo", "path": "./syntaxes/turbo.tmLanguage.json" - }] + }], + "snippets": [{ + "language": "turbo", + "path": "./snippets/turbo.json" + }], + "configuration": { + "title": "Turbo", + "properties": { + "turbo.lsp.path": { + "type": "string", + "default": "turbolang", + "description": "Path to the `turbolang` executable used to start the language server (`turbolang lsp`). Defaults to `turbolang` on your PATH." + }, + "turbo.lsp.enable": { + "type": "boolean", + "default": true, + "description": "Enable the Turbo language server (diagnostics, hover, go-to-definition, completions)." + } + } + } + }, + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/vscode": "^1.80.0" } } diff --git a/editors/vscode/turbo-lang/snippets/turbo.json b/editors/vscode/turbo-lang/snippets/turbo.json new file mode 100644 index 0000000..47eb88c --- /dev/null +++ b/editors/vscode/turbo-lang/snippets/turbo.json @@ -0,0 +1,133 @@ +{ + "Main function": { + "prefix": "main", + "body": ["fn main() {", "\t$0", "}"], + "description": "Program entry point" + }, + "Async main": { + "prefix": "amain", + "body": ["async fn main() {", "\t$0", "}"], + "description": "Async program entry point" + }, + "Function": { + "prefix": "fn", + "body": ["fn ${1:name}(${2:args}) -> ${3:int} {", "\t$0", "}"], + "description": "Function definition" + }, + "Async function": { + "prefix": "afn", + "body": ["async fn ${1:name}(${2:args}) -> ${3:int} {", "\t$0", "}"], + "description": "Async function definition" + }, + "Struct": { + "prefix": "struct", + "body": ["struct ${1:Name} {", "\t${2:field}: ${3:int}", "}"], + "description": "Struct definition" + }, + "Generic struct": { + "prefix": "structg", + "body": ["struct ${1:Name}<${2:T}> {", "\t${3:value}: ${2:T}", "}"], + "description": "Generic struct definition" + }, + "Enum (sum type)": { + "prefix": "type", + "body": ["type ${1:Name} {", "\t${2:Variant}", "\t${3:Other}(${4:int})", "}"], + "description": "Algebraic data type / enum" + }, + "Impl block": { + "prefix": "impl", + "body": ["impl ${1:Name} {", "\tfn ${2:method}(self) -> ${3:int} {", "\t\t$0", "\t}", "}"], + "description": "Implementation block" + }, + "Trait": { + "prefix": "trait", + "body": ["trait ${1:Name} {", "\tfn ${2:method}(self) -> ${3:str}", "}"], + "description": "Trait definition" + }, + "Match": { + "prefix": "match", + "body": ["match ${1:value} {", "\t${2:pattern} => ${3:result}", "\t_ => ${4:default}", "}"], + "description": "Pattern match" + }, + "If": { + "prefix": "if", + "body": ["if ${1:cond} {", "\t$0", "}"], + "description": "If statement" + }, + "If/else": { + "prefix": "ife", + "body": ["if ${1:cond} {", "\t$2", "} else {", "\t$0", "}"], + "description": "If/else statement" + }, + "If let": { + "prefix": "iflet", + "body": ["if let ${1:Some}(${2:x}) = ${3:opt} {", "\t$0", "}"], + "description": "If-let binding" + }, + "For-in": { + "prefix": "for", + "body": ["for ${1:item} in ${2:collection} {", "\t$0", "}"], + "description": "For-in loop" + }, + "For range": { + "prefix": "forr", + "body": ["for ${1:i} in ${2:0}..${3:n} {", "\t$0", "}"], + "description": "For loop over a range" + }, + "While": { + "prefix": "while", + "body": ["while ${1:cond} {", "\t$0", "}"], + "description": "While loop" + }, + "Let binding": { + "prefix": "let", + "body": ["let ${1:name} = ${2:value}"], + "description": "Immutable binding" + }, + "Mutable binding": { + "prefix": "letm", + "body": ["let mut ${1:name} = ${2:value}"], + "description": "Mutable binding" + }, + "Closure": { + "prefix": "closure", + "body": ["(${1:x}) => ${2:x}"], + "description": "Arrow closure" + }, + "Map": { + "prefix": "map", + "body": ["${1:items}.map((${2:x}) => ${3:x})"], + "description": "Map over a collection" + }, + "Filter": { + "prefix": "filter", + "body": ["${1:items}.filter((${2:x}) => ${3:cond})"], + "description": "Filter a collection" + }, + "Test": { + "prefix": "test", + "body": ["@test fn ${1:test_name}() {", "\tassert_eq(${2:actual}, ${3:expected})", "}"], + "description": "Test function" + }, + "Derive struct": { + "prefix": "derive", + "body": ["@derive(Eq, Clone, Display)", "struct ${1:Name} {", "\t${2:field}: ${3:int}", "}"], + "description": "Struct with derives" + }, + "HTTP server": { + "prefix": "server", + "body": [ + "fn main() {", + "\tlet app = http_server(${1:8080})", + "\troute(app, \"GET\", \"/\", (req) => respond_text(200, \"hello\"))", + "\thttp_listen(app)", + "}" + ], + "description": "Minimal HTTP server" + }, + "Spawn + await": { + "prefix": "spawn", + "body": ["let ${1:handle} = spawn ${2:task}()", "let ${3:result} = await ${1:handle}"], + "description": "Spawn an async task and await it" + } +} From 73e2d1081badbfb5354df92bc9a51fe732d5f52c Mon Sep 17 00:00:00 2001 From: ZVN DEV <78920650+zvndev@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:38:47 -0400 Subject: [PATCH 6/7] docs: align claims with reality (0.9.x) - README: drop the v0.8.x label, fix binary size 55KB -> 93KB (measured), builtins 64+ -> 100+, fix the VS Code extension line (publisher zvn-dev, 25 snippets), show the working arrow-closure form in map/filter/reduce, remove the LLVM perf row and backend section - COMPATIBILITY: update the version series to 0.9.x; drop the LLVM backend from the experimental list - CHANGELOG: note the LLVM backend removal under [Unreleased] - SYNTAX.md: add a banner clarifying it's the design vision (not the changelog), correct the derive list to Eq/Clone/Display, and retag map/object literals as planned rather than implemented --- CHANGELOG.md | 10 ++++++++++ COMPATIBILITY.md | 21 ++++++++++----------- README.md | 39 +++++++++++++-------------------------- design/SYNTAX.md | 21 ++++++++++++++++----- 4 files changed, 49 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9912b8..b4122d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] +### Removed +- **The experimental LLVM backend (`turbo-codegen-llvm`, `turbolang build --llvm`).** + Cranelift is now the single code-generation backend. The LLVM path was a + partial second implementation (roughly half the builtins), did not build + cleanly from the documented steps, and offered no measured speedup over + Cranelift — which already edges out `cc -O2` on the fib benchmark. Removing it + eliminates a large duplicated codegen surface and the cost of keeping two + backends at parity. A future optimizing backend, if pursued, should lower + through a shared mid-level IR rather than re-walking the AST a second time. + ## [0.9.0] - 2026-05-16 — Batteries-Included Stdlib Major stdlib expansion: 37 new builtins bringing TurboLang from 67 to 104 built-in functions. Inspired by the batteries-included philosophy — every dependency is liability, the stdlib must be complete enough that developers never need a package manager. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 195453a..85dd1a3 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -1,6 +1,6 @@ # Compatibility and Stability -Turbo is pre-1.0. This document describes what today's `0.7.x` releases +Turbo is pre-1.0. This document describes what today's `0.9.x` releases guarantee, what is still fluid, and what the `1.0` stability contract will mean when we cut it. It is a contract, not a marketing pitch — if you are shipping production code against a pre-1.0 Turbo you should @@ -11,14 +11,13 @@ read this in full. > Until `1.0`, expect breaking changes across minor releases. > Pin `turbolang --version` and vendor your toolchain for production. -We are intentionally dwelling in the `0.7.x` and then `0.8.x` series -for a long while, cutting many point releases, rather than racing to -`1.0`. That means: +We are intentionally dwelling in the `0.x` series for a long while, +cutting many point releases, rather than racing to `1.0`. That means: -- **Point releases (`0.7.6` → `0.7.7`)** — additive, bug fixes, no +- **Point releases (`0.9.0` → `0.9.1`)** — additive, bug fixes, no syntactic breaking changes. Safe to auto-update in CI if you re-run your test suite. -- **Minor releases (`0.7.x` → `0.8.0`)** — may break syntax, remove +- **Minor releases (`0.9.x` → `0.10.0`)** — may break syntax, remove deprecated builtins, reshape the stdlib, or renumber error codes in the `E05xx` range. Read the CHANGELOG before upgrading. - **Major release (`1.0`)** — the stability contract below kicks in. @@ -47,7 +46,7 @@ not promises today. - **Tier-1 platform support.** Every `1.x` release builds and passes the integration suite on all tier-1 platforms (see below). -## What Is Explicitly Fluid in `0.7.x` / `0.8.x` +## What Is Explicitly Fluid in `0.9.x` Do not build load-bearing production code against these unless you are prepared to update it on every minor release. @@ -72,9 +71,9 @@ are prepared to update it on every minor release. - **LSP wire behavior.** Diagnostics, hover contents, and completion items may gain or lose fields between minor releases. The editor extensions are updated in lockstep. -- **The experimental backends.** The WASM target and the LLVM backend - are flagged experimental in `CHANGELOG.md`; they may be reshaped or - temporarily removed. +- **The experimental WASM target.** The `--target wasm` backend is + flagged experimental in `CHANGELOG.md`; it may be reshaped between + minor releases. ## Platform Support Tiers @@ -108,7 +107,7 @@ disclosure policy in `SECURITY.md`. ## What This Means in Practice -- **If you are experimenting.** Track the latest `0.7.x`. Re-run your +- **If you are experimenting.** Track the latest `0.9.x`. Re-run your tests on every bump. File issues. - **If you are shipping something today.** Pin an exact Turbo version (`turbolang --version` in CI). Vendor the install script or the diff --git a/README.md b/README.md index 1be571f..d17eadc 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ turbolang build hello.tb # AOT — produce a native binary ./hello ``` -### Known Limitations (v0.8.x) +### Known Limitations (v0.9.x) > **Note — runtime string allocation:** The runtime uses a thread-local string arena that is freed after each JIT execution. Long-running AOT servers should be monitored for memory usage. Proper ARC-based string deallocation is planned for a future release. > @@ -97,7 +97,6 @@ Turbo compiles directly to machine code. Programs start instantly and run at nat - **JIT execution** via `turbolang run` for rapid development (Cranelift) - **AOT compilation** via `turbolang build` for production binaries (Cranelift) -- **Optimized AOT** via `turbolang build --llvm` for maximum performance (LLVM 18) - **WASM** via `turbolang build --target wasm` for WebAssembly output - **Cross-compilation** via `turbolang build --target linux-arm64` from macOS @@ -166,6 +165,7 @@ fn main() { ### Closures & Higher-Order Functions ```turbo +// Returned closures use the explicit form so their parameter types are known. fn make_adder(n: i64) -> fn(i64) -> i64 { |x: i64| -> i64 { x + n } } @@ -173,9 +173,11 @@ fn make_adder(n: i64) -> fn(i64) -> i64 { fn main() { let add5 = make_adder(5) let nums = [1, 2, 3, 4, 5] - let doubled = nums.map(|x: i64| -> i64 { x * 2 }) - let big = nums.filter(|x: i64| -> bool { x > 3 }) - let sum = reduce(nums, 0, |acc: i64, x: i64| -> i64 { acc + x }) + + // In map/filter/reduce, parameter types are inferred — use the short arrow form. + let doubled = nums.map((x) => x * 2) + let big = nums.filter((x) => x > 3) + let sum = reduce(nums, 0, (acc, x) => acc + x) print("sum: {sum}") } ``` @@ -268,7 +270,7 @@ fn main() { ### Standard Library -64+ built-in functions with no imports required. Method syntax works via UFCS -- `s.trim()` is equivalent to `trim(s)`. +100+ built-in functions with no imports required. Method syntax works via UFCS -- `s.trim()` is equivalent to `trim(s)`. | Category | Highlights | |----------|------------| @@ -333,7 +335,6 @@ See [`examples/speed-server/main.tb`](examples/speed-server/main.tb) |---------|-------------| | `turbolang run ` | Compile and run via JIT | | `turbolang build ` | Compile to native binary (Cranelift) | -| `turbolang build --llvm ` | Compile with LLVM optimizations | | `turbolang build --target wasm ` | Compile to WebAssembly | | `turbolang build --target linux-arm64 ` | Cross-compile for Linux ARM64 | | `turbolang build --target linux-x86 ` | Cross-compile for Linux x86_64 | @@ -388,12 +389,13 @@ Benchmarked on Apple Silicon (fib(40), recursive): | Language | Time | Binary Size | |----------|------|-------------| | Rust (rustc -O) | 180ms | 441 KB | -| **Turbo (Cranelift)** | **250ms** | **55 KB** | +| **Turbo (Cranelift)** | **250ms** | **93 KB** | | C (cc -O2) | 290ms | 33 KB | -| **Turbo (LLVM)** | **290ms** | **55 KB** | | Node.js | 580ms | N/A | | Python | 13.1s | N/A | +Turbo compiles through a single Cranelift backend — fast JIT for `run`, optimized AOT for `build` — and still edges out `cc -O2` on this benchmark while producing a self-contained ~90 KB binary. + ## Project Structure ``` @@ -422,7 +424,7 @@ Full specification lives in `design/`: [SYNTAX.md](design/SYNTAX.md), [TYPE-SYST ```bash # Unit tests (all crates) -cargo test --workspace --exclude turbo-codegen-llvm --manifest-path turbo/Cargo.toml +cargo test --workspace --manifest-path turbo/Cargo.toml # Integration tests (requires release build) cargo build --release -p turbo-cli --manifest-path turbo/Cargo.toml @@ -434,26 +436,11 @@ turbolang run turbo/tests/phase1/fibonacci.tb The test suite spans Rust unit tests, integration fixtures, and parity coverage; run the commands above for the current count. -## LLVM Backend - -The LLVM 18 backend ships with the Homebrew install for maximum performance: - -```bash -turbolang build --llvm myapp.tb -o myapp -``` - -For building from source with LLVM support: - -```bash -brew install llvm@18 -LLVM_SYS_180_PREFIX=/opt/homebrew/opt/llvm@18 cargo build --release -p turbo-cli --features turbo-cli/llvm -``` - ## Ecosystem | Tool | Install / Link | |------|----------------| -| **VS Code Extension** | `zvndev.turbo-lang` -- syntax highlighting, 23 snippets, LSP client | +| **VS Code Extension** | `zvn-dev.turbo-lang` -- syntax highlighting, 25 snippets, LSP client (diagnostics, hover, go-to-definition, completions) | | **Tree-sitter Grammar** | [ZVN-DEV/tree-sitter-turbo](https://github.com/ZVN-DEV/tree-sitter-turbo) | | **Homebrew** | `brew tap ZVN-DEV/turbo && brew install turbo-lang` | | **Docker** | [`distribution/Dockerfile`](distribution/Dockerfile) | diff --git a/design/SYNTAX.md b/design/SYNTAX.md index f56a56d..ad11589 100644 --- a/design/SYNTAX.md +++ b/design/SYNTAX.md @@ -1,5 +1,16 @@ # Syntax & Ergonomics +> **This is the design vision, not the changelog.** It describes Turbo's full +> intended surface syntax. Some constructs below are aspirational and not yet +> implemented (or only partially) — the inline `[Implemented]` / `[Planned]` +> tags can lag the compiler. The **authoritative** list of what works *today* +> is [`README.md`](../README.md) plus the compiler itself: if `turbolang check` +> accepts it, it's real. Notable not-yet-implemented sugar referenced here: +> anonymous object literals, `{K: V}` map literals with `m[k]` access, optional +> auto-wrapping (`let x: str? = "hi"`), `loop { break v }` as an expression, +> `.enumerate()`, default/named parameters, and struct spread. Derive currently +> supports `Eq`, `Clone`, and `Display` only (not `Debug`/`Hash`/`Serialize`/`Schema`). + ## Elegant by Design Turbo's surface syntax follows one north star: **JavaScript simplicity on the surface, Rust power underneath. Progressive disclosure.** @@ -450,7 +461,7 @@ let nums = [1, 2, 3, 4, 5] let mut items = [1, 2, 3] items.push(4) // push() builtin [Implemented] -// Maps — JS-like object literal syntax [Implemented] +// Maps — JS-like object literal syntax [Planned — today use hashmap() + hashmap_set] let mut scores = { "Alice": 100, "Bob": 85, @@ -570,13 +581,13 @@ struct Config { } Turbo takes a deliberately minimal approach to metaprogramming. Instead of a macro system, Turbo relies on three mechanisms that cover 95% of use cases with zero magic: -**1. `@derive(...)` — Automatic trait implementations.** The compiler generates boilerplate implementations for common traits. This replaces Rust's `derive` procedural macros with a built-in, well-defined expansion. +**1. `@derive(...)` — Automatic trait implementations.** The compiler generates boilerplate implementations for common traits. This replaces Rust's `derive` procedural macros with a built-in, well-defined expansion. *Today the compiler implements `Eq`, `Clone`, and `Display`; `Debug`/`Hash`/`Serialize`/`Schema` shown below are planned.* ``` -@derive(Debug, Eq, Hash, Clone, Serialize, Schema) +@derive(Eq, Clone, Display) // implemented today +// @derive(Debug, Hash, Serialize, Schema) // planned struct User { name: str, age: u32 } -// The compiler generates: Debug.fmt(), Eq.eq(), Hash.hash(), Clone.clone(), -// Serialize.serialize(), and a JSON Schema — all from the struct definition. +// The compiler generates Eq.eq(), Clone.clone(), and Display — from the struct definition. ``` **2. `const fn` — Compile-time computation.** Any function marked `const fn` runs at compile time when called in a `const` context. This replaces traditional macro-based code generation with regular, debuggable, type-checked functions. From 367d2bbcfddf84c90d0bcbdca57e502328914062 Mon Sep 17 00:00:00 2001 From: ZVN DEV <78920650+zvndev@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:38:56 -0400 Subject: [PATCH 7/7] chore: stop tracking internal planning docs and build artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Untrack 8 internal sprint/audit docs (AUDIT-REPORT, FINDINGS-REPORT, the three SPRINT-PLAN files, STDLIB-ROADMAP, TODO, turbo/PHASE1-POLISH-SPRINT) and a stray benchmark screenshot from the public repo root — they leak internal planning and contradict the current state. The .gitignore already had a section titled "Internal planning docs (should never be committed)"; these had slipped past it. Files remain on disk locally; they're now gitignored, along with the stray mock_agent/simple_agent build artifacts. --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index a2f2eb4..1bf9656 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ node_modules/ # Build outputs turbo_test_build hello +mock_agent +simple_agent +benchmark-final.png *.o *.d *.wasm @@ -32,6 +35,13 @@ docs/superpowers/ /PLAN-*.md /INDEX.md /turbo/cow-bug-audit.md +/AUDIT-REPORT.md +/FINDINGS-REPORT.md +/SPRINT-PLAN.md +/SPRINT-PLAN-*.md +/STDLIB-ROADMAP.md +/TODO.md +/turbo/PHASE1-POLISH-SPRINT.md # OMX tooling artifacts (logs, plans, state — local only, never committed) /.omx/