feat: multi-instance closures, richer stdlib, and Rust-style diagnostics

Introduce fat function pointers (BuxFn {code, env}) so capturing closures
are heap-allocated per value in both bootstrap and selfhost. Expand
Array/Map/Set/String/Test/Result APIs, add proper tuple codegen and
error snippets with multi-char underlines, golden diagnostic tests, and
LSP diagnostics via buxc check.
This commit is contained in:
2026-07-15 16:00:21 +03:00
parent 94e6806dda
commit 61ac06ab5f
48 changed files with 2789 additions and 362 deletions
+42 -2
View File
@@ -105,10 +105,50 @@ f"Hello, {name}" // Interpolated string — expressions inside {}
own T // Owned value (move semantics)
T[] // Slice (unsized)
T[N] // Fixed-size array
(T1, T2, T3) // Tuple
func(T1) -> T2 // Function type
(T1, T2, T3) // Tuple — access fields with .0, .1, .2
func(T1) -> T2 // Function pointer type
```
### Tuples
```bux
func Pair(a: int, b: int) -> (int, int) {
return (a, b);
}
func Main() -> int {
let t: (int, int) = Pair(10, 20);
PrintInt(t.0); // 10
PrintInt(t.1); // 20
return 0;
}
```
### Function pointers and closures
```bux
func Apply(f: func(int) -> int, x: int) -> int {
return f(x);
}
func Double(n: int) -> int { return n * 2; }
func MakeAdder(base: int) -> func(int) -> int {
// Each call allocates its own capture environment
return |a: int| -> int { return a + base; };
}
func Main() -> int {
let g: func(int) -> int = Double; // named func → fat pointer
let a10 = MakeAdder(10);
let a20 = MakeAdder(20);
// a10 and a20 are independent instances
return Apply(g, 21) + a10(1) + a20(1); // 42 + 11 + 21
}
```
`func(T) -> R` values are **fat pointers** `{ code, env }`:
- capturing closures store captures in a heap env
- capture-less closures and named functions use `env = null`
### Structs
```bux
struct Point {
+163
View File
@@ -0,0 +1,163 @@
# Bux — План към „добър“ език (v0.5 → v1.0)
> **Дата:** 2026-07-15
> **Текущо:** v0.5.0 — selfhost loop, gradual ownership, green threads, 26+ examples ✅
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
---
## Диагноза (къде сме)
| Слой | Състояние | Оценка |
|------|-----------|--------|
| Frontend (lex/parse) | Пълен Pratt parser, recovery | ★★★★☆ |
| Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ |
| HIR → C | Работи; tuples/func-ptr half-baked в bootstrap | ★★★☆☆ |
| Selfhost (`src/`) | ~12k LOC, binary-identical loop | ★★★★★ |
| Gradual ownership | `@[Checked]`, `&`/`&mut`, move, Drop | ★★★☆☆ (basic) |
| Concurrency | M:N tasks + channels + async | ★★★★☆ |
| Stdlib | 25+ модула, но колекциите са минимални | ★★★☆☆ |
| Tooling | `new/build/run/test/fmt`, LSP prototype, VSCode | ★★☆☆☆ |
| Ecosystem / registry | path+git deps; няма централен registry | ★☆☆☆☆ |
| Документация | Има, но drift (PLAN vs README версии) | ★★★☆☆ |
**Силна ниша:** gradual ownership (C-скорост на писане + opt-in Rust-safety).
**Слабо място:** ergonomics на stdlib + maturity на tooling + пълнота на borrow checker.
---
## Какво значи „добър“ за Bux
1. **Ежедневен DX** — колекции, string, assert, грешки, които разбираш за секунди.
2. **Предвидима безопасност**`@[Checked]` да хваща 80% от UAF/double-borrow без lifetime hell.
3. **Selfhost като dogfood** — компилаторът и apps (`nexus`, `boko`) са proof.
4. **Инструменти** — fmt, test, LSP, package install без ръчна магия.
5. **Стабилна спецификация** — LanguageRef = реалното поведение.
Не целим „по-добър Rust“. Целим **единствения език с gradual safety + Go-стил concurrency без GC**.
---
## Фази
### A — Ergonomics & Stdlib (P0, сега) 🔄
| # | Задача | Защо | Статус |
|---|--------|------|--------|
| A.1 | Array: Pop, Clear, IsEmpty, First, Last, Cap, Reserve | Без това колекциите са неудобни | ✅ (тази сесия) |
| A.2 | String: IsEmpty, ReplaceAll | Чести операции; само first-replace досега | ✅ (тази сесия) |
| A.3 | Os_Exit + Test_AssertEqString / richer asserts | Тестове и CLI без raw `bux_exit` | ✅ (тази сесия) |
| A.4 | Map_Remove / Set polish | Completeness на колекциите | ✅ (тази сесия) |
| A.5 | Iter: map/filter/fold върху closures | Higher-order без boilerplate | ⏳ |
| A.6 | Result helpers: Expect, UnwrapErr, Or | По-малко match boilerplate | ✅ (тази сесия) |
### B — Compiler Correctness (P0)
| # | Задача | Защо | Статус |
|---|--------|------|--------|
| B.1 | Proper tuple types в C backend | `(T,U)``Tuple_T_U` struct + `.0`/`.1` | ✅ |
| B.2 | Function pointer types | `func(T)->U` вече работи в LIR backend | ✅ |
| B.3 | Match expression до край в C (не `return "0"`) | Expression-context match |
| B.4 | Closures: multi-instance + loop/return в body | Реални higher-order callbacks |
| B.5 | По-добри diagnostics (snippet + hint) | DX #1 за нови потребители | ✅ |
| B.6 | Bootstrap ↔ selfhost feature parity | Operator overloading, string interp и в selfhost |
### C — Gradual Ownership 2.0 (P1)
| # | Задача | Защо |
|---|--------|------|
| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата |
| C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives |
| C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден |
| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path |
### D — Tooling (P1)
| # | Задача | Защо |
|---|--------|------|
| D.1 | LSP: hover, go-to-def, diagnostics (wire към sema) | IDE = adoption |
| D.2 | `bux fmt` стабилен + CI check | Единен style |
| D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly |
| D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib |
| D.5 | Golden tests за stdlib modules | Регресии без изненади |
### E — Ecosystem & v1.0 (P2)
| # | Задача | Защо |
|---|--------|------|
| E.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks |
| E.2 | 35 production-quality apps в `apps/` | Showcase |
| E.3 | Language freeze + semver policy | Trust |
| E.4 | Debugger/DWARF basics | Systems audience |
| E.5 | Benchmarks vs C/Zig/Nim (micro + nexus) | Marketing + regression |
---
## Препоръчан ред на работа
```
A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
↓ ↓
D (tooling) ←────────── dogfood apps
E (v1.0 ecosystem)
```
**Правило:** всяка сесия ship-ва нещо runnable (stdlib API, fix, example), не само docs.
---
## Acceptance criteria за „добър v1.0“
- [ ] Всички examples + selfhost-loop + 3 apps минават на CI
- [ ] Array/Map/String/Test API покрива 90% от ежедневните нужди
- [ ] `@[Checked]` хваща use-after-move + double `&mut` в documented subset
- [ ] `bux test` + `bux fmt` + `bux check` са default developer loop
- [ ] LanguageRef синхронизиран с компилатора
- [ ] Поне един външен проект (не в monorepo) build-ва с git dep
---
## Сесия 1 (stdlib ergonomics)
1. `Array_Pop`, `Array_Clear`, `Array_IsEmpty`, `Array_First`, `Array_Last`, `Array_Cap`, `Array_Reserve`
2. `String_IsEmpty`, `String_ReplaceAll`
3. `Os_Exit`
4. `Test_AssertEqString`, `Test_AssertNeqInt`, `Test_AssertEqBool`
5. Example + docs update
## Сесия 2 (collections + tuples)
1. `Map_Remove` / `Map_Clear` / `Map_IsEmpty` (+ StringMap)
2. `Set_Remove` / `Set_Clear` / `Set_IsEmpty`
3. `Result_Expect` / `Result_UnwrapErr` / `Result_Or`
4. `Option_Expect` / `Option_Or`
5. **Tuples:** `(T, U)``typedef struct { T _0; U _1; } Tuple_T_U` + field access `.0`/`.1`
6. Examples: `tuples`, `func_ptr`, `map_remove`
## Сесия 3 (diagnostics + collections)
1. **Rust-style errors** in bootstrap CLI: `--> file:line:col`, source snippet, `^` caret, `= help:` hints
2. `SourceLocation.file` propagated from lexer
3. Better caret for type-mismatch on `let` (points at initializer)
4. `Array_Contains` / `Array_IndexOf` / `Array_Extend`
5. `Iter_AnyEq` / `Iter_AllEq` / `Iter_Collect`
6. Selfhost `Diagnostic_Hint` for common messages
## Сесия 4 (diagnostics depth + LSP + strings)
1. **Multi-char underlines** (`^^^^^^^` under tokens/strings/idents)
2. Quoted-name highlighting for `undeclared identifier 'x'`
3. **Golden error tests** (`tests/error_golden/`, `make test-errors`)
4. **LSP** runs `buxc check` and publishes real diagnostics
5. `String_IsBlank` / `String_Repeat`
## Сесия 5 (multi-instance closures)
1. **Fat function pointers** for all `func(...)` types: `BuxFn { code(env, args...), env }`
2. Capturing closures: heap-allocate env per creation site (independent instances)
3. Capture-less closures + named funcs: adapters with `env = NULL`
4. Calls through func values: `f.code(f.env, args...)`
5. Example `multi_closure.bux` — MakeAdder(10)/MakeAdder(20) yield 11 and 21
6. **Selfhost parity:** same fat ABI in `src/hir_lower.bux` + `src/c_backend.bux` (makers, adapters)
```
+3 -1
View File
@@ -153,7 +153,9 @@ Array_Filter(nums, |x| { return x > 10; });
4. In thunk body: rewrite captured identifiers to `env_instance.x` via `hFieldAccess`.
5. C backend: emit env struct definition + global instance before thunk function.
**Limitations:** One global instance per closure AST node (no multiple instances). No loop/return support in closures yet.
**Status:** Multi-instance capturing closures work in **both** bootstrap and selfhost via fat
function pointers (`BuxFn { code, env }` + heap-allocated env per creation). Capture-less
closures and named functions use the same ABI (`env = NULL`, adapters for named funcs).
**Complexity:** High — touches parser, sema, type system, HIR/LIR backend.
+50 -1
View File
@@ -81,8 +81,19 @@ struct Array<T> {
|----------|-----------|-------------|
| `Array_New<T>` | `func Array_New<T>(cap: uint) -> Array<T>` | Create new array |
| `Array_Push<T>` | `func Array_Push<T>(arr: *Array<T>, value: T)` | Append element |
| `Array_Pop<T>` | `func Array_Pop<T>(arr: *Array<T>) -> T` | Remove and return last element |
| `Array_Contains<T>` | `func Array_Contains<T>(arr: *Array<T>, value: T) -> bool` | Linear search for value |
| `Array_IndexOf<T>` | `func Array_IndexOf<T>(arr: *Array<T>, value: T) -> int` | First index or -1 |
| `Array_Extend<T>` | `func Array_Extend<T>(arr: *Array<T>, other: *Array<T>)` | Append all from other |
| `Array_Get<T>` | `func Array_Get<T>(arr: *Array<T>, index: uint) -> T` | Get element at index |
| `Array_Set<T>` | `func Array_Set<T>(arr: *Array<T>, index: uint, value: T)` | Set element at index |
| `Array_First<T>` | `func Array_First<T>(arr: *Array<T>) -> T` | First element (bounds-checked) |
| `Array_Last<T>` | `func Array_Last<T>(arr: *Array<T>) -> T` | Last element (bounds-checked) |
| `Array_Len<T>` | `func Array_Len<T>(arr: *Array<T>) -> uint` | Get length |
| `Array_Cap<T>` | `func Array_Cap<T>(arr: *Array<T>) -> uint` | Get capacity |
| `Array_IsEmpty<T>` | `func Array_IsEmpty<T>(arr: *Array<T>) -> bool` | True if length is 0 |
| `Array_Clear<T>` | `func Array_Clear<T>(arr: *Array<T>)` | Set length to 0 (keeps capacity) |
| `Array_Reserve<T>` | `func Array_Reserve<T>(arr: *Array<T>, minCap: uint)` | Grow capacity if needed |
| `Array_Free<T>` | `func Array_Free<T>(arr: *Array<T>)` | Free memory |
### Example
@@ -128,6 +139,9 @@ struct Iter<T> {
| `Iter_Count<T>` | `func Iter_Count<T>(it: *Iter<T>) -> uint` | Count remaining elements |
| `Iter_Skip<T>` | `func Iter_Skip<T>(it: *Iter<T>, n: uint)` | Skip N elements |
| `Iter_Take<T>` | `func Iter_Take<T>(it: *Iter<T>, n: uint) -> Iter<T>` | Take first N elements as new iterator |
| `Iter_AnyEq<T>` | `func Iter_AnyEq<T>(it: *Iter<T>, value: T) -> bool` | True if any remaining element equals value |
| `Iter_AllEq<T>` | `func Iter_AllEq<T>(it: *Iter<T>, value: T) -> bool` | True if all remaining equal value |
| `Iter_Collect<T>` | `func Iter_Collect<T>(it: *Iter<T>) -> Array<T>` | Collect remaining into a new Array |
### Example
```bux
@@ -200,8 +214,12 @@ String manipulation utilities.
| Function | Signature | Description |
|----------|-----------|-------------|
| `String_IsEmpty` | `func String_IsEmpty(s: String) -> bool` | True if length is 0 |
| `String_IsBlank` | `func String_IsBlank(s: String) -> bool` | True if empty or only whitespace |
| `String_Repeat` | `func String_Repeat(s: String, count: uint) -> String` | Repeat string N times |
| `String_Find` | `func String_Find(haystack: String, needle: String) -> String` | Find substring (returns pointer; 0 = not found) |
| `String_Replace` | `func String_Replace(s: String, old: String, new: String) -> String` | Replace first occurrence |
| `String_ReplaceAll` | `func String_ReplaceAll(s: String, old: String, new: String) -> String` | Replace all non-overlapping occurrences |
| `String_Format1` | `func String_Format1(pattern: String, a0: String) -> String` | Format with 1 arg (`{0}`) |
| `String_Format2` | `func String_Format2(pattern: String, a0: String, a1: String) -> String` | Format with 2 args |
| `String_Format3` | `func String_Format3(pattern: String, a0: String, a1: String, a2: String) -> String` | Format with 3 args |
@@ -322,6 +340,9 @@ struct Set<T> {
| `Set_New<T>` | `func Set_New<T>(cap: uint) -> Set<T>` | Create set |
| `Set_Add<T>` | `func Set_Add<T>(s: *Set<T>, value: T)` | Insert element (ignores duplicates) |
| `Set_Has<T>` | `func Set_Has<T>(s: *Set<T>, value: T) -> bool` | Check membership |
| `Set_Remove<T>` | `func Set_Remove<T>(s: *Set<T>, value: T) -> bool` | Remove value |
| `Set_Clear<T>` | `func Set_Clear<T>(s: *Set<T>)` | Clear all elements |
| `Set_IsEmpty<T>` | `func Set_IsEmpty<T>(s: *Set<T>) -> bool` | True if empty |
| `Set_Len<T>` | `func Set_Len<T>(s: *Set<T>) -> uint` | Element count |
| `Set_Free<T>` | `func Set_Free<T>(s: *Set<T>)` | Free memory |
@@ -371,6 +392,9 @@ struct Map<K, V> {
| `Map_Set<K,V>` | `func Map_Set<K,V>(m: *Map<K,V>, key: K, value: V)` | Insert/update |
| `Map_Get<K,V>` | `func Map_Get<K,V>(m: *Map<K,V>, key: K) -> V` | Get value (zero if missing) |
| `Map_Has<K,V>` | `func Map_Has<K,V>(m: *Map<K,V>, key: K) -> bool` | Check key exists |
| `Map_Remove<K,V>` | `func Map_Remove<K,V>(m: *Map<K,V>, key: K) -> bool` | Remove key (true if present) |
| `Map_Clear<K,V>` | `func Map_Clear<K,V>(m: *Map<K,V>)` | Remove all entries (keeps capacity) |
| `Map_IsEmpty<K,V>` | `func Map_IsEmpty<K,V>(m: *Map<K,V>) -> bool` | True if no entries |
| `Map_Len<K,V>` | `func Map_Len<K,V>(m: *Map<K,V>) -> uint` | Entry count |
| `Map_Free<K,V>` | `func Map_Free<K,V>(m: *Map<K,V>)` | Free memory |
@@ -419,6 +443,9 @@ struct StringMap<V> {
| `StringMap_Set<V>` | `func StringMap_Set<V>(m: *StringMap<V>, key: String, value: V)` | Insert/update |
| `StringMap_Get<V>` | `func StringMap_Get<V>(m: *StringMap<V>, key: String) -> V` | Get value |
| `StringMap_Has<V>` | `func StringMap_Has<V>(m: *StringMap<V>, key: String) -> bool` | Check key exists |
| `StringMap_Remove<V>` | `func StringMap_Remove<V>(m: *StringMap<V>, key: String) -> bool` | Remove key |
| `StringMap_Clear<V>` | `func StringMap_Clear<V>(m: *StringMap<V>)` | Clear all entries |
| `StringMap_IsEmpty<V>` | `func StringMap_IsEmpty<V>(m: *StringMap<V>) -> bool` | True if empty |
| `StringMap_Len<V>` | `func StringMap_Len<V>(m: *StringMap<V>) -> uint` | Entry count |
| `StringMap_Free<V>` | `func StringMap_Free<V>(m: *StringMap<V>)` | Free memory |
@@ -914,7 +941,7 @@ func Main() -> int {
Operating system interface.
```bux
import Std::Os::{Os_ArgsCount, Os_Args, Os_GetEnv, Os_SetEnv, Os_GetCwd, Os_Chdir};
import Std::Os::{Os_ArgsCount, Os_Args, Os_GetEnv, Os_SetEnv, Os_GetCwd, Os_Chdir, Os_Exit};
```
| Function | Signature | Description |
@@ -925,6 +952,28 @@ import Std::Os::{Os_ArgsCount, Os_Args, Os_GetEnv, Os_SetEnv, Os_GetCwd, Os_Chdi
| `Os_SetEnv` | `func Os_SetEnv(name: String, value: String) -> bool` | Set environment variable |
| `Os_GetCwd` | `func Os_GetCwd() -> String` | Get current working directory |
| `Os_Chdir` | `func Os_Chdir(path: String) -> bool` | Change directory |
| `Os_Exit` | `func Os_Exit(code: int)` | Terminate process with exit code |
---
## Std::Test
Lightweight assertions for `bux test` and example programs.
```bux
import Std::Test::*;
```
| Function | Signature | Description |
|----------|-----------|-------------|
| `Test_Assert` | `func Test_Assert(cond: bool)` | Panic if false |
| `Test_AssertTrue` / `Test_AssertFalse` | `func ...(cond: bool)` | Boolean asserts |
| `Test_AssertEqInt` | `func Test_AssertEqInt(a: int, b: int)` | Integer equality |
| `Test_AssertNeqInt` | `func Test_AssertNeqInt(a: int, b: int)` | Integer inequality |
| `Test_AssertEqString` | `func Test_AssertEqString(a: String, b: String)` | String equality |
| `Test_AssertEqBool` | `func Test_AssertEqBool(a: bool, b: bool)` | Boolean equality |
| `Test_Fail` / `Test_Pass` | `func ...(msg: String)` | Explicit fail / log pass |
| `Test_Exit` | `func Test_Exit(code: int)` | Exit with code |
---