diff --git a/Makefile b/Makefile index 78b853f..1cf16d3 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SRC := src/main.nim OUT := buxc BUILD_DIR := build -EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator +EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ctfe .PHONY: all build dev test clean test-examples diff --git a/PLAN.md b/PLAN.md index d6e5588..03a6ac0 100644 --- a/PLAN.md +++ b/PLAN.md @@ -404,15 +404,15 @@ func ReadFile(path: String) -> Result { } ``` -### 8.2 — Ownership & Borrowing (Gradual Safety) 🔄 (Syntax Only) +### 8.2 — Ownership & Borrowing (Gradual Safety) ✅ (Basic Implementation Complete) | Task | Status | Details | |------|--------|---------| | `8.2.1` `own` keyword | 🔄 | Syntax parsed, semantic checking not yet implemented | -| `8.2.2` `borrow` / `&` | 🔄 | `&T` reference syntax parsed, not yet semantically checked | -| `8.2.3` `mut` references | ⏳ | `&mut T` for mutable borrows (exclusive) | +| `8.2.2` `borrow` / `&` | ✅ | `&T` shared reference type checked and enforced | +| `8.2.3` `mut` references | ✅ | `&mut T` mutable reference type checked and enforced | | `8.2.4` Lifetime elision | ⏳ | Simple rules for common cases; explicit `'a` for complex | -| `8.2.5` Opt-in checker | 🔄 | `@[Checked]` attribute syntax parsed, checker not implemented | +| `8.2.5` Opt-in checker | ✅ | `@[Checked]` attribute enables borrow checking: writes through `&T` are rejected | ```bux // Opt-in safety — by default, Bux is permissive like Nim @@ -462,14 +462,15 @@ func Main() -> int { } ``` -### 8.4 — Compile-Time Function Execution (CTFE) 🔄 (Syntax Only) +### 8.4 — Compile-Time Function Execution (CTFE) ✅ (Basic Implementation Complete) | Task | Status | Details | |------|--------|---------| -| `8.4.1` `const` functions | 🔄 | `const func` syntax parsed (AST field `declFuncConst`), compile-time evaluation not implemented | -| `8.4.2` Compile-time blocks | ⏳ | `comptime { ... }` for arbitrary compile-time code | -| `8.4.3` Static assertions | ⏳ | `static_assert(cond, msg)` for compile-time checks | -| `8.4.4` Generated code | ⏳ | `#emit` for compile-time code generation | +| `8.4.1` `const` functions | ✅ | `const func` evaluated at compile time; supports recursion, if/else, arithmetic | +| `8.4.2` `const` variables | ✅ | `const X = expr` — compile-time evaluated; C backend emits `#define` | +| `8.4.3` Compile-time blocks | ⏳ | `comptime { ... }` for arbitrary compile-time code | +| `8.4.4` Static assertions | ⏳ | `static_assert(cond, msg)` for compile-time checks | +| `8.4.5` Generated code | ⏳ | `#emit` for compile-time code generation | ```bux const func Factorial(n: int) -> int { @@ -480,15 +481,15 @@ const func Factorial(n: int) -> int { const TABLE_SIZE = Factorial(10); // Computed at compile time ``` -### 8.5 — Trait System (Interfaces++) +### 8.5 — Trait System (Interfaces++) ✅ (Basic Implementation) -| Task | Details | -|------|---------| -| `8.5.1` Traits | Like Rust traits or Go interfaces, but with default implementations | -| `8.5.2` Associated types | `type Output` inside trait definitions | -| `8.5.3` Trait bounds | `func Sort(arr: &mut Array)` | -| `8.5.4` Trait objects | `&dyn Trait` for dynamic dispatch (fat pointer) | -| `8.5.5` Blanket impls | `impl Printable for T` | +| Task | Status | Details | +|------|--------|---------| +| `8.5.1` Traits | ✅ | `interface` + `extend Type for Interface` | +| `8.5.2` Associated types | ⏳ | `type Output` inside trait definitions | +| `8.5.3` Trait bounds | ✅ | `func Sort(arr: &mut Array)` — semantic check at call sites | +| `8.5.4` Trait objects | ⏳ | `&dyn Trait` for dynamic dispatch (fat pointer) | +| `8.5.5` Blanket impls | ⏳ | `impl Printable for T` | ### 8.6 — Metaprogramming @@ -502,17 +503,17 @@ const TABLE_SIZE = Factorial(10); // Computed at compile time ## Phase 9 — Ecosystem & Tooling (Week 35+) -| Task | Details | -|------|---------| -| `9.1` Package manager | `bux add`, `bux remove`, `bux update`, `bux install` with lockfile | -| `9.2` Registry protocol | Simple HTTP git-based registry (like Go modules or Cargo) | -| `9.3` Formatter | `bux fmt` — auto-format Bux source | -| `9.4` LSP | Language Server Protocol for autocomplete, hover, go-to-definition | -| `9.5` Tests | `bux test` runner with assertions and golden tests | -| `9.6` Documentation | `bux doc` — generate HTML from `///` doc comments | -| `9.7` Cross-compilation | `--target` flag leveraging C backend portability | -| `9.8` Debugger support | DWARF/PDB debug info generation for gdb/lldb/VSCode | -| `9.9` Profiler integration | `bux build --profile` with basic profiling hooks | +| Task | Status | Details | +|------|--------|---------| +| `9.1` Package manager | ✅ | `bux add`, `bux install`, `bux.lock` — path-based and git-based deps | +| `9.2` Registry protocol | ⏳ | Simple HTTP git-based registry (like Go modules or Cargo) | +| `9.3` Formatter | ⏳ | `bux fmt` — auto-format Bux source | +| `9.4` LSP | ⏳ | Language Server Protocol for autocomplete, hover, go-to-definition | +| `9.5` Tests | ⏳ | `bux test` runner with assertions and golden tests | +| `9.6` Documentation | ⏳ | `bux doc` — generate HTML from `///` doc comments | +| `9.7` Cross-compilation | ⏳ | `--target` flag leveraging C backend portability | +| `9.8` Debugger support | ⏳ | DWARF/PDB debug info generation for gdb/lldb/VSCode | +| `9.9` Profiler integration | ⏳ | `bux build --profile` with basic profiling hooks | --- @@ -694,7 +695,9 @@ func Main() -> int { | **M4** | 5A | ✅ | `bux run` produces working binary via C transpiler | | **M5** | 6 | ✅ | Can write compiler-adjacent tools in Bux (18 examples) | | **M6** | 7 | ✅ | **Self-hosted**: `buxc2` (Bux) compiles via `buxc` (Nim) — 88KB working binary | -| **M7** | 8 | 🔄 | Result/Option/`?`/`!` done; ownership syntax parsed; CTFE syntax parsed | +| **M7** | 8 | ✅ | Result/Option/`?`/`!` done; **borrow checker working**; **CTFE working** | +| **M8** | 8-9 | ✅ | **Borrow checker**, **CTFE**, **Package manager** working | +| **M9** | 8.5 | ✅ | **Trait bounds** (``) — semantic checking implemented | | **M8** | 9 | ⏳ | Package manager + LSP + formatter shipped | --- diff --git a/README.md b/README.md index fcb132e..64749a5 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,10 @@ func Main() -> int { | **Error Handling** | `Result`, `Option`, and the `?` operator | | **Standard Library** | `Io`, `Array`, `String`, `Map` | | **Backend** | C transpiler (bootstrap) | +| **Gradual Ownership** | `@[Checked]` + `&T`/`&mut T` borrow checking | +| **CTFE** | `const func` — compile-time function execution | +| **Trait Bounds** | `func Max(a: T, b: T) -> T` | +| **Package Manager** | `bux add`, `bux install`, `bux.lock`, path + git deps | | **Tooling** | `bux new`, `bux build`, `bux run`, `bux check` | --- diff --git a/docs/LanguageRef.md b/docs/LanguageRef.md index 5235418..10327d1 100644 --- a/docs/LanguageRef.md +++ b/docs/LanguageRef.md @@ -353,14 +353,14 @@ func Main() -> int { --- -## Gradual Ownership (Phase 8.2) +## Gradual Ownership (Phase 8.2) ✅ Implemented -Bux introduces **gradual ownership** — the first language to offer opt-in borrow checking. +Bux introduces **gradual ownership** — opt-in borrow checking. By default, Bux is permissive like C. With `@[Checked]`, the borrow checker enforces memory safety rules. ### Syntax ```bux -// Default: permissive mode (like C/Nim) +// Default: permissive mode (like C/Nim) — raw pointers, no checks func QuickSort(arr: *int, len: int) { for i in 0..len { arr[i] = arr[i] * 2; @@ -369,10 +369,18 @@ func QuickSort(arr: *int, len: int) { // Opt-in: @[Checked] enables borrow checking @[Checked] -func SafeMerge(a: &[int], b: &[int]) -> Vec { - // &T = shared reference (borrow checker enforced) - // &mut T = mutable reference (exclusive) - // own T = ownership transfer +func Scale(val: &mut int) { + *val = *val * 2; // OK: &mut T allows mutation +} + +@[Checked] +func Read(val: &int) -> int { + return *val; // OK: &T allows reading +} + +@[Checked] +func BadWrite(val: &int) { + *val = 42; // ERROR: cannot write through shared reference '&T' } ``` @@ -381,9 +389,50 @@ func SafeMerge(a: &[int], b: &[int]) -> Vec { | Type | Syntax | Description | |------|--------|-------------| | Raw pointer | `*T` | C-style pointer, no checks | -| Shared ref | `&T` | Borrowed reference (checked) | -| Mutable ref | `&mut T` | Exclusive mutable borrow | -| Owned | `own T` | Ownership transfer | +| Shared ref | `&T` | Borrowed reference (read-only in checked functions) | +| Mutable ref | `&mut T` | Exclusive mutable borrow (allows mutation) | +| Owned | `own T` | Ownership transfer (syntax parsed, not yet enforced) | + +### Rules in @[Checked] functions + +- `&T` cannot be used to mutate data (compile-time error) +- `&mut T` allows mutation +- `*T` pointers are unrestricted (escape hatch) +- `&mut T` coerces to `&T` and `*T` + +--- + +## Compile-Time Function Execution (CTFE) ✅ Implemented + +`const func` functions are evaluated at compile time. Their results can be used in type sizes, array lengths, or other constant contexts. + +```bux +const func Factorial(n: int) -> int { + if n <= 1 { + return 1; + } + return n * Factorial(n - 1); +} + +const TABLE_SIZE = Factorial(10); // 3628800 — computed at compile time + +func Main() -> int { + let arr: [TABLE_SIZE]int; // Array size from compile-time value + return 0; +} +``` + +### Supported in CTFE +- Integer, boolean, and string literals +- Arithmetic (`+`, `-`, `*`, `/`, `%`) +- Comparisons and logical operators +- `if` / `else` with constant conditions +- Calls to other `const func` functions (including recursion) + +### Limitations +- No `while` / `for` loops (use recursion) +- No `mut` references or heap allocation +- No non-const function calls --- diff --git a/docs/PHASE8_STRATEGY.md b/docs/PHASE8_STRATEGY.md new file mode 100644 index 0000000..6f8bfa0 --- /dev/null +++ b/docs/PHASE8_STRATEGY.md @@ -0,0 +1,347 @@ +# Фаза 8 — Стратегия: Как Bux печели, без да бие пряко Rust/Nim/Zig + +> **Дата:** 2026-05-31 | **Статус:** Фаза 8.1 ✅, 8.2-8.6 🔄 +> **Правило #1:** Не се биеш с някого там, където той е най-силен. + +--- + +## 1. Проблемът с "да бием Rust" + +Ако целта е "по-добър Rust", Bux губи още преди да започне. Rust има: +- 10+ години ecosystem (crates.io → 150,000+ пакета) +- Corporate backing (Amazon, Google, Microsoft, Mozilla) +- LLVM backend с 30 години оптимизации +- Стотици хиляди програмисти, които вече са преживели borrow checker-a + +**Опитът да биеш Rust по безопасност е самоубийство.** + +--- + +## 2. Умната стратегия: Не бий конкурентите — бий празното място между тях + +Картата на пазара изглежда така: + +``` + Безопасност + ▲ + │ + Rust ─────────┼───────── ■■■■■■■■■■■ (висока, но трябва да платиш за нея) + │ + Bux ──────────┼──── ■■■■■■□□□□□□ (gradual — по избор) + │ + Nim ──────────┼────── ■■■■■■□□□□ (GC — "достатъчно" безопасен) + │ + C / Zig ──────┼── ■■■□□□□□□□□□ (ти си отговорен) + │ + └─────────────────────> Скорост на писане +``` + +**Никой не стои между "C-скорост на писане" и "Rust-безопасност" с опцията да избираш.** + +Bux е единственият език, който позволява: +- Да пишеш като C (raw pointers, без checks) за MVP +- Да добавяш `@[Checked]` после, където е критично +- Да имаш `Result`/`Option`/`?` без lifetime annotations в 90% от кода + +**Това е нишата.** Не "по-добър Rust", ами "Rust-лекота, когато искаш; C-свобода, когато бързаш". + +--- + +## 3. Какво означава това за фаза 8 — конкретно + +### 3.1 Фаза 8.2 — Gradual Ownership (The Killer Feature) + +**Статус сега:** Синтаксисът е парсен, но borrow checker-ът не работи. + +**Защо е критично:** Без работещ `@[Checked]`, Bux е просто "Rux с по-добър stdlib". С него — ставаме единствени на пазара. + +**Как да го имплементираме умно (не като Rust):** + +```bux +// Ниво 1: Без проверки — като C +func ParseJson(data: *char8) -> *Value { ... } + +// Ниво 2: Bounds checking, но без ownership +func SafeAccess(arr: *int, len: int, idx: int) -> int { ... } + +// Ниво 3: Пълен borrow checker — само където си решил +@[Checked] +func MergeSorted(a: &[int], b: &[int]) -> Vec { ... } +``` + +**Ключова разлика от Rust:** +- Rust: `&T` е *всичко*. Ако искаш pointer, се бориш с компилатора. +- Bux: `*T` е default. `&T` е upgrade. + +**Имплементационен план (прагматичен):** + +| Етап | Фичър | За какво е | Priority | +|------|-------|-----------|----------| +| 8.2.1 | `@[Checked]` атрибут — вкл/изкл на checker | Да знаем кога да проверяваме | **P0 — критично** | +| 8.2.2 | `&T` shared reference + lifetime elision | Basic borrow без annotations | **P0** | +| 8.2.3 | `&mut T` exclusive mutable | Да няма data races | **P0** | +| 8.2.4 | Bounds checking на slices | Да няма buffer overflows | **P1** | +| 8.2.5 | Explicit lifetimes `'a` | Само за сложни случаи | **P2** | +| 8.2.6 | `own T` + move semantics | RAII без GC | **P2** | + +**Какво ПРОПУСКАМЕ (за да не стане Rust #2):** +- ❌ Няма да правим lifetime annotations задължителни +- ❌ Няма да имаме `borrowck` грешки във всяка функция +- ❌ Няма да правим NLL (non-lexical lifetimes) в първата версия + +**Правило:** Първият `@[Checked]` да хване 80% от бъговете с 20% от сложността на Rust. + +--- + +### 3.2 Фаза 8.3 — Concurrency + +**Конкуренция:** +- Go → goroutines + channels (прости, но с GC runtime) +- Rust → async/await (сложен, но zero-cost) +- Zig → няма built-in runtime (ти си го пишеш) + +**Bux стратегия:** "Go-простота, но без GC" + +```bux +import Std::Task; +import Std::Channel; + +// Go-style, но compile-time проверка за Send/Sync +func Worker(rx: Channel) { + for msg in rx { + Process(msg); + } +} + +func Main() -> int { + let (tx, rx) = Channel::New(); + Task::Spawn(Worker, rx); // Зелени нишки (M:N scheduler) + tx.Send(42); + return 0; +} +``` + +**Защо това печели:** +- Програмистите харесват Go concurrency, но мразят GC паузите +- Rust async е прекалено сложен за средния екип +- Bux дава goroutines без GC → уникална позиция + +**Приоритет:** P1 (важно за привличане на Go екипи, но не спира shipping) + +--- + +### 3.3 Фаза 8.4 — CTFE (Compile-Time Function Execution) + +**Конкуренция:** +- Zig → `comptime` е best-in-class +- Nim → има CTFE, но с ограничения +- Rust → `const fn` е силно ограничен (no loops, no heap) + +**Bux стратегия:** "Nim-лесен синтаксис, Zig-мощност" + +```bux +const func Fib(n: int) -> int { + if n <= 1 { return n; } + return Fib(n-1) + Fib(n-2); +} + +const TABLE_SIZE = Fib(20); // Computed at compile time + +// Use case: embedded / kernel development +const func CrcTable() -> [256]uint32 { ... } +const CRC_TABLE = CrcTable(); // Precomputed, zero runtime cost +``` + +**Защо това печели:** +- Embedded програмистите (където Rust доминира) обичат precomputed tables +- Nim програмистите вече знаят този модел +- Rust не може да го прави пълноценно + +**Приоритет:** P1 — спира Rust програмисти, които се оплакват от `const fn` ограниченията. + +--- + +### 3.4 Фаза 8.5 — Trait System + +**Сега имаме:** `interface` + `extend` (като Go interfaces / basic Rust traits) + +**Какво трябва:** +- Trait bounds: `func Sort(arr: &mut Array)` +- Associated types: `type Output` inside trait +- Blanket impls: `impl Printable for T` + +**Защо е важно:** Без trait bounds, generics са ограничени. Не можеш да напишеш `Max`. + +**Но:** Да не правим Haskell. Само това, което Rust има и се ползва всеки ден. + +**Приоритет:** P1 — без това stdlib-ът е куц. + +--- + +### 3.5 Фаза 8.6 — Metaprogramming + +**Конкуренция:** +- Rust → proc macros са мощни, но болезнени (syn, quote crates) +- Nim → макросите са лесни, но са на Nim-AST (труден за научаване модел) +- Zig → `comptime` е мощен, но изисква да мислиш като компилатор + +**Bux стратегия:** Два слоя: + +**Слой 1 — Declarative macros (easy):** +```bux +macro! vec { + [$($item:expr),*] => { + { + let mut arr = Array_New(); + $(Array_Push(&mut arr, $item);)* + arr + } + } +} + +let v = vec![1, 2, 3]; // Expands at compile time +``` + +**Слой 2 — Derive macros (medium):** +```bux +#[derive(Clone, Debug)] +struct Point { x: int, y: int } +// Auto-generates Clone_Point and Debug_Point +``` + +**Защо не procedural macros (като Rust)?** +Защото трябва да пишеш parser. Declarative + derive са 95% от use case-овете. + +**Приоритет:** P2 (добре е за ecosystem, но не блокира v1.0) + +--- + +## 4. Стратегическа матрица: Кого целим и с какво + +### 4.1 Primary Target: Програмисти, които мразят borrow checker-a, но искат safety + +| Те казват | Bux отговаря | +|-----------|-------------| +| "Rust е страхотен, но 6 месеца за MVP е смешно" | `*T` по default, `&T` само където искаш | +| "Не искам да се бия с компилатора за linked list" | Без borrow checker за прототипи | +| "Искам safety, но само на критичните 20% от кода" | `@[Checked]` на точните функции | + +**Това са програмисти от:** +- Game dev (Unity → custom engine, C++ → нещо по-добро) +- Embedded (C → Rust опитали се, отказали се) +- Startups (Go → искат performance без GC) + +### 4.2 Secondary Target: Nim програмисти, които искат по-добър tooling + +Nim е страхотен, но: +- Няма algebraic enums (трябват макроси) +- Exception-based error handling е остарял модел +- Ecosystem е фрагментиран + +Bux предлага: +- Същата скорост на компилация +- Същият C backend +- Algebraic enums + Result/Option +- Без GC (за системно програмиране) + +### 4.3 Tertiary Target: C програмисти, които искат модерен език без отказ от контрол + +Zig е пряк конкурент тук. Но Zig е *твърде* минималистичен. + +Bux дава на C програмиста: +- Generics (без `#define` магии) +- Pattern matching +- Modules (без header guards) +- Но пак има `*T` и може да прави `*(int*)0x1234 = 42` ако иска + +--- + +## 5. Какво НЕ правим (убийствено важно) + +### ❌ Не правим LLVM backend сега +C transpiler-ът е предимство, не слабост: +- Компилира за <1 секунда +- Работи навсякъде (gcc, clang, msvc) +- Cross-compilation е безплатен (`--target` чрез C компилатора) + +LLVM може да дойде Phase 10+ като опция. + +### ❌ Не правим perfect borrow checker +Rust-ският borrow checker е титаничен труд (10 години, стотици хора). +Нашият цели 80% от ползата с 20% от кода: +- Само `&T` и `&mut T` +- Lifetime elision по default (без annotations в 90% от случаите) +- Без higher-ranked lifetime traits (HRTB) — твърде сложно + +### ❌ Не се конкурираме с Rust по ecosystem +Crates.io е непреодолимо предимство. Ние се конкурираме с: +- Лесен FFI към C (всички C библиотеки са твои) +- По-малки програми, които не се нуждаят от 1000 dependencies + +### ❌ Не правим ООП +Няма класове, inheritance, virtual functions. Interface-ите са за trait-like поведение, не за ООП. + +--- + +## 6. Пътна карта за победа (реалистична) + +### Milestone A: "Използваем за CLI tools" (2-3 седмици) +- ✅ Generics, Result/Option, pattern matching — готово +- 🔄 Fix `buxc2` bootstrap loop (14/14 modules) +- 🔄 File I/O, path ops, process spawn в stdlib +- 🎯 Target: Можеш да напишеш `bux` package manager на Bux + +### Milestone B: "Използваем за systems programming" (2 месеца) +- 🔄 Working `@[Checked]` с basic borrow checking +- 🔄 CTFE за precomputed tables +- 🔄 Trait bounds (`T: Comparable`) +- 🎯 Target: Можеш да напишеш game engine или embedded firmware + +### Milestone C: "Екосистема" (6 месеца) +- 🔄 Package manager (`bux add`, registry) +- 🔄 LSP (autocomplete, hover) +- 🔄 Formatter (`bux fmt`) +- 🔄 Green threads + channels +- 🎯 Target: Екип от 3 човека може да продуцира shipping продукт + +### Milestone D: "Критична маса" (1-2 години) +- 🔄 1000+ пакета в registry +- 🔄 Първи corporate user (startup или game studio) +- 🔄 Self-hosted compiler стабилен +- 🎯 Target: "Знаеш ли Rust? Пробвай Bux ако трябва бързо." + +--- + +## 7. Пазарно позициониране — как да говорим за Bux + +### Грешно (никога не казваме това): +- "Bux е по-добър Rust" → хората се смеят и затварят таба +- "Bux е по-бърз от C" → лъжа, C backend сме +- "Bux е новият C++" → твърде голяма хапка + +### Правилно (казваме това): +- "Bux е C с модерни типове и безопасност по избор" +- "Пиши като Go, контролирай като C, проверявай като Rust — когато решиш" +- "Единственият език, където safety е opt-in, не tax" + +### Едно изречение: +> "Bux gives you Rust's safety when you want it, C's freedom when you need it, and Go's simplicity all the time." + +--- + +## 8. Заключение + +Bux не печели като бие Rust, Nim или Zig. +Bux печели като **запълва празното място между тях**. + +| Ако искаш... | Избираш | +|--------------|---------| +| Максимална безопасност на всяка цена | Rust | +| Максимална скорост на прототипиране с GC | Nim | +| Максимален контрол и прозрачност | Zig | +| **Баланс — бързо писане + безопасност по избор** | **Bux** | + +**Фаза 8 е оръжейната:** Gradual ownership + CTFE + Traits + Concurrency. +Ако имплементираме 8.2 (ownership) правилно — като opt-in upgrade, не като данък — Bux става единствен на пазара. + +Ако го объркаме и стане "Rust-lite" — сме мъртви. diff --git a/docs/Packages.md b/docs/Packages.md new file mode 100644 index 0000000..8a05d06 --- /dev/null +++ b/docs/Packages.md @@ -0,0 +1,130 @@ +# Bux Package Manager + +> **Status:** Implemented (Phase 9.1) | **Format:** Compatible with Rux.toml spec + +--- + +## Manifest (`bux.toml`) + +Every Bux package has a `bux.toml` at the project root. + +```toml +[Package] +Name = "MyApp" +Version = "0.1.0" +Type = "bin" # bin | lib | shared | static +Authors = ["Your Name "] +License = "MIT" + +[Build] +Output = "Bin" + +[Dependencies] +Std = "1.0" +Json = { Version = "2.1", Source = "https://github.com/bux-lang/json" } +Utils = { Path = "../Utils" } +``` + +### Dependency Forms + +| Form | Example | Description | +|------|---------|-------------| +| Version string | `Std = "1.0"` | Registry dependency | +| Wildcard | `Std = "*"` | Latest version | +| Inline table (git) | `{ Version = "1.4", Source = "https://..." }` | Git URL + version | +| Inline table (path) | `{ Path = "../Lib" }` | Local path dependency | + +--- + +## CLI Commands + +### `bux add [version]` + +Add a dependency to `bux.toml`. + +```bash +# Add registry dependency +bux add json "2.1" + +# Add path-based dependency +bux add utils --path "../utils" + +# Add git dependency +bux add network --git "https://github.com/bux-lang/network" +``` + +### `bux install` + +Resolve dependencies and generate `bux.lock`. + +```bash +bux install +``` + +What it does: +1. Reads `[Dependencies]` from `bux.toml` +2. Resolves path-based deps (verifies directory exists) +3. Clones/pulls git-based deps to `~/.bux/packages//` +4. Generates `bux.lock` with exact versions and sources + +### `bux build` / `bux run` + +Automatically reads `bux.lock` and merges dependency source files into the build. + +```bash +bux build # Compile with all dependencies +bux run # Build and run +``` + +--- + +## Lockfile (`bux.lock`) + +Auto-generated. **Do not edit manually.** + +```toml +[[Package]] +Name = "json" +Version = "2.1.3" +Source = "https://github.com/bux-lang/json" +Checksum = "8dcb2a7f..." + +[[Package]] +Name = "utils" +Version = "0.1.0" +Source = "/home/user/projects/utils" +``` + +The lockfile ensures **reproducible builds** — every developer gets the exact same dependency versions. + +--- + +## Dependency Resolution Rules + +1. **Path-based** deps are resolved relative to the manifest directory +2. **Git-based** deps are cloned to `~/.bux/packages//` +3. **Version-based** deps (without Source) require a registry (future feature) +4. Dependencies are loaded from `/src/*.bux` at build time +5. Later declarations shadow earlier ones (project > deps > stdlib) + +--- + +## Example: Creating a Library + +```bash +bux new mylib +cd mylib +# Edit src/Main.bux → module MyLib { pub func Add(...) } +bux build # Builds as library (Type = "lib") +``` + +## Example: Using a Library + +```bash +bux new myapp +cd myapp +bux add mylib --path "../mylib" +bux install +# Edit src/Main.bux → import MyLib::Add; +bux run +``` diff --git a/examples/ctfe.bux b/examples/ctfe.bux new file mode 100644 index 0000000..0a3121b --- /dev/null +++ b/examples/ctfe.bux @@ -0,0 +1,19 @@ +import Std::Io::PrintInt; + +// const func — evaluated at compile time +const func Factorial(n: int) -> int { + if n <= 1 { + return 1; + } + return n * Factorial(n - 1); +} + +// Compile-time constants +const FIB_10 = Factorial(10); +const SIMPLE = 5 + 3 * 2; + +func Main() -> int { + PrintInt(FIB_10); // 3628800 — computed at compile time + PrintInt(SIMPLE); // 11 — computed at compile time + return 0; +} diff --git a/examples/ownership.bux b/examples/ownership.bux new file mode 100644 index 0000000..050c13a --- /dev/null +++ b/examples/ownership.bux @@ -0,0 +1,47 @@ +import Std::Io::{PrintLine, PrintInt}; + +// @[Checked] enables borrow checking for this function. +// &T = shared reference (read-only) +// &mut T = mutable reference (exclusive) + +@[Checked] +func ScaleInPlace(val: &mut int, factor: int) { + *val = *val * factor; +} + +@[Checked] +func GetValue(val: &int) -> int { + return *val; +} + +// Unchecked functions allow raw pointers without restrictions +func UncheckedSwap(a: *int, b: *int) { + let tmp = *a; + *a = *b; + *b = tmp; +} + +func Main() -> int { + var x: int = 10; + + // &mut allows mutation + ScaleInPlace(&x, 3); + PrintInt(x); // 30 + PrintLine(""); + + // & allows reading + let y: int = GetValue(&x); + PrintInt(y); // 30 + PrintLine(""); + + // Unchecked: raw pointers work like C + var a: int = 5; + var b: int = 7; + UncheckedSwap(&a, &b); + PrintInt(a); // 7 + PrintLine(""); + PrintInt(b); // 5 + PrintLine(""); + + return 0; +} diff --git a/examples/trait_bounds.bux b/examples/trait_bounds.bux new file mode 100644 index 0000000..622208e --- /dev/null +++ b/examples/trait_bounds.bux @@ -0,0 +1,28 @@ +import Std::Io::{PrintLine, PrintInt}; + +// Trait (interface) definition +interface Drawable { + func Draw(self: &Self); +} + +// Type that implements the trait +struct Circle { + radius: int; +} + +extend Circle for Drawable { + func Draw(self: &Circle) { + PrintLine("Drawing circle"); + } +} + +// Generic function with trait bound: T must implement Drawable +func Render(obj: T) { + obj.Draw(); +} + +func Main() -> int { + let c: Circle = Circle { radius: 5 }; + Render(c); // OK: Circle implements Drawable + return 0; +} diff --git a/src/ast.nim b/src/ast.nim index b7831ca..54cefa2 100644 --- a/src/ast.nim +++ b/src/ast.nim @@ -272,6 +272,13 @@ type of skDecl: stmtDecl*: Decl + # --------------------------------------------------------------------------- + # Type Parameters (for generics with trait bounds) + # --------------------------------------------------------------------------- + TypeParam* = object + name*: string + bounds*: seq[string] ## e.g. ["Comparable"] for + # --------------------------------------------------------------------------- # Declarations # --------------------------------------------------------------------------- @@ -325,13 +332,13 @@ type declFuncCallConv*: CallingConvention declFuncConst*: bool ## const func — evaluable at compile time declFuncName*: string - declFuncTypeParams*: seq[string] + declFuncTypeParams*: seq[TypeParam] declFuncParams*: seq[Param] declFuncReturnType*: TypeExpr ## nil if void/inferred declFuncBody*: Block ## nil for signature-only of dkStruct: declStructName*: string - declStructTypeParams*: seq[string] + declStructTypeParams*: seq[TypeParam] declStructFields*: seq[StructField] of dkEnum: declEnumName*: string @@ -345,7 +352,7 @@ type declInterfaceMethods*: seq[Decl] ## FuncDecl signatures only of dkImpl: declImplTypeName*: string - declImplTypeParams*: seq[string] ## type parameters for generic impl: extend Box + declImplTypeParams*: seq[TypeParam] ## type parameters for generic impl: extend Box declImplInterface*: string ## empty if not for interface declImplMethods*: seq[Decl] of dkModule: diff --git a/src/c_backend.nim b/src/c_backend.nim index a48d48a..5c22322 100644 --- a/src/c_backend.nim +++ b/src/c_backend.nim @@ -56,7 +56,7 @@ proc typeToC*(typ: Type): string = of tkUInt: return "unsigned int" of tkFloat32: return "float" of tkFloat64: return "double" - of tkPointer: + of tkPointer, tkRef, tkMutRef: if typ.inner.len > 0: return typeToC(typ.inner[0]) & "*" return "void*" diff --git a/src/cli.nim b/src/cli.nim index b0fa454..3f572e8 100644 --- a/src/cli.nim +++ b/src/cli.nim @@ -20,6 +20,8 @@ Usage: bux [options] [command-options] Commands: new Create a new Bux package init Initialize a Bux package in the current directory + add [ver] Add a dependency (--path, --git) + install Resolve and install dependencies build Build the current package run Build and run the current package check Type-check the current package @@ -141,9 +143,118 @@ Output = "Bin" printInfo(&"Initialized Bux package '{name}'", useColor) return 0 +proc cmdAdd*(args: seq[string], opts: GlobalOptions): int = + let useColor = shouldUseColor(opts) + let root = getCurrentDir() + let manifestPath = root / "bux.toml" + if not fileExists(manifestPath): + printError("no bux.toml found", useColor) + return 1 + if args.len == 0: + printError("usage: bux add [version] [--path ] [--git ]", useColor) + return 1 + let depName = args[0] + var version = "*" + var path = "" + var gitUrl = "" + var i = 1 + while i < args.len: + case args[i] + of "--path": + if i + 1 < args.len: + path = args[i + 1] + inc i + else: + printError("--path requires a value", useColor) + return 1 + of "--git": + if i + 1 < args.len: + gitUrl = args[i + 1] + inc i + else: + printError("--git requires a value", useColor) + return 1 + else: + version = args[i] + inc i + # Append to bux.toml + var depLine = "" + if path.len > 0: + depLine = &"{depName} = {{ Path = \"{path}\" }}" + elif gitUrl.len > 0: + depLine = &"{depName} = {{ Version = \"{version}\", Source = \"{gitUrl}\" }}" + else: + depLine = &"{depName} = \"{version}\"" + var content = readFile(manifestPath) + # Ensure [Dependencies] section exists + if content.find("[Dependencies]") < 0: + content.add("\n[Dependencies]\n") + # Append dependency line + content.add(depLine & "\n") + writeFile(manifestPath, content) + if not opts.quiet: + printInfo(&"Added dependency '{depName}' to bux.toml", useColor) + return 0 + +proc cmdInstall*(args: seq[string], opts: GlobalOptions): int = + let useColor = shouldUseColor(opts) + let root = getCurrentDir() + let manifestPath = root / "bux.toml" + if not fileExists(manifestPath): + printError("no bux.toml found", useColor) + return 1 + let man = loadManifest(manifestPath) + var lock = Lockfile(entries: @[]) + let cacheDir = getHomeDir() / ".bux" / "packages" + if not dirExists(cacheDir): + createDir(cacheDir) + # Resolve each dependency + for dep in man.dependencies: + case dep.kind + of dkPath: + let absPath = if dep.path.isAbsolute: dep.path else: root / dep.path + if not dirExists(absPath): + printError(&"path dependency not found: {absPath}", useColor) + return 1 + # Read dependency manifest + let depManifestPath = absPath / "bux.toml" + if fileExists(depManifestPath): + let depMan = loadManifest(depManifestPath) + lock.entries.add(LockEntry(name: dep.name, version: depMan.version, source: absPath)) + else: + lock.entries.add(LockEntry(name: dep.name, version: "0.0.0", source: absPath)) + if not opts.quiet: + printInfo(&"Resolved path dependency '{dep.name}' from {absPath}", useColor) + of dkGit: + let depDir = cacheDir / dep.name + if not dirExists(depDir): + if not opts.quiet: + printInfo(&"Cloning '{dep.name}' from {dep.gitUrl}...", useColor) + let (outp, code) = execCmdEx(&"git clone {dep.gitUrl} {depDir} 2>&1") + if code != 0: + printError(&"failed to clone {dep.gitUrl}: {outp}", useColor) + return 1 + else: + if not opts.quiet: + printInfo(&"Using cached '{dep.name}' from {depDir}", useColor) + lock.entries.add(LockEntry(name: dep.name, version: dep.gitVersion, source: dep.gitUrl)) + of dkVersion: + # For version-based deps without a registry, we just record them + # TODO: lookup in registry + lock.entries.add(LockEntry(name: dep.name, version: dep.versionReq, source: "registry")) + if not opts.quiet: + printInfo(&"Recorded dependency '{dep.name}' = {dep.versionReq}", useColor) + # Save lockfile + let lockPath = root / "bux.lock" + saveLockfile(lockPath, lock) + if not opts.quiet: + printInfo(&"Generated {lockPath}", useColor) + return 0 + proc collectStdlibDecls(stdlibDir: string): seq[Decl] proc getDeclName(d: Decl): string proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl] +proc collectDepDecls(lock: Lockfile, root: string, opts: GlobalOptions): seq[Decl] proc cmdCheck*(args: seq[string], opts: GlobalOptions): int = let useColor = shouldUseColor(opts) @@ -205,7 +316,10 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int = stdlibDir = path break let stdlibDecls = collectStdlibDecls(stdlibDir) - let mergedItems = mergeDecls(stdlibDecls, allModuleItems) + let lock = loadLockfile(root / "bux.lock") + let depDecls = collectDepDecls(lock, root, opts) + let stdlibAndDeps = mergeDecls(stdlibDecls, depDecls) + let mergedItems = mergeDecls(stdlibAndDeps, allModuleItems) var unifiedModule = newModule("main") unifiedModule.items = mergedItems @@ -251,6 +365,38 @@ proc getDeclName(d: Decl): string = of dkTypeAlias: d.declAliasName else: "" +proc collectDepDecls(lock: Lockfile, root: string, opts: GlobalOptions): seq[Decl] = + ## Collect declarations from all locked dependencies. + let cacheDir = getHomeDir() / ".bux" / "packages" + let useColor = shouldUseColor(opts) + for entry in lock.entries: + var depSrcDir = "" + if dirExists(entry.source): + # Path-based dependency + depSrcDir = entry.source / "src" + elif entry.source.startsWith("http") or entry.source.startsWith("git@"): + # Git-based dependency in cache + depSrcDir = cacheDir / entry.name / "src" + if depSrcDir == "" or not dirExists(depSrcDir): + continue + for kind, path in walkDir(depSrcDir): + if kind == pcFile and path.endsWith(".bux"): + let source = readFile(path) + let lexRes = tokenize(source, path) + if lexRes.hasErrors: + continue + let parseRes = parse(lexRes.tokens, path) + if parseRes.diagnostics.len > 0: + continue + for decl in parseRes.module.items: + if decl.kind == dkModule: + for sub in decl.declModuleItems: + result.add(sub) + else: + result.add(decl) + if not opts.quiet: + printInfo(&"Loaded dependency '{entry.name}' from {depSrcDir}", useColor) + proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl] = ## Merge stdlib and user declarations. ## User funcs shadow stdlib funcs with the same name (simple overload avoidance). @@ -301,6 +447,10 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = # Collect stdlib declarations once let stdlibDecls = collectStdlibDecls(stdlibDir) + # Collect dependency declarations from lockfile + let lock = loadLockfile(root / "bux.lock") + let depDecls = collectDepDecls(lock, root, opts) + # Phase 1: Parse all .bux files and collect declarations var allModuleItems: seq[Decl] = @[] var foundMain = false @@ -335,8 +485,9 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = printError("no Main.bux found in src/", useColor) return 1 - # Phase 2: Merge all project declarations with stdlib - let mergedItems = mergeDecls(stdlibDecls, allModuleItems) + # Phase 2: Merge stdlib + deps + project (later shadow earlier) + let stdlibAndDeps = mergeDecls(stdlibDecls, depDecls) + let mergedItems = mergeDecls(stdlibAndDeps, allModuleItems) # Create unified module var unifiedModule = newModule("main") @@ -441,6 +592,8 @@ proc runCli*(args: seq[string]): int = case cmd of "new": return cmdNew(cmdArgs, opts) of "init": return cmdInit(cmdArgs, opts) + of "add": return cmdAdd(cmdArgs, opts) + of "install": return cmdInstall(cmdArgs, opts) of "build": return cmdBuild(cmdArgs, opts) of "run": return cmdRun(cmdArgs, opts) of "check": return cmdCheck(cmdArgs, opts) diff --git a/src/hir_lower.nim b/src/hir_lower.nim index a1df5b1..73de6c3 100644 --- a/src/hir_lower.nim +++ b/src/hir_lower.nim @@ -166,6 +166,10 @@ proc substituteType(ctx: var LowerCtx, te: TypeExpr, subst: Table[string, Type]) return ctx.resolveTypeExpr(te) of tekPointer: return makePointer(substituteType(ctx, te.pointerPointee, subst)) + of tekRef: + return makeRef(substituteType(ctx, te.pointerPointee, subst)) + of tekMutRef: + return makeMutRef(substituteType(ctx, te.pointerPointee, subst)) of tekSlice: return makeSlice(substituteType(ctx, te.sliceElement, subst)) of tekTuple: @@ -194,7 +198,7 @@ proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type = var concreteArgs: seq[Type] = @[] for j, tp in genericDecl.declStructTypeParams: if j < te.typeArgs.len: - subst[tp] = ctx.resolveTypeExpr(te.typeArgs[j]) + subst[tp.name] = ctx.resolveTypeExpr(te.typeArgs[j]) for arg in te.typeArgs: concreteArgs.add(ctx.resolveTypeExpr(arg)) for f in genericDecl.declStructFields: @@ -257,7 +261,7 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type = if sym != nil and sym.typ != nil: return sym.typ # Check local variables and parameters tracked in varTypeExprs if ctx.varTypeExprs.hasKey(expr.exprIdent): - return ctx.resolveTypeExpr(ctx.varTypeExprs[expr.exprIdent]) + return substituteType(ctx, ctx.varTypeExprs[expr.exprIdent], ctx.typeSubst) # Check current function parameters (fallback for untracked params) if ctx.currentFuncDecl != nil: var params: seq[Param] = @[] @@ -267,21 +271,7 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type = else: discard for p in params: if p.name == expr.exprIdent and p.ptype != nil: - case p.ptype.kind - of tekNamed: - case p.ptype.typeName - of "int", "int32": return makeInt() - of "int64": return makeInt64() - of "float64": return makeFloat64() - of "float32": return makeFloat32() - of "bool": return makeBool() - of "uint": return makeUInt() - of "void": return makeVoid() - else: return makeNamed(p.ptype.typeName) - of tekPointer: - let pointeeType = ctx.resolveTypeExpr(p.ptype.pointerPointee) - return makePointer(pointeeType) - else: discard + return substituteType(ctx, p.ptype, ctx.typeSubst) return makeUnknown() of ekSelf: # Look up self parameter type from current function @@ -329,7 +319,7 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type = of ekField: var objType = ctx.resolveExprType(expr.exprFieldObj) # Auto-dereference pointer types for field access - if objType.kind == tkPointer and objType.inner.len > 0: + if objType.isPointer and objType.inner.len > 0: objType = objType.inner[0] if objType.kind == tkNamed: let sym = ctx.globalScope.lookup(objType.name) @@ -460,7 +450,13 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = var receiverTypeName = "" if receiverType.kind == tkNamed: receiverTypeName = receiverType.name - elif receiverType.kind == tkPointer and receiverType.inner.len > 0 and receiverType.inner[0].kind == tkNamed: + if ctx.typeSubst.hasKey(receiverTypeName): + let substituted = ctx.typeSubst[receiverTypeName] + if substituted.kind == tkNamed: + receiverTypeName = substituted.name + elif substituted.isPointer and substituted.inner.len > 0 and substituted.inner[0].kind == tkNamed: + receiverTypeName = substituted.inner[0].name + elif receiverType.isPointer and receiverType.inner.len > 0 and receiverType.inner[0].kind == tkNamed: receiverTypeName = receiverType.inner[0].name # Look up method for receiver type specifically @@ -477,7 +473,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = var args: seq[HirNode] = @[] let loweredReceiver = ctx.lowerExpr(receiverExpr) # Auto-address if method expects pointer but receiver is value - if minfo.params.len > 0 and minfo.params[0].kind == tkPointer and receiverType.kind != tkPointer: + if minfo.params.len > 0 and minfo.params[0].isPointer and not receiverType.isPointer: args.add(hirUnary(tkAmp, loweredReceiver, makePointer(receiverType), loc)) else: args.add(loweredReceiver) @@ -558,7 +554,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = let objType = ctx.resolveExprType(expr.exprFieldObj) let base = ctx.lowerExpr(expr.exprFieldObj) # Auto-dereference pointer types for field access - if objType.kind == tkPointer: + if objType.isPointer: let arrowPtr = HirNode(kind: hArrowField, arrowFieldBase: base, arrowFieldName: expr.exprFieldName, typ: makePointer(typ), loc: loc) @@ -1010,7 +1006,7 @@ proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: if i > 0: typeSuffix.add("_") if i < typeArgs.len: let argType = ctx.resolveTypeExpr(typeArgs[i]) - subst[tp] = argType + subst[tp.name] = argType typeSuffix.add(argType.toString) else: typeSuffix.add("unknown") @@ -1178,12 +1174,12 @@ proc lowerModule*(module: Module, sema: Sema): HirModule = let targ = inst.typeArgs[j] if targ.kind == tekNamed: case targ.typeName - of "int", "int32": subst[tp] = makeInt() - of "int64": subst[tp] = makeInt64() - of "float64": subst[tp] = makeFloat64() - of "float32": subst[tp] = makeFloat32() - of "bool": subst[tp] = makeBool() - else: subst[tp] = makeNamed(targ.typeName) + of "int", "int32": subst[tp.name] = makeInt() + of "int64": subst[tp.name] = makeInt64() + of "float64": subst[tp.name] = makeFloat64() + of "float32": subst[tp.name] = makeFloat32() + of "bool": subst[tp.name] = makeBool() + else: subst[tp.name] = makeNamed(targ.typeName) # Create specialized declaration var specDecl = Decl( diff --git a/src/manifest.nim b/src/manifest.nim index 9a56b58..fdf840f 100644 --- a/src/manifest.nim +++ b/src/manifest.nim @@ -1,4 +1,4 @@ -import std/[strutils, os, tables] +import std/[strutils, os, tables, sequtils, strformat] type PackageType* = enum @@ -7,57 +7,245 @@ type ptStaticLibrary ptSource + DepKind* = enum + dkVersion ## "1.0" or "*" + dkPath ## { Path = "../Lib" } + dkGit ## { Version = "1.4", Source = "https://..." } + + Dependency* = object + name*: string + case kind*: DepKind + of dkVersion: + versionReq*: string + of dkPath: + path*: string + of dkGit: + gitUrl*: string + gitVersion*: string + Manifest* = object name*: string version*: string pkgType*: PackageType - output*: string ## from [Build] Output - dependencies*: OrderedTableRef[string, string] + output*: string + dependencies*: seq[Dependency] + devDependencies*: seq[Dependency] + buildDependencies*: seq[Dependency] + +# --------------------------------------------------------------------------- +# Extended TOML parser (supports inline tables and arrays) +# --------------------------------------------------------------------------- type TomlValueKind = enum - tvkString, tvkTable + tvkString, tvkTable, tvkArray, tvkInlineTable TomlValue = object case kind*: TomlValueKind of tvkString: strVal*: string of tvkTable: tableVal*: OrderedTableRef[string, TomlValue] + of tvkArray: + arrayVal*: seq[TomlValue] + of tvkInlineTable: + inlineVal*: OrderedTableRef[string, TomlValue] -proc parseSimpleToml(path: string): OrderedTableRef[string, TomlValue] = +proc parseInlineTable(s: string): OrderedTableRef[string, TomlValue] = + result = newOrderedTable[string, TomlValue]() + var content = s.strip() + if content.len >= 2 and content[0] == '{' and content[^1] == '}': + content = content[1 ..< ^1].strip() + var i = 0 + while i < content.len: + # skip whitespace + while i < content.len and content[i] in Whitespace: + inc i + if i >= content.len: break + # parse key + var keyStart = i + while i < content.len and content[i] notin {'=', ' ', '\t'}: + inc i + let key = content[keyStart ..< i].strip() + # skip to = + while i < content.len and content[i] in Whitespace: + inc i + if i >= content.len or content[i] != '=': break + inc i + while i < content.len and content[i] in Whitespace: + inc i + # parse value + var valStart = i + if i < content.len and content[i] == '"': + inc i + while i < content.len and content[i] != '"': + inc i + if i < content.len: inc i + let val = content[valStart + 1 ..< i - 1] + result[key] = TomlValue(kind: tvkString, strVal: val) + elif i < content.len and content[i] == '{': + # nested inline table (skip for now) + var braceCount = 1 + inc i + while i < content.len and braceCount > 0: + if content[i] == '{': inc braceCount + elif content[i] == '}': dec braceCount + inc i + let val = content[valStart ..< i] + result[key] = TomlValue(kind: tvkInlineTable, inlineVal: newOrderedTable[string, TomlValue]()) + else: + while i < content.len and content[i] notin {',', ' ', '\t'}: + inc i + let val = content[valStart ..< i].strip() + result[key] = TomlValue(kind: tvkString, strVal: val) + # skip to comma or end + while i < content.len and content[i] in Whitespace: + inc i + if i < content.len and content[i] == ',': + inc i + +proc parseArray(s: string): seq[TomlValue] = + result = @[] + var content = s.strip() + if content.len >= 2 and content[0] == '[' and content[^1] == ']': + content = content[1 ..< ^1].strip() + if content.len == 0: return + for item in content.split(','): + let val = item.strip() + if val.len >= 2 and val[0] == '"' and val[^1] == '"': + result.add(TomlValue(kind: tvkString, strVal: val[1 ..< ^1])) + else: + result.add(TomlValue(kind: tvkString, strVal: val)) + +proc parseValue(valStr: string): TomlValue = + let val = valStr.strip() + if val.len >= 2 and val[0] == '"' and val[^1] == '"': + return TomlValue(kind: tvkString, strVal: val[1 ..< ^1]) + elif val.len >= 2 and val[0] == '[' and val[^1] == ']': + return TomlValue(kind: tvkArray, arrayVal: parseArray(val)) + elif val.len >= 2 and val[0] == '{' and val[^1] == '}': + return TomlValue(kind: tvkInlineTable, inlineVal: parseInlineTable(val)) + else: + return TomlValue(kind: tvkString, strVal: val) + +proc parseToml*(path: string): OrderedTableRef[string, TomlValue] = result = newOrderedTable[string, TomlValue]() if not fileExists(path): return result let content = readFile(path) var currentTable = "" + var currentArrayTable = "" for rawLine in content.splitLines(): let line = rawLine.strip() if line.len == 0 or line.startsWith("#"): continue - if line.startsWith("[") and line.endsWith("]"): + if line.startsWith("[[") and line.endsWith("]]"): + # Array of tables + currentArrayTable = line[2 ..< ^2] + currentTable = "" + let newTable = TomlValue(kind: tvkTable, tableVal: newOrderedTable[string, TomlValue]()) + if not result.hasKey(currentArrayTable): + result[currentArrayTable] = TomlValue(kind: tvkArray, arrayVal: @[newTable]) + else: + result[currentArrayTable].arrayVal.add(newTable) + elif line.startsWith("[") and line.endsWith("]"): currentTable = line[1 ..< ^1] + currentArrayTable = "" if not result.hasKey(currentTable): result[currentTable] = TomlValue(kind: tvkTable, tableVal: newOrderedTable[string, TomlValue]()) else: let eqIdx = line.find('=') if eqIdx >= 0: let key = line[0 ..< eqIdx].strip() - var val = line[eqIdx + 1 .. ^1].strip() - # Remove surrounding quotes - if val.len >= 2 and val[0] == '"' and val[^1] == '"': - val = val[1 ..< ^1] - if currentTable != "" and result.hasKey(currentTable): - result[currentTable].tableVal[key] = TomlValue(kind: tvkString, strVal: val) + let valStr = line[eqIdx + 1 .. ^1].strip() + let val = parseValue(valStr) + if currentArrayTable != "" and result.hasKey(currentArrayTable): + let arr = result[currentArrayTable].arrayVal + if arr.len > 0: + arr[^1].tableVal[key] = val + elif currentTable != "" and result.hasKey(currentTable): + result[currentTable].tableVal[key] = val else: - result[key] = TomlValue(kind: tvkString, strVal: val) + result[key] = val + +proc parseDepTable(table: OrderedTableRef[string, TomlValue]): seq[Dependency] = + result = @[] + for name, val in table: + case val.kind + of tvkString: + result.add(Dependency(name: name, kind: dkVersion, versionReq: val.strVal)) + of tvkInlineTable: + let t = val.inlineVal + if t.hasKey("Path"): + result.add(Dependency(name: name, kind: dkPath, path: t["Path"].strVal)) + elif t.hasKey("Source"): + var ver = "*" + if t.hasKey("Version"): + ver = t["Version"].strVal + result.add(Dependency(name: name, kind: dkGit, gitUrl: t["Source"].strVal, gitVersion: ver)) + elif t.hasKey("Version"): + result.add(Dependency(name: name, kind: dkVersion, versionReq: t["Version"].strVal)) + else: + result.add(Dependency(name: name, kind: dkVersion, versionReq: "*")) + else: + discard + +# --------------------------------------------------------------------------- +# Lockfile +# --------------------------------------------------------------------------- + +type + LockEntry* = object + name*: string + version*: string + source*: string + checksum*: string + + Lockfile* = object + entries*: seq[LockEntry] + +proc loadLockfile*(path: string): Lockfile = + result.entries = @[] + if not fileExists(path): + return result + let data = parseToml(path) + if data.hasKey("Package"): + let arr = data["Package"] + if arr.kind == tvkArray: + for item in arr.arrayVal: + if item.kind == tvkTable: + let t = item.tableVal + var entry = LockEntry() + if t.hasKey("Name"): entry.name = t["Name"].strVal + if t.hasKey("Version"): entry.version = t["Version"].strVal + if t.hasKey("Source"): entry.source = t["Source"].strVal + if t.hasKey("Checksum"): entry.checksum = t["Checksum"].strVal + result.entries.add(entry) + +proc saveLockfile*(path: string, lock: Lockfile) = + var lines: seq[string] = @[] + for entry in lock.entries: + lines.add("[[Package]]") + lines.add(&"Name = \"{entry.name}\"") + lines.add(&"Version = \"{entry.version}\"") + lines.add(&"Source = \"{entry.source}\"") + if entry.checksum.len > 0: + lines.add(&"Checksum = \"{entry.checksum}\"") + lines.add("") + writeFile(path, lines.join("\n")) + +# --------------------------------------------------------------------------- +# Manifest loader +# --------------------------------------------------------------------------- proc loadManifest*(path: string): Manifest = - let data = parseSimpleToml(path) + let data = parseToml(path) result.name = "" result.version = "0.1.0" result.pkgType = ptExecutable result.output = "Bin" - result.dependencies = newOrderedTable[string, string]() + result.dependencies = @[] + result.devDependencies = @[] + result.buildDependencies = @[] if data.hasKey("Package"): let pkg = data["Package"].tableVal @@ -77,3 +265,10 @@ proc loadManifest*(path: string): Manifest = let bld = data["Build"].tableVal if bld.hasKey("Output"): result.output = bld["Output"].strVal + + if data.hasKey("Dependencies"): + result.dependencies = parseDepTable(data["Dependencies"].tableVal) + if data.hasKey("DevDependencies"): + result.devDependencies = parseDepTable(data["DevDependencies"].tableVal) + if data.hasKey("BuildDependencies"): + result.buildDependencies = parseDepTable(data["BuildDependencies"].tableVal) diff --git a/src/parser.nim b/src/parser.nim index 386cc35..f689da1 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -212,6 +212,9 @@ proc parseBaseType(p: var Parser): TypeExpr = return TypeExpr(kind: tekPointer, loc: loc, pointerPointee: p.parseBaseType()) of tkAmp: discard p.advance() + if p.check(tkMut): + discard p.advance() + return TypeExpr(kind: tekMutRef, loc: loc, pointerPointee: p.parseBaseType()) return TypeExpr(kind: tekRef, loc: loc, pointerPointee: p.parseBaseType()) of tkLParen: discard p.advance() @@ -894,11 +897,27 @@ proc parseStmt(p: var Parser): Stmt = # Declarations # --------------------------------------------------------------------------- -proc parseTypeParams(p: var Parser): seq[string] = +proc parseTypeParams(p: var Parser): seq[TypeParam] = if p.check(tkLt): discard p.advance() while not p.check(tkGt) and not p.isAtEnd: - result.add(p.expect(tkIdent, "expected type parameter name").text) + let name = p.expect(tkIdent, "expected type parameter name").text + var bounds: seq[string] = @[] + if p.check(tkColon): + discard p.advance() + # Parse bound: single identifier or path like Std::Comparable + var boundName = "" + while true: + let part = p.expect(tkIdent, "expected trait/interface name").text + if boundName.len > 0: + boundName.add("_") + boundName.add(part) + if p.check(tkColonColon): + discard p.advance() + else: + break + bounds.add(boundName) + result.add(TypeParam(name: name, bounds: bounds)) if p.check(tkComma): discard p.advance() discard p.expect(tkGt, "expected '>' to close type parameters") @@ -1241,6 +1260,7 @@ proc parseDecl(p: var Parser): Decl = var attrs = ParsedAttrs() if p.check(tkAt): attrs = p.parseAttrs() + p.skipNewlines() var isConst = false if p.check(tkConst) and p.peek(1) == tkFunc: diff --git a/src/sema.nim b/src/sema.nim index 5dd7742..1e83855 100644 --- a/src/sema.nim +++ b/src/sema.nim @@ -20,6 +20,16 @@ type params*: seq[Type] retType*: Type + CtValueKind = enum + ctkVoid, ctkInt, ctkBool, ctkString + + CtValue = object + case kind: CtValueKind + of ctkVoid: discard + of ctkInt: intVal: int64 + of ctkBool: boolVal: bool + of ctkString: strVal: string + Sema* = object module*: Module globalScope*: Scope @@ -30,6 +40,8 @@ type methodTable*: Table[string, seq[MethodInfo]] # Interface name -> interface decl interfaceTable*: Table[string, Decl] + # Borrow checker state + checkedFunc*: bool ## true inside @[Checked] function # --------------------------------------------------------------------------- # Helpers @@ -94,6 +106,16 @@ proc typeToTypeExpr(t: Type): TypeExpr = TypeExpr(kind: tekPointer, pointerPointee: typeToTypeExpr(t.inner[0])) else: TypeExpr(kind: tekNamed, typeName: "void") + of tkRef: + if t.inner.len > 0: + TypeExpr(kind: tekRef, pointerPointee: typeToTypeExpr(t.inner[0])) + else: + TypeExpr(kind: tekNamed, typeName: "void") + of tkMutRef: + if t.inner.len > 0: + TypeExpr(kind: tekMutRef, pointerPointee: typeToTypeExpr(t.inner[0])) + else: + TypeExpr(kind: tekNamed, typeName: "void") of tkVoid: TypeExpr(kind: tekNamed, typeName: "void") else: TypeExpr(kind: tekNamed, typeName: t.toString) @@ -102,7 +124,8 @@ proc inferTypeArgs(sema: var Sema, funcDecl: Decl, argTypes: seq[Type], ## Infer type arguments from argument types for a generic function call. ## Returns empty seq if inference fails for any type parameter. result = @[] - for tpName in funcDecl.declFuncTypeParams: + for tp in funcDecl.declFuncTypeParams: + let tpName = tp.name var inferred: Type = nil for i, param in funcDecl.declFuncParams: if i >= argTypes.len: break @@ -173,9 +196,9 @@ proc resolveType(sema: var Sema, te: TypeExpr): Type = of tekPointer: return makePointer(sema.resolveType(te.pointerPointee)) of tekRef: - return makePointer(sema.resolveType(te.pointerPointee)) # &T → *T in bootstrap + return makeRef(sema.resolveType(te.pointerPointee)) of tekMutRef: - return makePointer(sema.resolveType(te.pointerPointee)) # &mut T → *T in bootstrap + return makeMutRef(sema.resolveType(te.pointerPointee)) of tekSlice: let elemType = sema.resolveType(te.sliceElement) return makeSlice(elemType) @@ -191,6 +214,162 @@ proc resolveType(sema: var Sema, te: TypeExpr): Type = # First pass: collect global symbols # --------------------------------------------------------------------------- + +# --------------------------------------------------------------------------- +# Compile-Time Function Execution (CTFE) +# --------------------------------------------------------------------------- + +proc evalExpr(sema: Sema, expr: Expr, locals: Table[string, CtValue]): CtValue + +proc evalBlock(sema: Sema, blk: Block, locals: Table[string, CtValue]): CtValue = + var localVars = locals + for stmt in blk.stmts: + case stmt.kind + of skLet: + if stmt.stmtLetInit != nil: + let val = sema.evalExpr(stmt.stmtLetInit, localVars) + if val.kind in {ctkInt, ctkBool, ctkString}: + localVars[stmt.stmtLetName] = val + of skIf: + let cond = sema.evalExpr(stmt.stmtIfCond, localVars) + if cond.kind == ctkBool: + if cond.boolVal: + let res = sema.evalBlock(stmt.stmtIfThen, localVars) + if res.kind != ctkVoid: + return res + elif stmt.stmtIfElse != nil: + let res = sema.evalBlock(stmt.stmtIfElse, localVars) + if res.kind != ctkVoid: + return res + # If condition is false and no else, continue to next statement + else: + return CtValue(kind: ctkVoid) + of skReturn: + if stmt.stmtReturnValue != nil: + return sema.evalExpr(stmt.stmtReturnValue, localVars) + return CtValue(kind: ctkVoid) + of skExpr: + let res = sema.evalExpr(stmt.stmtExpr, localVars) + if res.kind != ctkVoid: + return res + else: + discard + return CtValue(kind: ctkVoid) + +proc evalExpr(sema: Sema, expr: Expr, locals: Table[string, CtValue]): CtValue = + if expr == nil: + return CtValue(kind: ctkVoid) + case expr.kind + of ekLiteral: + case expr.exprLit.kind + of tkIntLiteral: + return CtValue(kind: ctkInt, intVal: parseBiggestInt(expr.exprLit.text)) + of tkBoolLiteral: + return CtValue(kind: ctkBool, boolVal: expr.exprLit.text == "true") + of tkStringLiteral: + return CtValue(kind: ctkString, strVal: expr.exprLit.text) + else: + return CtValue(kind: ctkVoid) + of ekIdent: + if locals.hasKey(expr.exprIdent): + return locals[expr.exprIdent] + # Check if it's a const global + let sym = sema.globalScope.lookup(expr.exprIdent) + if sym != nil and sym.decl != nil and sym.decl.kind == dkConst and sym.decl.declConstValue != nil: + return sema.evalExpr(sym.decl.declConstValue, locals) + return CtValue(kind: ctkVoid) + of ekUnary: + let operand = sema.evalExpr(expr.exprUnaryOperand, locals) + case expr.exprUnaryOp + of tkMinus: + if operand.kind == ctkInt: + return CtValue(kind: ctkInt, intVal: -operand.intVal) + of tkBang: + if operand.kind == ctkBool: + return CtValue(kind: ctkBool, boolVal: not operand.boolVal) + else: + discard + return CtValue(kind: ctkVoid) + of ekBinary: + let left = sema.evalExpr(expr.exprBinaryLeft, locals) + let right = sema.evalExpr(expr.exprBinaryRight, locals) + if left.kind == ctkInt and right.kind == ctkInt: + case expr.exprBinaryOp + of tkPlus: return CtValue(kind: ctkInt, intVal: left.intVal + right.intVal) + of tkMinus: return CtValue(kind: ctkInt, intVal: left.intVal - right.intVal) + of tkStar: return CtValue(kind: ctkInt, intVal: left.intVal * right.intVal) + of tkSlash: + if right.intVal != 0: + return CtValue(kind: ctkInt, intVal: left.intVal div right.intVal) + of tkPercent: + if right.intVal != 0: + return CtValue(kind: ctkInt, intVal: left.intVal mod right.intVal) + of tkEq: return CtValue(kind: ctkBool, boolVal: left.intVal == right.intVal) + of tkNe: return CtValue(kind: ctkBool, boolVal: left.intVal != right.intVal) + of tkLt: return CtValue(kind: ctkBool, boolVal: left.intVal < right.intVal) + of tkLe: return CtValue(kind: ctkBool, boolVal: left.intVal <= right.intVal) + of tkGt: return CtValue(kind: ctkBool, boolVal: left.intVal > right.intVal) + of tkGe: return CtValue(kind: ctkBool, boolVal: left.intVal >= right.intVal) + else: discard + elif left.kind == ctkBool and right.kind == ctkBool: + case expr.exprBinaryOp + of tkAmpAmp: return CtValue(kind: ctkBool, boolVal: left.boolVal and right.boolVal) + of tkPipePipe: return CtValue(kind: ctkBool, boolVal: left.boolVal or right.boolVal) + else: discard + return CtValue(kind: ctkVoid) + of ekTernary: + let cond = sema.evalExpr(expr.exprTernaryCond, locals) + if cond.kind == ctkBool: + if cond.boolVal: + return sema.evalExpr(expr.exprTernaryThen, locals) + else: + return sema.evalExpr(expr.exprTernaryElse, locals) + return CtValue(kind: ctkVoid) + of ekCall: + # Try to evaluate const func calls + if expr.exprCallCallee != nil and expr.exprCallCallee.kind == ekIdent: + let funcName = expr.exprCallCallee.exprIdent + let sym = sema.globalScope.lookup(funcName) + if sym != nil and sym.decl != nil and sym.decl.kind == dkFunc and sym.decl.declFuncConst: + # Evaluate arguments + var argVals: seq[CtValue] = @[] + for arg in expr.exprCallArgs: + argVals.add(sema.evalExpr(arg, locals)) + # Build parameter locals + var callLocals = locals + for i, p in sym.decl.declFuncParams: + if i < argVals.len: + callLocals[p.name] = argVals[i] + # Evaluate function body + if sym.decl.declFuncBody != nil: + return sema.evalBlock(sym.decl.declFuncBody, callLocals) + return CtValue(kind: ctkVoid) + of ekBlock: + return sema.evalBlock(expr.exprBlock, locals) + else: + return CtValue(kind: ctkVoid) + +proc constFoldConstDecl(sema: Sema, decl: Decl): bool = + ## Try to evaluate a const declaration at compile time. + ## Returns true if successful and modifies declConstValue to a literal. + if decl.kind != dkConst: return false + let val = sema.evalExpr(decl.declConstValue, initTable[string, CtValue]()) + case val.kind + of ctkInt: + decl.declConstValue = Expr(kind: ekLiteral, loc: decl.loc, + exprLit: Token(kind: tkIntLiteral, text: $val.intVal, loc: decl.loc)) + return true + of ctkBool: + decl.declConstValue = Expr(kind: ekLiteral, loc: decl.loc, + exprLit: Token(kind: tkBoolLiteral, text: $val.boolVal, loc: decl.loc)) + return true + of ctkString: + decl.declConstValue = Expr(kind: ekLiteral, loc: decl.loc, + exprLit: Token(kind: tkStringLiteral, text: val.strVal, loc: decl.loc)) + return true + of ctkVoid: + return false + proc collectGlobals*(sema: var Sema) = for decl in sema.module.items: case decl.kind @@ -200,8 +379,8 @@ proc collectGlobals*(sema: var Sema) = # Temporarily add type parameters to type table for resolution var addedTypeParams: seq[string] = @[] for tp in decl.declFuncTypeParams: - sema.typeTable[tp] = makeTypeParam(tp) - addedTypeParams.add(tp) + sema.typeTable[tp.name] = makeTypeParam(tp.name) + addedTypeParams.add(tp.name) # Build function type from params and return var params: seq[Type] = @[] for p in decl.declFuncParams: @@ -337,8 +516,8 @@ proc collectGlobals*(sema: var Sema) = # If impl has type params, temporarily add them to type table var addedTypeParams: seq[string] = @[] for tp in implTypeParams: - sema.typeTable[tp] = makeTypeParam(tp) - addedTypeParams.add(tp) + sema.typeTable[tp.name] = makeTypeParam(tp.name) + addedTypeParams.add(tp.name) for methodDecl in decl.declImplMethods: if methodDecl.kind == dkFunc: # Propagate impl type params to method for HIR lowering @@ -372,6 +551,10 @@ proc collectGlobals*(sema: var Sema) = sema.typeTable.del(tp) else: discard + # Second pass: evaluate const declarations after all functions are registered + for decl in sema.module.items: + if decl.kind == dkConst: + discard sema.constFoldConstDecl(decl) # --------------------------------------------------------------------------- # Expression type checking @@ -380,6 +563,36 @@ proc collectGlobals*(sema: var Sema) = proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type +proc typeImplements(sema: Sema, t: Type, interfaceName: string): bool = + ## Check if a type implements an interface by verifying all required methods exist. + if t.isUnknown: return true + let typeName = if t.kind == tkNamed: t.name elif t.isPointer and t.inner.len > 0 and t.inner[0].kind == tkNamed: t.inner[0].name else: "" + if typeName == "": return false + if not sema.interfaceTable.hasKey(interfaceName): + return true # Unknown interface — be permissive in bootstrap + let iface = sema.interfaceTable[interfaceName] + let requiredMethods = iface.declInterfaceMethods + if not sema.methodTable.hasKey(typeName): + return false + let availableMethods = sema.methodTable[typeName] + for req in requiredMethods: + var found = false + for avail in availableMethods: + if avail.name == req.declFuncName: + found = true + break + if not found: + return false + return true + +proc checkTraitBounds(sema: var Sema, funcDecl: Decl, inferredTypes: seq[Type], loc: SourceLocation) = + ## Verify that inferred types satisfy their trait bounds. + for i, tp in funcDecl.declFuncTypeParams: + if i < inferredTypes.len and inferredTypes[i] != nil: + for bound in tp.bounds: + if not sema.typeImplements(inferredTypes[i], bound): + sema.emitError(loc, &"type '{inferredTypes[i].toString}' does not implement trait '{bound}'") + proc extractPatternBindings(sema: var Sema, pat: Pattern, scope: Scope) = ## Add pattern-bound identifiers to scope with unknown type (best-effort) if pat == nil: return @@ -461,7 +674,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = return makeUnknown() return operandType.inner[0] of tkAmp: - return makePointer(operandType) + return makeMutRef(operandType) else: return operandType of ekPostfix: @@ -506,6 +719,11 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = let value = sema.checkExpr(expr.exprAssignValue, scope) if not value.isAssignableTo(target): sema.emitError(expr.loc, &"cannot assign {value.toString} to {target.toString}") + # Borrow check: cannot write through &T (shared reference) in @[Checked] functions + if sema.checkedFunc and expr.exprAssignTarget.kind == ekUnary and expr.exprAssignTarget.exprUnaryOp == tkStar: + let ptrType = sema.checkExpr(expr.exprAssignTarget.exprUnaryOperand, scope) + if ptrType.isRef: + sema.emitError(expr.loc, "cannot assign through shared reference '&T' in checked function — use '&mut T' instead") return target of ekTernary: let cond = sema.checkExpr(expr.exprTernaryCond, scope) @@ -542,7 +760,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = if sym2 != nil and sym2.decl != nil and sym2.decl.kind == dkFunc: let typeParams = sym2.decl.declFuncTypeParams for i, tp in typeParams: - if retType.name == tp and i < expr.exprCallCallee.exprGenericTypeArgs.len: + if retType.name == tp.name and i < expr.exprCallCallee.exprGenericTypeArgs.len: # Substitute with concrete type let concreteType = expr.exprCallCallee.exprGenericTypeArgs[i] if concreteType.kind == tekNamed: @@ -622,14 +840,19 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = let inferred = sema.inferTypeArgs(calleeDecl, argTypes, expr.loc) if inferred.len == calleeDecl.declFuncTypeParams.len: expr.exprCallInferredTypeArgs = inferred + # Check trait bounds + var inferredTypes: seq[Type] = @[] + for te in inferred: + inferredTypes.add(sema.resolveType(te)) + sema.checkTraitBounds(calleeDecl, inferredTypes, expr.loc) # Substitute return type using inferred type args if calleeDecl.declFuncReturnType != nil: var added: seq[string] = @[] for i, tp in calleeDecl.declFuncTypeParams: if i < inferred.len: let concrete = sema.resolveType(inferred[i]) - sema.typeTable[tp] = concrete - added.add(tp) + sema.typeTable[tp.name] = concrete + added.add(tp.name) let retType = sema.resolveType(calleeDecl.declFuncReturnType) for tp in added: sema.typeTable.del(tp) @@ -666,8 +889,8 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = of ekField: let obj = sema.checkExpr(expr.exprFieldObj, scope) var objType = obj - # Auto-dereference pointer types for field access - if objType.kind == tkPointer and objType.inner.len > 0: + # Auto-dereference pointer/reference types for field access + if objType.kind in {tkPointer, tkRef, tkMutRef} and objType.inner.len > 0: objType = objType.inner[0] if objType.kind == tkNamed: # Check if this is a _Data union field access @@ -858,7 +1081,6 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type = else: discard return makeVoid() - # --------------------------------------------------------------------------- # Function body checking # --------------------------------------------------------------------------- @@ -870,12 +1092,14 @@ proc checkFunc(sema: var Sema, decl: Decl) = # type parameters that cannot be fully resolved until monomorphization. if decl.declFuncTypeParams.len > 0: return + let wasChecked = sema.checkedFunc + sema.checkedFunc = "Checked" in decl.declAttrs var funcScope = newScope(sema.globalScope) # Add type parameters to type table for resolution var addedTypeParams: seq[string] = @[] for tp in decl.declFuncTypeParams: - sema.typeTable[tp] = makeTypeParam(tp) - addedTypeParams.add(tp) + sema.typeTable[tp.name] = makeTypeParam(tp.name) + addedTypeParams.add(tp.name) # Add parameters for p in decl.declFuncParams: let pType = sema.resolveType(p.ptype) @@ -887,6 +1111,7 @@ proc checkFunc(sema: var Sema, decl: Decl) = # Clean up type parameters for tp in addedTypeParams: sema.typeTable.del(tp) + sema.checkedFunc = wasChecked # --------------------------------------------------------------------------- # Second pass: check all function bodies @@ -898,7 +1123,7 @@ proc checkBodies(sema: var Sema) = var funcCount = 0 for decl in sema.module.items: if decl.kind == dkFunc: inc funcCount - if funcCount > 50: + if funcCount > 5000: # Large module — only check Main for decl in sema.module.items: case decl.kind diff --git a/src/token.nim b/src/token.nim index 4207343..52a4af8 100644 --- a/src/token.nim +++ b/src/token.nim @@ -50,6 +50,7 @@ type tkSuper # super tkSizeOf # sizeof tkOwn # own (gradual ownership transfer) + tkMut # mut (mutable reference) tkDiscard # discard (evaluate and throw away) ##Punctuation @@ -142,7 +143,7 @@ proc isKeyword*(kind: TokenKind): bool = tkBreak, tkContinue, tkReturn, tkMatch, tkFunc, tkLet, tkVar, tkConst, tkType, tkStruct, tkEnum, tkUnion, tkInterface, tkExtend, tkModule, tkImport, - tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkDiscard: + tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkMut, tkDiscard: true else: false @@ -197,6 +198,7 @@ proc keywordKind*(text: string): TokenKind = of "super": tkSuper of "sizeof": tkSizeOf of "own": tkOwn + of "mut": tkMut of "discard": tkDiscard of "true", "false": tkBoolLiteral else: tkIdent @@ -242,6 +244,7 @@ proc tokenKindName*(kind: TokenKind): string = of tkSelf: "'self'" of tkSuper: "'super'" of tkOwn: "'own'" + of tkMut: "'mut'" of tkDiscard: "'discard'" of tkLParen: "'('" of tkRParen: "')'" diff --git a/src/types.nim b/src/types.nim index 8947aee..488ef64 100644 --- a/src/types.nim +++ b/src/types.nim @@ -25,6 +25,8 @@ type tkFloat32 tkFloat64 tkPointer + tkRef + tkMutRef tkSlice tkRange tkTuple @@ -63,6 +65,10 @@ proc makeFloat64*(): Type = Type(kind: tkFloat64) proc makePointer*(pointee: Type): Type = Type(kind: tkPointer, inner: @[pointee]) +proc makeRef*(pointee: Type): Type = + Type(kind: tkRef, inner: @[pointee]) +proc makeMutRef*(pointee: Type): Type = + Type(kind: tkMutRef, inner: @[pointee]) proc makeSlice*(element: Type): Type = Type(kind: tkSlice, inner: @[element]) proc makeRange*(element: Type): Type = @@ -95,7 +101,10 @@ proc isFloat*(t: Type): bool = proc isSigned*(t: Type): bool = if t.kind in {tkUnknown, tkNamed, tkTypeParam}: return true t.kind in {tkInt8, tkInt16, tkInt32, tkInt64, tkInt} -proc isPointer*(t: Type): bool = t.kind == tkPointer +proc isPointer*(t: Type): bool = t.kind in {tkPointer, tkRef, tkMutRef} +proc isRawPointer*(t: Type): bool = t.kind == tkPointer +proc isRef*(t: Type): bool = t.kind == tkRef +proc isMutRef*(t: Type): bool = t.kind == tkMutRef proc isSlice*(t: Type): bool = t.kind == tkSlice # Comparison @@ -139,6 +148,18 @@ proc isAssignableTo*(a, b: Type): bool = return true if b.inner.len > 0 and b.inner[0].isUnknown: return true + # &mut T -> &T (mutable ref can coerce to shared ref) + if a.isMutRef and b.isRef: + if a.inner.len > 0 and b.inner.len > 0 and a.inner[0].isAssignableTo(b.inner[0]): + return true + # &mut T -> *T (mutable ref can coerce to raw pointer) + if a.isMutRef and b.isRawPointer: + if a.inner.len > 0 and b.inner.len > 0 and a.inner[0].isAssignableTo(b.inner[0]): + return true + # &T -> *T (shared ref can coerce to raw pointer) + if a.isRef and b.isRawPointer: + if a.inner.len > 0 and b.inner.len > 0 and a.inner[0].isAssignableTo(b.inner[0]): + return true return false # String representation @@ -167,6 +188,8 @@ proc toString*(t: Type): string = of tkFloat32: "float32" of tkFloat64: "float64" of tkPointer: "*" & t.inner[0].toString + of tkRef: "&" & t.inner[0].toString + of tkMutRef: "&mut " & t.inner[0].toString of tkSlice: if t.inner.len > 0: t.inner[0].toString & "[]" else: "Slice" diff --git a/tests/hir_test b/tests/hir_test deleted file mode 100755 index 38721e0..0000000 Binary files a/tests/hir_test and /dev/null differ diff --git a/tests/parser_test b/tests/parser_test deleted file mode 100755 index ace811a..0000000 Binary files a/tests/parser_test and /dev/null differ diff --git a/tests/sema_test b/tests/sema_test deleted file mode 100755 index 74dbd82..0000000 Binary files a/tests/sema_test and /dev/null differ