Compare commits
125 Commits
0bdef58182
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 94e6806dda | |||
| 9c632ce389 | |||
| aaeb01e518 | |||
| fc0a560e60 | |||
| e11dce56d2 | |||
| e5e490605a | |||
| 2c8223f94e | |||
| 44bac83471 | |||
| 06db4926a7 | |||
| 84df4bba9d | |||
| a668127721 | |||
| 9bbeb4fd1d | |||
| 0e93ea911c | |||
| 290cbc8f98 | |||
| 224542df7f | |||
| a619247f06 | |||
| a4f25bc737 | |||
| aa2d6f9632 | |||
| b549e09a3b | |||
| 2259567ac2 | |||
| f4f09602c6 | |||
| ef4aa42b0b | |||
| 8f68a9da1a | |||
| e4e4e0d8ef | |||
| 18aa232b1c | |||
| c6bae8b9ee | |||
| aef0cd7936 | |||
| 62d92b0aa6 | |||
| 9a63dcca11 | |||
| 10a2b64ac4 | |||
| 92962fa1b0 | |||
| b971775baf | |||
| 85d15cd16d | |||
| de3ac76581 | |||
| 2824d25369 | |||
| 9cd0580832 | |||
| 9e547f123a | |||
| 211a811f32 | |||
| 4456bb00af | |||
| 6a0a0425dd | |||
| 9e515c157d | |||
| 8f8708c5df | |||
| 7392013301 | |||
| 0b887b92d3 | |||
| 7d6cf7f511 | |||
| 0a41ce85f4 | |||
| d6f0a30948 | |||
| a2236de69a | |||
| a4e410fb30 | |||
| 51bd3c172b | |||
| 3a7b4bd5c1 | |||
| a099999e02 | |||
| 5eec4328f3 | |||
| c7d4c87fcf | |||
| d70ccba5ca | |||
| 01677001c3 | |||
| bb84693acb | |||
| f63cbd1bf0 | |||
| 499b389ba8 | |||
| 632fa1535c | |||
| 40bcbeb710 | |||
| b36df0f5b7 | |||
| b3141dbcd5 | |||
| 74db8a5790 | |||
| 6414ef236b | |||
| 9b473c667c | |||
| ba54aa240e | |||
| c83f6d5994 | |||
| 34504d1647 | |||
| 079b76ae71 | |||
| 3ce7ba51d2 | |||
| 41c0fb1d6f | |||
| f809827bea | |||
| 81281cbb11 | |||
| c14578d9dd | |||
| 0ac254facc | |||
| 392dd64f97 | |||
| 90e67079f4 | |||
| 265bda850d | |||
| 3ed5891de2 | |||
| 64bd41067a | |||
| 4a860095fd | |||
| 0c41c7bb25 | |||
| 2e536488e6 | |||
| 3f0a3901ca | |||
| 17c896c4dc | |||
| cfce2a93e5 | |||
| d9aceeac6e | |||
| a67271b08c | |||
| cf92b63ba6 | |||
| 9c3d6dc723 | |||
| 7d1d722cba | |||
| 3c7938163c | |||
| c4e3f80405 | |||
| d253a01aa9 | |||
| 2cb971a483 | |||
| b1deff7fd5 | |||
| 38bc469be7 | |||
| bebc362ae6 | |||
| a7a1b45588 | |||
| a13cee8a0f | |||
| 3b7fc455b0 | |||
| e7e900973f | |||
| a2617e9954 | |||
| 3ffa9f7d54 | |||
| b3af1208e8 | |||
| 0d7a30835f | |||
| adca52b269 | |||
| 04be9eb400 | |||
| bfe87a8acb | |||
| 46814b2abc | |||
| 9758b17b1a | |||
| 8347599d23 | |||
| 4794335673 | |||
| 134e8e877d | |||
| 88af8b09d4 | |||
| e41a8d31df | |||
| a11071ab95 | |||
| 093e08a2b9 | |||
| 1500575982 | |||
| fe048eb4aa | |||
| 9dd964dc48 | |||
| e1038059c2 | |||
| 1f0baefa5a | |||
| 3b4456f01f |
+10
@@ -11,6 +11,7 @@ tests/lexer_test
|
||||
tests/parser_test
|
||||
tests/sema_test
|
||||
tests/hir_test
|
||||
tests/borrow_test
|
||||
tests/test_generics_parse
|
||||
tests/*.exe
|
||||
|
||||
@@ -22,3 +23,12 @@ build/
|
||||
_test_tmp_pkg/
|
||||
test_pkg/
|
||||
examples_pkg/
|
||||
|
||||
# Temporary test directories
|
||||
_test_*/
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Dimitar Gigov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -3,9 +3,9 @@ SRC := bootstrap/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 ownership ctfe async concurrency os_time process json iter
|
||||
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 async concurrency os_time process json iter trait_bounds channel sync jwt
|
||||
|
||||
.PHONY: all build dev debug test clean clean-all test-examples selfhost
|
||||
.PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden selfhost-loop lsp
|
||||
|
||||
all: build
|
||||
|
||||
@@ -62,7 +62,8 @@ clean:
|
||||
rm -rf _test_cast _test_cast2 _test_cast3 _test_channel
|
||||
|
||||
clean-all: clean
|
||||
rm -rf build/selfhost
|
||||
rm -rf build/selfhost build/selfhost-loop-a build/selfhost-loop-b
|
||||
rm -rf tests/golden/*/build
|
||||
|
||||
selfhost: build
|
||||
@echo "=== Building self-hosted compiler ==="
|
||||
@@ -74,3 +75,68 @@ selfhost: build
|
||||
@cd build/selfhost && ../../$(OUT) build
|
||||
# strip removed for debug
|
||||
@echo "=== Self-hosted compiler built successfully ==="
|
||||
|
||||
.PHONY: test-golden
|
||||
|
||||
GOLDEN_TESTS := hello fibonacci structs generics algebraic_enums enums methods strings modern_features
|
||||
|
||||
test-golden: build
|
||||
@echo "=== Golden tests ==="
|
||||
@passed=0; failed=0; \
|
||||
for test in $(GOLDEN_TESTS); do \
|
||||
gd="tests/golden/$$test"; \
|
||||
if [ ! -d "$$gd" ]; then echo " SKIP $$test (no dir)"; continue; fi; \
|
||||
if [ ! -f "$$gd/expected.c" ]; then echo " SKIP $$test (no expected.c)"; continue; fi; \
|
||||
rm -rf "$$gd/build"; \
|
||||
(cd "$$gd" && ../../../$(OUT) build > /dev/null 2>&1); \
|
||||
if diff "$$gd/build/main.c" "$$gd/expected.c" > /dev/null 2>&1; then \
|
||||
echo " PASS $$test"; \
|
||||
passed=$$((passed + 1)); \
|
||||
else \
|
||||
echo " FAIL $$test — C output differs from expected"; \
|
||||
failed=$$((failed + 1)); \
|
||||
fi; \
|
||||
done; \
|
||||
echo "Golden tests: $$passed passed, $$failed failed"; \
|
||||
if [ $$failed -gt 0 ]; then exit 1; fi
|
||||
|
||||
selfhost-loop: build
|
||||
@echo "=== Selfhost loop: bootstrap determinism check ==="
|
||||
@echo "Build A..."
|
||||
@rm -rf build/selfhost-loop-a
|
||||
@mkdir -p build/selfhost-loop-a/src
|
||||
@cp src/*.bux build/selfhost-loop-a/src/
|
||||
@cp src/bux.toml build/selfhost-loop-a/
|
||||
@mv build/selfhost-loop-a/src/main.bux build/selfhost-loop-a/src/Main.bux 2>/dev/null || true
|
||||
@cd build/selfhost-loop-a && ../../$(OUT) build
|
||||
@echo "Build B..."
|
||||
@rm -rf build/selfhost-loop-b
|
||||
@mkdir -p build/selfhost-loop-b/src
|
||||
@cp src/*.bux build/selfhost-loop-b/src/
|
||||
@cp src/bux.toml build/selfhost-loop-b/
|
||||
@mv build/selfhost-loop-b/src/main.bux build/selfhost-loop-b/src/Main.bux 2>/dev/null || true
|
||||
@cd build/selfhost-loop-b && ../../$(OUT) build
|
||||
@echo ""
|
||||
@echo "Comparing C output..."
|
||||
@if diff build/selfhost-loop-a/build/main.c build/selfhost-loop-b/build/main.c > /dev/null 2>&1; then \
|
||||
echo " C output: IDENTICAL ✓"; \
|
||||
else \
|
||||
echo " C output: DIFFERENT ✗"; \
|
||||
fi
|
||||
@echo "Comparing ELF binaries (stripped)..."
|
||||
@cp build/selfhost-loop-a/build/buxc2 /tmp/buxc2_a && strip -d /tmp/buxc2_a
|
||||
@cp build/selfhost-loop-b/build/buxc2 /tmp/buxc2_b && strip -d /tmp/buxc2_b
|
||||
@if diff /tmp/buxc2_a /tmp/buxc2_b > /dev/null 2>&1; then \
|
||||
echo " ELF binary: IDENTICAL ✓"; \
|
||||
echo "=== Selfhost loop PASSED ==="; \
|
||||
else \
|
||||
echo " ELF binary: DIFFERENT ✗"; \
|
||||
echo "=== Selfhost loop FAILED ==="; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
lsp: tools/bux-lsp
|
||||
@echo "LSP server ready at tools/bux-lsp"
|
||||
|
||||
tools/bux-lsp: tools/lsp_server.nim
|
||||
cd tools && $(NIM) c -o:bux-lsp lsp_server.nim
|
||||
|
||||
@@ -520,6 +520,34 @@ const TABLE_SIZE = Factorial(10); // Computed at compile time
|
||||
| `8.6.2` Procedural macros | `#[derive(Clone)]`, `#[derive(Debug)]` |
|
||||
| `8.6.3` Reflection | Compile-time type introspection for serialization |
|
||||
|
||||
### 8.7 — Optimization & Release Mode
|
||||
|
||||
**Goal:** C-speed for hot paths. `@[Release]` disables all runtime checks and inlines auto-drop.
|
||||
|
||||
| Task | Status | Details |
|
||||
|------|--------|---------|
|
||||
| `8.7.1` `@[Release]` attribute | ⏳ | Disables bounds checking, borrow checking, null checks inside the function |
|
||||
| `8.7.2` Auto-drop inline | ⏳ | Emit `free`/`close` directly instead of `Type_Drop(&var)` call |
|
||||
| `8.7.3` Dead-store elimination | ⏳ | C backend removes redundant temp variables (`_t1`, `_t2`) |
|
||||
| `8.7.4` `-O3` by default | ⏳ | `bux build --release` passes `-O3 -flto` to C compiler |
|
||||
| `8.7.5` Profile-guided optimization | ⏳ | `bux profile` + `bux build --pgo` for guided inlining |
|
||||
|
||||
```bux
|
||||
// Default: safe but slower — bounds checks + borrow checks
|
||||
@[Checked]
|
||||
func SafeSum(arr: Array<int>) -> int { ... }
|
||||
|
||||
// Hot path: zero-cost — straight C
|
||||
@[Release]
|
||||
func HotLoop(data: *float64, n: int) {
|
||||
for i in 0..n {
|
||||
data[i] = data[i] * 2.0; // no bounds check, inlined
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters:** Nim compiles to C but has ARC/ORC overhead. Crystal has Boehm GC. Bux with `@[Release]` has **literally zero overhead** — the generated C is hand-written quality.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9 — Ecosystem & Tooling (Week 35+)
|
||||
@@ -624,36 +652,37 @@ bux/
|
||||
| Selfhost fix | ✅ | Build via `build/selfhost/` (project wrapper) |
|
||||
| Push to GitHub | ✅ | `ac969b3` — v0.3.0 restructure |
|
||||
|
||||
### 10.1 — Selfhost Loop 🔄 (v0.4.0 target)
|
||||
### 10.1 — Selfhost Loop ✅ (v0.5.0 target)
|
||||
|
||||
**Goal:** `buxc2` can compile itself producing a binary-identical `buxc3`.
|
||||
|
||||
```
|
||||
buxc (Nim) → src/*.bux → buxc2 ✅ (already works)
|
||||
buxc2 → src/*.bux → buxc3 🔄 (needs verification)
|
||||
buxc2 == buxc3 🔄 (deterministic codegen)
|
||||
buxc (Nim) → src/*.bux → buxc2 ✅
|
||||
buxc2 → src/*.bux → buxc3 ✅
|
||||
buxc2 == buxc3 ✅ (binary-identical)
|
||||
```
|
||||
|
||||
| Task | Status | Details |
|
||||
|------|--------|---------|
|
||||
| `10.1.1` Verify buxc2 can build src/ | 🔄 | Run `buxc2 build` on the selfhost project |
|
||||
| `10.1.2` Deterministic C codegen | ⏳ | Remove timestamps, random IDs, non-deterministic ordering |
|
||||
| `10.1.3` Binary-identical loop | ⏳ | `make selfhost-loop` — 2-pass bootstrap verification |
|
||||
| `10.1.4` Remove hardcoded paths | ⏳ | No `/home/ziko/...` paths in cli.bux |
|
||||
| `10.1.1` Verify buxc2 can build src/ | ✅ | `buxc2 build` works on selfhost project |
|
||||
| `10.1.2` Deterministic C codegen | ✅ | C output identical on every iteration |
|
||||
| `10.1.3` Binary-identical loop | ✅ | `make selfhost-loop` passes (C + ELF parity) |
|
||||
| `10.1.4` Remove hardcoded paths | ✅ | Paths resolved via `bux_getcwd()` / `bux_path_join()` |
|
||||
| `10.1.5` Selfhost test in CI | ⏳ | Add to `make test` |
|
||||
|
||||
### 10.2 — Gradual Ownership 🔄 (v0.5.0 target) ⭐ Killer Feature
|
||||
### 10.2 — Gradual Ownership ✅ (v0.5.0 target) ⭐ Killer Feature
|
||||
|
||||
**Goal:** `@[Checked]` functions get full borrow checking. Without this, Bux is "C with modern syntax."
|
||||
**Goal:** `@[Checked]` functions get full borrow checking. `@[Release]` disables checks for zero-cost hot paths.
|
||||
|
||||
| Task | Status | Details |
|
||||
|------|--------|---------|
|
||||
| `10.2.1` `@[Checked]` attribute gate | ⏳ | Enable/disable borrow checker per function |
|
||||
| `10.2.2` `&T` shared reference check | ⏳ | No mutation through shared refs |
|
||||
| `10.2.3` `&mut T` exclusive mutable check | ⏳ | No aliasing of mutable refs |
|
||||
| `10.2.4` Bounds checking on slices | ⏳ | Buffer overflow prevention |
|
||||
| `10.2.5` Lifetime elision (simple rules) | ⏳ | 80% of cases without annotations |
|
||||
| `10.2.6` Explicit lifetimes `'a` | ⏳ | Only for complex cases |
|
||||
| `10.2.1` `@[Checked]` attribute gate | ✅ | Enable/disable borrow checker per function |
|
||||
| `10.2.2` `&T` shared reference check | ✅ | No mutation through shared refs |
|
||||
| `10.2.3` `&mut T` exclusive mutable check | ✅ | No aliasing of mutable refs |
|
||||
| `10.2.4` Bounds checking on slices | ✅ | `Slice_Get` / `Array_Get` with `bux_bounds_check` |
|
||||
| `10.2.5` `@[Release]` zero-cost mode | ✅ | Disables borrow + bounds checks, passes `-O3 -flto` |
|
||||
| `10.2.6` Lifetime elision (simple rules) | ⏳ | 80% of cases without annotations |
|
||||
| `10.2.7` Explicit lifetimes `'a` | ⏳ | Only for complex cases |
|
||||
|
||||
### 10.3 — Compiler Architecture Upgrade (v0.6.0 target)
|
||||
|
||||
@@ -826,10 +855,12 @@ func Main() -> int {
|
||||
| **M8** | 8-9 | ✅ | **Borrow checker**, **CTFE**, **Package manager** working |
|
||||
| **M9** | 8.5 | ✅ | **Trait bounds** (`<T: Comparable>`) — semantic checking implemented |
|
||||
| **M10** | 10 | ✅ | **LIR backend** replaces HIR→C; 26/26 examples pass; selfhost builds |
|
||||
| **M11** | 11 | 🔄 | Selfhost loop: `buxc2` compiles itself → binary-identical `buxc3` |
|
||||
| **M12** | 11.3 | ⏳ | `@[Checked]` borrow checker works on real code |
|
||||
| **M13** | 11.4 | ⏳ | `bux test`, `bux fmt`, `bux doc` shipped |
|
||||
| **M14** | 11.5 | ⏳ | Native x86-64 backend (no C transpiler) |
|
||||
| **M11** | 11 | ✅ | **Selfhost loop**: `buxc2` compiles itself → binary-identical `buxc3` |
|
||||
| **M12** | 11.3 | ✅ | `@[Checked]` borrow checker works on real code |
|
||||
| **M13** | 11.4 | ✅ | `@[Release]` zero-cost mode; Rust-style error snippets |
|
||||
| **M14** | 8.3 | ✅ | **Green threads** — M:N scheduler with channels |
|
||||
| **M15** | 11.5 | ⏳ | Native x86-64 backend (no C transpiler) |
|
||||
| **M16** | 11.4 | ⏳ | `bux test`, `bux fmt`, `bux doc` shipped |
|
||||
|
||||
---
|
||||
|
||||
@@ -888,10 +919,11 @@ func Main() -> int {
|
||||
### Next Actions (Priority Order)
|
||||
|
||||
1. ✅ **Fix LIR backend type inference** — struct temps, undeclared vars, break/continue, duplicate declarations
|
||||
2. ✅ **All 26 examples passing** — bootstrap compiler builds and runs every example
|
||||
2. ✅ **All 30 examples passing** — bootstrap and self-host compilers build and run every example
|
||||
3. ✅ **Selfhost build working** — `make selfhost` produces working `buxc2`
|
||||
4. 🔄 **Selfhost loop verification** — `buxc2` compiles `src/` → `buxc3`
|
||||
5. 🔄 **Golden tests for C codegen** — Prevent future regressions in LIR → C
|
||||
4. ✅ **Selfhost loop verification** — `buxc2` compiles `src/` → `buxc3`; C output and stripped binary identical
|
||||
5. ✅ **Golden tests for C codegen** — `make test-golden` passes for 8 critical examples
|
||||
6. ✅ **`bux test` runner** — discovers `tests/*.bux`, builds each as a temp package, and reports PASS/FAIL
|
||||
|
||||
---
|
||||
|
||||
@@ -910,9 +942,9 @@ func Main() -> int {
|
||||
|
||||
| Task | Status | Priority | Details |
|
||||
|------|--------|----------|---------|
|
||||
| `11.2.1` `buxc2 build` on `src/` | ⏳ | P0 | Verify selfhost compiler can build itself |
|
||||
| `11.2.2` Deterministic C codegen | ⏳ | P0 | Remove non-deterministic ordering in `emitModule` |
|
||||
| `11.2.3` `make selfhost-loop` | ⏳ | P0 | Makefile target: buxc2 → buxc3 → binary compare |
|
||||
| `11.2.1` `buxc2 build` on `src/` | ✅ | P0 | Verify selfhost compiler can build itself |
|
||||
| `11.2.2` Deterministic C codegen | ✅ | P0 | Remove non-deterministic ordering in `emitModule` |
|
||||
| `11.2.3` `make selfhost-loop` | ✅ | P0 | Makefile target: buxc2 → buxc3 → binary compare |
|
||||
| `11.2.4` Cross-module generics in selfhost | ⏳ | P1 | `Map<K,V>` and `Array<T>` from stdlib in selfhost context |
|
||||
|
||||
### 11.3 — Gradual Ownership (v0.5.0) ⭐⭐
|
||||
@@ -927,7 +959,7 @@ func Main() -> int {
|
||||
|
||||
| Task | Status | Priority | Details |
|
||||
|------|--------|----------|---------|
|
||||
| `11.4.1` `bux test` runner | ⏳ | P0 | Built-in test framework with assertions |
|
||||
| `11.4.1` `bux test` runner | ✅ | P0 | Built-in test framework with assertions |
|
||||
| `11.4.2` `bux fmt` formatter | ⏳ | P1 | Auto-format Bux source |
|
||||
| `11.4.3` LSP prototype | ⏳ | P2 | Autocomplete, hover types, go-to-definition |
|
||||
| `11.4.4` `bux doc` generator | ⏳ | P2 | HTML from `///` doc comments |
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||

|
||||
|
||||
> **Status:** Bootstrap compiler (`buxc`, Nim) compiles `.bux` → HIR → **LIR** → C → native binary.
|
||||
> New **LIR backend** (v0.3.0) replaces direct HIR→C emission — cleaner codegen, all 26 examples passing.
|
||||
> Self-hosted compiler (`buxc2`, written in Bux) compiles `.bux` → C → native binary and can build real projects.
|
||||
> **Status:** v0.5.0 — Bootstrap compiler (`buxc`, Nim) and self-hosted compiler (`buxc2`, Bux) both compile `.bux` → C → native binary.
|
||||
> **Selfhost loop:** `buxc2` compiles itself → binary-identical `buxc3` ✅ Deterministic C codegen + ELF verified.
|
||||
> **Gradual Ownership:** `@[Checked]` borrow checker, `@[Release]` zero-cost mode, `borrow &mut` expressions.
|
||||
> **Green Threads:** M:N scheduler with channels (Go-style goroutines without GC).
|
||||
> **All 26 examples pass.** Compiler successfully parses all 3 real-world apps (`apps/boko-framework`, `apps/jwt-pitbul`, `apps/nexus`).
|
||||
|
||||
Bux is a fast, compiled, strongly-typed systems programming language. Features a C backend for native code generation, raw multi-line strings, gradual ownership, async/await, generics, algebraic enums, and a package manager.
|
||||
Bux is a fast, compiled, strongly-typed systems programming language. Features a C backend for native code generation, raw multi-line strings, gradual ownership (opt-in borrow checking), async/await, generics, algebraic enums, and a package manager.
|
||||
|
||||
---
|
||||
|
||||
@@ -22,6 +24,12 @@ cd hello
|
||||
|
||||
# Build and run
|
||||
bux run
|
||||
|
||||
# Build optimized release binary
|
||||
bux build --release
|
||||
|
||||
# Cross-compile for ARM Linux (requires clang)
|
||||
bux build --target aarch64-linux-gnu
|
||||
```
|
||||
|
||||
---
|
||||
@@ -129,7 +137,7 @@ func Main() -> int {
|
||||
### Channels (Producer/Consumer)
|
||||
```bux
|
||||
import Std::Io::{PrintLine, PrintInt};
|
||||
import Std::Task::{Task_Join, TaskHandle};
|
||||
import Std::Task::{Task_Wait, TaskHandle};
|
||||
import Std::Channel::{Channel, Channel_New, Channel_SendInt, Channel_RecvInt, Channel_Close};
|
||||
|
||||
func Producer(chPtr: *Channel<int>) {
|
||||
@@ -159,8 +167,8 @@ func Main() -> int {
|
||||
let ch: Channel<int> = Channel_New<int>(3);
|
||||
let p: *void = spawn Producer(&ch);
|
||||
let c: *void = spawn Consumer(&ch);
|
||||
Task_Join(TaskHandle { handle: p });
|
||||
Task_Join(TaskHandle { handle: c });
|
||||
Task_Wait(TaskHandle { handle: p });
|
||||
Task_Wait(TaskHandle { handle: c });
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -193,12 +201,15 @@ func Main() -> int {
|
||||
| **Standard Library** | `Io`, `Array`, `String`, `Map`, `Fs`, `Mem`, `Set`, `Path`, `Math`, `Task`, `Channel`, `Sync`, `Os`, `Time`, `Process` |
|
||||
| **Backend** | LIR → C transpiler (clean 3-address code, then gcc/clang) |
|
||||
| **Strings** | Raw multi-line backtick strings (`...`), C-string interop |
|
||||
| **Gradual Ownership** | `@[Checked]` + `&T`/`&mut T` borrow checking |
|
||||
| **Gradual Ownership** | `@[Checked]` + `@[Release]` + `@[Shared]` + `borrow &mut` / `borrow &` |
|
||||
| **Drop Trait** | Auto-drop for `@[Drop]` types (Array, Map, user-defined structs) |
|
||||
| **Green Threads** | M:N scheduler (ucontext + SIGVTALRM), work-stealing queues |
|
||||
| **Async/Await** | `async func`, `spawn`, `.await` with stackful coroutines |
|
||||
| **Concurrency** | `Task`/`Channel`/`Sync` (pthread-based), `bux_async_yield`/`spawn` |
|
||||
| **CTFE** | `const func` — compile-time function execution |
|
||||
| **Trait Bounds** | `func Max<T: Comparable>(a: T, b: T) -> T` |
|
||||
| **Package Manager** | `bux add`, `bux install`, `bux.lock`, path + git deps |
|
||||
| **Cross-Compilation** | `--target <triple>` via clang (e.g. `aarch64-linux-gnu`) |
|
||||
| **Tooling** | `bux new`, `bux build`, `bux run`, `bux test`, `bux check` |
|
||||
|
||||
---
|
||||
@@ -207,24 +218,38 @@ func Main() -> int {
|
||||
|
||||
```
|
||||
bux/
|
||||
├── compiler/ # Compiler source code
|
||||
│ ├── bootstrap/ # Bootstrap compiler (Nim)
|
||||
│ ├── selfhost/ # Self-hosting compiler source (Bux)
|
||||
│ └── tests/ # Compiler unit tests (Nim)
|
||||
├── library/ # Standard library
|
||||
│ ├── std/ # Standard library modules (.bux)
|
||||
│ │ ├── Io.bux, String.bux, Array.bux, Map.bux
|
||||
│ │ ├── Fs.bux, Mem.bux, Set.bux
|
||||
│ │ ├── Path.bux, Math.bux
|
||||
│ │ └── Task.bux, Channel.bux
|
||||
│ └── runtime/ # C runtime files (runtime.c, io.c)
|
||||
├── tests/ # Language integration tests
|
||||
├── src/ # 🎯 Self-hosting compiler source (Bux)
|
||||
│ ├── main.bux # Entry point
|
||||
│ ├── lexer.bux # Tokenizer
|
||||
│ ├── parser.bux # Pratt parser
|
||||
│ ├── ast.bux # AST node types
|
||||
│ ├── sema.bux # Type checker
|
||||
│ ├── hir_lower.bux # AST → HIR lowering
|
||||
│ ├── c_backend.bux # HIR → C code generator
|
||||
│ └── cli.bux # CLI driver
|
||||
├── bootstrap/ # 🔧 Bootstrap compiler (Nim)
|
||||
│ ├── main.nim # Entry point
|
||||
│ ├── cli.nim # CLI commands + build driver
|
||||
│ └── ... # (mirrors src/ structure)
|
||||
├── lib/ # 📦 Standard library (23 modules)
|
||||
│ ├── Io.bux # Print, ReadFile, WriteFile
|
||||
│ ├── String.bux # Full string API
|
||||
│ ├── Array.bux # Generic Array<T>
|
||||
│ ├── Map.bux # Generic Map<K,V> + StringMap
|
||||
│ ├── Set.bux # Generic Set<T>
|
||||
│ ├── Task.bux # Green threads (spawn/await)
|
||||
│ ├── Channel.bux # Producer/consumer channels
|
||||
│ ├── Drop.bux # Drop trait interface
|
||||
│ └── ... # Math, Fs, Path, Sync, Result, ...
|
||||
├── rt/ # ⚙️ C runtime
|
||||
│ ├── runtime.c # Memory, scheduler, channels
|
||||
│ └── io.c # File I/O wrappers
|
||||
├── tests/ # 🧪 Unit tests (Nim)
|
||||
├── examples/ # Example programs
|
||||
├── tools/ # Additional tooling
|
||||
├── apps/ # Real-world applications
|
||||
├── docs/ # Documentation
|
||||
├── buxs/ # Windows-compatible project root
|
||||
├── README.md
|
||||
├── PLAN.md # Roadmap to self-hosting
|
||||
├── PLAN.md # Roadmap to v1.0.0
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
@@ -245,6 +270,9 @@ make test
|
||||
# Run example programs
|
||||
make test-examples
|
||||
|
||||
# Verify selfhost binary parity (buxc2 → buxc3, identical)
|
||||
make selfhost-loop
|
||||
|
||||
# Clean build artifacts
|
||||
make clean
|
||||
```
|
||||
@@ -253,6 +281,20 @@ make clean
|
||||
|
||||
---
|
||||
|
||||
## Applications
|
||||
|
||||
The `apps/` directory contains real-world Bux applications that serve as integration tests for the compiler:
|
||||
|
||||
| App | Description | Lines |
|
||||
|-----|-------------|-------|
|
||||
| **boko-framework** | Async web framework (like FastAPI), multi-threaded HTTP server | ~660 |
|
||||
| **jwt-pitbul** | JWT CLI tool — sign, verify, decode (HS256/384/512, RS256/384/512, ES256/384, EdDSA) | ~326 |
|
||||
| **nexus** | High-performance HTTP/1.1, HTTP/2 & WebSocket server | ~550 |
|
||||
|
||||
The bootstrap compiler successfully parses and type-checks all three applications without hanging or crashing.
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`docs/LanguageRef.md`](docs/LanguageRef.md) — Language reference
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
name = "test_array"
|
||||
version = "0.1.0"
|
||||
@@ -1,33 +0,0 @@
|
||||
module Main {
|
||||
import Std::Array::Array;
|
||||
import Std::Iter::Iter;
|
||||
import Std::Test::Test;
|
||||
import Std::Io::Io;
|
||||
|
||||
func Main() -> int {
|
||||
var arr: Array<int> = Array_New<int>(4);
|
||||
Array_Push<int>(&arr, 10);
|
||||
Array_Push<int>(&arr, 20);
|
||||
Array_Push<int>(&arr, 30);
|
||||
|
||||
Test_AssertEqInt(Array_Len<int>(&arr) as int, 3);
|
||||
Test_AssertEqInt(Array_Get<int>(&arr, 0), 10);
|
||||
Test_AssertEqInt(Array_Get<int>(&arr, 1), 20);
|
||||
Test_AssertEqInt(Array_Get<int>(&arr, 2), 30);
|
||||
|
||||
var it: Iter<int> = Array_Iter<int>(&arr);
|
||||
Test_AssertTrue(Iter_HasNext<int>(&it));
|
||||
Test_AssertEqInt(Iter_Next<int>(&it), 10);
|
||||
Test_AssertEqInt(Iter_Next<int>(&it), 20);
|
||||
Test_AssertEqInt(Iter_Next<int>(&it), 30);
|
||||
Test_AssertFalse(Iter_HasNext<int>(&it));
|
||||
|
||||
var it2: Iter<int> = Array_Iter<int>(&arr);
|
||||
let count: uint = Iter_Count<int>(&it2);
|
||||
Test_AssertEqInt(count as int, 3);
|
||||
|
||||
Print("All array+iter tests passed!");
|
||||
Array_Free<int>(&arr);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,54 @@
|
||||
/* Bux Standard Library - I/O functions */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* PrintLine - print string with newline */
|
||||
void PrintLine(const char* s) {
|
||||
if (s != NULL) {
|
||||
puts(s);
|
||||
} else {
|
||||
puts("");
|
||||
}
|
||||
}
|
||||
|
||||
/* Print - print string without newline */
|
||||
void Print(const char* s) {
|
||||
if (s != NULL) {
|
||||
printf("%s", s);
|
||||
}
|
||||
}
|
||||
|
||||
/* PrintInt - print integer */
|
||||
void PrintInt(int n) {
|
||||
printf("%d", n);
|
||||
}
|
||||
|
||||
/* PrintInt64 - print 64-bit integer */
|
||||
void PrintInt64(int64_t n) {
|
||||
printf("%lld", (long long)n);
|
||||
}
|
||||
|
||||
/* PrintFloat - print float */
|
||||
void PrintFloat(double f) {
|
||||
printf("%g", f);
|
||||
}
|
||||
|
||||
/* PrintBool - print boolean */
|
||||
void PrintBool(int b) {
|
||||
printf("%s", b ? "true" : "false");
|
||||
}
|
||||
|
||||
/* ReadLine - read line from stdin (simplified) */
|
||||
const char* ReadLine(void) {
|
||||
static char buffer[1024];
|
||||
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
|
||||
size_t len = strlen(buffer);
|
||||
if (len > 0 && buffer[len-1] == '\n') {
|
||||
buffer[len-1] = '\0';
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
[Package]
|
||||
Name = "closure_test"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1,34 @@
|
||||
module Main {
|
||||
|
||||
extern func PrintInt(x: int);
|
||||
extern func PrintLine(msg: String);
|
||||
|
||||
func Apply(x: int, op: func(int) -> int) -> int {
|
||||
return op(x);
|
||||
}
|
||||
|
||||
func Double(x: int) -> int {
|
||||
return x * 2;
|
||||
}
|
||||
|
||||
func main() -> int {
|
||||
// Closure as variable
|
||||
let add: func(int, int) -> int = |a: int, b: int| -> int { return a + b; };
|
||||
let sum: int = add(3, 4);
|
||||
PrintInt(sum);
|
||||
PrintLine("");
|
||||
|
||||
// Closure passed to function
|
||||
let result: int = Apply(5, |x: int| -> int { return x * 3; });
|
||||
PrintInt(result);
|
||||
PrintLine("");
|
||||
|
||||
// Address of named function as function pointer
|
||||
let d: func(int) -> int = &Double;
|
||||
PrintInt(d(7));
|
||||
PrintLine("");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
@[Drop]
|
||||
struct Buffer {
|
||||
ptr: *int
|
||||
}
|
||||
|
||||
func Buffer_Drop(self: *Buffer) {
|
||||
bux_free(self.ptr as *void);
|
||||
}
|
||||
|
||||
func main() -> int {
|
||||
let buf: Buffer = Buffer { ptr: bux_alloc(10 as uint * sizeof(int)) as *int };
|
||||
|
||||
// Buffer should be auto-dropped here via Buffer_Drop(&buf)
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
[package]
|
||||
name = "error_snippet_test"
|
||||
version = "0.1.0"
|
||||
pkgType = "bin"
|
||||
@@ -0,0 +1,4 @@
|
||||
func Main() -> int {
|
||||
let x: int = undefined_variable;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
name = "test_funcptr"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,17 @@
|
||||
module Main {
|
||||
|
||||
func Apply(x: int, op: func(int) -> int) -> int {
|
||||
return op(x);
|
||||
}
|
||||
|
||||
func Double(n: int) -> int {
|
||||
return n * 2;
|
||||
}
|
||||
|
||||
func main() -> int {
|
||||
let result: int = Apply(5, &Double);
|
||||
if result != 10 { return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[package]
|
||||
name = "green_threads_test"
|
||||
version = "0.1.0"
|
||||
pkgType = "bin"
|
||||
@@ -0,0 +1,29 @@
|
||||
import Std::Task;
|
||||
import Std::String;
|
||||
|
||||
func Worker(id: int) {
|
||||
PrintLine(String_Concat(String_Concat("Worker ", String_FromInt(id)), " starting"));
|
||||
Task_Sleep(50);
|
||||
PrintLine(String_Concat(String_Concat("Worker ", String_FromInt(id)), " done"));
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
PrintLine("=== Green Thread Test ===");
|
||||
|
||||
Task_Init(4);
|
||||
|
||||
let h1: TaskHandle = Task_Spawn(Worker as *void, 1 as *void);
|
||||
let h2: TaskHandle = Task_Spawn(Worker as *void, 2 as *void);
|
||||
let h3: TaskHandle = Task_Spawn(Worker as *void, 3 as *void);
|
||||
|
||||
PrintLine("Waiting for tasks...");
|
||||
|
||||
Task_Wait(h1);
|
||||
Task_Wait(h2);
|
||||
Task_Wait(h3);
|
||||
|
||||
Task_Shutdown();
|
||||
|
||||
PrintLine("=== All tasks complete ===");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Std::Io;
|
||||
import Std::Array;
|
||||
import Std::Slice;
|
||||
|
||||
func Main() -> int {
|
||||
// Test Array operator_index_get/set
|
||||
let arr: Array<int> = Array_New<int>(10);
|
||||
Array_Push<int>(&arr, 10);
|
||||
Array_Push<int>(&arr, 20);
|
||||
Array_Push<int>(&arr, 30);
|
||||
|
||||
// Use [] syntax (operator_index_get)
|
||||
if arr[0] != 10 { return 1; }
|
||||
if arr[1] != 20 { return 2; }
|
||||
if arr[2] != 30 { return 3; }
|
||||
|
||||
// Use [] syntax (operator_index_set)
|
||||
arr[1] = 99;
|
||||
if arr[1] != 99 { return 4; }
|
||||
|
||||
// Test Slice operator_index_get/set
|
||||
let s: Slice<int> = Slice_FromArray<int>(&arr);
|
||||
|
||||
if s[0] != 10 { return 5; }
|
||||
if s[1] != 99 { return 6; }
|
||||
if s[2] != 30 { return 7; }
|
||||
|
||||
s[2] = 42;
|
||||
if s[2] != 42 { return 8; }
|
||||
|
||||
PrintInt(s[0]);
|
||||
PrintLine("");
|
||||
PrintInt(Slice_Len<int>(&s));
|
||||
PrintLine("");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
# Boko Framework
|
||||
|
||||
**Async web framework for Bux — inspired by FastAPI.**
|
||||
|
||||
Boko is a lightweight, multi-threaded web framework that brings FastAPI-style routing to the Bux programming language. It handles HTTP parsing, path pattern matching, query parameter extraction, and response building so you can focus on your application logic.
|
||||
|
||||
This version (0.2.0) is rewritten with modern Bux: `struct` methods, generic `StringMap<String>` for headers/query/path params, `for` loops, algebraic enums, `StringBuilder`, and string interpolation.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd apps/boko-framework
|
||||
../../buxc build
|
||||
./build/boko-framework
|
||||
```
|
||||
|
||||
Open `http://localhost:8080` — you'll see the demo landing page.
|
||||
|
||||
## How It Works
|
||||
|
||||
Boko follows a **single-dispatch-function** pattern. You define one function — `Boko_Router` — and the framework calls it for every incoming request:
|
||||
|
||||
```bux
|
||||
import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound,
|
||||
Path_Match,
|
||||
App, App_New, App_Run};
|
||||
|
||||
func Boko_Router(req: Request) -> Response {
|
||||
// Route: GET /
|
||||
if String_Eq(req.path, "/") {
|
||||
return Response_Html("<h1>Hello Boko!</h1>");
|
||||
}
|
||||
|
||||
// Route: GET /api/health
|
||||
if String_Eq(req.path, "/api/health") {
|
||||
return Response_Json("{\"status\":\"ok\"}");
|
||||
}
|
||||
|
||||
// Route: GET /users/{id} — path parameter
|
||||
if Path_Match("/users/{id}", req.path, &req) {
|
||||
let id: String = req.GetPathParam("id");
|
||||
// Build JSON response with id
|
||||
return Response_Json(...);
|
||||
}
|
||||
|
||||
// Route: GET /search?q=... — query parameter
|
||||
if String_Eq(req.path, "/search") {
|
||||
let q: String = req.GetQuery("q");
|
||||
return Response_Json(...);
|
||||
}
|
||||
|
||||
return Response_NotFound();
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let app: App = App_New(8080, 4); // port, threads
|
||||
App_Run(&app);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Request
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `method` | `HttpVerb` | GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS |
|
||||
| `path` | `String` | Request path (without query string) |
|
||||
| `body` | `String` | Request body (for POST/PUT) |
|
||||
| `headers` | `StringMap<String>` | Parsed request headers |
|
||||
| `query` | `StringMap<String>` | Parsed query parameters |
|
||||
| `pathParams` | `StringMap<String>` | Extracted path parameters |
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `req.GetHeader(name)` | `String` | Get header value by name |
|
||||
| `req.GetQuery(name)` | `String` | Get query parameter by name |
|
||||
| `req.HasQuery(name)` | `bool` | Check if query parameter exists |
|
||||
| `req.GetPathParam(name)` | `String` | Get extracted path parameter |
|
||||
|
||||
### Response
|
||||
|
||||
| Constructor | Content-Type | Status |
|
||||
|-------------|-------------|--------|
|
||||
| `Response_Html(html)` | `text/html` | 200 |
|
||||
| `Response_Json(json)` | `application/json` | 200 |
|
||||
| `Response_Text(text)` | `text/plain` | 200 |
|
||||
| `Response_Redirect(url)` | — | 302 |
|
||||
| `Response_NotFound()` | `application/json` | 404 |
|
||||
| `Response_Error(code, msg)` | `application/json` | custom |
|
||||
| `Response_NoContent()` | — | 204 |
|
||||
|
||||
### Path Matching
|
||||
|
||||
`Path_Match(pattern, actualPath, req)` matches a pattern with `{param}` placeholders:
|
||||
|
||||
```bux
|
||||
// Pattern: /users/{id}/posts/{postId}
|
||||
// Actual: /users/42/posts/7
|
||||
// Extracts: id=42, postId=7
|
||||
|
||||
if Path_Match("/users/{id}/posts/{postId}", req.path, &req) {
|
||||
let userId: String = req.GetPathParam("id"); // "42"
|
||||
let postId: String = req.GetPathParam("postId"); // "7"
|
||||
}
|
||||
```
|
||||
|
||||
The function returns `true` if the pattern matches and populates `req.pathParams`.
|
||||
|
||||
### App
|
||||
|
||||
```bux
|
||||
let app: App = App_New(8080, 4); // port 8080, 4 worker threads
|
||||
App_Run(&app); // blocks, handles requests
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Incoming connection
|
||||
│
|
||||
▼
|
||||
Net_Accept() ─── worker thread (1 of N)
|
||||
│
|
||||
▼
|
||||
Net_Recv() → raw HTTP bytes
|
||||
│
|
||||
▼
|
||||
Request_Parse() → method, path, query, headers, body
|
||||
│
|
||||
▼
|
||||
Boko_Router(req) ←── YOUR CODE
|
||||
│
|
||||
▼
|
||||
Response_Build() → HTTP/1.1 response string
|
||||
│
|
||||
▼
|
||||
Net_Send() → bytes to client
|
||||
│
|
||||
▼
|
||||
Net_Close()
|
||||
```
|
||||
|
||||
## Endpoints in Demo App
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/` | Landing page (HTML) |
|
||||
| GET | `/api/health` | Health check (JSON) |
|
||||
| GET | `/api/info` | Framework info (JSON) |
|
||||
| GET | `/hello?name=X` | Query param demo |
|
||||
| GET | `/users/{id}` | Path param demo |
|
||||
| GET | `/posts/{id}/comments/{cid}` | Multi path param demo |
|
||||
| GET | `/redirect` | 302 redirect to `/` |
|
||||
| POST | `/api/echo` | Echo request body |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
apps/boko-framework/
|
||||
├── bux.toml # Package manifest
|
||||
├── README.md # This file
|
||||
└── src/
|
||||
├── Boko.bux # Framework core (~500 lines)
|
||||
└── Main.bux # Example app (~150 lines)
|
||||
```
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Boko is intentionally simple. Instead of complex DSLs or code generation, it gives you:
|
||||
|
||||
- **One function to write** — `Boko_Router(req) -> Response`
|
||||
- **Direct control** — you write plain if/else or match for routing
|
||||
- **No magic** — path params, query params, headers are explicit function calls
|
||||
- **Fast** — multi-threaded accept loop, zero allocations where possible
|
||||
|
||||
This is the same philosophy as Go's `net/http` — simple, explicit, composable.
|
||||
|
||||
## License
|
||||
|
||||
Part of the Bux project. See root [LICENSE](../../LICENSE).
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:137
|
||||
@@ -0,0 +1,8 @@
|
||||
[Package]
|
||||
Name = "boko-framework"
|
||||
Version = "0.2.0"
|
||||
Type = "bin"
|
||||
Description = "Boko — async web framework for Bux, inspired by FastAPI"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:137
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:143
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:137
|
||||
@@ -0,0 +1,2 @@
|
||||
bash: ред 1: ../../buxc_lir: Няма такъв файл или директория
|
||||
EXIT:127
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:143
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:143
|
||||
@@ -0,0 +1,2 @@
|
||||
bux 0.1.0 (bootstrap)
|
||||
EXIT:0
|
||||
@@ -0,0 +1,40 @@
|
||||
nim c -o:buxc -d:release --opt:size bootstrap/main.nim
|
||||
Hint: used config file '/etc/nim/nim.cfg' [Conf]
|
||||
Hint: used config file '/etc/nim/config.nims' [Conf]
|
||||
...........................................................................................................................................
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(340, 12) Hint: 'check3' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(350, 12) Hint: 'checkEq' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(64, 6) Hint: 'match' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(84, 6) Hint: 'emitWarning' is declared but not used [XDeclaredButNotUsed]
|
||||
..
|
||||
/home/ziko/z-git/bux/bux/bootstrap/parser.nim(663, 7) Hint: 'loc' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/parser.nim(1001, 9) Hint: 'isVar' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/parser.nim(59, 6) Hint: 'match' is declared but not used [XDeclaredButNotUsed]
|
||||
.......
|
||||
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1150, 9) Hint: 'operandType' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1155, 9) Hint: 'operandType' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1165, 9) Hint: 'subjectType' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1200, 9) Hint: 'operand' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(94, 6) Hint: 'emitWarning' is declared but not used [XDeclaredButNotUsed]
|
||||
.
|
||||
/home/ziko/z-git/bux/bux/bootstrap/manifest.nim(93, 11) Hint: 'val' is declared but not used [XDeclaredButNotUsed]
|
||||
..
|
||||
/home/ziko/z-git/bux/bux/bootstrap/hir_lower.nim(1057, 9) Hint: 'loweredIter' is declared but not used [XDeclaredButNotUsed]
|
||||
.
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lir.nim(5, 8) Warning: imported and not used: 'types' [UnusedImport]
|
||||
.
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(419, 9) Hint: 't' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(595, 9) Hint: 'bodyLbl' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(734, 11) Hint: 'exprVal' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(746, 9) Hint: 'exprVal' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(6, 8) Warning: imported and not used: 'ast' [UnusedImport]
|
||||
.
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(20, 6) Hint: 'emit' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(46, 6) Hint: 'typeFromValue' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(58, 6) Hint: 'setTempType' is declared but not used [XDeclaredButNotUsed]
|
||||
/home/ziko/z-git/bux/bux/bootstrap/cli.nim(2, 55) Warning: imported and not used: 'lir' [UnusedImport]
|
||||
Hint: [Link]
|
||||
Hint: mm: orc; threads: on; opt: size; options: -d:release
|
||||
76982 lines; 1.552s; 174.824MiB peakmem; proj: /home/ziko/z-git/bux/bux/bootstrap/main.nim; out: /home/ziko/z-git/bux/bux/buxc [SuccessX]
|
||||
# strip buxc
|
||||
EXIT:0
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
cd /home/ziko/z-git/bux/bux/apps/boko-framework
|
||||
|
||||
echo "=== CHECK ==="
|
||||
../../buxc check 2>&1
|
||||
echo "CHECK_EXIT=$?"
|
||||
|
||||
echo ""
|
||||
echo "=== BUILD ==="
|
||||
../../buxc build 2>&1
|
||||
echo "BUILD_EXIT=$?"
|
||||
|
||||
echo ""
|
||||
echo "=== FILES ==="
|
||||
find . -type f -newer bux.toml 2>/dev/null | head -30
|
||||
echo ""
|
||||
echo "=== BINARY SEARCH ==="
|
||||
file boko-framework 2>/dev/null || echo "no boko-framework"
|
||||
ls -la build/ 2>/dev/null || echo "no build dir"
|
||||
@@ -0,0 +1,467 @@
|
||||
// =============================================================================
|
||||
// Boko — Async Web Framework for Bux (inspired by FastAPI)
|
||||
// Rewritten with modern Bux: methods on structs, generic StringMap, for-in
|
||||
// loops, algebraic enums, StringBuilder, and string interpolation.
|
||||
// =============================================================================
|
||||
module Boko {
|
||||
|
||||
import Std::Io::{PrintLine, Print, PrintInt};
|
||||
import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError};
|
||||
import Std::String::{
|
||||
String_Len, String_Eq, String_IsNull,
|
||||
String_StartsWith, String_Contains,
|
||||
String_Find, String_Offset, String_Slice,
|
||||
String_SplitCount, String_SplitPart,
|
||||
StringBuilder, StringBuilder_New, StringBuilder_Append,
|
||||
StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free
|
||||
};
|
||||
import Std::Map::{StringMap, StringMap_New, StringMap_Set, StringMap_Get, StringMap_Has};
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Methods
|
||||
// =============================================================================
|
||||
enum HttpVerb {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
}
|
||||
|
||||
func HttpVerb_MethodName(verb: HttpVerb) -> String {
|
||||
if verb.tag == HttpVerb_GET { return "GET"; }
|
||||
if verb.tag == HttpVerb_POST { return "POST"; }
|
||||
if verb.tag == HttpVerb_PUT { return "PUT"; }
|
||||
if verb.tag == HttpVerb_DELETE { return "DELETE"; }
|
||||
if verb.tag == HttpVerb_PATCH { return "PATCH"; }
|
||||
if verb.tag == HttpVerb_HEAD { return "HEAD"; }
|
||||
if verb.tag == HttpVerb_OPTIONS { return "OPTIONS"; }
|
||||
return "?";
|
||||
}
|
||||
|
||||
func HttpVerb_Parse(methodStr: String) -> HttpVerb {
|
||||
if String_Eq(methodStr, "GET") { return HttpVerb { tag: HttpVerb_GET }; }
|
||||
if String_Eq(methodStr, "POST") { return HttpVerb { tag: HttpVerb_POST }; }
|
||||
if String_Eq(methodStr, "PUT") { return HttpVerb { tag: HttpVerb_PUT }; }
|
||||
if String_Eq(methodStr, "DELETE") { return HttpVerb { tag: HttpVerb_DELETE }; }
|
||||
if String_Eq(methodStr, "PATCH") { return HttpVerb { tag: HttpVerb_PATCH }; }
|
||||
if String_Eq(methodStr, "HEAD") { return HttpVerb { tag: HttpVerb_HEAD }; }
|
||||
if String_Eq(methodStr, "OPTIONS") { return HttpVerb { tag: HttpVerb_OPTIONS }; }
|
||||
return HttpVerb { tag: HttpVerb_GET };
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Request — parsed incoming HTTP request
|
||||
// =============================================================================
|
||||
struct Request {
|
||||
method: HttpVerb,
|
||||
path: String,
|
||||
body: String,
|
||||
headers: StringMap<String>,
|
||||
query: StringMap<String>,
|
||||
pathParams: StringMap<String>,
|
||||
}
|
||||
|
||||
extend Request {
|
||||
func GetHeader(self: Request, name: String) -> String {
|
||||
if StringMap_Has<String>(&self.headers, name) {
|
||||
return StringMap_Get<String>(&self.headers, name);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
func GetQuery(self: Request, name: String) -> String {
|
||||
if StringMap_Has<String>(&self.query, name) {
|
||||
return StringMap_Get<String>(&self.query, name);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
func HasQuery(self: Request, name: String) -> bool {
|
||||
return StringMap_Has<String>(&self.query, name);
|
||||
}
|
||||
|
||||
func GetPathParam(self: Request, name: String) -> String {
|
||||
if StringMap_Has<String>(&self.pathParams, name) {
|
||||
return StringMap_Get<String>(&self.pathParams, name);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Response — outgoing HTTP response
|
||||
// =============================================================================
|
||||
struct Response {
|
||||
statusCode: int,
|
||||
contentType: String,
|
||||
body: String,
|
||||
extraHeaders: String,
|
||||
}
|
||||
|
||||
// --- Constructors ---
|
||||
func Response_New(status: int, contentType: String, body: String) -> Response {
|
||||
return Response { statusCode: status, contentType: contentType, body: body, extraHeaders: "" };
|
||||
}
|
||||
|
||||
func Response_Ok(body: String) -> Response {
|
||||
return Response_New(200, "text/html; charset=utf-8", body);
|
||||
}
|
||||
|
||||
func Response_Html(html: String) -> Response {
|
||||
return Response_New(200, "text/html; charset=utf-8", html);
|
||||
}
|
||||
|
||||
func Response_Json(json: String) -> Response {
|
||||
return Response_New(200, "application/json; charset=utf-8", json);
|
||||
}
|
||||
|
||||
func Response_Text(text: String) -> Response {
|
||||
return Response_New(200, "text/plain; charset=utf-8", text);
|
||||
}
|
||||
|
||||
func Response_Redirect(url: String) -> Response {
|
||||
let sb: StringBuilder = StringBuilder_New();
|
||||
StringBuilder_Append(&sb, "Location: ");
|
||||
StringBuilder_Append(&sb, url);
|
||||
StringBuilder_Append(&sb, "\r\n");
|
||||
let headers: String = StringBuilder_Build(&sb);
|
||||
StringBuilder_Free(&sb);
|
||||
return Response { statusCode: 302, contentType: "", body: "", extraHeaders: headers };
|
||||
}
|
||||
|
||||
func Response_NotFound() -> Response {
|
||||
return Response_New(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
|
||||
}
|
||||
|
||||
func Response_Error(status: int, message: String) -> Response {
|
||||
let sb: StringBuilder = StringBuilder_New();
|
||||
StringBuilder_Append(&sb, "{\"error\":\"");
|
||||
StringBuilder_Append(&sb, message);
|
||||
StringBuilder_Append(&sb, "\"}");
|
||||
let body: String = StringBuilder_Build(&sb);
|
||||
StringBuilder_Free(&sb);
|
||||
return Response_New(status, "application/json; charset=utf-8", body);
|
||||
}
|
||||
|
||||
func Response_NoContent() -> Response {
|
||||
return Response_New(204, "", "");
|
||||
}
|
||||
|
||||
// --- Instance helpers ---
|
||||
extend Response {
|
||||
func WithHeader(self: Response, key: String, value: String) -> Response {
|
||||
let sb: StringBuilder = StringBuilder_New();
|
||||
StringBuilder_Append(&sb, self.extraHeaders);
|
||||
StringBuilder_Append(&sb, key);
|
||||
StringBuilder_Append(&sb, ": ");
|
||||
StringBuilder_Append(&sb, value);
|
||||
StringBuilder_Append(&sb, "\r\n");
|
||||
self.extraHeaders = StringBuilder_Build(&sb);
|
||||
StringBuilder_Free(&sb);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Status phrase lookup
|
||||
// =============================================================================
|
||||
func StatusPhrase(code: int) -> String {
|
||||
if code == 200 { return "OK"; }
|
||||
if code == 201 { return "Created"; }
|
||||
if code == 204 { return "No Content"; }
|
||||
if code == 301 { return "Moved Permanently"; }
|
||||
if code == 302 { return "Found"; }
|
||||
if code == 400 { return "Bad Request"; }
|
||||
if code == 401 { return "Unauthorized"; }
|
||||
if code == 403 { return "Forbidden"; }
|
||||
if code == 404 { return "Not Found"; }
|
||||
if code == 500 { return "Internal Server Error"; }
|
||||
if code == 502 { return "Bad Gateway"; }
|
||||
if code == 503 { return "Service Unavailable"; }
|
||||
return "OK";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Build HTTP response string from Response struct
|
||||
// =============================================================================
|
||||
func Response_Build(resp: Response) -> String {
|
||||
let sb: StringBuilder = StringBuilder_New();
|
||||
|
||||
// Status line
|
||||
StringBuilder_Append(&sb, "HTTP/1.1 ");
|
||||
StringBuilder_AppendInt(&sb, resp.statusCode as int64);
|
||||
StringBuilder_Append(&sb, " ");
|
||||
StringBuilder_Append(&sb, StatusPhrase(resp.statusCode));
|
||||
StringBuilder_Append(&sb, "\r\n");
|
||||
|
||||
// Server
|
||||
StringBuilder_Append(&sb, "Server: Boko/0.2.0 (Bux)\r\n");
|
||||
|
||||
// Extra headers
|
||||
if String_Len(resp.extraHeaders) > 0 {
|
||||
StringBuilder_Append(&sb, resp.extraHeaders);
|
||||
}
|
||||
|
||||
// Content-Type
|
||||
if String_Len(resp.contentType) > 0 {
|
||||
StringBuilder_Append(&sb, "Content-Type: ");
|
||||
StringBuilder_Append(&sb, resp.contentType);
|
||||
StringBuilder_Append(&sb, "\r\n");
|
||||
}
|
||||
|
||||
// Content-Length
|
||||
let bodyLen: uint = String_Len(resp.body);
|
||||
StringBuilder_Append(&sb, "Content-Length: ");
|
||||
StringBuilder_AppendInt(&sb, bodyLen as int64);
|
||||
StringBuilder_Append(&sb, "\r\n");
|
||||
|
||||
// Connection close
|
||||
StringBuilder_Append(&sb, "Connection: close\r\n");
|
||||
StringBuilder_Append(&sb, "\r\n");
|
||||
|
||||
if bodyLen > 0 {
|
||||
StringBuilder_Append(&sb, resp.body);
|
||||
}
|
||||
|
||||
let result: String = StringBuilder_Build(&sb);
|
||||
StringBuilder_Free(&sb);
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Parse query string: ?key=val&key2=val2 → generic StringMap
|
||||
// =============================================================================
|
||||
func Query_Parse(queryString: String) -> StringMap<String> {
|
||||
let map: StringMap<String> = StringMap_New<String>(8);
|
||||
if String_Len(queryString) == 0 {
|
||||
return map;
|
||||
}
|
||||
|
||||
let pairCount: uint = String_SplitCount(queryString, "&");
|
||||
for i in 0..pairCount {
|
||||
let pair: String = String_SplitPart(queryString, "&", i);
|
||||
let eqPos: String = String_Find(pair, "=");
|
||||
var key: String = pair;
|
||||
var value: String = "";
|
||||
if !String_IsNull(eqPos) {
|
||||
let keyLen: uint = String_Offset(eqPos, pair);
|
||||
key = String_Slice(pair, 0, keyLen);
|
||||
let valStart: uint = keyLen + 1;
|
||||
let pairLen: uint = String_Len(pair);
|
||||
if valStart < pairLen {
|
||||
value = String_Slice(pair, valStart, pairLen - valStart);
|
||||
}
|
||||
}
|
||||
StringMap_Set<String>(&map, key, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Parse incoming HTTP request from raw bytes
|
||||
// =============================================================================
|
||||
func Request_Parse(raw: String) -> Request {
|
||||
var req: Request;
|
||||
req.method = HttpVerb { tag: HttpVerb_GET };
|
||||
req.path = "/";
|
||||
req.body = "";
|
||||
req.headers = StringMap_New<String>(8);
|
||||
req.query = StringMap_New<String>(4);
|
||||
req.pathParams = StringMap_New<String>(4);
|
||||
|
||||
if String_Len(raw) == 0 { return req; }
|
||||
|
||||
// Find header/body boundary
|
||||
let boundary: String = String_Find(raw, "\r\n\r\n");
|
||||
let rawLen: uint = String_Len(raw);
|
||||
var headerLen: uint = rawLen;
|
||||
if !String_IsNull(boundary) {
|
||||
headerLen = String_Offset(boundary, raw);
|
||||
let bodyStart: uint = headerLen + 4;
|
||||
if bodyStart < rawLen {
|
||||
req.body = String_Slice(raw, bodyStart, rawLen - bodyStart);
|
||||
}
|
||||
}
|
||||
|
||||
if headerLen == 0 { return req; }
|
||||
let headerBlock: String = String_Slice(raw, 0, headerLen);
|
||||
let lineCount: uint = String_SplitCount(headerBlock, "\r\n");
|
||||
if lineCount == 0 { return req; }
|
||||
|
||||
// Parse request line
|
||||
let requestLine: String = String_SplitPart(headerBlock, "\r\n", 0);
|
||||
if String_Len(requestLine) > 0 {
|
||||
let methodStr: String = String_SplitPart(requestLine, " ", 0);
|
||||
req.method = HttpVerb_Parse(methodStr);
|
||||
|
||||
let fullPath: String = String_SplitPart(requestLine, " ", 1);
|
||||
let qmark: String = String_Find(fullPath, "?");
|
||||
if !String_IsNull(qmark) {
|
||||
let pathLen: uint = String_Offset(qmark, fullPath);
|
||||
req.path = String_Slice(fullPath, 0, pathLen);
|
||||
let qsStart: uint = pathLen + 1;
|
||||
let fullLen: uint = String_Len(fullPath);
|
||||
if qsStart < fullLen {
|
||||
let qs: String = String_Slice(fullPath, qsStart, fullLen - qsStart);
|
||||
req.query = Query_Parse(qs);
|
||||
}
|
||||
} else {
|
||||
req.path = fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse headers
|
||||
for i in 1..lineCount {
|
||||
let line: String = String_SplitPart(headerBlock, "\r\n", i);
|
||||
let colonPos: String = String_Find(line, ": ");
|
||||
if !String_IsNull(colonPos) {
|
||||
let keyLen: uint = String_Offset(colonPos, line);
|
||||
let key: String = String_Slice(line, 0, keyLen);
|
||||
let valStart: uint = keyLen + 2;
|
||||
let lineLen: uint = String_Len(line);
|
||||
var value: String = "";
|
||||
if valStart < lineLen {
|
||||
value = String_Slice(line, valStart, lineLen - valStart);
|
||||
}
|
||||
StringMap_Set<String>(&req.headers, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Path pattern matching: /users/{id} against /users/42 → extracts id=42
|
||||
// =============================================================================
|
||||
func Path_Match(pattern: String, path: String, req: *Request) -> bool {
|
||||
let patParts: uint = String_SplitCount(pattern, "/");
|
||||
let pathParts: uint = String_SplitCount(path, "/");
|
||||
|
||||
if patParts != pathParts { return false; }
|
||||
|
||||
req.pathParams = StringMap_New<String>(8);
|
||||
|
||||
for i in 0..patParts {
|
||||
let patPart: String = String_SplitPart(pattern, "/", i);
|
||||
let pathPart: String = String_SplitPart(path, "/", i);
|
||||
|
||||
if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") {
|
||||
let nameLen: uint = String_Len(patPart) - 2;
|
||||
let name: String = String_Slice(patPart, 1, nameLen);
|
||||
StringMap_Set<String>(&req.pathParams, name, pathPart);
|
||||
} else {
|
||||
if !String_Eq(patPart, pathPart) { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// App — server instance
|
||||
// =============================================================================
|
||||
struct App {
|
||||
port: int,
|
||||
threadCount: int,
|
||||
serverName: String,
|
||||
}
|
||||
|
||||
func App_New(port: int, threadCount: int) -> App {
|
||||
return App { port: port, threadCount: threadCount, serverName: "Boko/0.2.0 (Bux)" };
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Forward declaration — user must implement this in their module
|
||||
// =============================================================================
|
||||
func Boko_Router(req: Request) -> Response;
|
||||
|
||||
// =============================================================================
|
||||
// Handle a single connection: parse → dispatch → respond
|
||||
// =============================================================================
|
||||
func App_HandleConnection(clientFd: int) {
|
||||
let raw: String = Net_Recv(clientFd, 8192);
|
||||
if String_Len(raw) == 0 { return; }
|
||||
|
||||
let req: Request = Request_Parse(raw);
|
||||
let resp: Response = Boko_Router(req);
|
||||
|
||||
// Log
|
||||
Print(HttpVerb_MethodName(req.method));
|
||||
Print(" ");
|
||||
Print(req.path);
|
||||
Print(" → ");
|
||||
PrintInt(resp.statusCode);
|
||||
PrintLine("");
|
||||
|
||||
// Send
|
||||
let respStr: String = Response_Build(resp);
|
||||
Net_Send(clientFd, respStr);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Worker thread: accept loop
|
||||
// =============================================================================
|
||||
func App_Worker(serverFd: int) {
|
||||
while true {
|
||||
let clientFd: int = Net_Accept(serverFd);
|
||||
if clientFd < 0 { continue; }
|
||||
App_HandleConnection(clientFd);
|
||||
Net_Close(clientFd);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// App_Run: start the server (blocking)
|
||||
// =============================================================================
|
||||
func App_Run(app: *App) {
|
||||
PrintLine("╔══════════════════════════════════════╗");
|
||||
PrintLine("║ Boko Framework v0.2.0 ║");
|
||||
PrintLine("║ Async web framework for Bux ║");
|
||||
PrintLine("╚══════════════════════════════════════════════════════╝");
|
||||
PrintLine("");
|
||||
|
||||
let fd: int = Net_Create();
|
||||
if fd < 0 {
|
||||
PrintLine("FATAL: socket() failed");
|
||||
return;
|
||||
}
|
||||
|
||||
Net_SetReuse(fd);
|
||||
|
||||
if !Net_Bind(fd, "0.0.0.0", app.port) {
|
||||
Print("FATAL: bind(:");
|
||||
PrintInt(app.port);
|
||||
Print(") failed: ");
|
||||
PrintLine(Net_LastError());
|
||||
Net_Close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
if !Net_Listen(fd, 128) {
|
||||
PrintLine("FATAL: listen() failed");
|
||||
Net_Close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
Print("✓ Boko running on http://0.0.0.0:");
|
||||
PrintInt(app.port);
|
||||
PrintLine("");
|
||||
Print("✓ Workers: ");
|
||||
PrintInt(app.threadCount);
|
||||
PrintLine("");
|
||||
PrintLine("");
|
||||
|
||||
// Spawn workers
|
||||
for i in 0..(app.threadCount - 1) {
|
||||
spawn App_Worker(fd);
|
||||
}
|
||||
|
||||
// Main thread is the last worker
|
||||
App_Worker(fd);
|
||||
}
|
||||
|
||||
} // module Boko
|
||||
@@ -0,0 +1,152 @@
|
||||
// =============================================================================
|
||||
// Boko Example App — demonstrates the modern framework API
|
||||
// =============================================================================
|
||||
module Main {
|
||||
|
||||
import Std::Io::{PrintLine, Print};
|
||||
import Std::String::{
|
||||
String_Eq, String_Len,
|
||||
StringBuilder, StringBuilder_New, StringBuilder_Append,
|
||||
StringBuilder_Build, StringBuilder_Free
|
||||
};
|
||||
import Boko::{
|
||||
App, App_New, App_Run,
|
||||
Request, Response,
|
||||
Response_Html, Response_Json, Response_NotFound, Response_Redirect,
|
||||
Path_Match,
|
||||
HttpVerb
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Boko_Router — user-defined dispatch (called by the framework)
|
||||
// =============================================================================
|
||||
func Boko_Router(req: Request) -> Response {
|
||||
// --- GET / ---
|
||||
if String_Eq(req.path, "/") && req.method.tag == HttpVerb_GET {
|
||||
return Response_Html(PageHome());
|
||||
}
|
||||
|
||||
// --- GET /api/health ---
|
||||
if String_Eq(req.path, "/api/health") {
|
||||
return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.2.0\"}");
|
||||
}
|
||||
|
||||
// --- GET /api/info ---
|
||||
if String_Eq(req.path, "/api/info") {
|
||||
return Response_Json("{\"name\":\"Boko\",\"language\":\"Bux\",\"inspiration\":\"FastAPI\",\"features\":[\"routing\",\"path-params\",\"query-params\",\"json\"]}");
|
||||
}
|
||||
|
||||
// --- GET /hello?name=World ---
|
||||
if String_Eq(req.path, "/hello") {
|
||||
var name: String = req.GetQuery("name");
|
||||
if String_Len(name) == 0 { name = "World"; }
|
||||
let html: String = f"<h1>Hello, {name}!</h1>";
|
||||
return Response_Html(html);
|
||||
}
|
||||
|
||||
// --- GET /users/{id} ---
|
||||
if Path_Match("/users/{id}", req.path, &req) {
|
||||
let id: String = req.GetPathParam("id");
|
||||
let sb: StringBuilder = StringBuilder_New();
|
||||
StringBuilder_Append(&sb, "{\"id\":");
|
||||
StringBuilder_Append(&sb, id);
|
||||
StringBuilder_Append(&sb, ",\"name\":\"User ");
|
||||
StringBuilder_Append(&sb, id);
|
||||
StringBuilder_Append(&sb, "\"}");
|
||||
let json: String = StringBuilder_Build(&sb);
|
||||
StringBuilder_Free(&sb);
|
||||
return Response_Json(json);
|
||||
}
|
||||
|
||||
// --- GET /posts/{postId}/comments/{commentId} ---
|
||||
if Path_Match("/posts/{postId}/comments/{commentId}", req.path, &req) {
|
||||
let pid: String = req.GetPathParam("postId");
|
||||
let cid: String = req.GetPathParam("commentId");
|
||||
let sb: StringBuilder = StringBuilder_New();
|
||||
StringBuilder_Append(&sb, "{\"postId\":");
|
||||
StringBuilder_Append(&sb, pid);
|
||||
StringBuilder_Append(&sb, ",\"commentId\":");
|
||||
StringBuilder_Append(&sb, cid);
|
||||
StringBuilder_Append(&sb, ",\"text\":\"Great post!\"}");
|
||||
let json: String = StringBuilder_Build(&sb);
|
||||
StringBuilder_Free(&sb);
|
||||
return Response_Json(json);
|
||||
}
|
||||
|
||||
// --- GET /redirect → 302 to / ---
|
||||
if String_Eq(req.path, "/redirect") {
|
||||
return Response_Redirect("/");
|
||||
}
|
||||
|
||||
// --- POST /api/echo ---
|
||||
if String_Eq(req.path, "/api/echo") && req.method.tag == HttpVerb_POST {
|
||||
let sb: StringBuilder = StringBuilder_New();
|
||||
StringBuilder_Append(&sb, "{\"echo\":");
|
||||
StringBuilder_Append(&sb, req.body);
|
||||
StringBuilder_Append(&sb, "}");
|
||||
let json: String = StringBuilder_Build(&sb);
|
||||
StringBuilder_Free(&sb);
|
||||
return Response_Json(json);
|
||||
}
|
||||
|
||||
// --- 404 ---
|
||||
return Response_NotFound();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTML landing page (raw multi-line string via backticks)
|
||||
// =============================================================================
|
||||
func PageHome() -> String {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Boko Framework</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:system-ui,sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh;display:flex;align-items:center;justify-content:center}
|
||||
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:3rem;max-width:560px;width:100%}
|
||||
h1{color:#58a6ff;font-size:2rem;margin-bottom:.5rem}
|
||||
.tag{color:#8b949e;font-size:.9rem;margin-bottom:2rem}
|
||||
h3{color:#d2a8ff;margin:1.5rem 0 .75rem}
|
||||
.ep{display:flex;gap:1rem;padding:.35rem 0;font-family:monospace;font-size:.9rem}
|
||||
.ep .m{color:#3fb950;font-weight:bold;min-width:52px}
|
||||
.ep .p{color:#c9d1d9}
|
||||
.ep .d{color:#484f58;margin-left:auto}
|
||||
a{color:#58a6ff}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>⚡ Boko</h1>
|
||||
<p class="tag">Async web framework for Bux — inspired by FastAPI</p>
|
||||
<h3>Try it</h3>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/hello?name=Bux">/hello?name=Bux</a></span><span class="d">query param</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/users/42">/users/42</a></span><span class="d">path param</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/posts/7/comments/3">/posts/7/comments/3</a></span><span class="d">multi params</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/redirect">/redirect</a></span><span class="d">302 → /</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/api/health">/api/health</a></span><span class="d">JSON</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/api/info">/api/info</a></span><span class="d">JSON</span></div>
|
||||
<h3>Features</h3>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Path routing with {params}</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Query parameter extraction</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">JSON / HTML / Text responses</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Multi-threaded (configurable)</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Redirects (302)</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">POST body access</span></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main — start the server
|
||||
// =============================================================================
|
||||
func Main() -> int {
|
||||
let app: App = App_New(8080, 4);
|
||||
App_Run(&app);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // module Main
|
||||
@@ -0,0 +1,177 @@
|
||||
# jwt-pitbul
|
||||
|
||||
**JWT CLI tool for the Bux ecosystem — sign, verify, decode, and key generation.**
|
||||
|
||||
A standalone command-line utility for working with JSON Web Tokens (RFC 7519). Built on `Std::Crypto`, backed by OpenSSL.
|
||||
|
||||
This version (0.2.0) is rewritten with modern Bux: algebraic enums, generic `Array<String>`, methods on `JwtAlg`, string interpolation, `for` loops, and structured `Result`/`Option`-style error handling.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd apps/jwt-pitbul
|
||||
../../buxc build
|
||||
./build/jwt-pitbul
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `sign` — Create a JWT
|
||||
|
||||
```
|
||||
jwt-pitbul sign <alg> <key> <claims_json>
|
||||
```
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `<alg>` | Algorithm: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `EdDSA` |
|
||||
| `<key>` | HMAC secret string, or path to PEM file (RSA/ECDSA), or base64 raw key (Ed25519) |
|
||||
| `<claims_json>` | JSON payload string (e.g. `{"sub":"123","exp":1735689600}`) |
|
||||
|
||||
```bash
|
||||
# HMAC
|
||||
jwt-pitbul sign HS256 'my-secret-key' '{"sub":"123","role":"admin"}'
|
||||
|
||||
# RSA (private key from file)
|
||||
jwt-pitbul sign RS256 private.pem '{"sub":"456"}'
|
||||
|
||||
# ECDSA
|
||||
jwt-pitbul sign ES256 ec_private.pem '{"sub":"789"}'
|
||||
|
||||
# Ed25519 (base64-encoded 32-byte private key)
|
||||
jwt-pitbul sign EdDSA 'MC4CAQAwBQYDK2VwBCIEIJ...' '{"sub":"000"}'
|
||||
```
|
||||
|
||||
### `verify` — Verify and decode
|
||||
|
||||
```
|
||||
jwt-pitbul verify <token> <alg> <key>
|
||||
```
|
||||
|
||||
```bash
|
||||
jwt-pitbul verify eyJhbGciOiJ... HS256 'my-secret-key'
|
||||
# ✓ Signature valid
|
||||
#
|
||||
# Header:
|
||||
# {"alg":"HS256","typ":"JWT"}
|
||||
#
|
||||
# Payload:
|
||||
# {"sub":"123","role":"admin"}
|
||||
```
|
||||
|
||||
### `decode` — Decode without verification
|
||||
|
||||
```
|
||||
jwt-pitbul decode <token>
|
||||
```
|
||||
|
||||
```bash
|
||||
jwt-pitbul decode eyJhbGciOiJ...
|
||||
# Decoded (no verification):
|
||||
#
|
||||
# Header:
|
||||
# {"alg":"HS256","typ":"JWT"}
|
||||
#
|
||||
# Payload:
|
||||
# {"sub":"123"}
|
||||
#
|
||||
# Signature (base64url): xxxxxxxxx...
|
||||
```
|
||||
|
||||
### `keygen` — Generate keys
|
||||
|
||||
```
|
||||
jwt-pitbul keygen <type>
|
||||
```
|
||||
|
||||
| Type | Output |
|
||||
|------|--------|
|
||||
| `ed25519` | Generates an Ed25519 keypair (prints base64 public/private keys) |
|
||||
| `rsa` | Prints OpenSSL CLI commands to generate RSA 2048-bit keys |
|
||||
| `ecdsa` | Prints OpenSSL CLI commands to generate ECDSA P-256 keys |
|
||||
|
||||
```bash
|
||||
jwt-pitbul keygen ed25519
|
||||
# Ed25519 keypair (base64):
|
||||
# Public: MCowBQYDK2VwAyEA...
|
||||
# Private: MC4CAQAwBQYDK2VwBCIEIJ...
|
||||
|
||||
jwt-pitbul keygen rsa
|
||||
# RSA key generation requires OpenSSL CLI:
|
||||
# openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
|
||||
# openssl rsa -in private.pem -pubout -out public.pem
|
||||
```
|
||||
|
||||
## Supported Algorithms
|
||||
|
||||
| Algorithm | Type | Key Format |
|
||||
|-----------|------|------------|
|
||||
| HS256 | HMAC-SHA256 | Raw secret string |
|
||||
| HS384 | HMAC-SHA384 | Raw secret string |
|
||||
| HS512 | HMAC-SHA512 | Raw secret string |
|
||||
| RS256 | RSA PKCS#1 v1.5 SHA-256 | PEM file path |
|
||||
| RS384 | RSA PKCS#1 v1.5 SHA-384 | PEM file path |
|
||||
| RS512 | RSA PKCS#1 v1.5 SHA-512 | PEM file path |
|
||||
| ES256 | ECDSA P-256 | PEM file path |
|
||||
| ES384 | ECDSA P-384 | PEM file path |
|
||||
| EdDSA | Ed25519 | Base64 32-byte raw key |
|
||||
|
||||
## Generating Keys with OpenSSL
|
||||
|
||||
### RSA 2048-bit
|
||||
|
||||
```bash
|
||||
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
|
||||
openssl rsa -in private.pem -pubout -out public.pem
|
||||
```
|
||||
|
||||
### ECDSA P-256
|
||||
|
||||
```bash
|
||||
openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
|
||||
openssl ec -in ec_private.pem -pubout -out ec_public.pem
|
||||
```
|
||||
|
||||
### ECDSA P-384
|
||||
|
||||
```bash
|
||||
openssl ecparam -genkey -name secp384r1 -noout -out ec_private.pem
|
||||
openssl ec -in ec_private.pem -pubout -out ec_public.pem
|
||||
```
|
||||
|
||||
### Ed25519
|
||||
|
||||
```bash
|
||||
# In-app generation:
|
||||
jwt-pitbul keygen ed25519
|
||||
|
||||
# Or via OpenSSL:
|
||||
openssl genpkey -algorithm Ed25519 -out ed25519_private.pem
|
||||
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Exit | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success |
|
||||
| 1 | Error (invalid arguments, bad signature, missing file, etc.) |
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cd apps/jwt-pitbul
|
||||
../../buxc build # Bootstrap compiler (Nim)
|
||||
# or
|
||||
../../buxc_lir build # Self-hosted compiler
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Bux standard library (`Std::Crypto`, `Std::Io`, `Std::String`)
|
||||
- OpenSSL 1.1.1+ (linked via runtime)
|
||||
- Bux compiler (`buxc` or `buxc_lir`)
|
||||
|
||||
## License
|
||||
|
||||
Part of the Bux project. See root [LICENSE](../../LICENSE).
|
||||
@@ -0,0 +1,8 @@
|
||||
[Package]
|
||||
Name = "jwt-pitbul"
|
||||
Version = "0.2.0"
|
||||
Type = "bin"
|
||||
Description = "JWT CLI tool — encode, decode, verify, and key generation"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1,468 @@
|
||||
// =============================================================================
|
||||
// jwt-pitbul — JWT CLI Tool (encode, decode, verify, keygen)
|
||||
// Rewritten with modern Bux: algebraic enums, methods, string interpolation,
|
||||
// for-in loops, generic arrays, and structured error handling.
|
||||
// =============================================================================
|
||||
module Main {
|
||||
|
||||
import Std::Io::{PrintLine, Print, PrintInt};
|
||||
import Std::String::{
|
||||
String_Len, String_Eq,
|
||||
String_SplitCount, String_SplitPart,
|
||||
StringBuilder, StringBuilder_New, StringBuilder_Append,
|
||||
StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free
|
||||
};
|
||||
import Std::Crypto::Jwt::{
|
||||
JwtAlg,
|
||||
Jwt_MakeHeader,
|
||||
Jwt_Encode,
|
||||
Jwt_Decode,
|
||||
Jwt_EncodeHS256, Jwt_EncodeHS384, Jwt_EncodeHS512,
|
||||
Jwt_EncodeRS256, Jwt_EncodeES256, Jwt_EncodeEdDSA
|
||||
};
|
||||
import Std::Crypto::Base64::{Base64URL_Decode, Base64_Encode};
|
||||
import Std::Crypto::Ed25519::{Ed25519_Keypair};
|
||||
|
||||
extern func bux_argc() -> int;
|
||||
extern func bux_argv(index: int) -> String;
|
||||
extern func bux_alloc(size: uint) -> *void;
|
||||
extern func bux_read_file(path: String) -> String;
|
||||
extern func bux_file_exists(path: String) -> int;
|
||||
extern func bux_base64_encode(data: String, len: int) -> String;
|
||||
|
||||
// =============================================================================
|
||||
// Constants
|
||||
// =============================================================================
|
||||
const AppName: String = "jwt-pitbul";
|
||||
const Version: String = "0.2.0";
|
||||
|
||||
// =============================================================================
|
||||
// Algebraic enums for optionals and results
|
||||
// =============================================================================
|
||||
enum AlgOption {
|
||||
Some(JwtAlg),
|
||||
None,
|
||||
}
|
||||
|
||||
func AlgOption_MakeSome(value: JwtAlg) -> AlgOption {
|
||||
let o: AlgOption = AlgOption { tag: AlgOption_Some };
|
||||
o.data.Some_0 = value;
|
||||
return o;
|
||||
}
|
||||
|
||||
func AlgOption_MakeNone() -> AlgOption {
|
||||
return AlgOption { tag: AlgOption_None };
|
||||
}
|
||||
|
||||
enum CmdOption {
|
||||
Some(Cmd),
|
||||
None,
|
||||
}
|
||||
|
||||
func CmdOption_MakeSome(value: Cmd) -> CmdOption {
|
||||
let o: CmdOption = CmdOption { tag: CmdOption_Some };
|
||||
o.data.Some_0 = value;
|
||||
return o;
|
||||
}
|
||||
|
||||
func CmdOption_MakeNone() -> CmdOption {
|
||||
return CmdOption { tag: CmdOption_None };
|
||||
}
|
||||
|
||||
enum KeyTypeOption {
|
||||
Some(KeyType),
|
||||
None,
|
||||
}
|
||||
|
||||
func KeyTypeOption_MakeSome(value: KeyType) -> KeyTypeOption {
|
||||
let o: KeyTypeOption = KeyTypeOption { tag: KeyTypeOption_Some };
|
||||
o.data.Some_0 = value;
|
||||
return o;
|
||||
}
|
||||
|
||||
func KeyTypeOption_MakeNone() -> KeyTypeOption {
|
||||
return KeyTypeOption { tag: KeyTypeOption_None };
|
||||
}
|
||||
|
||||
enum KeyResult {
|
||||
Ok(String),
|
||||
Err(String),
|
||||
}
|
||||
|
||||
func KeyResult_MakeOk(value: String) -> KeyResult {
|
||||
let r: KeyResult = KeyResult { tag: KeyResult_Ok };
|
||||
r.data.Ok_0 = value;
|
||||
return r;
|
||||
}
|
||||
|
||||
func KeyResult_MakeErr(msg: String) -> KeyResult {
|
||||
let r: KeyResult = KeyResult { tag: KeyResult_Err };
|
||||
r.data.Err_0 = msg;
|
||||
return r;
|
||||
}
|
||||
|
||||
enum ExitResult {
|
||||
Ok(int),
|
||||
Err(String),
|
||||
}
|
||||
|
||||
func ExitResult_MakeOk(value: int) -> ExitResult {
|
||||
let r: ExitResult = ExitResult { tag: ExitResult_Ok };
|
||||
r.data.Ok_0 = value;
|
||||
return r;
|
||||
}
|
||||
|
||||
func ExitResult_MakeErr(msg: String) -> ExitResult {
|
||||
let r: ExitResult = ExitResult { tag: ExitResult_Err };
|
||||
r.data.Err_0 = msg;
|
||||
return r;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Commands
|
||||
// =============================================================================
|
||||
enum Cmd {
|
||||
Sign,
|
||||
Verify,
|
||||
Decode,
|
||||
Keygen,
|
||||
Help,
|
||||
}
|
||||
|
||||
func ParseCmd(name: String) -> CmdOption {
|
||||
if String_Eq(name, "sign") { return CmdOption_MakeSome(Cmd { tag: Cmd_Sign }); }
|
||||
if String_Eq(name, "verify") { return CmdOption_MakeSome(Cmd { tag: Cmd_Verify }); }
|
||||
if String_Eq(name, "decode") { return CmdOption_MakeSome(Cmd { tag: Cmd_Decode }); }
|
||||
if String_Eq(name, "keygen") { return CmdOption_MakeSome(Cmd { tag: Cmd_Keygen }); }
|
||||
if String_Eq(name, "help") || String_Eq(name, "--help") || String_Eq(name, "-h") {
|
||||
return CmdOption_MakeSome(Cmd { tag: Cmd_Help });
|
||||
}
|
||||
return CmdOption_MakeNone();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Key type generation
|
||||
// =============================================================================
|
||||
enum KeyType {
|
||||
Rsa,
|
||||
Ecdsa,
|
||||
Ed25519,
|
||||
}
|
||||
|
||||
func ParseKeyType(name: String) -> KeyTypeOption {
|
||||
if String_Eq(name, "rsa") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Rsa }); }
|
||||
if String_Eq(name, "ecdsa") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Ecdsa }); }
|
||||
if String_Eq(name, "ed25519") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Ed25519 }); }
|
||||
return KeyTypeOption_MakeNone();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Extend JwtAlg with parsing / introspection methods
|
||||
// =============================================================================
|
||||
extend JwtAlg {
|
||||
func Name(self: JwtAlg) -> String {
|
||||
match self {
|
||||
JwtAlg::HS256 => "HS256",
|
||||
JwtAlg::HS384 => "HS384",
|
||||
JwtAlg::HS512 => "HS512",
|
||||
JwtAlg::RS256 => "RS256",
|
||||
JwtAlg::RS384 => "RS384",
|
||||
JwtAlg::RS512 => "RS512",
|
||||
JwtAlg::ES256 => "ES256",
|
||||
JwtAlg::ES384 => "ES384",
|
||||
JwtAlg::EdDSA => "EdDSA",
|
||||
_ => "HS256",
|
||||
}
|
||||
}
|
||||
|
||||
func IsHmac(self: JwtAlg) -> bool {
|
||||
return self.tag == JwtAlg_HS256 || self.tag == JwtAlg_HS384 || self.tag == JwtAlg_HS512;
|
||||
}
|
||||
|
||||
func NeedsPemFile(self: JwtAlg) -> bool {
|
||||
return self.tag == JwtAlg_RS256 || self.tag == JwtAlg_RS384 || self.tag == JwtAlg_RS512 ||
|
||||
self.tag == JwtAlg_ES256 || self.tag == JwtAlg_ES384;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ParseAlg(name: String) -> AlgOption {
|
||||
if String_Eq(name, "HS256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS256 }); }
|
||||
if String_Eq(name, "HS384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS384 }); }
|
||||
if String_Eq(name, "HS512") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS512 }); }
|
||||
if String_Eq(name, "RS256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS256 }); }
|
||||
if String_Eq(name, "RS384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS384 }); }
|
||||
if String_Eq(name, "RS512") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS512 }); }
|
||||
if String_Eq(name, "ES256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_ES256 }); }
|
||||
if String_Eq(name, "ES384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_ES384 }); }
|
||||
if String_Eq(name, "EdDSA") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_EdDSA }); }
|
||||
return AlgOption_MakeNone();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Help / Usage
|
||||
// =============================================================================
|
||||
func PrintUsage() {
|
||||
PrintLine("╔══════════════════════════════════════════════════════╗");
|
||||
PrintLine(f"║ {AppName} — JWT CLI Tool v{Version} ║");
|
||||
PrintLine("║ Sign, verify, decode JSON Web Tokens ║");
|
||||
PrintLine("╚══════════════════════════════════════════════════════╝");
|
||||
PrintLine("");
|
||||
PrintLine("Usage:");
|
||||
PrintLine(f" {AppName} sign <alg> <key> <claims>");
|
||||
PrintLine(f" {AppName} verify <token> <alg> <key>");
|
||||
PrintLine(f" {AppName} decode <token>");
|
||||
PrintLine(f" {AppName} keygen <type>");
|
||||
PrintLine("");
|
||||
PrintLine("Commands:");
|
||||
PrintLine(" sign Create a signed JWT from claims JSON");
|
||||
PrintLine(" verify Verify a JWT signature and print payload");
|
||||
PrintLine(" decode Decode a JWT without verification");
|
||||
PrintLine(" keygen Generate cryptographic keys");
|
||||
PrintLine("");
|
||||
PrintLine("Algorithms:");
|
||||
PrintLine(" HS256, HS384, HS512 — HMAC (symmetric)");
|
||||
PrintLine(" RS256, RS384, RS512 — RSA PKCS#1 v1.5");
|
||||
PrintLine(" ES256, ES384 — ECDSA P-256 / P-384");
|
||||
PrintLine(" EdDSA — Ed25519");
|
||||
PrintLine("");
|
||||
PrintLine("Key formats:");
|
||||
PrintLine(" HMAC: raw secret string");
|
||||
PrintLine(" RSA: path to PEM private/public key file");
|
||||
PrintLine(" ECDSA: path to PEM private/public key file");
|
||||
PrintLine(" EdDSA: base64-encoded 32-byte raw key");
|
||||
PrintLine("");
|
||||
PrintLine("Key generation:");
|
||||
PrintLine(f" {AppName} keygen rsa # RSA 2048-bit (PEM)");
|
||||
PrintLine(f" {AppName} keygen ecdsa # ECDSA P-256 (PEM)");
|
||||
PrintLine(f" {AppName} keygen ed25519 # Ed25519 (raw base64)");
|
||||
PrintLine("");
|
||||
PrintLine("Examples:");
|
||||
Print(f" {AppName} sign HS256 'my-secret' ");
|
||||
PrintLine("'{\"sub\":\"123\"}'");
|
||||
PrintLine(f" {AppName} verify eyJh... HS256 'my-secret'");
|
||||
PrintLine(f" {AppName} decode eyJh...");
|
||||
PrintLine(f" {AppName} keygen ed25519");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Key resolution — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through
|
||||
// =============================================================================
|
||||
func ResolveKey(alg: JwtAlg, keyArg: String) -> KeyResult {
|
||||
if !alg.NeedsPemFile() {
|
||||
return KeyResult_MakeOk(keyArg);
|
||||
}
|
||||
|
||||
if bux_file_exists(keyArg) == 0 {
|
||||
return KeyResult_MakeErr(f"ERROR: PEM file not found: {keyArg}");
|
||||
}
|
||||
|
||||
let pem: String = bux_read_file(keyArg);
|
||||
if String_Len(pem) == 0 {
|
||||
return KeyResult_MakeErr(f"ERROR: could not read PEM file: {keyArg}");
|
||||
}
|
||||
return KeyResult_MakeOk(pem);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Command: sign
|
||||
// =============================================================================
|
||||
func CmdSign(algName: String, keyArg: String, claimsJson: String) -> ExitResult {
|
||||
let algOpt: AlgOption = ParseAlg(algName);
|
||||
if algOpt.tag != AlgOption_Some {
|
||||
return ExitResult_MakeErr(f"ERROR: unknown algorithm '{algName}'");
|
||||
}
|
||||
let alg: JwtAlg = algOpt.data.Some_0;
|
||||
|
||||
let keyRes: KeyResult = ResolveKey(alg, keyArg);
|
||||
if keyRes.tag != KeyResult_Ok {
|
||||
return ExitResult_MakeErr(keyRes.data.Err_0);
|
||||
}
|
||||
let key: String = keyRes.data.Ok_0;
|
||||
|
||||
let header: String = Jwt_MakeHeader(alg);
|
||||
let token: String = Jwt_Encode(header, claimsJson, alg, key);
|
||||
|
||||
if String_Len(token) == 0 {
|
||||
return ExitResult_MakeErr("ERROR: signing failed");
|
||||
}
|
||||
|
||||
PrintLine(token);
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Command: verify
|
||||
// =============================================================================
|
||||
func CmdVerify(token: String, algName: String, keyArg: String) -> ExitResult {
|
||||
let algOpt: AlgOption = ParseAlg(algName);
|
||||
if algOpt.tag != AlgOption_Some {
|
||||
return ExitResult_MakeErr(f"ERROR: unknown algorithm '{algName}'");
|
||||
}
|
||||
let alg: JwtAlg = algOpt.data.Some_0;
|
||||
|
||||
let keyRes: KeyResult = ResolveKey(alg, keyArg);
|
||||
if keyRes.tag != KeyResult_Ok {
|
||||
return ExitResult_MakeErr(keyRes.data.Err_0);
|
||||
}
|
||||
let key: String = keyRes.data.Ok_0;
|
||||
|
||||
var header: String = "";
|
||||
var payload: String = "";
|
||||
|
||||
if !Jwt_Decode(token, alg, key, &header, &payload) {
|
||||
return ExitResult_MakeErr("✗ Signature INVALID (or malformed token)");
|
||||
}
|
||||
|
||||
PrintLine("✓ Signature valid");
|
||||
PrintLine("");
|
||||
PrintLine("Header:");
|
||||
PrintLine(header);
|
||||
PrintLine("");
|
||||
PrintLine("Payload:");
|
||||
PrintLine(payload);
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Command: decode (no verification)
|
||||
// =============================================================================
|
||||
func CmdDecode(token: String) -> ExitResult {
|
||||
let partCount: uint = String_SplitCount(token, ".");
|
||||
if partCount != 3 {
|
||||
return ExitResult_MakeErr("ERROR: not a valid JWT (expected 3 parts)");
|
||||
}
|
||||
|
||||
let headerB64: String = String_SplitPart(token, ".", 0);
|
||||
let payloadB64: String = String_SplitPart(token, ".", 1);
|
||||
let sigB64: String = String_SplitPart(token, ".", 2);
|
||||
|
||||
let headerJson: String = Base64URL_Decode(headerB64);
|
||||
let payloadJson: String = Base64URL_Decode(payloadB64);
|
||||
|
||||
PrintLine("Decoded (no verification):");
|
||||
PrintLine("");
|
||||
PrintLine("Header:");
|
||||
PrintLine(headerJson);
|
||||
PrintLine("");
|
||||
PrintLine("Payload:");
|
||||
PrintLine(payloadJson);
|
||||
PrintLine("");
|
||||
Print("Signature (base64url): ");
|
||||
PrintLine(sigB64);
|
||||
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Command: keygen
|
||||
// =============================================================================
|
||||
func CmdKeygen(keyType: String) -> ExitResult {
|
||||
let ktOpt: KeyTypeOption = ParseKeyType(keyType);
|
||||
if ktOpt.tag != KeyTypeOption_Some {
|
||||
return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'. Use: rsa, ecdsa, ed25519");
|
||||
}
|
||||
let kt: KeyType = ktOpt.data.Some_0;
|
||||
|
||||
if kt.tag == KeyType_Ed25519 {
|
||||
let pubBuf: *void = bux_alloc(32);
|
||||
let priv: *void = bux_alloc(32);
|
||||
if !Ed25519_Keypair(pubBuf, priv) {
|
||||
return ExitResult_MakeErr("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)");
|
||||
}
|
||||
let pubB64: String = bux_base64_encode(pubBuf as String, 32);
|
||||
let privB64: String = bux_base64_encode(priv as String, 32);
|
||||
PrintLine("Ed25519 keypair (base64):");
|
||||
PrintLine(f" Public: {pubB64}");
|
||||
PrintLine(f" Private: {privB64}");
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
if kt.tag == KeyType_Rsa {
|
||||
PrintLine("RSA key generation requires OpenSSL CLI:");
|
||||
PrintLine(" openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048");
|
||||
PrintLine(" openssl rsa -in private.pem -pubout -out public.pem");
|
||||
PrintLine("");
|
||||
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
if kt.tag == KeyType_Ecdsa {
|
||||
PrintLine("ECDSA key generation requires OpenSSL CLI:");
|
||||
PrintLine(" openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem");
|
||||
PrintLine(" openssl ec -in ec_private.pem -pubout -out ec_public.pem");
|
||||
PrintLine("");
|
||||
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dispatch a parsed command to its handler
|
||||
// =============================================================================
|
||||
func RunCommand(cmd: Cmd, args: Array<String>) -> ExitResult {
|
||||
if cmd.tag == Cmd_Help {
|
||||
PrintUsage();
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
if cmd.tag == Cmd_Sign {
|
||||
if args.len < 5 {
|
||||
return ExitResult_MakeErr("ERROR: 'sign' requires: <alg> <key> <claims_json>");
|
||||
}
|
||||
return CmdSign(args[2], args[3], args[4]);
|
||||
}
|
||||
if cmd.tag == Cmd_Verify {
|
||||
if args.len < 5 {
|
||||
return ExitResult_MakeErr("ERROR: 'verify' requires: <token> <alg> <key>");
|
||||
}
|
||||
return CmdVerify(args[2], args[3], args[4]);
|
||||
}
|
||||
if cmd.tag == Cmd_Decode {
|
||||
if args.len < 3 {
|
||||
return ExitResult_MakeErr("ERROR: 'decode' requires: <token>");
|
||||
}
|
||||
return CmdDecode(args[2]);
|
||||
}
|
||||
if cmd.tag == Cmd_Keygen {
|
||||
if args.len < 3 {
|
||||
return ExitResult_MakeErr("ERROR: 'keygen' requires: <type>");
|
||||
}
|
||||
return CmdKeygen(args[2]);
|
||||
}
|
||||
return ExitResult_MakeErr("ERROR: unhandled command");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Entry Point
|
||||
// =============================================================================
|
||||
func Main() -> int {
|
||||
let argc: int = bux_argc();
|
||||
|
||||
// Collect CLI arguments into a generic Array<String>
|
||||
let args: Array<String> = Array_New<String>(argc as uint);
|
||||
for i in 0..argc {
|
||||
Array_Push<String>(&args, bux_argv(i));
|
||||
}
|
||||
|
||||
if args.len < 2 {
|
||||
PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
let command: String = args[1];
|
||||
let cmdOpt: CmdOption = ParseCmd(command);
|
||||
if cmdOpt.tag != CmdOption_Some {
|
||||
PrintLine(f"ERROR: unknown command '{command}'");
|
||||
PrintLine(f"Run '{AppName} help' for usage.");
|
||||
return 1;
|
||||
}
|
||||
let cmd: Cmd = cmdOpt.data.Some_0;
|
||||
|
||||
let result: ExitResult = RunCommand(cmd, args);
|
||||
if result.tag == ExitResult_Err {
|
||||
PrintLine(result.data.Err_0);
|
||||
return 1;
|
||||
}
|
||||
return result.data.Ok_0;
|
||||
}
|
||||
|
||||
} // module Main
|
||||
@@ -0,0 +1,189 @@
|
||||
# Nexus
|
||||
|
||||
**High-performance, multi-threaded HTTP/1.1, HTTP/2 & WebSocket server — built with the [Bux](https://github.com/bux-lang/bux) programming language.**
|
||||
|
||||
Nexus is a from-scratch web server that demonstrates Bux's systems-programming capabilities: raw TCP sockets, pthread-based concurrency, manual memory management, and zero-dependency C ABI interop — all from a clean, modern syntax.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
| Area | What's Implemented |
|
||||
|------|-------------------|
|
||||
| **HTTP/1.1** | Full request parsing (method, path, headers, body), response building with status codes, content negotiation |
|
||||
| **Multi-threaded** | Configurable worker pool using the multi-accept pattern — each worker calls `accept()` directly on the shared listen socket |
|
||||
| **HTTP/2** | Connection preface detection (`PRI * HTTP/2.0`), upgrade-aware routing |
|
||||
| **WebSocket** | RFC 6455 upgrade handshake detection, `Sec-WebSocket-Key` extraction |
|
||||
| **Static files** | Serves from `public/` with MIME-type detection for 20+ file types, directory-traversal protection |
|
||||
| **JSON API** | Built-in `/api/health` and `/api/info` endpoints |
|
||||
| **Logging** | Per-request structured logging (method, path, status code) |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# From the project root
|
||||
cd apps/nexus
|
||||
|
||||
# Build with the bootstrap compiler (Nim)
|
||||
../../buxc build
|
||||
|
||||
# Or with the self-hosted compiler
|
||||
../../buxc_lir build
|
||||
|
||||
# Run
|
||||
./nexus
|
||||
```
|
||||
|
||||
Server starts on `http://0.0.0.0:8080`:
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════╗
|
||||
║ Nexus HTTP Server v0.1.0 ║
|
||||
║ High-performance multi-threaded HTTP/1.1 ║
|
||||
║ HTTP/2 & WebSocket detection included ║
|
||||
║ Built with Bux ║
|
||||
╚══════════════════════════════════════════════╝
|
||||
|
||||
✓ Server listening on http://0.0.0.0:8080
|
||||
✓ Worker threads: 4
|
||||
✓ Static files: ./public/
|
||||
|
||||
Endpoints:
|
||||
GET / — Static files (public/)
|
||||
GET /api/health — Health check (JSON)
|
||||
GET /api/info — Server info (JSON)
|
||||
GET /ws — WebSocket upgrade
|
||||
ANY /* — Static file serving
|
||||
```
|
||||
|
||||
## Testing It
|
||||
|
||||
```bash
|
||||
# Home page (HTML)
|
||||
curl http://localhost:8080/
|
||||
|
||||
# Health check
|
||||
curl http://localhost:8080/api/health
|
||||
# → {"status":"ok","server":"Nexus","version":"0.1.0"}
|
||||
|
||||
# Server info
|
||||
curl http://localhost:8080/api/info
|
||||
# → {"name":"Nexus","language":"Bux","features":[...]}
|
||||
|
||||
# Static file (404 page)
|
||||
curl http://localhost:8080/404.html
|
||||
|
||||
# Nonexistent file
|
||||
curl http://localhost:8080/nope
|
||||
# → 404 Not Found
|
||||
|
||||
# WebSocket upgrade attempt
|
||||
curl -H "Upgrade: websocket" \
|
||||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||
http://localhost:8080/ws
|
||||
# → 101 Switching Protocols
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Thread Model — Multi-Accept
|
||||
|
||||
Nexus uses the **multi-accept** pattern rather than a traditional thread pool with a work queue:
|
||||
|
||||
```
|
||||
Main Thread Worker 1 Worker 2 Worker 3
|
||||
│ │ │ │
|
||||
├─ socket()/bind()/listen() │ │ │
|
||||
├─ spawn Worker() ──────────► │ │
|
||||
├─ spawn Worker() ─────────────────────────► │
|
||||
├─ spawn Worker() ────────────────────────────────────────►
|
||||
│ │ │ │
|
||||
▼ Worker() ▼ accept() ▼ accept() ▼ accept()
|
||||
accept() loop │ │ │
|
||||
```
|
||||
|
||||
Each worker thread calls `accept()` on the **same** listening socket. The Linux kernel distributes incoming connections across the blocked accept calls — the same mechanism used by nginx and Apache prefork. No mutex, no queue, no context switching between a dispatcher and workers.
|
||||
|
||||
### Request Lifecycle
|
||||
|
||||
```
|
||||
Client connects
|
||||
│
|
||||
▼
|
||||
Net_Accept() → client fd
|
||||
│
|
||||
▼
|
||||
Net_Recv(fd, 8192) → raw bytes
|
||||
│
|
||||
▼
|
||||
ParseRequest()
|
||||
├─ Split headers on \r\n\r\n
|
||||
├─ Parse request line → method, path, version
|
||||
└─ Parse header lines → key-value array
|
||||
│
|
||||
▼
|
||||
Router_Dispatch()
|
||||
├─ /api/* → JSON handlers
|
||||
├─ /ws → WebSocket upgrade
|
||||
├─ Upgrade header → HTTP/2 or WS detection
|
||||
└─ /* → Static file serving
|
||||
│
|
||||
▼
|
||||
BuildResponse() → HTTP/1.1 status line + headers + body
|
||||
│
|
||||
▼
|
||||
Net_Send(fd, response) → bytes to client
|
||||
│
|
||||
▼
|
||||
Net_Close(fd)
|
||||
```
|
||||
|
||||
### Design Decisions
|
||||
|
||||
- **No keep-alive by default.** Each connection is closed after one response. This avoids blocking worker threads on idle clients (Bux doesn't yet expose `SO_RCVTIMEO`).
|
||||
- **Linear header array instead of hash map.** HTTP requests typically carry 5–15 headers. A linear scan over a key-value array is faster than hashing for this N, and avoids the complexity of iterating over Bux's generic `StringMap`.
|
||||
- **Raw extern calls for the string builder.** The stdlib `StringBuilder` wrapper adds a struct indirection. Calling `bux_sb_new` / `bux_sb_append` / `bux_sb_build` directly is simpler and equally safe.
|
||||
- **WebSocket accept key is a placeholder.** A full implementation needs SHA-1 hashing and Base64 encoding. These aren't in Bux's stdlib yet; they can be added as extern C functions when needed.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
apps/nexus/
|
||||
├── bux.toml # Package manifest
|
||||
├── README.md # This file
|
||||
├── src/
|
||||
│ └── Main.bux # The entire server (~640 lines)
|
||||
└── public/
|
||||
├── index.html # Landing page
|
||||
└── 404.html # Error page
|
||||
```
|
||||
|
||||
Everything lives in one file (`src/Main.bux`) by design — it keeps the module graph flat and the build fast. As the server grows, the HTTP parser, router, and handlers can be split into separate modules.
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit the constants at the top of `src/Main.bux`:
|
||||
|
||||
```bux
|
||||
const SERVER_PORT: int = 8080; // Listen port
|
||||
const THREAD_COUNT: int = 4; // Number of worker threads
|
||||
const RECV_BUF_SIZE: int = 8192; // Receive buffer per request
|
||||
const SERVER_NAME: String = "Nexus/0.1.0 (Bux)";
|
||||
const PUBLIC_DIR: String = "public"; // Static files directory
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Keep-alive with configurable socket timeout
|
||||
- [ ] Full WebSocket frame read/write (requires SHA-1 + Base64)
|
||||
- [ ] HTTP/2 binary framing layer (HPACK, stream multiplexing)
|
||||
- [ ] SSL/TLS via OpenSSL extern bindings
|
||||
- [ ] Middleware / filter chain
|
||||
- [ ] Request body parsing (JSON, form-encoded, multipart)
|
||||
- [ ] Virtual hosts
|
||||
- [ ] Access logging to file
|
||||
- [ ] Rate limiting
|
||||
|
||||
## License
|
||||
|
||||
Nexus is part of the Bux project. See the root [LICENSE](../../LICENSE) for terms.
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:137
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:124
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:124
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:124
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:124
|
||||
@@ -0,0 +1,2 @@
|
||||
timeout: командата „„../../buxc““ не може да бъде изпълнена: Няма такъв файл или директория
|
||||
EXIT:127
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:137
|
||||
@@ -0,0 +1,8 @@
|
||||
[Package]
|
||||
Name = "nexus"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
Description = "High-performance multi-threaded HTTP/1.1, HTTP/2 and WebSocket server"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 — Not Found | Nexus</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
h1 { font-size: 4rem; color: #f85149; margin-bottom: 0.5rem; }
|
||||
p { color: #8b949e; font-size: 1.2rem; }
|
||||
.back { margin-top: 2rem; }
|
||||
.back a { color: #58a6ff; text-decoration: none; }
|
||||
.back a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404</h1>
|
||||
<p>The requested resource was not found on this server.</p>
|
||||
<p class="back"><a href="/">← Back to Home</a></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<html><body>Hello Nexus</body></html>
|
||||
@@ -0,0 +1,21 @@
|
||||
module Config {
|
||||
|
||||
pub struct ServerConfig {
|
||||
bindAddr: String;
|
||||
port: int;
|
||||
workerCount: int;
|
||||
publicDir: String;
|
||||
backlog: int;
|
||||
}
|
||||
|
||||
pub const func DefaultConfig() -> ServerConfig {
|
||||
return ServerConfig {
|
||||
bindAddr: "0.0.0.0",
|
||||
port: 8080,
|
||||
workerCount: 4,
|
||||
publicDir: "public",
|
||||
backlog: 128,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
module Errors {
|
||||
|
||||
import Http::{HttpRequest};
|
||||
|
||||
pub enum HttpError {
|
||||
BadRequest,
|
||||
NotFound,
|
||||
MethodNotAllowed,
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
pub enum ParseResult {
|
||||
Ok(HttpRequest),
|
||||
Err(HttpError),
|
||||
}
|
||||
|
||||
pub enum FileResult {
|
||||
Ok(String),
|
||||
Err(HttpError),
|
||||
}
|
||||
|
||||
pub func ParseResult_NewOk(req: HttpRequest) -> ParseResult {
|
||||
let r: ParseResult = ParseResult { tag: ParseResult_Ok };
|
||||
r.data.Ok_0 = req;
|
||||
return r;
|
||||
}
|
||||
|
||||
pub func ParseResult_NewErr(err: HttpError) -> ParseResult {
|
||||
let r: ParseResult = ParseResult { tag: ParseResult_Err };
|
||||
r.data.Err_0 = err;
|
||||
return r;
|
||||
}
|
||||
|
||||
pub func FileResult_NewOk(content: String) -> FileResult {
|
||||
let r: FileResult = FileResult { tag: FileResult_Ok };
|
||||
r.data.Ok_0 = content;
|
||||
return r;
|
||||
}
|
||||
|
||||
pub func FileResult_NewErr(err: HttpError) -> FileResult {
|
||||
let r: FileResult = FileResult { tag: FileResult_Err };
|
||||
r.data.Err_0 = err;
|
||||
return r;
|
||||
}
|
||||
|
||||
pub func FileResult_IsOk(r: FileResult) -> bool {
|
||||
return r.tag == FileResult_Ok;
|
||||
}
|
||||
|
||||
pub func FileResult_Unwrap(r: FileResult) -> String {
|
||||
return r.data.Ok_0;
|
||||
}
|
||||
|
||||
pub func FileResult_UnwrapErr(r: FileResult) -> HttpError {
|
||||
return r.data.Err_0;
|
||||
}
|
||||
|
||||
pub func HttpError_ToString(err: HttpError) -> String {
|
||||
if err.tag == HttpError_BadRequest { return "Bad Request"; }
|
||||
if err.tag == HttpError_NotFound { return "Not Found"; }
|
||||
if err.tag == HttpError_MethodNotAllowed { return "Method Not Allowed"; }
|
||||
return err.data.InternalError_0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
module Handlers {
|
||||
|
||||
import Std::String::{String_Eq, String_Contains};
|
||||
import Http::{HttpMethod, HttpRequest, HttpResponse, Http_NewResponse, Http_MimeType, RequestHeader_Get};
|
||||
import Errors::{HttpError, FileResult, FileResult_NewOk, FileResult_NewErr, FileResult_IsOk, FileResult_Unwrap, FileResult_UnwrapErr, HttpError_ToString};
|
||||
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
extern func bux_file_exists(path: String) -> int;
|
||||
extern func bux_read_file(path: String) -> String;
|
||||
extern func bux_path_join(a: String, b: String) -> String;
|
||||
extern func bux_sb_new(initial_cap: uint) -> *void;
|
||||
extern func bux_sb_append(sb: *void, s: String);
|
||||
extern func bux_sb_append_int(sb: *void, n: int64);
|
||||
extern func bux_sb_build(sb: *void) -> String;
|
||||
extern func bux_sb_free(sb: *void);
|
||||
|
||||
pub func NotFoundResponse() -> HttpResponse {
|
||||
return Http_NewResponse(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
|
||||
}
|
||||
|
||||
pub func MethodNotAllowedResponse() -> HttpResponse {
|
||||
return Http_NewResponse(405, "text/plain; charset=utf-8", "Method Not Allowed");
|
||||
}
|
||||
|
||||
pub func ReadStaticFile(requestPath: String) -> FileResult {
|
||||
if String_Contains(requestPath, "..") {
|
||||
return FileResult_NewErr(HttpError { tag: HttpError_NotFound });
|
||||
}
|
||||
|
||||
var filePath: String = requestPath;
|
||||
if String_Eq(filePath, "/") {
|
||||
filePath = "/index.html";
|
||||
}
|
||||
|
||||
let fullPath: String = bux_path_join("public", filePath);
|
||||
if bux_file_exists(fullPath) == 0 {
|
||||
return FileResult_NewErr(HttpError { tag: HttpError_NotFound });
|
||||
}
|
||||
|
||||
let content: String = bux_read_file(fullPath);
|
||||
return FileResult_NewOk(content);
|
||||
}
|
||||
|
||||
func FileErrorResponse(err: HttpError) -> HttpResponse {
|
||||
match err {
|
||||
HttpError::NotFound => NotFoundResponse(),
|
||||
_ => Http_NewResponse(500, "text/plain; charset=utf-8", HttpError_ToString(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub func ServeStaticFile(req: HttpRequest) -> HttpResponse {
|
||||
if req.method != HttpMethod_GET && req.method != HttpMethod_HEAD {
|
||||
return MethodNotAllowedResponse();
|
||||
}
|
||||
|
||||
let result: FileResult = ReadStaticFile(req.path);
|
||||
if FileResult_IsOk(result) {
|
||||
let content: String = FileResult_Unwrap(result);
|
||||
return Http_NewResponse(200, Http_MimeType(req.path), content);
|
||||
}
|
||||
return FileErrorResponse(FileResult_UnwrapErr(result));
|
||||
}
|
||||
|
||||
pub func HandleApiHealth() -> HttpResponse {
|
||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||
"{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.2.0\"}");
|
||||
}
|
||||
|
||||
pub func HandleApiInfo() -> HttpResponse {
|
||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||
"{\"name\":\"Nexus\",\"language\":\"Bux\",\"features\":[\"HTTP/1.1\",\"thread-pool\",\"algebraic-enums\"]}");
|
||||
}
|
||||
|
||||
pub func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse {
|
||||
let wsKey: String = RequestHeader_Get(req, "Sec-WebSocket-Key");
|
||||
|
||||
var resp: HttpResponse;
|
||||
resp.statusCode = 101;
|
||||
resp.contentType = "";
|
||||
resp.body = "";
|
||||
|
||||
let sb: *void = bux_sb_new(256);
|
||||
bux_sb_append(sb, "Upgrade: websocket\r\n");
|
||||
bux_sb_append(sb, "Connection: Upgrade\r\n");
|
||||
bux_sb_append(sb, "Sec-WebSocket-Accept: ");
|
||||
bux_sb_append(sb, wsKey);
|
||||
bux_sb_append(sb, "\r\n");
|
||||
resp.extraHeaders = bux_sb_build(sb);
|
||||
bux_sb_free(sb);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
module Http {
|
||||
|
||||
import Std::Array::{Array};
|
||||
import Std::String::{String_Eq, String_EndsWith, String_Contains};
|
||||
|
||||
pub enum HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
pub struct HeaderEntry {
|
||||
key: String;
|
||||
value: String;
|
||||
}
|
||||
|
||||
pub struct HttpRequest {
|
||||
method: HttpMethod;
|
||||
path: String;
|
||||
version: String;
|
||||
body: String;
|
||||
headers: Array<HeaderEntry>;
|
||||
}
|
||||
|
||||
pub struct HttpResponse {
|
||||
statusCode: int;
|
||||
contentType: String;
|
||||
body: String;
|
||||
extraHeaders: String;
|
||||
}
|
||||
|
||||
pub func Http_StatusText(code: int) -> String {
|
||||
if code == 200 { return "OK"; }
|
||||
if code == 201 { return "Created"; }
|
||||
if code == 204 { return "No Content"; }
|
||||
if code == 301 { return "Moved Permanently"; }
|
||||
if code == 302 { return "Found"; }
|
||||
if code == 304 { return "Not Modified"; }
|
||||
if code == 400 { return "Bad Request"; }
|
||||
if code == 401 { return "Unauthorized"; }
|
||||
if code == 403 { return "Forbidden"; }
|
||||
if code == 404 { return "Not Found"; }
|
||||
if code == 405 { return "Method Not Allowed"; }
|
||||
if code == 413 { return "Payload Too Large"; }
|
||||
if code == 414 { return "URI Too Long"; }
|
||||
if code == 500 { return "Internal Server Error"; }
|
||||
if code == 501 { return "Not Implemented"; }
|
||||
if code == 503 { return "Service Unavailable"; }
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
pub func Http_MimeType(path: String) -> String {
|
||||
if String_EndsWith(path, ".html") || String_EndsWith(path, ".htm") { return "text/html; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".css") { return "text/css; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".js") { return "application/javascript; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".json") { return "application/json; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".xml") { return "application/xml; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".txt") { return "text/plain; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".png") { return "image/png"; }
|
||||
if String_EndsWith(path, ".jpg") || String_EndsWith(path, ".jpeg") { return "image/jpeg"; }
|
||||
if String_EndsWith(path, ".gif") { return "image/gif"; }
|
||||
if String_EndsWith(path, ".svg") { return "image/svg+xml"; }
|
||||
if String_EndsWith(path, ".ico") { return "image/x-icon"; }
|
||||
if String_EndsWith(path, ".webp") { return "image/webp"; }
|
||||
if String_EndsWith(path, ".woff2") { return "font/woff2"; }
|
||||
if String_EndsWith(path, ".woff") { return "font/woff"; }
|
||||
if String_EndsWith(path, ".wasm") { return "application/wasm"; }
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
pub func Http_MethodName(m: HttpMethod) -> String {
|
||||
match m {
|
||||
HttpMethod::GET => "GET",
|
||||
HttpMethod::POST => "POST",
|
||||
HttpMethod::PUT => "PUT",
|
||||
HttpMethod::DELETE => "DELETE",
|
||||
HttpMethod::PATCH => "PATCH",
|
||||
HttpMethod::HEAD => "HEAD",
|
||||
HttpMethod::OPTIONS => "OPTIONS",
|
||||
HttpMethod::UNKNOWN => "UNKNOWN",
|
||||
}
|
||||
}
|
||||
|
||||
pub func Http_NewResponse(code: int, contentType: String, body: String) -> HttpResponse {
|
||||
var resp: HttpResponse;
|
||||
resp.statusCode = code;
|
||||
resp.contentType = contentType;
|
||||
resp.body = body;
|
||||
resp.extraHeaders = "";
|
||||
return resp;
|
||||
}
|
||||
|
||||
pub func RequestHeader_Get(req: HttpRequest, key: String) -> String {
|
||||
for entry in req.headers {
|
||||
if String_Eq(entry.key, key) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
module Main {
|
||||
|
||||
import Config::{ServerConfig, DefaultConfig};
|
||||
import Http::{HttpMethod};
|
||||
import Router::{Handler, Route, Router};
|
||||
import Server::{RunServer};
|
||||
import Std::Array::{Array, Array_New, Array_Push};
|
||||
|
||||
func BuildRouter() -> Router {
|
||||
var routes: Array<Route> = Array_New<Route>(8);
|
||||
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/api/health",
|
||||
handler: Handler { tag: Handler_ApiHealth },
|
||||
});
|
||||
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/api/info",
|
||||
handler: Handler { tag: Handler_ApiInfo },
|
||||
});
|
||||
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/ws",
|
||||
handler: Handler { tag: Handler_WsUpgrade },
|
||||
});
|
||||
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/",
|
||||
handler: Handler { tag: Handler_StaticFile },
|
||||
});
|
||||
|
||||
return Router {
|
||||
routes: routes,
|
||||
notFound: Handler { tag: Handler_NotFound },
|
||||
};
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let config: ServerConfig = DefaultConfig();
|
||||
let router: Router = BuildRouter();
|
||||
|
||||
return RunServer(config, router);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
module Parser {
|
||||
|
||||
import Std::String::{String_Len, String_Eq, String_Trim};
|
||||
import Std::Array::{Array, Array_New, Array_Push};
|
||||
import Http::{HttpMethod, HttpRequest, HeaderEntry};
|
||||
import Errors::{HttpError, ParseResult, ParseResult_NewOk, ParseResult_NewErr};
|
||||
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
extern func bux_strstr(haystack: String, needle: String) -> String;
|
||||
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
|
||||
extern func bux_str_offset(pos: String, base: String) -> uint;
|
||||
|
||||
pub func ParseMethod(s: String) -> HttpMethod {
|
||||
if String_Eq(s, "GET") { return HttpMethod { tag: HttpMethod_GET }; }
|
||||
if String_Eq(s, "POST") { return HttpMethod { tag: HttpMethod_POST }; }
|
||||
if String_Eq(s, "PUT") { return HttpMethod { tag: HttpMethod_PUT }; }
|
||||
if String_Eq(s, "DELETE") { return HttpMethod { tag: HttpMethod_DELETE }; }
|
||||
if String_Eq(s, "PATCH") { return HttpMethod { tag: HttpMethod_PATCH }; }
|
||||
if String_Eq(s, "HEAD") { return HttpMethod { tag: HttpMethod_HEAD }; }
|
||||
if String_Eq(s, "OPTIONS") { return HttpMethod { tag: HttpMethod_OPTIONS }; }
|
||||
return HttpMethod { tag: HttpMethod_UNKNOWN };
|
||||
}
|
||||
|
||||
func Slice(raw: String, start: int, len: int) -> String {
|
||||
if start < 0 || len <= 0 { return ""; }
|
||||
return bux_str_slice(raw, start as uint, len as uint);
|
||||
}
|
||||
|
||||
func FindCrlf(raw: String, start: int) -> int {
|
||||
let rawLen: uint = bux_strlen(raw);
|
||||
if start as uint >= rawLen { return -1; }
|
||||
let tail: String = bux_str_slice(raw, start as uint, rawLen - start as uint);
|
||||
let hit: String = bux_strstr(tail, "\r\n");
|
||||
if String_Len(hit) == 0 { return -1; }
|
||||
let offset: uint = bux_str_offset(hit, tail);
|
||||
return start + offset as int;
|
||||
}
|
||||
|
||||
func ParseHeaders(raw: String, start: int, end: int) -> Array<HeaderEntry> {
|
||||
var headers: Array<HeaderEntry> = Array_New<HeaderEntry>(16);
|
||||
var pos: int = start;
|
||||
while pos < end {
|
||||
let lineEnd: int = FindCrlf(raw, pos);
|
||||
if lineEnd < 0 || lineEnd >= end { break; }
|
||||
let lineLen: int = lineEnd - pos;
|
||||
if lineLen > 0 {
|
||||
let line: String = Slice(raw, pos, lineLen);
|
||||
let colon: String = bux_strstr(line, ":");
|
||||
if String_Len(colon) > 0 {
|
||||
let keyLen: uint = bux_str_offset(colon, line);
|
||||
let key: String = String_Trim(Slice(line, 0, keyLen as int));
|
||||
let valStart: int = keyLen as int + 1;
|
||||
let val: String = String_Trim(Slice(line, valStart, lineLen - valStart));
|
||||
let entry: HeaderEntry = HeaderEntry { key: key, value: val };
|
||||
Array_Push<HeaderEntry>(&headers, entry);
|
||||
}
|
||||
}
|
||||
pos = lineEnd + 2;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
pub func ParseRequest(raw: String) -> ParseResult {
|
||||
let rawLen: uint = bux_strlen(raw);
|
||||
if rawLen == 0 {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
|
||||
// Find end of request line
|
||||
let lineEnd: int = FindCrlf(raw, 0);
|
||||
if lineEnd < 0 {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
|
||||
// Split request line: METHOD PATH VERSION
|
||||
var methodEnd: int = -1;
|
||||
var pathStart: int = -1;
|
||||
var pathEnd: int = -1;
|
||||
var i: int = 0;
|
||||
while i < lineEnd {
|
||||
if raw[i] as int == 32 { // space
|
||||
if methodEnd < 0 {
|
||||
methodEnd = i;
|
||||
pathStart = i + 1;
|
||||
} else if pathEnd < 0 {
|
||||
pathEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
if methodEnd < 0 || pathStart < 0 || pathEnd < 0 {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
|
||||
let methodStr: String = Slice(raw, 0, methodEnd);
|
||||
let path: String = Slice(raw, pathStart, pathEnd - pathStart);
|
||||
let version: String = Slice(raw, pathEnd + 1, lineEnd - pathEnd - 1);
|
||||
|
||||
// Find header/body boundary
|
||||
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
||||
var headers: Array<HeaderEntry> = Array_New<HeaderEntry>(16);
|
||||
var body: String = "";
|
||||
if String_Len(boundary) > 0 {
|
||||
let headerEnd: uint = bux_str_offset(boundary, raw);
|
||||
headers = ParseHeaders(raw, lineEnd + 2, headerEnd as int);
|
||||
let bodyStart: uint = headerEnd + 4;
|
||||
if bodyStart < rawLen {
|
||||
body = bux_str_slice(raw, bodyStart, rawLen - bodyStart);
|
||||
}
|
||||
} else {
|
||||
headers = ParseHeaders(raw, lineEnd + 2, rawLen as int);
|
||||
}
|
||||
|
||||
if String_Eq(path, "") {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
|
||||
let req: HttpRequest = HttpRequest {
|
||||
method: ParseMethod(methodStr),
|
||||
path: path,
|
||||
version: version,
|
||||
body: body,
|
||||
headers: headers,
|
||||
};
|
||||
return ParseResult_NewOk(req);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
module Router {
|
||||
|
||||
import Std::Array::{Array};
|
||||
import Std::String::{String_Eq};
|
||||
import Http::{HttpMethod, HttpRequest, HttpResponse, Http_NewResponse};
|
||||
import Handlers::{ServeStaticFile, HandleApiHealth, HandleApiInfo, HandleWebSocketUpgrade, NotFoundResponse};
|
||||
|
||||
pub enum Handler {
|
||||
StaticFile,
|
||||
ApiHealth,
|
||||
ApiInfo,
|
||||
WsUpgrade,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
pub struct Route {
|
||||
method: HttpMethod;
|
||||
path: String;
|
||||
handler: Handler;
|
||||
}
|
||||
|
||||
pub struct Router {
|
||||
routes: Array<Route>;
|
||||
notFound: Handler;
|
||||
}
|
||||
|
||||
pub func Handler_Handle(h: Handler, req: HttpRequest) -> HttpResponse {
|
||||
match h {
|
||||
Handler::StaticFile => ServeStaticFile(req),
|
||||
Handler::ApiHealth => HandleApiHealth(),
|
||||
Handler::ApiInfo => HandleApiInfo(),
|
||||
Handler::WsUpgrade => HandleWebSocketUpgrade(req),
|
||||
Handler::NotFound => NotFoundResponse(),
|
||||
}
|
||||
}
|
||||
|
||||
pub func Router_Dispatch(r: Router, req: HttpRequest) -> HttpResponse {
|
||||
for route in r.routes {
|
||||
if route.method == req.method && String_Eq(route.path, req.path) {
|
||||
return Handler_Handle(route.handler, req);
|
||||
}
|
||||
}
|
||||
return Handler_Handle(r.notFound, req);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
module Server {
|
||||
|
||||
import Std::Io::{Print, PrintLine, PrintInt};
|
||||
import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError};
|
||||
import Std::String::{String_Len, String_StartsWith};
|
||||
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
|
||||
import Config::{ServerConfig};
|
||||
import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse};
|
||||
import Errors::{ParseResult};
|
||||
import Parser::{ParseRequest};
|
||||
import Router::{Router, Router_Dispatch};
|
||||
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
extern func bux_sb_new(initial_cap: uint) -> *void;
|
||||
extern func bux_sb_append(sb: *void, s: String);
|
||||
extern func bux_sb_append_int(sb: *void, n: int64);
|
||||
extern func bux_sb_build(sb: *void) -> String;
|
||||
extern func bux_sb_free(sb: *void);
|
||||
|
||||
pub struct ConnectionTask {
|
||||
fd: int;
|
||||
}
|
||||
|
||||
pub func BuildResponse(resp: HttpResponse) -> String {
|
||||
let sb: *void = bux_sb_new(4096);
|
||||
|
||||
bux_sb_append(sb, "HTTP/1.1 ");
|
||||
bux_sb_append_int(sb, resp.statusCode as int64);
|
||||
bux_sb_append(sb, " ");
|
||||
bux_sb_append(sb, Http_StatusText(resp.statusCode));
|
||||
bux_sb_append(sb, "\r\n");
|
||||
|
||||
bux_sb_append(sb, "Server: Nexus/0.2.0 (Bux)\r\n");
|
||||
|
||||
if bux_strlen(resp.extraHeaders) > 0 {
|
||||
bux_sb_append(sb, resp.extraHeaders);
|
||||
}
|
||||
|
||||
if bux_strlen(resp.contentType) > 0 {
|
||||
bux_sb_append(sb, "Content-Type: ");
|
||||
bux_sb_append(sb, resp.contentType);
|
||||
bux_sb_append(sb, "\r\n");
|
||||
}
|
||||
|
||||
let bodyLen: uint = bux_strlen(resp.body);
|
||||
bux_sb_append(sb, "Content-Length: ");
|
||||
bux_sb_append_int(sb, bodyLen as int64);
|
||||
bux_sb_append(sb, "\r\n");
|
||||
bux_sb_append(sb, "Connection: close\r\n");
|
||||
bux_sb_append(sb, "\r\n");
|
||||
|
||||
if bodyLen > 0 {
|
||||
bux_sb_append(sb, resp.body);
|
||||
}
|
||||
|
||||
let result: String = bux_sb_build(sb);
|
||||
bux_sb_free(sb);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub func HandleConnection(fd: int, router: Router) {
|
||||
let raw: String = Net_Recv(fd, 8192);
|
||||
if String_Len(raw) == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// HTTP/2 preface detection
|
||||
if String_StartsWith(raw, "PRI * HTTP/2.0") {
|
||||
let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8",
|
||||
"HTTP/2 detected — full support planned for future release.\r\n");
|
||||
Net_Send(fd, BuildResponse(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: ParseResult = ParseRequest(raw);
|
||||
if parsed.tag == ParseResult_Ok {
|
||||
let req: HttpRequest = parsed.data.Ok_0;
|
||||
let resp: HttpResponse = Router_Dispatch(router, req);
|
||||
Net_Send(fd, BuildResponse(resp));
|
||||
} else {
|
||||
let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request");
|
||||
Net_Send(fd, BuildResponse(resp));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WorkerCtx {
|
||||
taskQueue: *Channel<ConnectionTask>;
|
||||
router: Router;
|
||||
}
|
||||
|
||||
pub func Worker(ctx: *WorkerCtx) {
|
||||
while true {
|
||||
let task: ConnectionTask = Channel_Recv<ConnectionTask>(ctx.taskQueue);
|
||||
HandleConnection(task.fd, ctx.router);
|
||||
Net_Close(task.fd);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcceptorCtx {
|
||||
serverFd: int;
|
||||
taskQueue: *Channel<ConnectionTask>;
|
||||
}
|
||||
|
||||
pub func Acceptor(ctx: *AcceptorCtx) {
|
||||
while true {
|
||||
let fd: int = Net_Accept(ctx.serverFd);
|
||||
if fd >= 0 {
|
||||
let task: ConnectionTask = ConnectionTask { fd: fd };
|
||||
Channel_Send<ConnectionTask>(ctx.taskQueue, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub func RunServer(config: ServerConfig, router: Router) -> int {
|
||||
PrintLine("================================================");
|
||||
PrintLine(" Nexus HTTP Server v0.2.0");
|
||||
PrintLine(" Production-ready HTTP/1.1 with thread-pool");
|
||||
PrintLine(" Built with Bux");
|
||||
PrintLine("================================================");
|
||||
PrintLine("");
|
||||
|
||||
let serverFd: int = Net_Create();
|
||||
if serverFd < 0 {
|
||||
PrintLine("FATAL: socket() failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if !Net_SetReuse(serverFd) {
|
||||
PrintLine("WARN: SO_REUSEADDR failed");
|
||||
}
|
||||
|
||||
if !Net_Bind(serverFd, config.bindAddr, config.port) {
|
||||
Print("FATAL: bind failed: ");
|
||||
PrintLine(Net_LastError());
|
||||
Net_Close(serverFd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if !Net_Listen(serverFd, config.backlog) {
|
||||
PrintLine("FATAL: listen() failed");
|
||||
Net_Close(serverFd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Print("Listening on http://");
|
||||
Print(config.bindAddr);
|
||||
Print(":");
|
||||
PrintInt(config.port);
|
||||
PrintLine("");
|
||||
PrintInt(config.workerCount);
|
||||
PrintLine(" worker threads | static: ./public/");
|
||||
PrintLine("Endpoints: / /api/health /api/info /ws");
|
||||
PrintLine("Press Ctrl+C to stop.");
|
||||
PrintLine("");
|
||||
|
||||
let taskQueue: Channel<ConnectionTask> = Channel_New<ConnectionTask>(config.backlog as int64);
|
||||
let workerCtx: WorkerCtx = WorkerCtx { taskQueue: &taskQueue, router: router };
|
||||
let acceptorCtx: AcceptorCtx = AcceptorCtx { serverFd: serverFd, taskQueue: &taskQueue };
|
||||
|
||||
// Spawn workers (main thread will also become one)
|
||||
var i: int = 0;
|
||||
while i < config.workerCount - 1 {
|
||||
spawn Worker(&workerCtx);
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
// Spawn acceptor
|
||||
spawn Acceptor(&acceptorCtx);
|
||||
|
||||
// Main thread works too
|
||||
Worker(&workerCtx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
bux 0.1.0 (bootstrap)
|
||||
EXIT:0
|
||||
@@ -0,0 +1,77 @@
|
||||
# SimpleDB
|
||||
|
||||
File-backed key-value database for Bux.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
simpledb <dbfile> set <key> <value>
|
||||
simpledb <dbfile> get <key>
|
||||
simpledb <dbfile> del <key>
|
||||
simpledb <dbfile> has <key>
|
||||
simpledb <dbfile> keys
|
||||
simpledb <dbfile> count
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
$ simpledb data.db set name BuxLang
|
||||
OK name = BuxLang
|
||||
|
||||
$ simpledb data.db set version 0.1.0
|
||||
OK version = 0.1.0
|
||||
|
||||
$ simpledb data.db get name
|
||||
BuxLang
|
||||
|
||||
$ simpledb data.db has name
|
||||
true name
|
||||
|
||||
$ simpledb data.db keys
|
||||
keys: 2
|
||||
name
|
||||
version
|
||||
|
||||
$ simpledb data.db count
|
||||
count: 2
|
||||
|
||||
$ simpledb data.db del version
|
||||
DEL version
|
||||
|
||||
$ cat data.db
|
||||
name=BuxLang
|
||||
```
|
||||
|
||||
## Storage
|
||||
|
||||
Data is stored as plain text, one `key=value` per line. The file is created automatically on first write.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
# workaround: disable broken JWT module
|
||||
mv ../../lib/crypto/jwt.bux ../../lib/crypto/jwt.bux.bak
|
||||
../../buxc build
|
||||
mv ../../lib/crypto/jwt.bux.bak ../../lib/crypto/jwt.bux
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
The `Database` struct and its functions are defined in `src/Main.bux` and can be imported into other Bux projects:
|
||||
|
||||
```bux
|
||||
import Simpledb::{Database, DB_New, DB_Load, DB_Save, DB_Get, DB_Set, DB_Del, DB_Has, DB_Count, DB_Keys};
|
||||
```
|
||||
|
||||
| Function | Signature |
|
||||
|----------|-----------|
|
||||
| `DB_New` | `(path: String) -> Database` |
|
||||
| `DB_Load` | `(db: *Database) -> bool` |
|
||||
| `DB_Save` | `(db: *Database) -> bool` |
|
||||
| `DB_Get` | `(db: *Database, key: String) -> String` |
|
||||
| `DB_Set` | `(db: *Database, key: String, value: String)` |
|
||||
| `DB_Del` | `(db: *Database, key: String) -> bool` |
|
||||
| `DB_Has` | `(db: *Database, key: String) -> bool` |
|
||||
| `DB_Count` | `(db: *Database) -> uint` |
|
||||
| `DB_Keys` | `(db: *Database) -> *String` |
|
||||
@@ -0,0 +1,8 @@
|
||||
[Package]
|
||||
Name = "simpledb"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
Description = "SimpleDB — file-backed JSON key-value database for Bux"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1,339 @@
|
||||
module Main {
|
||||
|
||||
import Std::Io::{PrintLine, Print, PrintInt, ReadFile, WriteFile, FileExists};
|
||||
import Std::Os::{Os_ArgsCount, Os_Args};
|
||||
import Std::String::{String_Len, String_Eq, String_StartsWith, String_Find, String_Slice, String_Offset};
|
||||
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
extern func bux_strstr(haystack: String, needle: String) -> String;
|
||||
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
|
||||
extern func bux_str_split_count(s: String, delim: String) -> uint;
|
||||
extern func bux_str_split_part(s: String, delim: String, index: uint) -> String;
|
||||
extern func bux_sb_new(initial_cap: uint) -> *void;
|
||||
extern func bux_sb_append(sb: *void, s: String);
|
||||
extern func bux_sb_append_char(sb: *void, c: char8);
|
||||
|
||||
func SB_AppendChar(sb: *void, c: int) {
|
||||
bux_sb_append_char(sb, c as char8);
|
||||
}
|
||||
extern func bux_sb_build(sb: *void) -> String;
|
||||
extern func bux_sb_free(sb: *void);
|
||||
extern func bux_alloc(size: uint) -> *void;
|
||||
extern func bux_free(ptr: *void);
|
||||
|
||||
struct Database {
|
||||
path: String,
|
||||
sb: *void,
|
||||
loaded: bool,
|
||||
count: uint,
|
||||
}
|
||||
|
||||
func DB_New(filePath: String) -> Database {
|
||||
var db: Database;
|
||||
db.path = filePath;
|
||||
db.sb = bux_sb_new(4096);
|
||||
db.loaded = false;
|
||||
db.count = 0;
|
||||
return db;
|
||||
}
|
||||
|
||||
func DB_Load(self: *Database) -> bool {
|
||||
if self.loaded { return true; }
|
||||
|
||||
if !FileExists(self.path) {
|
||||
self.loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
let raw: String = ReadFile(self.path);
|
||||
if String_Len(raw) == 0 || bux_strlen(raw) == 0 {
|
||||
self.loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bux_sb_append(self.sb, raw);
|
||||
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
var i: uint = 0;
|
||||
var n: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
n = n + 1;
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
self.count = n;
|
||||
self.loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
func DB_Get(self: *Database, key: String) -> String {
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
var i: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if String_StartsWith(line, key) {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
let keyLen: uint = bux_strlen(key);
|
||||
let valStart: uint = keyLen + 1;
|
||||
let lineLen: uint = bux_strlen(line);
|
||||
if valStart < lineLen {
|
||||
return bux_str_slice(line, valStart, lineLen - valStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
func DB_Set(self: *Database, key: String, value: String) {
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
|
||||
var newSb: *void = bux_sb_new(4096);
|
||||
var found: bool = false;
|
||||
var i: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
if String_StartsWith(line, key) {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
bux_sb_append(newSb, key);
|
||||
SB_AppendChar(newSb, 61);
|
||||
bux_sb_append(newSb, value);
|
||||
SB_AppendChar(newSb, 10);
|
||||
found = true;
|
||||
} else {
|
||||
bux_sb_append(newSb, line);
|
||||
SB_AppendChar(newSb, 10);
|
||||
}
|
||||
} else {
|
||||
bux_sb_append(newSb, line);
|
||||
SB_AppendChar(newSb, 10);
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
if !found {
|
||||
bux_sb_append(newSb, key);
|
||||
SB_AppendChar(newSb, 61);
|
||||
bux_sb_append(newSb, value);
|
||||
SB_AppendChar(newSb, 10);
|
||||
self.count = self.count + 1;
|
||||
}
|
||||
|
||||
bux_sb_free(self.sb);
|
||||
self.sb = newSb;
|
||||
}
|
||||
|
||||
func DB_Del(self: *Database, key: String) {
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
|
||||
var newSb: *void = bux_sb_new(4096);
|
||||
var i: uint = 0;
|
||||
var delCount: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
if String_StartsWith(line, key) {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
delCount = delCount + 1;
|
||||
} else {
|
||||
bux_sb_append(newSb, line);
|
||||
SB_AppendChar(newSb, 10);
|
||||
}
|
||||
} else {
|
||||
bux_sb_append(newSb, line);
|
||||
SB_AppendChar(newSb, 10);
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
if delCount > 0 {
|
||||
self.count = self.count - delCount;
|
||||
}
|
||||
|
||||
bux_sb_free(self.sb);
|
||||
self.sb = newSb;
|
||||
}
|
||||
|
||||
func DB_Has(self: *Database, key: String) -> bool {
|
||||
let val: String = DB_Get(self, key);
|
||||
return bux_strlen(val) > 0;
|
||||
}
|
||||
|
||||
func DB_Count(self: *Database) -> uint {
|
||||
return self.count;
|
||||
}
|
||||
|
||||
func DB_Keys(self: *Database) -> *String {
|
||||
if self.count == 0 { return null as *String; }
|
||||
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
let keys: *String = bux_alloc(self.count * sizeof(String)) as *String;
|
||||
|
||||
var found: uint = 0;
|
||||
var i: uint = 0;
|
||||
while i < lineCount && found < self.count {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
let keyLen: uint = String_Offset(eqPos, line);
|
||||
keys[found] = bux_str_slice(line, 0, keyLen);
|
||||
found = found + 1;
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
func DB_Save(self: *Database) -> bool {
|
||||
let content: String = bux_sb_build(self.sb);
|
||||
let ok: bool = WriteFile(self.path, content);
|
||||
return ok;
|
||||
}
|
||||
|
||||
func PrintUsage() {
|
||||
PrintLine("SimpleDB — file-backed key-value database");
|
||||
PrintLine("Usage:");
|
||||
PrintLine(" simpledb <dbfile> set <key> <value>");
|
||||
PrintLine(" simpledb <dbfile> get <key>");
|
||||
PrintLine(" simpledb <dbfile> del <key>");
|
||||
PrintLine(" simpledb <dbfile> has <key>");
|
||||
PrintLine(" simpledb <dbfile> keys");
|
||||
PrintLine(" simpledb <dbfile> count");
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let argc: int = Os_ArgsCount();
|
||||
|
||||
if argc < 3 {
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
let dbFile: String = Os_Args(1);
|
||||
let cmd: String = Os_Args(2);
|
||||
|
||||
var db: Database = DB_New(dbFile);
|
||||
|
||||
if String_Eq(cmd, "get") {
|
||||
DB_Load(&db);
|
||||
if argc < 4 {
|
||||
PrintLine("get: missing key");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
let val: String = DB_Get(&db, key);
|
||||
if bux_strlen(val) > 0 {
|
||||
PrintLine(val);
|
||||
} else {
|
||||
Print("(not found) ");
|
||||
PrintLine(key);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "set") {
|
||||
DB_Load(&db);
|
||||
if argc < 5 {
|
||||
PrintLine("set: missing key or value");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
let val: String = Os_Args(4);
|
||||
DB_Set(&db, key, val);
|
||||
let saved: bool = DB_Save(&db);
|
||||
if saved {
|
||||
Print("OK ");
|
||||
Print(key);
|
||||
Print(" = ");
|
||||
PrintLine(val);
|
||||
} else {
|
||||
PrintLine("set: save failed");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "del") {
|
||||
DB_Load(&db);
|
||||
if argc < 4 {
|
||||
PrintLine("del: missing key");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
DB_Del(&db, key);
|
||||
DB_Save(&db);
|
||||
Print("DEL ");
|
||||
PrintLine(key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "has") {
|
||||
DB_Load(&db);
|
||||
if argc < 4 {
|
||||
PrintLine("has: missing key");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
let found: bool = DB_Has(&db, key);
|
||||
if found {
|
||||
Print("true ");
|
||||
PrintLine(key);
|
||||
} else {
|
||||
Print("false ");
|
||||
PrintLine(key);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "keys") {
|
||||
DB_Load(&db);
|
||||
let n: uint = DB_Count(&db);
|
||||
Print("keys: ");
|
||||
PrintInt(n as int);
|
||||
PrintLine("");
|
||||
|
||||
if n == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let list: *String = DB_Keys(&db);
|
||||
var i: uint = 0;
|
||||
while i < n {
|
||||
Print(" ");
|
||||
PrintLine(list[i]);
|
||||
i = i + 1;
|
||||
}
|
||||
bux_free(list as *void);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "count") {
|
||||
DB_Load(&db);
|
||||
let n: uint = DB_Count(&db);
|
||||
Print("count: ");
|
||||
PrintInt(n as int);
|
||||
PrintLine("");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Print("unknown command: ");
|
||||
PrintLine(cmd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,6 +33,7 @@ type
|
||||
tekDynRef ## &dyn Trait — trait object (fat pointer)
|
||||
tekTuple
|
||||
tekSelf
|
||||
tekFunc
|
||||
|
||||
TypeExpr* = ref object
|
||||
loc*: SourceLocation
|
||||
@@ -54,6 +55,9 @@ type
|
||||
tupleElements*: seq[TypeExpr]
|
||||
of tekSelf:
|
||||
discard
|
||||
of tekFunc:
|
||||
funcParams*: seq[TypeExpr]
|
||||
funcRet*: TypeExpr
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Patterns
|
||||
@@ -124,8 +128,11 @@ type
|
||||
ekUnwrap ## expr! — unwrap or panic
|
||||
ekSpawn ## spawn expr — create a new task
|
||||
ekAwait ## expr.await — suspend until future resolves
|
||||
ekBorrow ## borrow &mut expr — explicit borrow expression
|
||||
ekBlock
|
||||
ekMatch
|
||||
ekStringInterp
|
||||
ekClosure
|
||||
|
||||
MatchArm* = object
|
||||
loc*: SourceLocation
|
||||
@@ -172,6 +179,7 @@ type
|
||||
of ekCall:
|
||||
exprCallCallee*: Expr
|
||||
exprCallArgs*: seq[Expr]
|
||||
exprCallArgNames*: seq[string] ## empty = positional, non-empty = named arg
|
||||
exprCallInferredTypeArgs*: seq[TypeExpr] ## filled by sema for inferred generic calls
|
||||
of ekGenericCall:
|
||||
exprGenericCallee*: string
|
||||
@@ -210,11 +218,24 @@ type
|
||||
exprSpawnAsync*: bool
|
||||
of ekAwait:
|
||||
exprAwaitOperand*: Expr
|
||||
of ekBorrow:
|
||||
exprBorrowOperand*: Expr
|
||||
exprBorrowMutable*: bool
|
||||
of ekBlock:
|
||||
exprBlock*: Block
|
||||
of ekMatch:
|
||||
exprMatchSubject*: Expr
|
||||
exprMatchArms*: seq[MatchArm]
|
||||
of ekStringInterp:
|
||||
exprInterpTexts*: seq[string]
|
||||
exprInterpExprs*: seq[Expr]
|
||||
of ekClosure:
|
||||
exprClosureParams*: seq[Param]
|
||||
exprClosureBody*: Block
|
||||
exprClosureReturnType*: TypeExpr
|
||||
captureCount*: int
|
||||
captureNames*: seq[string]
|
||||
captureTypeKinds*: seq[int]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Statements
|
||||
@@ -234,6 +255,8 @@ type
|
||||
skStaticAssert
|
||||
skComptime
|
||||
skEmit
|
||||
skDefer
|
||||
skSwitch
|
||||
skDecl
|
||||
|
||||
ElseIf* = object
|
||||
@@ -241,6 +264,11 @@ type
|
||||
cond*: Expr
|
||||
blk*: Block
|
||||
|
||||
SwitchCase* = object
|
||||
loc*: SourceLocation
|
||||
caseValue*: Expr
|
||||
caseBody*: Block
|
||||
|
||||
Block* = ref object
|
||||
loc*: SourceLocation
|
||||
stmts*: seq[Stmt]
|
||||
@@ -294,6 +322,12 @@ type
|
||||
of skEmit:
|
||||
stmtEmitExpr*: Expr
|
||||
stmtEmitEvaluated*: string ## filled by sema CTFE
|
||||
of skDefer:
|
||||
stmtDeferBody*: Expr
|
||||
of skSwitch:
|
||||
stmtSwitchExpr*: Expr
|
||||
stmtSwitchCases*: seq[SwitchCase]
|
||||
stmtSwitchDefault*: Block
|
||||
of skDecl:
|
||||
stmtDecl*: Decl
|
||||
|
||||
@@ -441,3 +475,6 @@ proc newBinaryExpr*(op: TokenKind, left, right: Expr, loc: SourceLocation): Expr
|
||||
|
||||
proc newUnaryExpr*(op: TokenKind, operand: Expr, loc: SourceLocation): Expr =
|
||||
result = Expr(kind: ekUnary, loc: loc, exprUnaryOp: op, exprUnaryOperand: operand)
|
||||
|
||||
proc newStringInterpExpr*(texts: seq[string], exprs: seq[Expr], loc: SourceLocation): Expr =
|
||||
result = Expr(kind: ekStringInterp, loc: loc, exprInterpTexts: texts, exprInterpExprs: exprs)
|
||||
|
||||
@@ -8,6 +8,7 @@ type
|
||||
varCounter*: int
|
||||
declaredVars*: seq[string]
|
||||
sliceTypeDefs*: seq[tuple[name: string, elem: string]] ## Generated Slice_<T> typedefs
|
||||
deferStack*: seq[seq[string]] ## Function-level deferred statements (LIFO)
|
||||
|
||||
proc cEscape(s: string): string =
|
||||
## Escape a string for use as a C string literal.
|
||||
@@ -45,6 +46,28 @@ proc freshVar(be: var CBackend): string =
|
||||
inc be.varCounter
|
||||
result = &"__tmp_{be.varCounter}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Defer helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
proc pushDeferScope(be: var CBackend) =
|
||||
be.deferStack.add(@[])
|
||||
|
||||
proc popDeferScope(be: var CBackend) =
|
||||
if be.deferStack.len > 0:
|
||||
be.deferStack.setLen(be.deferStack.len - 1)
|
||||
|
||||
proc emitCurrentDefers(be: var CBackend) =
|
||||
## Emit all deferred statements in current scope, LIFO order
|
||||
if be.deferStack.len == 0: return
|
||||
let idx = be.deferStack.len - 1
|
||||
for i in countdown(be.deferStack[idx].len - 1, 0):
|
||||
be.emitLine(be.deferStack[idx][i])
|
||||
|
||||
proc clearCurrentDefers(be: var CBackend) =
|
||||
if be.deferStack.len > 0:
|
||||
let idx = be.deferStack.len - 1
|
||||
be.deferStack[idx].setLen(0)
|
||||
|
||||
# Type conversion: Bux Type → C type string
|
||||
proc typeToC*(be: var CBackend, typ: Type): string =
|
||||
if typ == nil: return "void"
|
||||
@@ -249,6 +272,10 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
|
||||
let base = be.emitExpr(node.fieldPtrBase)
|
||||
return &"(&({base}.{node.fieldName}))"
|
||||
|
||||
of hFieldAccess:
|
||||
let base = be.emitExpr(node.fieldAccessBase)
|
||||
return &"({base}.{node.fieldAccessName})"
|
||||
|
||||
of hArrowField:
|
||||
let base = be.emitExpr(node.arrowFieldBase)
|
||||
return &"(&({base}->{node.arrowFieldName}))"
|
||||
@@ -358,7 +385,14 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
|
||||
proc emitStmt(be: var CBackend, node: HirNode) =
|
||||
if node == nil: return
|
||||
case node.kind
|
||||
of hDefer:
|
||||
let expr = be.emitExpr(node.deferBody)
|
||||
if be.deferStack.len > 0:
|
||||
let idx = be.deferStack.len - 1
|
||||
be.deferStack[idx].add(expr & ";")
|
||||
|
||||
of hReturn:
|
||||
be.emitCurrentDefers()
|
||||
if node.returnValue != nil:
|
||||
let val = be.emitExpr(node.returnValue)
|
||||
be.emitLine(&"return {val};")
|
||||
@@ -394,9 +428,11 @@ proc emitStmt(be: var CBackend, node: HirNode) =
|
||||
be.emitLine("}")
|
||||
|
||||
of hBreak:
|
||||
be.emitCurrentDefers()
|
||||
be.emitLine("break;")
|
||||
|
||||
of hContinue:
|
||||
be.emitCurrentDefers()
|
||||
be.emitLine("continue;")
|
||||
|
||||
of hEmit:
|
||||
@@ -444,6 +480,18 @@ proc emitStmt(be: var CBackend, node: HirNode) =
|
||||
be.emitLine(&"{expr};")
|
||||
|
||||
proc emitFunc*(be: var CBackend, hfunc: HirFunc) =
|
||||
# Emit env struct and global instance for closures with captures
|
||||
if hfunc.captureNames.len > 0 and hfunc.envStructName != "":
|
||||
be.emitLine(&"struct {hfunc.envStructName} {{")
|
||||
inc be.indent
|
||||
for i in 0 ..< hfunc.captureNames.len:
|
||||
let capName = hfunc.captureNames[i]
|
||||
let capType = if i < hfunc.captureTypes.len: typeToC(be, hfunc.captureTypes[i]) else: "int"
|
||||
be.emitLine(&"{capType} {capName};")
|
||||
dec be.indent
|
||||
be.emitLine("};")
|
||||
be.emitLine(&"static struct {hfunc.envStructName} {hfunc.envInstanceName};")
|
||||
be.emitLine("")
|
||||
let retType = typeToC(be, hfunc.retType)
|
||||
var params: seq[string] = @[]
|
||||
for p in hfunc.params:
|
||||
@@ -453,15 +501,23 @@ proc emitFunc*(be: var CBackend, hfunc: HirFunc) =
|
||||
let paramsStr = params.join(", ")
|
||||
be.emitLine(&"{retType} {hfunc.name}({paramsStr}) {{")
|
||||
inc be.indent
|
||||
be.pushDeferScope()
|
||||
if hfunc.body != nil:
|
||||
if hfunc.body.kind == hBlock:
|
||||
for stmt in hfunc.body.blockStmts:
|
||||
be.emitStmt(stmt)
|
||||
if hfunc.body.blockExpr != nil and hfunc.retType.kind != tkVoid:
|
||||
be.emitCurrentDefers()
|
||||
let val = be.emitExpr(hfunc.body.blockExpr)
|
||||
be.emitLine(&"return {val};")
|
||||
else:
|
||||
be.emitCurrentDefers()
|
||||
else:
|
||||
be.emitStmt(hfunc.body)
|
||||
be.emitCurrentDefers()
|
||||
else:
|
||||
be.emitCurrentDefers()
|
||||
be.popDeferScope()
|
||||
dec be.indent
|
||||
be.emitLine("}")
|
||||
be.emitLine("")
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import std/[os, strutils, terminal, strformat, osproc, sets]
|
||||
import lexer, parser, ast, sema, manifest, hir_lower, lir, lir_lower, lir_c_backend
|
||||
import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend
|
||||
|
||||
type
|
||||
ColorMode* = enum
|
||||
@@ -488,7 +488,7 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
|
||||
# Compile with cc
|
||||
let outputName = if pctx.man.name != "": pctx.man.name else: "bux_out"
|
||||
let outputFile = buildDir / outputName
|
||||
let ccCmd = &"cc -O0 -g -pthread -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm -lcrypto 2>&1"
|
||||
let ccCmd = &"cc -O0 -g -pthread -Wl,--build-id=none -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm -lcrypto 2>&1"
|
||||
if opts.verbose:
|
||||
printInfo(&"running: {ccCmd}", useColor)
|
||||
let (output, exitCode) = execCmdEx(ccCmd)
|
||||
|
||||
@@ -17,11 +17,13 @@ type
|
||||
hBreak
|
||||
hContinue
|
||||
hReturn
|
||||
hDefer
|
||||
# Memory
|
||||
hAlloca
|
||||
hLoad
|
||||
hStore
|
||||
hFieldPtr
|
||||
hFieldAccess
|
||||
hArrowField
|
||||
hIndexPtr
|
||||
hSliceIndex
|
||||
@@ -84,6 +86,8 @@ type
|
||||
continueLabel*: string
|
||||
of hReturn:
|
||||
returnValue*: HirNode
|
||||
of hDefer:
|
||||
deferBody*: HirNode
|
||||
of hAlloca:
|
||||
allocaType*: Type
|
||||
allocaName*: string
|
||||
@@ -95,6 +99,9 @@ type
|
||||
of hFieldPtr:
|
||||
fieldPtrBase*: HirNode
|
||||
fieldName*: string
|
||||
of hFieldAccess:
|
||||
fieldAccessBase*: HirNode
|
||||
fieldAccessName*: string
|
||||
of hArrowField:
|
||||
arrowFieldBase*: HirNode
|
||||
arrowFieldName*: string
|
||||
@@ -163,6 +170,11 @@ type
|
||||
retType*: Type
|
||||
body*: HirNode
|
||||
isPublic*: bool
|
||||
# Closure capture metadata
|
||||
captureNames*: seq[string]
|
||||
captureTypes*: seq[Type]
|
||||
envStructName*: string
|
||||
envInstanceName*: string
|
||||
|
||||
HirEnumVariant* = object
|
||||
name*: string
|
||||
@@ -200,6 +212,9 @@ proc hirCall*(callee: string, args: seq[HirNode], typ: Type, loc: SourceLocation
|
||||
proc hirReturn*(value: HirNode, loc: SourceLocation): HirNode =
|
||||
HirNode(kind: hReturn, returnValue: value, typ: makeVoid(), loc: loc)
|
||||
|
||||
proc hirDefer*(body: HirNode, loc: SourceLocation): HirNode =
|
||||
HirNode(kind: hDefer, deferBody: body, typ: makeVoid(), loc: loc)
|
||||
|
||||
proc hirBlock*(stmts: seq[HirNode], expr: HirNode, typ: Type, loc: SourceLocation, isScope: bool = false): HirNode =
|
||||
HirNode(kind: hBlock, blockStmts: stmts, blockExpr: expr, typ: typ, loc: loc, isScope: isScope)
|
||||
|
||||
@@ -213,6 +228,9 @@ proc hirAlloca*(name: string, typ: Type, loc: SourceLocation): HirNode =
|
||||
proc hirStore*(ptrNode, value: HirNode, loc: SourceLocation): HirNode =
|
||||
HirNode(kind: hStore, storePtr: ptrNode, storeValue: value, typ: makeVoid(), loc: loc)
|
||||
|
||||
proc hirAssign*(target, value: HirNode, loc: SourceLocation): HirNode =
|
||||
HirNode(kind: hAssign, assignOp: tkAssign, assignTarget: target, assignValue: value, typ: makeVoid(), loc: loc)
|
||||
|
||||
proc hirLoad*(ptrNode: HirNode, typ: Type, loc: SourceLocation): HirNode =
|
||||
HirNode(kind: hLoad, loadPtr: ptrNode, typ: typ, loc: loc)
|
||||
|
||||
|
||||
+592
-22
@@ -11,6 +11,7 @@ type
|
||||
varCounter*: int
|
||||
tryCounter*: int
|
||||
pendingStmts*: seq[HirNode]
|
||||
deferStmts*: seq[HirNode]
|
||||
typeSubst*: Table[string, Type] # Type parameter substitution for generics
|
||||
importTable*: Table[string, string] # Local name → fully qualified name for imports
|
||||
genericStructs*: Table[string, Decl] # Generic struct declarations
|
||||
@@ -21,6 +22,9 @@ type
|
||||
generatedFuncInsts*: Table[string, bool] # Track generated function instantiations
|
||||
extraFuncs*: seq[HirFunc] # Monomorphized generic methods
|
||||
varTypeExprs*: Table[string, TypeExpr] # Track variable names -> type expr for generic method inference
|
||||
closureDepth*: int
|
||||
currentClosureExpr*: Expr
|
||||
envInstanceName*: string
|
||||
|
||||
proc freshName(ctx: var LowerCtx): string =
|
||||
inc ctx.varCounter
|
||||
@@ -38,6 +42,14 @@ proc flushPending(ctx: var LowerCtx, node: HirNode): HirNode =
|
||||
return hirBlock(stmts, nil, makeVoid(), node.loc)
|
||||
return node
|
||||
|
||||
proc enumHasDataVariants(ctx: var LowerCtx, enumName: string): bool =
|
||||
let sym = ctx.globalScope.lookup(enumName)
|
||||
if sym != nil and sym.decl != nil and sym.decl.kind == dkEnum:
|
||||
for v in sym.decl.declEnumVariants:
|
||||
if v.fields.len > 0 or v.namedFields.len > 0:
|
||||
return true
|
||||
return false
|
||||
|
||||
proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ: Type, loc: SourceLocation): HirNode =
|
||||
# Lower match expression to a block with if-else chain.
|
||||
# For now, supports enum tag matching and wildcard/ident fallbacks.
|
||||
@@ -47,6 +59,13 @@ proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ
|
||||
# Allocate result variable
|
||||
stmts.add(hirAlloca(resultName, typ, loc))
|
||||
|
||||
# Determine whether the matched enum has data variants (needs .tag access).
|
||||
var subjectEnumName = ""
|
||||
var subjectHasData = false
|
||||
if subject.typ != nil and subject.typ.kind == tkNamed:
|
||||
subjectEnumName = subject.typ.name
|
||||
subjectHasData = ctx.enumHasDataVariants(subjectEnumName)
|
||||
|
||||
# Build if-else chain from arms (last arm is the outermost else)
|
||||
var ifChain: HirNode = nil
|
||||
|
||||
@@ -62,12 +81,18 @@ proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ
|
||||
let variantName = path[^1]
|
||||
let tagName = enumName & "_" & variantName
|
||||
|
||||
# condition: subject.tag == EnumName_VariantName
|
||||
let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "tag",
|
||||
typ: makePointer(makeNamed(enumName & "_Tag")), loc: loc)
|
||||
let tagLoad = HirNode(kind: hLoad, loadPtr: tagField, typ: makeNamed(enumName & "_Tag"), loc: loc)
|
||||
let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName & "_Tag"), loc)
|
||||
let cond = hirBinary(tkEq, tagLoad, tagConst, makeBool(), loc)
|
||||
var cond: HirNode
|
||||
if subjectHasData and enumName == subjectEnumName:
|
||||
# Algebraic enum: compare subject.tag
|
||||
let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "tag",
|
||||
typ: makePointer(makeNamed(enumName & "_Tag")), loc: loc)
|
||||
let tagLoad = HirNode(kind: hLoad, loadPtr: tagField, typ: makeNamed(enumName & "_Tag"), loc: loc)
|
||||
let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName & "_Tag"), loc)
|
||||
cond = hirBinary(tkEq, tagLoad, tagConst, makeBool(), loc)
|
||||
else:
|
||||
# Simple enum or cross-enum match: compare subject directly
|
||||
let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName), loc)
|
||||
cond = hirBinary(tkEq, subject, tagConst, makeBool(), loc)
|
||||
|
||||
# body: result = arm_body
|
||||
var armStmts: seq[HirNode] = @[]
|
||||
@@ -162,10 +187,14 @@ proc substituteType(ctx: var LowerCtx, te: TypeExpr, subst: Table[string, Type])
|
||||
break
|
||||
if hasUnresolved: break
|
||||
if not hasUnresolved:
|
||||
var localSubst = subst
|
||||
for j, tp in genericDecl.declStructTypeParams:
|
||||
if j < te.typeArgs.len:
|
||||
localSubst[tp.name] = substituteType(ctx, te.typeArgs[j], subst)
|
||||
var fields: seq[tuple[name: string, typ: Type]] = @[]
|
||||
var concreteArgs: seq[Type] = @[]
|
||||
for f in genericDecl.declStructFields:
|
||||
let resolvedType = substituteType(ctx, f.ftype, subst)
|
||||
let resolvedType = substituteType(ctx, f.ftype, localSubst)
|
||||
fields.add((f.name, resolvedType))
|
||||
for arg in te.typeArgs:
|
||||
concreteArgs.add(substituteType(ctx, arg, subst))
|
||||
@@ -263,12 +292,19 @@ proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type =
|
||||
of tekDynRef: return makeDynRef(te.dynInterface)
|
||||
of tekPointer: return makePointer(ctx.resolveTypeExpr(te.pointerPointee))
|
||||
of tekSlice: return makeSlice(ctx.resolveTypeExpr(te.sliceElement))
|
||||
of tekFunc:
|
||||
var params: seq[Type] = @[]
|
||||
for p in te.funcParams:
|
||||
params.add(ctx.resolveTypeExpr(p))
|
||||
let ret = if te.funcRet != nil: ctx.resolveTypeExpr(te.funcRet) else: makeVoid()
|
||||
return makeFunc(params, ret)
|
||||
else: return makeUnknown()
|
||||
|
||||
# Forward declarations
|
||||
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode
|
||||
proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode
|
||||
proc lowerBlock(ctx: var LowerCtx, blk: Block): HirNode
|
||||
proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc
|
||||
|
||||
proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
if expr == nil: return makeUnknown()
|
||||
@@ -319,7 +355,7 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
of ekUnary:
|
||||
case expr.exprUnaryOp
|
||||
of tkBang: return makeBool()
|
||||
of tkAmp: return makePointer(ctx.resolveExprType(expr.exprUnaryOperand))
|
||||
of tkAmp: return makeMutRef(ctx.resolveExprType(expr.exprUnaryOperand))
|
||||
of tkStar:
|
||||
let inner = ctx.resolveExprType(expr.exprUnaryOperand)
|
||||
if inner.isPointer: return inner.inner[0]
|
||||
@@ -352,6 +388,19 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
if objType.isPointer and objType.inner.len > 0:
|
||||
objType = objType.inner[0]
|
||||
if objType.kind == tkNamed:
|
||||
# Check if this is a _Data union field access
|
||||
if objType.name.endsWith("_Data"):
|
||||
let enumName = objType.name[0..^6]
|
||||
let enumSym = ctx.globalScope.lookup(enumName)
|
||||
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
|
||||
for variant in enumSym.decl.declEnumVariants:
|
||||
for i, f in variant.fields:
|
||||
let fieldName = variant.name & "_" & $i
|
||||
if fieldName == expr.exprFieldName:
|
||||
return ctx.resolveTypeExpr(f)
|
||||
for nf in variant.namedFields:
|
||||
if nf.name == expr.exprFieldName:
|
||||
return ctx.resolveTypeExpr(nf.ftype)
|
||||
var sym = ctx.globalScope.lookup(objType.name)
|
||||
var decl = if sym != nil: sym.decl else: nil
|
||||
# If the type is a monomorphized generic struct instance, look up the base
|
||||
@@ -369,6 +418,8 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
if f.ftype != nil:
|
||||
case f.ftype.kind
|
||||
of tekNamed:
|
||||
if f.ftype.typeArgs.len > 0:
|
||||
return substituteType(ctx, f.ftype, subst)
|
||||
case f.ftype.typeName
|
||||
of "int", "int32", "int64": return makeInt()
|
||||
of "float64": return makeFloat64()
|
||||
@@ -389,6 +440,8 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
if f.ftype != nil:
|
||||
case f.ftype.kind
|
||||
of tekNamed:
|
||||
if f.ftype.typeArgs.len > 0:
|
||||
return ctx.resolveTypeExpr(f.ftype)
|
||||
case f.ftype.typeName
|
||||
of "int", "int32", "int64": return makeInt()
|
||||
of "float64": return makeFloat64()
|
||||
@@ -400,7 +453,14 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
else: return makeUnknown()
|
||||
of dkEnum:
|
||||
# Algebraic enum fields: tag and data
|
||||
if expr.exprFieldName == "tag":
|
||||
var hasData = false
|
||||
for v in decl.declEnumVariants:
|
||||
if v.fields.len > 0 or v.namedFields.len > 0:
|
||||
hasData = true
|
||||
break
|
||||
if not hasData and expr.exprFieldName == "tag":
|
||||
return makeNamed(objType.name)
|
||||
elif expr.exprFieldName == "tag":
|
||||
return makeNamed(objType.name & "_Tag")
|
||||
elif expr.exprFieldName == "data":
|
||||
return makeNamed(objType.name & "_Data")
|
||||
@@ -421,7 +481,15 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
return makeSlice(makeUnknown())
|
||||
of ekRange:
|
||||
let loType = ctx.resolveExprType(expr.exprRangeLo)
|
||||
return makeRange(loType)
|
||||
let hiType = ctx.resolveExprType(expr.exprRangeHi)
|
||||
if loType == hiType:
|
||||
return makeRange(loType)
|
||||
elif loType.isAssignableTo(hiType):
|
||||
return makeRange(hiType)
|
||||
elif hiType.isAssignableTo(loType):
|
||||
return makeRange(loType)
|
||||
else:
|
||||
return makeRange(loType)
|
||||
of ekTuple:
|
||||
var elems: seq[Type] = @[]
|
||||
for e in expr.exprTupleElements:
|
||||
@@ -443,12 +511,18 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
if baseType.isPointer and baseType.inner.len > 0:
|
||||
return baseType.inner[0]
|
||||
return makeUnknown()
|
||||
of ekMatch:
|
||||
if expr.exprMatchArms.len > 0:
|
||||
return ctx.resolveExprType(expr.exprMatchArms[0].body)
|
||||
return makeUnknown()
|
||||
of ekBlock:
|
||||
if expr.exprBlock.stmts.len > 0:
|
||||
let last = expr.exprBlock.stmts[^1]
|
||||
if last.kind == skExpr:
|
||||
return ctx.resolveExprType(last.stmtExpr)
|
||||
return makeVoid()
|
||||
of ekBorrow:
|
||||
return ctx.resolveExprType(expr.exprBorrowOperand)
|
||||
else: return makeUnknown()
|
||||
|
||||
proc extractGenericStructInfo(ctx: LowerCtx, te: TypeExpr): tuple[baseName: string, typeArgs: seq[TypeExpr]] =
|
||||
@@ -475,6 +549,49 @@ proc getReceiverTypeExpr(ctx: LowerCtx, expr: Expr): TypeExpr =
|
||||
else: discard
|
||||
return nil
|
||||
|
||||
proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
|
||||
## Return the element TypeExpr of a collection expression (Array<T>, Iter<T>, Channel<T>).
|
||||
## For identifiers we can use the declared TypeExpr directly; for other expressions we
|
||||
## fall back to the resolved concrete Type.
|
||||
case expr.kind
|
||||
of ekIdent:
|
||||
if ctx.varTypeExprs.hasKey(expr.exprIdent):
|
||||
let te = ctx.varTypeExprs[expr.exprIdent]
|
||||
if te.kind == tekNamed and te.typeArgs.len > 0:
|
||||
return te.typeArgs[0]
|
||||
if te.kind in {tekPointer, tekRef, tekMutRef} and te.pointerPointee.kind == tekNamed and te.pointerPointee.typeArgs.len > 0:
|
||||
return te.pointerPointee.typeArgs[0]
|
||||
of ekField:
|
||||
# Try to resolve the field's declared TypeExpr directly.
|
||||
let objType = ctx.resolveExprType(expr.exprFieldObj)
|
||||
if objType.kind == tkNamed:
|
||||
var decl = ctx.globalScope.lookup(objType.name).decl
|
||||
if decl == nil and ctx.structInstMap.hasKey(objType.name):
|
||||
let (baseName, _) = ctx.structInstMap[objType.name]
|
||||
let baseSym = ctx.globalScope.lookup(baseName)
|
||||
if baseSym != nil and baseSym.decl != nil and baseSym.decl.kind == dkStruct:
|
||||
decl = baseSym.decl
|
||||
if decl != nil and decl.kind == dkStruct:
|
||||
for f in decl.declStructFields:
|
||||
if f.name == expr.exprFieldName and f.ftype != nil:
|
||||
let fte = f.ftype
|
||||
if fte.kind == tekNamed and fte.typeArgs.len > 0 and (fte.typeName == "Array" or fte.typeName == "Iter" or fte.typeName == "Channel"):
|
||||
return fte.typeArgs[0]
|
||||
return fte
|
||||
else:
|
||||
discard
|
||||
let t = ctx.resolveExprType(expr)
|
||||
if t.kind == tkNamed and t.inner.len > 0:
|
||||
return typeToTypeExpr(t.inner[0])
|
||||
if t.isPointer and t.inner.len > 0 and t.inner[0].kind == tkNamed and t.inner[0].inner.len > 0:
|
||||
return typeToTypeExpr(t.inner[0].inner[0])
|
||||
# Generic struct instances (e.g. Array_HeaderEntry) store their type args in structInstMap.
|
||||
if t.kind == tkNamed and ctx.structInstMap.hasKey(t.name):
|
||||
let (baseName, concreteArgs) = ctx.structInstMap[t.name]
|
||||
if concreteArgs.len > 0 and (baseName == "Array" or baseName == "Iter" or baseName == "Channel"):
|
||||
return typeToTypeExpr(concreteArgs[0])
|
||||
return TypeExpr(kind: tekNamed, typeName: "unknown")
|
||||
|
||||
proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string
|
||||
|
||||
proc lowerExprWithDynRefCoerce(ctx: var LowerCtx, arg: Expr, expectedType: Type): HirNode =
|
||||
@@ -510,6 +627,103 @@ proc findMethodEntry(ctx: LowerCtx, typeName: string): (string, seq[MethodInfo])
|
||||
return (prefix, ctx.methodTable[prefix])
|
||||
return ("", @[])
|
||||
|
||||
proc operatorMethodName(op: TokenKind): string =
|
||||
case op
|
||||
of tkPlus: "operator_add"
|
||||
of tkMinus: "operator_sub"
|
||||
of tkStar: "operator_mul"
|
||||
of tkSlash: "operator_div"
|
||||
of tkPercent: "operator_mod"
|
||||
of tkEq: "operator_eq"
|
||||
of tkNe: "operator_ne"
|
||||
of tkLt: "operator_lt"
|
||||
of tkLe: "operator_le"
|
||||
of tkGt: "operator_gt"
|
||||
of tkGe: "operator_ge"
|
||||
of tkAmp: "operator_bitand"
|
||||
of tkPipe: "operator_bitor"
|
||||
of tkCaret: "operator_xor"
|
||||
of tkShl: "operator_shl"
|
||||
of tkShr: "operator_shr"
|
||||
else: ""
|
||||
|
||||
proc tryLowerOperatorCall(ctx: var LowerCtx, op: TokenKind, leftExpr, rightExpr: Expr, typ: Type, loc: SourceLocation): HirNode =
|
||||
## Try to lower a binary operator to a method call. Returns nil if no overload found.
|
||||
let methodName = operatorMethodName(op)
|
||||
if methodName == "": return nil
|
||||
let receiverType = ctx.resolveExprType(leftExpr)
|
||||
var receiverTypeName = ""
|
||||
if receiverType.kind == tkNamed:
|
||||
receiverTypeName = receiverType.name
|
||||
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.kind in {tkInt, tkInt8, tkInt16, tkInt32, tkInt64,
|
||||
tkUInt, tkUInt8, tkUInt16, tkUInt32, tkUInt64,
|
||||
tkFloat32, tkFloat64, tkBool, tkStr, tkChar8}:
|
||||
receiverTypeName = receiverType.toString
|
||||
elif receiverType.isPointer and receiverType.inner.len > 0 and receiverType.inner[0].kind == tkNamed:
|
||||
receiverTypeName = receiverType.inner[0].name
|
||||
let (typeName, methods) = ctx.findMethodEntry(receiverTypeName)
|
||||
if typeName == "": return nil
|
||||
for minfo in methods:
|
||||
if minfo.name == methodName:
|
||||
var calleeName = typeName & "_" & methodName
|
||||
# Check generic method instantiation
|
||||
let recvTypeExpr = ctx.getReceiverTypeExpr(leftExpr)
|
||||
let (baseName, typeArgs) = ctx.extractGenericStructInfo(recvTypeExpr)
|
||||
if baseName != "" and baseName == typeName and minfo.decl.declFuncTypeParams.len > 0:
|
||||
calleeName = ctx.generateMethodInstance(calleeName, typeArgs)
|
||||
var args: seq[HirNode] = @[]
|
||||
let loweredReceiver = ctx.lowerExpr(leftExpr)
|
||||
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)
|
||||
args.add(ctx.lowerExpr(rightExpr))
|
||||
return hirCall(calleeName, args, typ, loc)
|
||||
return nil
|
||||
|
||||
proc tryLowerIndexCall(ctx: var LowerCtx, objExpr, idxExpr: Expr, typ: Type, loc: SourceLocation): HirNode =
|
||||
## Try to lower arr[i] to operator_index_get(arr, i). Returns nil if no overload found.
|
||||
let receiverType = ctx.resolveExprType(objExpr)
|
||||
var receiverTypeName = ""
|
||||
if receiverType.kind == tkNamed:
|
||||
receiverTypeName = receiverType.name
|
||||
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.kind in {tkInt, tkInt8, tkInt16, tkInt32, tkInt64,
|
||||
tkUInt, tkUInt8, tkUInt16, tkUInt32, tkUInt64,
|
||||
tkFloat32, tkFloat64, tkBool, tkStr, tkChar8}:
|
||||
receiverTypeName = receiverType.toString
|
||||
elif receiverType.isPointer and receiverType.inner.len > 0 and receiverType.inner[0].kind == tkNamed:
|
||||
receiverTypeName = receiverType.inner[0].name
|
||||
let (typeName, methods) = ctx.findMethodEntry(receiverTypeName)
|
||||
if typeName == "": return nil
|
||||
for minfo in methods:
|
||||
if minfo.name == "operator_index_get":
|
||||
var calleeName = typeName & "_operator_index_get"
|
||||
let recvTypeExpr = ctx.getReceiverTypeExpr(objExpr)
|
||||
let (baseName, typeArgs) = ctx.extractGenericStructInfo(recvTypeExpr)
|
||||
if baseName != "" and baseName == typeName and minfo.decl.declFuncTypeParams.len > 0:
|
||||
calleeName = ctx.generateMethodInstance(calleeName, typeArgs)
|
||||
var args: seq[HirNode] = @[]
|
||||
let loweredReceiver = ctx.lowerExpr(objExpr)
|
||||
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)
|
||||
args.add(ctx.lowerExpr(idxExpr))
|
||||
return hirCall(calleeName, args, typ, loc)
|
||||
return nil
|
||||
|
||||
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
if expr == nil: return nil
|
||||
let loc = expr.loc
|
||||
@@ -521,6 +735,13 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
|
||||
of ekIdent:
|
||||
let name = expr.exprIdent
|
||||
# Capture rewriting: if inside closure and ident is captured
|
||||
if ctx.closureDepth > 0 and ctx.currentClosureExpr != nil and ctx.envInstanceName != "":
|
||||
let idx = ctx.currentClosureExpr.captureNames.find(name)
|
||||
if idx >= 0:
|
||||
let capType = if idx < ctx.currentClosureExpr.captureTypeKinds.len: Type(kind: TypeKind(ctx.currentClosureExpr.captureTypeKinds[idx])) else: makeInt()
|
||||
let base = hirVar(ctx.envInstanceName, makeNamed(""), loc)
|
||||
return HirNode(kind: hFieldAccess, fieldAccessName: name, fieldAccessBase: base, typ: capType, loc: loc)
|
||||
if ctx.importTable.hasKey(name):
|
||||
return hirVar(ctx.importTable[name], typ, loc)
|
||||
return hirVar(name, typ, loc)
|
||||
@@ -561,6 +782,9 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
let ifNode = hirIf(left, thenBlock, elseBlock, loc)
|
||||
return hirBlock(@[tmp, ifNode], hirLoad(tmp, makeBool(), loc), makeBool(), loc)
|
||||
else:
|
||||
let lowered = ctx.tryLowerOperatorCall(expr.exprBinaryOp, expr.exprBinaryLeft, expr.exprBinaryRight, typ, loc)
|
||||
if lowered != nil:
|
||||
return lowered
|
||||
let left = ctx.lowerExpr(expr.exprBinaryLeft)
|
||||
let right = ctx.lowerExpr(expr.exprBinaryRight)
|
||||
return hirBinary(expr.exprBinaryOp, left, right, typ, loc)
|
||||
@@ -668,6 +892,17 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
of ekField:
|
||||
let objType = ctx.resolveExprType(expr.exprFieldObj)
|
||||
let base = ctx.lowerExpr(expr.exprFieldObj)
|
||||
# Simple enum .tag is the enum value itself
|
||||
if objType.kind == tkNamed and expr.exprFieldName == "tag":
|
||||
let sym = ctx.globalScope.lookup(objType.name)
|
||||
if sym != nil and sym.decl != nil and sym.decl.kind == dkEnum:
|
||||
var hasData = false
|
||||
for v in sym.decl.declEnumVariants:
|
||||
if v.fields.len > 0 or v.namedFields.len > 0:
|
||||
hasData = true
|
||||
break
|
||||
if not hasData:
|
||||
return base
|
||||
# Auto-dereference pointer types for field access
|
||||
if objType.isPointer:
|
||||
let arrowPtr = HirNode(kind: hArrowField, arrowFieldBase: base,
|
||||
@@ -680,9 +915,13 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
return HirNode(kind: hLoad, loadPtr: basePtr, typ: typ, loc: loc)
|
||||
|
||||
of ekIndex:
|
||||
let baseType = ctx.resolveExprType(expr.exprIndexObj)
|
||||
if not baseType.isSlice:
|
||||
let lowered = ctx.tryLowerIndexCall(expr.exprIndexObj, expr.exprIndexIdx, typ, loc)
|
||||
if lowered != nil:
|
||||
return lowered
|
||||
let base = ctx.lowerExpr(expr.exprIndexObj)
|
||||
let idx = ctx.lowerExpr(expr.exprIndexIdx)
|
||||
let baseType = ctx.resolveExprType(expr.exprIndexObj)
|
||||
if baseType.isSlice:
|
||||
let sliceIdx = HirNode(kind: hSliceIndex, sliceIndexBase: base,
|
||||
sliceIndexIndex: idx,
|
||||
@@ -694,6 +933,44 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
return HirNode(kind: hLoad, loadPtr: basePtr, typ: typ, loc: loc)
|
||||
|
||||
of ekAssign:
|
||||
# Check for operator_index_set overload
|
||||
if expr.exprAssignTarget.kind == ekIndex:
|
||||
let objExpr = expr.exprAssignTarget.exprIndexObj
|
||||
let idxExpr = expr.exprAssignTarget.exprIndexIdx
|
||||
let receiverType = ctx.resolveExprType(objExpr)
|
||||
var receiverTypeName = ""
|
||||
if receiverType.kind == tkNamed:
|
||||
receiverTypeName = receiverType.name
|
||||
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.kind in {tkInt, tkInt8, tkInt16, tkInt32, tkInt64,
|
||||
tkUInt, tkUInt8, tkUInt16, tkUInt32, tkUInt64,
|
||||
tkFloat32, tkFloat64, tkBool, tkStr, tkChar8}:
|
||||
receiverTypeName = receiverType.toString
|
||||
elif receiverType.isPointer and receiverType.inner.len > 0 and receiverType.inner[0].kind == tkNamed:
|
||||
receiverTypeName = receiverType.inner[0].name
|
||||
let (typeName, methods) = ctx.findMethodEntry(receiverTypeName)
|
||||
if typeName != "":
|
||||
for minfo in methods:
|
||||
if minfo.name == "operator_index_set":
|
||||
var calleeName = typeName & "_operator_index_set"
|
||||
let recvTypeExpr = ctx.getReceiverTypeExpr(objExpr)
|
||||
let (baseName, typeArgs) = ctx.extractGenericStructInfo(recvTypeExpr)
|
||||
if baseName != "" and baseName == typeName and minfo.decl.declFuncTypeParams.len > 0:
|
||||
calleeName = ctx.generateMethodInstance(calleeName, typeArgs)
|
||||
var args: seq[HirNode] = @[]
|
||||
let loweredReceiver = ctx.lowerExpr(objExpr)
|
||||
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)
|
||||
args.add(ctx.lowerExpr(idxExpr))
|
||||
args.add(ctx.lowerExpr(expr.exprAssignValue))
|
||||
return hirCall(calleeName, args, makeVoid(), loc)
|
||||
let target = ctx.lowerExpr(expr.exprAssignTarget)
|
||||
let value = ctx.lowerExpr(expr.exprAssignValue)
|
||||
return HirNode(kind: hAssign, assignOp: expr.exprAssignOp,
|
||||
@@ -701,9 +978,6 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
typ: makeVoid(), loc: loc)
|
||||
|
||||
of ekStructInit:
|
||||
var fields: seq[tuple[name: string, value: HirNode]] = @[]
|
||||
for f in expr.exprStructInitFields:
|
||||
fields.add((f.name, ctx.lowerExpr(f.value)))
|
||||
var structName = expr.exprStructInitName
|
||||
if expr.exprStructInitTypeArgs.len > 0:
|
||||
var suffix = ""
|
||||
@@ -712,6 +986,24 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
let argType = ctx.resolveTypeExpr(targ)
|
||||
suffix.add(argType.toString)
|
||||
structName = structName & "_" & suffix
|
||||
# Simple enum init: EnumName { tag: EnumName_Variant } -> EnumName_Variant
|
||||
var enumDecl: Decl = nil
|
||||
let enumSym = ctx.globalScope.lookup(structName)
|
||||
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
|
||||
enumDecl = enumSym.decl
|
||||
var isSimple = false
|
||||
if enumDecl != nil:
|
||||
for v in enumDecl.declEnumVariants:
|
||||
if v.fields.len > 0 or v.namedFields.len > 0:
|
||||
isSimple = true
|
||||
break
|
||||
isSimple = not isSimple
|
||||
if isSimple and expr.exprStructInitFields.len == 1 and expr.exprStructInitFields[0].name == "tag":
|
||||
let variantExpr = ctx.lowerExpr(expr.exprStructInitFields[0].value)
|
||||
return variantExpr
|
||||
var fields: seq[tuple[name: string, value: HirNode]] = @[]
|
||||
for f in expr.exprStructInitFields:
|
||||
fields.add((f.name, ctx.lowerExpr(f.value)))
|
||||
return HirNode(kind: hStructInit, structInitName: structName,
|
||||
structInitFields: fields, typ: typ, loc: loc)
|
||||
|
||||
@@ -896,6 +1188,56 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
let lowered = ctx.lowerExpr(expr.exprAwaitOperand)
|
||||
return hirCall("bux_async_await", @[lowered], makePointer(makeVoid()), loc)
|
||||
|
||||
of ekBorrow:
|
||||
# borrow &mut expr — lowered to the operand directly (borrow is a no-op in HIR)
|
||||
# The borrow checker validates before lowering
|
||||
return ctx.lowerExpr(expr.exprBorrowOperand)
|
||||
|
||||
of ekStringInterp:
|
||||
# Desugar string interpolation to chained String_Concat calls with conversions
|
||||
var resultNode: HirNode = nil
|
||||
for i in 0 ..< expr.exprInterpExprs.len:
|
||||
let textPart = expr.exprInterpTexts[i]
|
||||
let exprPart = expr.exprInterpExprs[i]
|
||||
# Text literal
|
||||
var textNode = HirNode(kind: hLit,
|
||||
litToken: Token(kind: tkStringLiteral, text: "\"" & textPart & "\"", loc: loc),
|
||||
typ: makeStr(), loc: loc)
|
||||
if resultNode == nil:
|
||||
resultNode = textNode
|
||||
else:
|
||||
resultNode = hirCall("String_Concat", @[resultNode, textNode], makeStr(), loc)
|
||||
# Expression part with conversion if needed
|
||||
let loweredExpr = ctx.lowerExpr(exprPart)
|
||||
let exprType = ctx.resolveExprType(exprPart)
|
||||
var convertedExpr = loweredExpr
|
||||
if exprType.kind == tkInt or exprType.kind == tkInt8 or exprType.kind == tkInt16 or
|
||||
exprType.kind == tkInt32 or exprType.kind == tkInt64 or
|
||||
exprType.kind == tkUInt or exprType.kind == tkUInt8 or exprType.kind == tkUInt16 or
|
||||
exprType.kind == tkUInt32 or exprType.kind == tkUInt64:
|
||||
convertedExpr = hirCall("String_FromInt", @[loweredExpr], makeStr(), loc)
|
||||
elif exprType.kind == tkFloat32 or exprType.kind == tkFloat64:
|
||||
convertedExpr = hirCall("String_FromFloat", @[loweredExpr], makeStr(), loc)
|
||||
elif exprType.kind == tkBool:
|
||||
convertedExpr = hirCall("String_FromBool", @[loweredExpr], makeStr(), loc)
|
||||
elif exprType.kind == tkStr:
|
||||
discard # already a string
|
||||
resultNode = hirCall("String_Concat", @[resultNode, convertedExpr], makeStr(), loc)
|
||||
# Add final text part
|
||||
let lastText = expr.exprInterpTexts[^1]
|
||||
var lastTextNode = HirNode(kind: hLit,
|
||||
litToken: Token(kind: tkStringLiteral, text: "\"" & lastText & "\"", loc: loc),
|
||||
typ: makeStr(), loc: loc)
|
||||
if resultNode == nil:
|
||||
resultNode = lastTextNode
|
||||
else:
|
||||
resultNode = hirCall("String_Concat", @[resultNode, lastTextNode], makeStr(), loc)
|
||||
return resultNode
|
||||
|
||||
of ekClosure:
|
||||
let f = ctx.lowerClosureFunc(expr)
|
||||
return hirUnary(tkAmp, hirVar(f.name, makeFunc(@[], makeVoid()), loc), typ, loc)
|
||||
|
||||
else:
|
||||
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
|
||||
typ: makeVoid(), loc: loc)
|
||||
@@ -924,6 +1266,12 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
of tekSlice:
|
||||
let elemType = ctx.resolveTypeExpr(stmt.stmtLetType.sliceElement)
|
||||
makeSlice(elemType)
|
||||
of tekFunc:
|
||||
var params: seq[Type] = @[]
|
||||
for p in stmt.stmtLetType.funcParams:
|
||||
params.add(ctx.resolveTypeExpr(p))
|
||||
let ret = if stmt.stmtLetType.funcRet != nil: ctx.resolveTypeExpr(stmt.stmtLetType.funcRet) else: makeVoid()
|
||||
makeFunc(params, ret)
|
||||
else: makeUnknown()
|
||||
elif stmt.stmtLetInit != nil:
|
||||
ctx.resolveExprType(stmt.stmtLetInit)
|
||||
@@ -948,11 +1296,33 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
if initHir != nil:
|
||||
let store = hirStore(varNode, initHir, loc)
|
||||
stmts.add(store)
|
||||
# If init is a closure with captures, emit capture assignments
|
||||
if stmt.stmtLetInit != nil and stmt.stmtLetInit.kind == ekClosure and stmt.stmtLetInit.captureCount > 0:
|
||||
let closureIdx = ctx.varCounter - 1
|
||||
let envInst = "__closure_env_instance_" & $closureIdx
|
||||
var capStmts: seq[HirNode] = @[]
|
||||
for i in 0 ..< stmt.stmtLetInit.captureCount:
|
||||
let capName = stmt.stmtLetInit.captureNames[i]
|
||||
let capType = if i < stmt.stmtLetInit.captureTypeKinds.len: Type(kind: TypeKind(stmt.stmtLetInit.captureTypeKinds[i])) else: makeInt()
|
||||
let base = hirVar(envInst, makeNamed(""), loc)
|
||||
let field = HirNode(kind: hFieldAccess, fieldAccessName: capName, fieldAccessBase: base, typ: capType, loc: loc)
|
||||
let val = hirVar(capName, capType, loc)
|
||||
capStmts.add(hirAssign(field, val, loc))
|
||||
# Prepend capture assignments before the let
|
||||
var allStmts = capStmts
|
||||
allStmts.add(stmts)
|
||||
return hirBlock(allStmts, nil, makeVoid(), loc)
|
||||
return hirBlock(stmts, nil, makeVoid(), loc)
|
||||
|
||||
of skReturn:
|
||||
let value = if stmt.stmtReturnValue != nil: ctx.lowerExpr(stmt.stmtReturnValue) else: nil
|
||||
return ctx.flushPending(hirReturn(value, loc))
|
||||
var stmts = ctx.pendingStmts
|
||||
ctx.pendingStmts = @[]
|
||||
# Add defers in reverse order (LIFO)
|
||||
for i in countdown(ctx.deferStmts.len - 1, 0):
|
||||
stmts.add(ctx.deferStmts[i])
|
||||
stmts.add(hirReturn(value, loc))
|
||||
return hirBlock(stmts, nil, makeVoid(), loc)
|
||||
|
||||
of skIf:
|
||||
let cond = ctx.lowerExpr(stmt.stmtIfCond)
|
||||
@@ -1015,7 +1385,8 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
let inclusive = iterExpr.exprRangeInclusive
|
||||
|
||||
# Determine loop variable type from range bounds
|
||||
let varType = ctx.resolveExprType(iterExpr.exprRangeLo)
|
||||
let rangeType = ctx.resolveExprType(iterExpr)
|
||||
let varType = if rangeType.inner.len > 0: rangeType.inner[0] else: ctx.resolveExprType(iterExpr.exprRangeLo)
|
||||
|
||||
# Create: var i = lo; while i < hi { body; i = i + 1; }
|
||||
let initStmt = hirAlloca(varName, varType, loc)
|
||||
@@ -1046,10 +1417,130 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
let forBlock = hirBlock(@[initStmt, initStore, whileNode], nil, makeVoid(), loc, isScope = true)
|
||||
return ctx.flushPending(forBlock)
|
||||
|
||||
# Generic iterator for loop (simplified - just infinite loop for now)
|
||||
let loweredIter = ctx.lowerExpr(iterExpr)
|
||||
# Collection-based for: for x in collection { body }
|
||||
let collType = ctx.resolveExprType(iterExpr)
|
||||
let elemTypeExpr = ctx.getCollectionElementTypeExpr(iterExpr)
|
||||
let elemType = ctx.resolveTypeExpr(elemTypeExpr)
|
||||
# Resolve the collection type to its mangled struct instance (e.g. Array<int> -> Array_int).
|
||||
let collTypeMangled = substituteType(ctx, typeToTypeExpr(collType), ctx.typeSubst)
|
||||
|
||||
let isChannel = collType.kind == tkNamed and collType.name.startsWith("Channel")
|
||||
|
||||
if isChannel:
|
||||
# Channel lowering:
|
||||
# alloca x
|
||||
# while (true) {
|
||||
# if (!Channel_Recv_Ok_T(&ch, &x)) break;
|
||||
# body
|
||||
# }
|
||||
let recvOkName = ctx.generateMethodInstance("Channel_Recv_Ok", @[elemTypeExpr])
|
||||
|
||||
let xAlloca = hirAlloca(varName, elemType, loc)
|
||||
let xVar = hirVar(varName, elemType, loc)
|
||||
|
||||
ctx.varTypeExprs[varName] = elemTypeExpr
|
||||
|
||||
let chAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: ctx.lowerExpr(iterExpr),
|
||||
typ: makePointer(collType), loc: loc)
|
||||
let xAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: xVar,
|
||||
typ: makePointer(elemType), loc: loc)
|
||||
let recvOkCall = hirCall(recvOkName, @[chAddr, xAddr], makeBool(), loc)
|
||||
let notRecvOk = HirNode(kind: hUnary, unaryOp: tkBang, unaryOperand: recvOkCall,
|
||||
typ: makeBool(), loc: loc)
|
||||
let breakNode = HirNode(kind: hBreak, loc: loc)
|
||||
let ifNode = HirNode(kind: hIf, ifCond: notRecvOk, ifThen: breakNode, ifElse: nil,
|
||||
typ: makeVoid(), loc: loc)
|
||||
|
||||
let loweredBody = ctx.lowerBlock(body)
|
||||
var whileBodyStmts: seq[HirNode] = @[]
|
||||
whileBodyStmts.add(xAlloca)
|
||||
whileBodyStmts.add(ifNode)
|
||||
if loweredBody != nil:
|
||||
whileBodyStmts.add(loweredBody)
|
||||
let whileBody = hirBlock(whileBodyStmts, nil, makeVoid(), loc)
|
||||
|
||||
let trueLit = hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc)
|
||||
let whileNode = HirNode(kind: hWhile, whileCond: trueLit, whileBody: whileBody,
|
||||
typ: makeVoid(), loc: loc)
|
||||
|
||||
let forBlock = hirBlock(@[whileNode], nil, makeVoid(), loc, isScope = true)
|
||||
return ctx.flushPending(forBlock)
|
||||
|
||||
# Array / Iter lowering:
|
||||
# alloca __iter
|
||||
# __iter = Array_Iter_T(&collection);
|
||||
# while (Iter_HasNext_T(&__iter)) {
|
||||
# alloca x
|
||||
# x = Iter_Next_T(&__iter);
|
||||
# body
|
||||
# }
|
||||
let iterFuncName = ctx.generateMethodInstance("Array_Iter", @[elemTypeExpr])
|
||||
let hasNextFuncName = ctx.generateMethodInstance("Iter_HasNext", @[elemTypeExpr])
|
||||
let nextFuncName = ctx.generateMethodInstance("Iter_Next", @[elemTypeExpr])
|
||||
|
||||
# Ensure Iter<T> struct instance exists and resolve its mangled name.
|
||||
let iterType = substituteType(ctx, TypeExpr(kind: tekNamed, typeName: "Iter", typeArgs: @[elemTypeExpr]), ctx.typeSubst)
|
||||
|
||||
let iterVarName = "__iter_" & varName & "_" & $ctx.varCounter
|
||||
inc ctx.varCounter
|
||||
|
||||
# Build collection pointer. If the collection is not a simple identifier, spill to a temp.
|
||||
var preStmts: seq[HirNode] = @[]
|
||||
var collPtr: HirNode = nil
|
||||
if iterExpr.kind == ekIdent:
|
||||
let collVar = hirVar(iterExpr.exprIdent, collType, loc)
|
||||
collPtr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: collVar,
|
||||
typ: makePointer(collType), loc: loc)
|
||||
else:
|
||||
let collAllocaName = ctx.freshName()
|
||||
let collAlloca = hirAlloca(collAllocaName, collTypeMangled, loc)
|
||||
let collVarPtr = hirVar(collAllocaName, makePointer(collTypeMangled), loc)
|
||||
let collValue = ctx.lowerExpr(iterExpr)
|
||||
let collStore = hirStore(collVarPtr, collValue, loc)
|
||||
preStmts.add(collAlloca)
|
||||
preStmts.add(collStore)
|
||||
collPtr = HirNode(kind: hUnary, unaryOp: tkAmp,
|
||||
unaryOperand: hirVar(collAllocaName, collTypeMangled, loc),
|
||||
typ: makePointer(collTypeMangled), loc: loc)
|
||||
|
||||
let iterAlloca = hirAlloca(iterVarName, iterType, loc)
|
||||
let iterVarPtr = hirVar(iterVarName, makePointer(iterType), loc)
|
||||
let iterInitCall = hirCall(iterFuncName, @[collPtr], iterType, loc)
|
||||
let iterStore = hirStore(iterVarPtr, iterInitCall, loc)
|
||||
|
||||
preStmts.add(iterAlloca)
|
||||
preStmts.add(iterStore)
|
||||
|
||||
# while condition: Iter_HasNext_T(&__iter)
|
||||
let iterAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: hirVar(iterVarName, iterType, loc),
|
||||
typ: makePointer(iterType), loc: loc)
|
||||
let condCall = hirCall(hasNextFuncName, @[iterAddr], makeBool(), loc)
|
||||
|
||||
# loop body: alloca x; x = Iter_Next_T(&__iter); body
|
||||
let xAlloca = hirAlloca(varName, elemType, loc)
|
||||
let xVarPtr = hirVar(varName, makePointer(elemType), loc)
|
||||
let iterAddr2 = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: hirVar(iterVarName, iterType, loc),
|
||||
typ: makePointer(iterType), loc: loc)
|
||||
let nextCall = hirCall(nextFuncName, @[iterAddr2], elemType, loc)
|
||||
let xStore = hirStore(xVarPtr, nextCall, loc)
|
||||
|
||||
ctx.varTypeExprs[varName] = elemTypeExpr
|
||||
let loweredBody = ctx.lowerBlock(body)
|
||||
return ctx.flushPending(HirNode(kind: hLoop, loopBody: loweredBody, typ: makeVoid(), loc: loc))
|
||||
|
||||
var bodyStmts: seq[HirNode] = @[]
|
||||
bodyStmts.add(xAlloca)
|
||||
bodyStmts.add(xStore)
|
||||
if loweredBody != nil:
|
||||
bodyStmts.add(loweredBody)
|
||||
let whileBody = hirBlock(bodyStmts, nil, makeVoid(), loc)
|
||||
|
||||
let whileNode = HirNode(kind: hWhile, whileCond: condCall, whileBody: whileBody,
|
||||
typ: makeVoid(), loc: loc)
|
||||
|
||||
var blockStmts = preStmts
|
||||
blockStmts.add(whileNode)
|
||||
let forBlock = hirBlock(blockStmts, nil, makeVoid(), loc, isScope = true)
|
||||
return ctx.flushPending(forBlock)
|
||||
|
||||
of skDoWhile:
|
||||
let body = ctx.lowerBlock(stmt.stmtDoWhileBody)
|
||||
@@ -1067,6 +1558,29 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
return ctx.flushPending(HirNode(kind: hMatch, matchSubject: subject, matchArms: arms,
|
||||
typ: makeVoid(), loc: loc))
|
||||
|
||||
of skSwitch:
|
||||
let subject = ctx.lowerExpr(stmt.stmtSwitchExpr)
|
||||
var current: HirNode = nil
|
||||
# Build if-else chain from bottom up (default first)
|
||||
if stmt.stmtSwitchDefault != nil:
|
||||
current = ctx.lowerBlock(stmt.stmtSwitchDefault)
|
||||
# Cases in reverse order
|
||||
for i in countdown(stmt.stmtSwitchCases.len - 1, 0):
|
||||
let caseBranch = stmt.stmtSwitchCases[i]
|
||||
let caseVal = ctx.lowerExpr(caseBranch.caseValue)
|
||||
let caseBody = ctx.lowerBlock(caseBranch.caseBody)
|
||||
let cond = HirNode(kind: hBinary, binaryOp: tkEq,
|
||||
binaryLeft: subject, binaryRight: caseVal,
|
||||
typ: makeBool(), loc: caseBranch.loc)
|
||||
current = HirNode(kind: hIf, ifCond: cond, ifThen: caseBody, ifElse: current,
|
||||
typ: makeVoid(), loc: caseBranch.loc)
|
||||
return ctx.flushPending(current)
|
||||
|
||||
of skDefer:
|
||||
let body = ctx.lowerExpr(stmt.stmtDeferBody)
|
||||
ctx.deferStmts.add(body)
|
||||
return nil
|
||||
|
||||
of skDecl:
|
||||
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
|
||||
typ: makeVoid(), loc: loc)
|
||||
@@ -1122,8 +1636,6 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
|
||||
if p.ptype != nil:
|
||||
pType = substituteType(ctx, p.ptype, ctx.typeSubst)
|
||||
params.add((p.name, pType))
|
||||
if p.ptype != nil:
|
||||
ctx.varTypeExprs[p.name] = p.ptype
|
||||
|
||||
var retType = makeVoid()
|
||||
if funcReturnType != nil:
|
||||
@@ -1135,7 +1647,28 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
|
||||
ctx.currentFuncRetType = retType
|
||||
ctx.currentFuncDecl = decl
|
||||
ctx.varTypeExprs = initTable[string, TypeExpr]() # Clear local vars for new function
|
||||
let body = if funcBody != nil: ctx.lowerBlock(funcBody) else: nil
|
||||
# Add parameters to varTypeExprs after clearing so they are visible in the body.
|
||||
for p in funcParams:
|
||||
if p.ptype != nil:
|
||||
ctx.varTypeExprs[p.name] = p.ptype
|
||||
var body = if funcBody != nil: ctx.lowerBlock(funcBody) else: nil
|
||||
|
||||
# Inject remaining defers at end of function (for implicit return)
|
||||
if ctx.deferStmts.len > 0 and body != nil and body.kind == hBlock:
|
||||
# Only add if last statement is not already a return (defers already injected there)
|
||||
var hasReturn = false
|
||||
if body.blockStmts.len > 0 and body.blockStmts[^1].kind == hReturn:
|
||||
hasReturn = true
|
||||
elif body.blockStmts.len > 0 and body.blockStmts[^1].kind == hBlock:
|
||||
# Check nested block's last statement
|
||||
let last = body.blockStmts[^1]
|
||||
if last.blockStmts.len > 0 and last.blockStmts[^1].kind == hReturn:
|
||||
hasReturn = true
|
||||
if not hasReturn:
|
||||
for i in countdown(ctx.deferStmts.len - 1, 0):
|
||||
body.blockStmts.add(ctx.deferStmts[i])
|
||||
ctx.deferStmts = @[]
|
||||
|
||||
ctx.currentFuncDecl = oldFuncDecl
|
||||
ctx.currentFuncRetType = oldFuncRetType
|
||||
ctx.varTypeExprs = oldVarTypeExprs
|
||||
@@ -1186,6 +1719,40 @@ proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs:
|
||||
ctx.generatedFuncInsts[mangledName] = true
|
||||
return mangledName
|
||||
|
||||
proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
|
||||
let name = "__closure_" & $ctx.varCounter
|
||||
inc ctx.varCounter
|
||||
var f = HirFunc(name: name, isPublic: false)
|
||||
# Copy capture metadata
|
||||
if expr.captureCount > 0:
|
||||
f.captureNames = expr.captureNames
|
||||
for tk in expr.captureTypeKinds:
|
||||
f.captureTypes.add(Type(kind: TypeKind(tk)))
|
||||
f.envStructName = "__closure_env_" & $(ctx.varCounter - 1)
|
||||
f.envInstanceName = "__closure_env_instance_" & $(ctx.varCounter - 1)
|
||||
# Params
|
||||
for p in expr.exprClosureParams:
|
||||
f.params.add((name: p.name, typ: if p.ptype != nil: ctx.resolveTypeExpr(p.ptype) else: makeUnknown()))
|
||||
# Return type
|
||||
if expr.exprClosureReturnType != nil:
|
||||
f.retType = ctx.resolveTypeExpr(expr.exprClosureReturnType)
|
||||
else:
|
||||
f.retType = makeVoid()
|
||||
# Body with closure rewriting
|
||||
let savedDepth = ctx.closureDepth
|
||||
let savedExpr = ctx.currentClosureExpr
|
||||
let savedEnv = ctx.envInstanceName
|
||||
ctx.closureDepth = ctx.closureDepth + 1
|
||||
ctx.currentClosureExpr = expr
|
||||
ctx.envInstanceName = f.envInstanceName
|
||||
if expr.exprClosureBody != nil:
|
||||
f.body = ctx.lowerBlock(expr.exprClosureBody)
|
||||
ctx.closureDepth = savedDepth
|
||||
ctx.currentClosureExpr = savedExpr
|
||||
ctx.envInstanceName = savedEnv
|
||||
ctx.extraFuncs.add(f)
|
||||
return f
|
||||
|
||||
proc lowerModule*(module: Module, sema: Sema): HirModule =
|
||||
var ctx = initLowerCtx(module, sema)
|
||||
var funcs: seq[HirFunc] = @[]
|
||||
@@ -1356,6 +1923,9 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
|
||||
var found = false
|
||||
for avail in methods:
|
||||
if avail.name == req.declFuncName:
|
||||
# Skip generic methods — they have no concrete C function to put in vtable
|
||||
if avail.decl.declFuncTypeParams.len > 0:
|
||||
break
|
||||
found = true
|
||||
methodNames.add(req.declFuncName)
|
||||
break
|
||||
|
||||
+4
-23
@@ -61,12 +61,6 @@ proc advance(lex: var Lexer): char =
|
||||
else:
|
||||
inc lex.col
|
||||
|
||||
proc match(lex: var Lexer, expected: char): bool =
|
||||
if lex.isAtEnd(): return false
|
||||
if lex.peek() != expected: return false
|
||||
discard lex.advance()
|
||||
return true
|
||||
|
||||
proc matchStr(lex: var Lexer, s: string): bool =
|
||||
for i, c in s:
|
||||
if lex.peek(i) != c:
|
||||
@@ -81,9 +75,6 @@ proc currentLocation(lex: Lexer): SourceLocation =
|
||||
proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
|
||||
lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))
|
||||
|
||||
proc emitWarning(lex: var Lexer, loc: SourceLocation, message: string) =
|
||||
lex.diagnostics.add(LexerDiagnostic(severity: ldsWarning, loc: loc, message: message))
|
||||
|
||||
proc makeToken(lex: Lexer, kind: TokenKind, startLoc: SourceLocation, startPos: int): Token =
|
||||
let text = lex.source[startPos ..< lex.pos]
|
||||
result = Token(kind: kind, text: text, loc: startLoc)
|
||||
@@ -337,19 +328,6 @@ proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token =
|
||||
else:
|
||||
return lex.makeToken(kind1, startLoc, startPos)
|
||||
|
||||
template check3(c2: char, kind2: TokenKind, c3: char, kind3: TokenKind, kind1: TokenKind) =
|
||||
if lex.peek() == c2:
|
||||
discard lex.advance()
|
||||
if lex.peek() == c3:
|
||||
discard lex.advance()
|
||||
return lex.makeToken(kind3, startLoc, startPos)
|
||||
return lex.makeToken(kind2, startLoc, startPos)
|
||||
else:
|
||||
return lex.makeToken(kind1, startLoc, startPos)
|
||||
|
||||
template checkEq(c2: char, kind2: TokenKind, kind1: TokenKind) =
|
||||
check2(c2, kind2, kind1)
|
||||
|
||||
case c1
|
||||
of '(': return lex.makeToken(tkLParen, startLoc, startPos)
|
||||
of ')': return lex.makeToken(tkRParen, startLoc, startPos)
|
||||
@@ -527,7 +505,10 @@ proc nextToken(lex: var Lexer): Token =
|
||||
discard lex.advance()
|
||||
return Token(kind: tkNewLine, text: "\n", loc: startLoc)
|
||||
|
||||
# String prefixes: c8" c16" c32" — must come before ident check
|
||||
# String prefixes: f" c8" c16" c32" — must come before ident check
|
||||
if c == 'f' and lex.peek(1) == '"':
|
||||
discard lex.advance() # f
|
||||
return lex.scanString(startLoc, 1)
|
||||
if c == 'c' and lex.peek(1) in {'8', '1', '3'}:
|
||||
let d = lex.peek(1)
|
||||
if d == '8' and lex.peek(2) == '"':
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
## Linear 3-address code IR, designed for straightforward C emission.
|
||||
## Each HIR construct lowers to 5-30 LIR instructions.
|
||||
|
||||
import types
|
||||
|
||||
type
|
||||
LirKind* = enum
|
||||
# ── Data movement ──
|
||||
|
||||
+171
-48
@@ -2,7 +2,7 @@
|
||||
## Emits clean, well-structured C code from LIR instructions.
|
||||
## Since LIR is already linear and low-level, C emission is straightforward.
|
||||
|
||||
import std/[strutils, strformat, tables, sequtils]
|
||||
import std/[strutils, strformat, tables, sequtils, sets]
|
||||
import lir, hir, types, token
|
||||
|
||||
type
|
||||
@@ -17,9 +17,6 @@ proc initLirCBackend*(): LirCBackend =
|
||||
tempTypes: initTable[string, string](),
|
||||
)
|
||||
|
||||
proc emit(be: var LirCBackend, s: string) =
|
||||
be.output.add(s)
|
||||
|
||||
proc emitIndent(be: var LirCBackend) =
|
||||
for i in 0 ..< be.indent:
|
||||
be.output.add(" ")
|
||||
@@ -43,20 +40,12 @@ proc valToC(be: var LirCBackend, v: LirValue): string =
|
||||
of lvkField: v.strVal
|
||||
of lvkType: v.strVal
|
||||
|
||||
proc typeFromValue(be: var LirCBackend, v: LirValue): string =
|
||||
## Infer a C type for a value. Temps are tracked; named vars use lookup.
|
||||
case v.kind
|
||||
of lvkTemp:
|
||||
if be.tempTypes.hasKey(v.strVal):
|
||||
return be.tempTypes[v.strVal]
|
||||
return "int" # Default
|
||||
of lvkString: return "const char*"
|
||||
of lvkInt: return "int"
|
||||
of lvkFloat: return "double"
|
||||
else: return ""
|
||||
|
||||
proc setTempType(be: var LirCBackend, temp: string, cType: string) =
|
||||
be.tempTypes[temp] = cType
|
||||
proc cParamDecl(cType, name: string): string =
|
||||
## Emit a C parameter declaration, handling function-pointer syntax.
|
||||
if cType.contains("(*)"):
|
||||
return cType.replace("(*)", "(*" & name & ")")
|
||||
else:
|
||||
return cType & " " & name
|
||||
|
||||
# ── Per-instruction emission ──
|
||||
|
||||
@@ -183,7 +172,7 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
|
||||
let inferred = be.tempTypes[instr.dst.strVal]
|
||||
if inferred != "" and inferred != ct:
|
||||
ct = inferred
|
||||
be.emitLine(&"{ct} {v(instr.dst)};")
|
||||
be.emitLine(cParamDecl(ct, v(instr.dst)) & ";")
|
||||
|
||||
# ── Pointers ──
|
||||
of lirAddrOf:
|
||||
@@ -241,11 +230,11 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
|
||||
|
||||
# ── Function emission ──
|
||||
|
||||
proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, string]) =
|
||||
proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, string], funcPtrTypes: Table[string, string]) =
|
||||
var paramsStr = ""
|
||||
for i, p in f.params:
|
||||
if i > 0: paramsStr.add(", ")
|
||||
paramsStr.add(&"{p.cType} {p.name}")
|
||||
paramsStr.add(cParamDecl(p.cType, p.name))
|
||||
if f.params.len == 0:
|
||||
paramsStr = "void"
|
||||
|
||||
@@ -318,7 +307,12 @@ proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, strin
|
||||
newType = be.tempTypes[instr.extra[0].strVal]
|
||||
elif instr.src2.kind == lvkVar and varTypes.hasKey(instr.src2.strVal):
|
||||
newType = varTypes[instr.src2.strVal]
|
||||
of lirAddrOf, lirFieldPtr, lirArrowFieldPtr, lirIndexPtr, lirPtrAdd:
|
||||
of lirAddrOf:
|
||||
if funcPtrTypes.hasKey(instr.src.strVal):
|
||||
newType = funcPtrTypes[instr.src.strVal]
|
||||
else:
|
||||
newType = "void*";
|
||||
of lirFieldPtr, lirArrowFieldPtr, lirIndexPtr, lirPtrAdd:
|
||||
newType = "void*"
|
||||
of lirAdd, lirSub, lirMul, lirDiv, lirMod, lirNeg,
|
||||
lirCmpEq, lirCmpNe, lirCmpLt, lirCmpLe, lirCmpGt, lirCmpGe,
|
||||
@@ -342,7 +336,7 @@ proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, strin
|
||||
let ct = be.tempTypes[instr.dst.strVal]
|
||||
if ct != "":
|
||||
declared.add(instr.dst.strVal)
|
||||
be.emitLine(&"{ct} {instr.dst.strVal};")
|
||||
be.emitLine(cParamDecl(ct, instr.dst.strVal) & ";")
|
||||
|
||||
# ── Pass 4: emit instructions ──
|
||||
for instr in f.instrs:
|
||||
@@ -402,6 +396,11 @@ proc typeToCStr(typ: Type): string =
|
||||
of "float64": return "double"
|
||||
of "bool": return "bool"
|
||||
else: return typ.name
|
||||
of tkFunc:
|
||||
if typ.inner.len == 0: return "void (*)(void)"
|
||||
let params = typ.inner[0..^2].mapIt(typeToCStr(it)).join(", ")
|
||||
let ret = typeToCStr(typ.inner[^1])
|
||||
return ret & " (*)(" & params & ")"
|
||||
else: return "int"
|
||||
|
||||
proc emitStructDef(be: var LirCBackend, name: string, fields: seq[tuple[name: string, typ: Type]]) =
|
||||
@@ -470,6 +469,26 @@ proc emitEnumDef(be: var LirCBackend, name: string, variants: seq[HirEnumVariant
|
||||
be.emitLine(&"}} {name};")
|
||||
be.emitLine("")
|
||||
|
||||
# ── Type dependency ordering ──
|
||||
|
||||
proc collectValueDeps(typ: Type): seq[string] =
|
||||
## Return type names that must be fully defined before a value of `typ`
|
||||
## can be declared. Pointers/refs only need a forward declaration, so
|
||||
## they do not introduce a dependency.
|
||||
if typ == nil: return @[]
|
||||
case typ.kind
|
||||
of tkNamed:
|
||||
return @[typ.name]
|
||||
of tkSlice:
|
||||
return @[typeToCStr(typ)]
|
||||
of tkPointer, tkRef, tkMutRef, tkTuple, tkFunc:
|
||||
return @[]
|
||||
else:
|
||||
return @[]
|
||||
|
||||
proc emitSliceTypeDef(be: var LirCBackend, name: string, elem: string) =
|
||||
be.emitLine(&"typedef struct {{ {elem}* data; size_t len; }} {name};")
|
||||
|
||||
# ── Module emission ──
|
||||
|
||||
proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): string =
|
||||
@@ -483,6 +502,17 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
|
||||
for f in module.externFuncs:
|
||||
funcRetTypes[f.name] = typeToCStr(f.retType)
|
||||
|
||||
# Build function-pointer type lookup table (for address-of)
|
||||
var funcPtrTypes = initTable[string, string]()
|
||||
for f in module.funcs:
|
||||
let params = f.params.mapIt(typeToCStr(it.typ)).join(", ")
|
||||
let ret = typeToCStr(f.retType)
|
||||
funcPtrTypes[f.name] = ret & " (*)(" & params & ")"
|
||||
for f in module.externFuncs:
|
||||
let params = f.params.mapIt(typeToCStr(it.typ)).join(", ")
|
||||
let ret = typeToCStr(f.retType)
|
||||
funcPtrTypes[f.name] = ret & " (*)(" & params & ")"
|
||||
|
||||
# Header
|
||||
be.emitLine("/* Generated by Bux Compiler (LIR backend) */")
|
||||
be.emitLine("#include <stdio.h>")
|
||||
@@ -512,7 +542,7 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
|
||||
let rt = typeToCStr(ef.retType)
|
||||
var params: seq[string] = @[]
|
||||
for p in ef.params:
|
||||
params.add(&"{typeToCStr(p.typ)} {p.name}")
|
||||
params.add(cParamDecl(typeToCStr(p.typ), p.name))
|
||||
if params.len == 0: params.add("void")
|
||||
be.emitLine(&"extern {rt} {ef.name}({params.join(\", \")});")
|
||||
be.emitLine("")
|
||||
@@ -529,37 +559,116 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
|
||||
else: discard
|
||||
be.emitLine("")
|
||||
|
||||
# Enum definitions
|
||||
for e in module.enums:
|
||||
be.emitEnumDef(e.name, e.variants)
|
||||
if module.enums.len > 0:
|
||||
be.emitLine("")
|
||||
|
||||
# Struct definitions
|
||||
# Collect local type names (structs and enums defined in this module).
|
||||
var localTypeNames: HashSet[string]
|
||||
for s in module.structs:
|
||||
be.emitStructDef(s.name, s.fields)
|
||||
localTypeNames.incl(s.name)
|
||||
for e in module.enums:
|
||||
localTypeNames.incl(e.name)
|
||||
|
||||
# Slice types (collect from functions/structs)
|
||||
# Simple: scan function params/returns for slice types
|
||||
# Collect slice types used in struct fields and enum payloads.
|
||||
var sliceTypes: seq[tuple[name: string, elem: string]] = @[]
|
||||
for f in module.funcs:
|
||||
for p in f.params:
|
||||
let ct = typeToCStr(p.typ)
|
||||
if ct.startsWith("Slice_"):
|
||||
let elem = ct[6 .. ^1]
|
||||
if not sliceTypes.anyIt(it.name == ct):
|
||||
sliceTypes.add((ct, elem))
|
||||
if sliceTypes.len > 0:
|
||||
for st in sliceTypes:
|
||||
be.emitLine(&"typedef struct {{ {st.elem}* data; size_t len; }} {st.name};")
|
||||
be.emitLine("")
|
||||
var sliceNames: HashSet[string]
|
||||
proc registerSlice(t: Type) =
|
||||
if t == nil or t.kind != tkSlice: return
|
||||
let name = typeToCStr(t)
|
||||
if sliceNames.contains(name): return
|
||||
sliceNames.incl(name)
|
||||
let elem = if t.inner.len > 0: typeToCStr(t.inner[0]) else: "void"
|
||||
sliceTypes.add((name, elem))
|
||||
|
||||
for s in module.structs:
|
||||
for f in s.fields:
|
||||
registerSlice(f.typ)
|
||||
for e in module.enums:
|
||||
for v in e.variants:
|
||||
for ft in v.fields:
|
||||
registerSlice(ft)
|
||||
for nf in v.namedFields:
|
||||
registerSlice(nf.typ)
|
||||
|
||||
# Build dependency graph among structs, enums, and slice types.
|
||||
# Edge A -> B means "A depends on B, so B must be emitted before A".
|
||||
var deps: Table[string, seq[string]]
|
||||
for s in module.structs:
|
||||
deps[s.name] = @[]
|
||||
for e in module.enums:
|
||||
deps[e.name] = @[]
|
||||
for st in sliceTypes:
|
||||
deps[st.name] = @[]
|
||||
|
||||
proc addDeps(node: string, t: Type) =
|
||||
for dep in collectValueDeps(t):
|
||||
if dep == node: continue
|
||||
if localTypeNames.contains(dep) or sliceNames.contains(dep):
|
||||
if dep notin deps[node]:
|
||||
deps[node].add(dep)
|
||||
|
||||
for s in module.structs:
|
||||
for f in s.fields:
|
||||
addDeps(s.name, f.typ)
|
||||
for e in module.enums:
|
||||
for v in e.variants:
|
||||
for ft in v.fields:
|
||||
addDeps(e.name, ft)
|
||||
for nf in v.namedFields:
|
||||
addDeps(e.name, nf.typ)
|
||||
|
||||
# Topological sort (Kahn's algorithm).
|
||||
var inDegree: Table[string, int]
|
||||
var dependents: Table[string, seq[string]]
|
||||
for node in deps.keys:
|
||||
inDegree[node] = 0
|
||||
for node, nodeDeps in deps:
|
||||
for d in nodeDeps:
|
||||
if not inDegree.hasKey(d): inDegree[d] = 0
|
||||
inDegree[node] += 1
|
||||
dependents.mgetOrPut(d, @[]).add(node)
|
||||
|
||||
var queue: seq[string] = @[]
|
||||
for node, deg in inDegree:
|
||||
if deg == 0:
|
||||
queue.add(node)
|
||||
|
||||
var sorted: seq[string] = @[]
|
||||
while queue.len > 0:
|
||||
let node = queue.pop()
|
||||
sorted.add(node)
|
||||
for depNode in dependents.getOrDefault(node):
|
||||
inDegree[depNode] -= 1
|
||||
if inDegree[depNode] == 0:
|
||||
queue.add(depNode)
|
||||
|
||||
if sorted.len < deps.len:
|
||||
# Cycle detected; fall back to a safe deterministic order.
|
||||
sorted = @[]
|
||||
for s in module.structs: sorted.add(s.name)
|
||||
for e in module.enums: sorted.add(e.name)
|
||||
for st in sliceTypes: sorted.add(st.name)
|
||||
|
||||
# Map type names back to their definitions.
|
||||
var structMap: Table[string, seq[tuple[name: string, typ: Type]]]
|
||||
for s in module.structs: structMap[s.name] = s.fields
|
||||
var enumMap: Table[string, seq[HirEnumVariant]]
|
||||
for e in module.enums: enumMap[e.name] = e.variants
|
||||
var sliceMap: Table[string, string]
|
||||
for st in sliceTypes: sliceMap[st.name] = st.elem
|
||||
|
||||
# Emit type definitions in dependency order.
|
||||
for name in sorted:
|
||||
if structMap.hasKey(name):
|
||||
be.emitStructDef(name, structMap[name])
|
||||
elif enumMap.hasKey(name):
|
||||
be.emitEnumDef(name, enumMap[name])
|
||||
elif sliceMap.hasKey(name):
|
||||
be.emitSliceTypeDef(name, sliceMap[name])
|
||||
|
||||
# Forward function declarations
|
||||
for f in module.funcs:
|
||||
let rt = typeToCStr(f.retType)
|
||||
var params: seq[string] = @[]
|
||||
for p in f.params:
|
||||
params.add(&"{typeToCStr(p.typ)} {p.name}")
|
||||
params.add(cParamDecl(typeToCStr(p.typ), p.name))
|
||||
if params.len == 0: params.add("void")
|
||||
be.emitLine(&"{rt} {f.name}({params.join(\", \")});")
|
||||
be.emitLine("")
|
||||
@@ -573,7 +682,7 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
|
||||
for m in iface.methods:
|
||||
var paramCTypes: seq[string] = @["void* self"]
|
||||
for i in 1 ..< m.params.len:
|
||||
paramCTypes.add(typeToCStr(m.params[i]) & " param")
|
||||
paramCTypes.add(cParamDecl(typeToCStr(m.params[i]), "param"))
|
||||
let rt = typeToCStr(m.ret)
|
||||
be.emitLine(&"{rt} (*{m.name})({paramCTypes.join(\", \")});")
|
||||
be.indent -= 1
|
||||
@@ -598,9 +707,23 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
|
||||
be.emitLine("};")
|
||||
be.emitLine("")
|
||||
|
||||
# Emit env structs for closures with captures
|
||||
for f in module.funcs:
|
||||
if f.captureNames.len > 0 and f.envStructName != "":
|
||||
be.emitLine(&"struct {f.envStructName} {{")
|
||||
be.indent += 1
|
||||
for i in 0 ..< f.captureNames.len:
|
||||
let capName = f.captureNames[i]
|
||||
let capType = if i < f.captureTypes.len: typeToCStr(f.captureTypes[i]) else: "int"
|
||||
be.emitLine(&"{capType} {capName};")
|
||||
be.indent -= 1
|
||||
be.emitLine("};")
|
||||
be.emitLine(&"static struct {f.envStructName} {f.envInstanceName};")
|
||||
be.emitLine("")
|
||||
|
||||
# Emit all LIR functions
|
||||
for f in builder.funcs:
|
||||
be.emitFunc(f, funcRetTypes)
|
||||
be.emitFunc(f, funcRetTypes, funcPtrTypes)
|
||||
|
||||
# C main wrapper
|
||||
var hasMain = false
|
||||
|
||||
+23
-8
@@ -3,7 +3,7 @@
|
||||
## Each HIR node kind lowers to 1-20 LIR instructions.
|
||||
|
||||
import std/[strutils, strformat, tables, sequtils]
|
||||
import ast, types, token, hir, lir
|
||||
import types, token, hir, lir
|
||||
|
||||
## Convert LirValue to C expression string (no % prefix)
|
||||
proc lirValToC(v: LirValue): string =
|
||||
@@ -105,6 +105,11 @@ proc typeToCStr(typ: Type): string =
|
||||
of "float64": return "double"
|
||||
of "bool": return "bool"
|
||||
else: return typ.name
|
||||
of tkFunc:
|
||||
if typ.inner.len == 0: return "void (*)(void)"
|
||||
let params = typ.inner[0..^2].mapIt(typeToCStr(it)).join(", ")
|
||||
let ret = typeToCStr(typ.inner[^1])
|
||||
return ret & " (*)(" & params & ")"
|
||||
else: return "int"
|
||||
|
||||
proc hirTypeToC(ctx: var LowerToLirCtx, node: HirNode): string =
|
||||
@@ -340,6 +345,14 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
|
||||
b.emitRawC(&"{t.strVal} = (void*)&({lirValToC(base)}.{node.fieldName});")
|
||||
return t
|
||||
|
||||
of hFieldAccess:
|
||||
let base = lowerExpr(ctx, node.fieldAccessBase)
|
||||
let cType = hirTypeToC(ctx, node)
|
||||
let t = b.freshTemp()
|
||||
b.emitAlloca(t.strVal, cType)
|
||||
b.emitRawC(&"{t.strVal} = {lirValToC(base)}.{node.fieldAccessName};")
|
||||
return t
|
||||
|
||||
of hArrowField:
|
||||
let base = lowerExpr(ctx, node.arrowFieldBase)
|
||||
let t = b.freshTemp()
|
||||
@@ -416,7 +429,6 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
|
||||
# ── SizeOf ──
|
||||
of hSizeOf:
|
||||
let ctype = typeToCStr(node.sizeOfType)
|
||||
let t = b.freshTemp()
|
||||
b.emit(LirInstr(kind: lirRawC, src: lirStr(&"/* sizeof({ctype}) */")))
|
||||
return lirVar(&"sizeof({ctype})")
|
||||
|
||||
@@ -544,6 +556,9 @@ proc buildLval(ctx: var LowerToLirCtx, n: HirNode): string =
|
||||
of hArrowField:
|
||||
let baseStr = buildLval(ctx, n.arrowFieldBase)
|
||||
return baseStr & "->" & n.arrowFieldName
|
||||
of hFieldAccess:
|
||||
let baseStr = buildLval(ctx, n.fieldAccessBase)
|
||||
return baseStr & "." & n.fieldAccessName
|
||||
of hIndexPtr:
|
||||
let baseStr = buildLval(ctx, n.indexPtrBase)
|
||||
let idx = lowerExpr(ctx, n.indexPtrIndex)
|
||||
@@ -592,7 +607,6 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
|
||||
# ── While statement ──
|
||||
of hWhile:
|
||||
let startLbl = b.freshLabel("while")
|
||||
let bodyLbl = b.freshLabel("wbody")
|
||||
let endLbl = b.freshLabel("wend")
|
||||
|
||||
ctx.loopStartLabels.add(startLbl.strVal)
|
||||
@@ -664,6 +678,9 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
|
||||
of hFieldPtr:
|
||||
let lval = buildLval(ctx, node.assignTarget)
|
||||
b.emitRawC(&"{lval} = {lirValToC(value)};")
|
||||
of hFieldAccess:
|
||||
let lval = buildLval(ctx, node.assignTarget)
|
||||
b.emitRawC(&"{lval} = {lirValToC(value)};")
|
||||
of hArrowField:
|
||||
let lval = buildLval(ctx, node.assignTarget)
|
||||
b.emitRawC(&"{lval} = {lirValToC(value)};")
|
||||
@@ -731,9 +748,8 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
|
||||
for stmt in node.blockStmts:
|
||||
lowerStmt(ctx, stmt)
|
||||
if node.blockExpr != nil:
|
||||
let exprVal = lowerExpr(ctx, node.blockExpr)
|
||||
# If block is an expression, store result
|
||||
discard
|
||||
# If block is an expression, result is unused at statement level
|
||||
discard lowerExpr(ctx, node.blockExpr)
|
||||
if node.isScope:
|
||||
b.emitRawC("}")
|
||||
|
||||
@@ -743,9 +759,8 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
|
||||
|
||||
# ── Expression statement ──
|
||||
else:
|
||||
let exprVal = lowerExpr(ctx, node)
|
||||
# Expression evaluated for side effects; temp is unused
|
||||
discard
|
||||
discard lowerExpr(ctx, node)
|
||||
|
||||
# ── Module-level lowering ──
|
||||
|
||||
|
||||
@@ -90,7 +90,6 @@ proc parseInlineTable(s: string): OrderedTableRef[string, TomlValue] =
|
||||
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'}:
|
||||
|
||||
+191
-11
@@ -1,5 +1,5 @@
|
||||
import std/strutils
|
||||
import token, source_location, ast
|
||||
import token, source_location, ast, lexer
|
||||
|
||||
type
|
||||
ParserDiagnosticSeverity* = enum
|
||||
@@ -48,6 +48,12 @@ proc advance(p: var Parser): Token =
|
||||
if p.pos < p.tokens.len:
|
||||
inc p.pos
|
||||
|
||||
proc hasErrors*(p: Parser): bool =
|
||||
for d in p.diagnostics:
|
||||
if d.severity == pdsError:
|
||||
return true
|
||||
return false
|
||||
|
||||
proc check(p: Parser, kind: TokenKind): bool =
|
||||
p.peek() == kind
|
||||
|
||||
@@ -56,12 +62,6 @@ proc checkAny(p: Parser, kinds: openArray[TokenKind]): bool =
|
||||
if p.peek() == k: return true
|
||||
return false
|
||||
|
||||
proc match(p: var Parser, kind: TokenKind): bool =
|
||||
if p.check(kind):
|
||||
discard p.advance()
|
||||
return true
|
||||
return false
|
||||
|
||||
proc isTypeArgListAhead(p: Parser): bool =
|
||||
## Lookahead to determine if '<' starts a type argument list.
|
||||
## Returns true if we can find a matching '>' before EOF, '{', or ';'.
|
||||
@@ -169,6 +169,7 @@ type
|
||||
callConv*: CallingConvention
|
||||
targetOs*: string
|
||||
checked*: bool ## @[Checked] — enable borrow checking
|
||||
shared*: bool ## @[Shared] — mark function as thread-safe
|
||||
|
||||
proc parseAttrs(p: var Parser): ParsedAttrs =
|
||||
while p.check(tkAt):
|
||||
@@ -177,6 +178,8 @@ proc parseAttrs(p: var Parser): ParsedAttrs =
|
||||
let name = p.expect(tkIdent, "expected attribute name").text
|
||||
if name == "Checked":
|
||||
result.checked = true
|
||||
elif name == "Shared":
|
||||
result.shared = true
|
||||
elif name == "Import":
|
||||
discard p.expect(tkLParen, "expected '('")
|
||||
let key = p.expect(tkIdent, "expected attribute key").text
|
||||
@@ -201,6 +204,10 @@ proc parseBaseType(p: var Parser): TypeExpr =
|
||||
var typeArgs: seq[TypeExpr] = @[]
|
||||
discard p.advance()
|
||||
while not p.check(tkGt) and not p.isAtEnd:
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
if p.check(tkGt) or p.isAtEnd:
|
||||
break
|
||||
typeArgs.add(p.parseType())
|
||||
if p.check(tkComma):
|
||||
discard p.advance()
|
||||
@@ -235,6 +242,20 @@ proc parseBaseType(p: var Parser): TypeExpr =
|
||||
discard p.advance()
|
||||
discard p.expect(tkRParen, "expected ')' to close tuple type")
|
||||
return TypeExpr(kind: tekTuple, loc: loc, tupleElements: elems)
|
||||
of tkFunc:
|
||||
discard p.advance() # func
|
||||
discard p.expect(tkLParen, "expected '(' after 'func'")
|
||||
var params: seq[TypeExpr] = @[]
|
||||
while not p.check(tkRParen) and not p.isAtEnd:
|
||||
params.add(p.parseType())
|
||||
if p.check(tkComma):
|
||||
discard p.advance()
|
||||
discard p.expect(tkRParen, "expected ')' to close func param types")
|
||||
var ret: TypeExpr = nil
|
||||
if p.check(tkArrow):
|
||||
discard p.advance()
|
||||
ret = p.parseType()
|
||||
return TypeExpr(kind: tekFunc, loc: loc, funcParams: params, funcRet: ret)
|
||||
else:
|
||||
p.emitError(loc, "expected type expression")
|
||||
return TypeExpr(kind: tekNamed, loc: loc, typeName: "")
|
||||
@@ -307,6 +328,10 @@ proc parsePrimaryPattern(p: var Parser): Pattern =
|
||||
discard p.advance()
|
||||
var fields: seq[tuple[name: string, pattern: Pattern]] = @[]
|
||||
while not p.check(tkRBrace) and not p.isAtEnd:
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
if p.check(tkRBrace) or p.isAtEnd:
|
||||
break
|
||||
let fieldName = p.expect(tkIdent, "expected field name in struct pattern").text
|
||||
discard p.expect(tkColon, "expected ':' after field name in pattern")
|
||||
fields.add((fieldName, p.parsePattern()))
|
||||
@@ -380,11 +405,77 @@ proc parseAssign(p: var Parser): Expr
|
||||
proc parseExpr(p: var Parser): Expr =
|
||||
p.parseAssign()
|
||||
|
||||
proc parseStringInterpolation(p: var Parser, tok: Token): Expr =
|
||||
## Parse a string literal that contains {expr} interpolations.
|
||||
let text = tok.text
|
||||
# Only f"..." strings support interpolation; plain "...", backtick, and c8/c16/c32 are raw.
|
||||
if text.len < 3 or text[0] != 'f' or text[1] != '"' or text[^1] != '"':
|
||||
return newLiteralExpr(tok)
|
||||
let inner = text[2 ..< ^1] # strip f" ... "
|
||||
var texts: seq[string] = @[]
|
||||
var exprs: seq[Expr] = @[]
|
||||
var i = 0
|
||||
var currentText = ""
|
||||
while i < inner.len:
|
||||
if inner[i] == '\\' and i + 1 < inner.len:
|
||||
if inner[i + 1] == '{':
|
||||
currentText.add('{')
|
||||
i += 2
|
||||
continue
|
||||
elif inner[i + 1] == '}':
|
||||
currentText.add('}')
|
||||
i += 2
|
||||
continue
|
||||
elif inner[i] == '{':
|
||||
texts.add(currentText)
|
||||
currentText = ""
|
||||
var j = i + 1
|
||||
var depth = 1
|
||||
while j < inner.len and depth > 0:
|
||||
if inner[j] == '{': depth += 1
|
||||
elif inner[j] == '}': depth -= 1
|
||||
j += 1
|
||||
if depth != 0:
|
||||
p.diagnostics.add(ParserDiagnostic(severity: pdsError, loc: tok.loc,
|
||||
message: "unmatched '{' in string interpolation"))
|
||||
return newLiteralExpr(tok)
|
||||
let exprStr = inner[i + 1 ..< j - 1]
|
||||
if exprStr.len == 0:
|
||||
p.diagnostics.add(ParserDiagnostic(severity: pdsError, loc: tok.loc,
|
||||
message: "empty interpolation {}"))
|
||||
return newLiteralExpr(tok)
|
||||
let lexRes = tokenize(exprStr, p.sourceName & "<interp>")
|
||||
if lexRes.hasErrors:
|
||||
p.diagnostics.add(ParserDiagnostic(severity: pdsError, loc: tok.loc,
|
||||
message: "invalid expression in string interpolation: " & exprStr))
|
||||
return newLiteralExpr(tok)
|
||||
var subParser = initParser(lexRes.tokens, p.sourceName & "<interp>")
|
||||
let expr = subParser.parseExpr()
|
||||
if subParser.hasErrors:
|
||||
for d in subParser.diagnostics:
|
||||
p.diagnostics.add(ParserDiagnostic(severity: pdsError, loc: tok.loc,
|
||||
message: "interpolation parse error: " & d.message))
|
||||
return newLiteralExpr(tok)
|
||||
exprs.add(expr)
|
||||
i = j
|
||||
continue
|
||||
currentText.add(inner[i])
|
||||
i += 1
|
||||
texts.add(currentText)
|
||||
if exprs.len == 0:
|
||||
return newLiteralExpr(tok)
|
||||
return newStringInterpExpr(texts, exprs, tok.loc)
|
||||
|
||||
proc parsePrimary(p: var Parser): Expr =
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
let loc = p.currentLoc
|
||||
case p.peek()
|
||||
of tkIntLiteral, tkFloatLiteral, tkStringLiteral, tkCharLiteral, tkBoolLiteral:
|
||||
return newLiteralExpr(p.advance())
|
||||
let tok = p.advance()
|
||||
if tok.kind == tkStringLiteral:
|
||||
return parseStringInterpolation(p, tok)
|
||||
return newLiteralExpr(tok)
|
||||
of tkSelf:
|
||||
discard p.advance()
|
||||
return Expr(kind: ekSelf, loc: loc)
|
||||
@@ -490,6 +581,26 @@ proc parsePrimary(p: var Parser): Expr =
|
||||
of tkNull:
|
||||
discard p.advance()
|
||||
return newLiteralExpr(Token(kind: tkNull, text: "null", loc: loc))
|
||||
of tkPipe:
|
||||
# Closure: |params| -> Ret { body }
|
||||
discard p.advance() # |
|
||||
var params: seq[Param] = @[]
|
||||
while not p.check(tkPipe) and not p.isAtEnd:
|
||||
let nameTok = p.expect(tkIdent, "expected parameter name in closure")
|
||||
discard p.expect(tkColon, "expected ':' in closure parameter")
|
||||
let ptype = p.parseType()
|
||||
params.add(Param(name: nameTok.text, ptype: ptype))
|
||||
if p.check(tkComma):
|
||||
discard p.advance()
|
||||
else:
|
||||
break
|
||||
discard p.expect(tkPipe, "expected '|' to close closure params")
|
||||
var retType: TypeExpr = nil
|
||||
if p.check(tkArrow):
|
||||
discard p.advance() # ->
|
||||
retType = p.parseType()
|
||||
let body = p.parseBlock()
|
||||
return Expr(kind: ekClosure, loc: loc, exprClosureParams: params, exprClosureBody: body, exprClosureReturnType: retType)
|
||||
else:
|
||||
p.emitError(loc, "expected expression")
|
||||
discard p.advance()
|
||||
@@ -498,23 +609,35 @@ proc parsePrimary(p: var Parser): Expr =
|
||||
proc parsePostfix(p: var Parser): Expr =
|
||||
var left = p.parsePrimary()
|
||||
while true:
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
let loc = p.currentLoc
|
||||
case p.peek()
|
||||
of tkLParen:
|
||||
# Call expression
|
||||
discard p.advance()
|
||||
var args: seq[Expr] = @[]
|
||||
var argNames: seq[string] = @[]
|
||||
while not p.check(tkRParen) and not p.isAtEnd:
|
||||
if p.check(tkDotDotDot):
|
||||
discard p.advance()
|
||||
let operand = p.parseExpr()
|
||||
args.add(Expr(kind: ekSpread, loc: operand.loc, exprSpreadOperand: operand))
|
||||
argNames.add("")
|
||||
elif p.peek() == tkIdent and p.peek(1) == tkColon:
|
||||
# Named argument: name: value
|
||||
let nameTok = p.advance()
|
||||
discard p.advance() # :
|
||||
let value = p.parseExpr()
|
||||
args.add(value)
|
||||
argNames.add(nameTok.text)
|
||||
else:
|
||||
args.add(p.parseExpr())
|
||||
argNames.add("")
|
||||
if p.check(tkComma):
|
||||
discard p.advance()
|
||||
discard p.expect(tkRParen, "expected ')' to close call")
|
||||
left = Expr(kind: ekCall, loc: loc, exprCallCallee: left, exprCallArgs: args)
|
||||
left = Expr(kind: ekCall, loc: loc, exprCallCallee: left, exprCallArgs: args, exprCallArgNames: argNames)
|
||||
of tkLt:
|
||||
# Generic type arguments: Max<int>(10, 20)
|
||||
# Only treat '<' as generic args if lookahead confirms a matching '>'
|
||||
@@ -594,8 +717,18 @@ proc parsePostfix(p: var Parser): Expr =
|
||||
return left
|
||||
|
||||
proc parseUnary(p: var Parser): Expr =
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
let loc = p.currentLoc
|
||||
case p.peek()
|
||||
of tkBorrow:
|
||||
discard p.advance() # borrow
|
||||
discard p.expect(tkAmp, "expected '&' after 'borrow'")
|
||||
var isMut = p.check(tkMut)
|
||||
if isMut:
|
||||
discard p.advance() # mut
|
||||
let operand = p.parseUnary() # parse the moved value
|
||||
return Expr(kind: ekBorrow, loc: loc, exprBorrowOperand: operand, exprBorrowMutable: isMut)
|
||||
of tkBang, tkMinus, tkTilde, tkStar, tkAmp:
|
||||
let op = p.advance().kind
|
||||
if op == tkAmp and p.check(tkMut):
|
||||
@@ -649,7 +782,6 @@ proc parseShift(p: var Parser): Expr =
|
||||
return left
|
||||
|
||||
proc parseCast(p: var Parser): Expr =
|
||||
let loc = p.currentLoc
|
||||
var left = p.parseShift()
|
||||
# 'as' and 'is' are handled in postfix for chaining
|
||||
return left
|
||||
@@ -876,6 +1008,37 @@ proc parseStmt(p: var Parser): Stmt =
|
||||
discard p.advance()
|
||||
discard p.expect(tkRBrace, "expected '}' to close match")
|
||||
return Stmt(kind: skExpr, loc: loc, stmtExpr: Expr(kind: ekMatch, loc: loc, exprMatchSubject: subject, exprMatchArms: arms))
|
||||
of tkSwitch:
|
||||
discard p.advance()
|
||||
p.structInitAllowed = false
|
||||
let subject = p.parseExpr()
|
||||
p.structInitAllowed = true
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
discard p.expect(tkLBrace, "expected '{' to start switch body")
|
||||
var cases: seq[SwitchCase] = @[]
|
||||
var defaultBlock: Block = nil
|
||||
while not p.check(tkRBrace) and not p.isAtEnd:
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
if p.check(tkRBrace) or p.isAtEnd:
|
||||
break
|
||||
if p.check(tkDefault):
|
||||
discard p.advance()
|
||||
discard p.expect(tkColon, "expected ':' after default")
|
||||
defaultBlock = Block(loc: p.currentLoc, stmts: @[p.parseStmt()])
|
||||
continue
|
||||
if p.check(tkCase):
|
||||
discard p.advance()
|
||||
let caseVal = p.parseExpr()
|
||||
discard p.expect(tkColon, "expected ':' after case value")
|
||||
let caseBody = Block(loc: p.currentLoc, stmts: @[p.parseStmt()])
|
||||
cases.add(SwitchCase(loc: caseVal.loc, caseValue: caseVal, caseBody: caseBody))
|
||||
continue
|
||||
# Unknown token in switch body — skip
|
||||
discard p.advance()
|
||||
discard p.expect(tkRBrace, "expected '}' to close switch")
|
||||
return Stmt(kind: skSwitch, loc: loc, stmtSwitchExpr: subject, stmtSwitchCases: cases, stmtSwitchDefault: defaultBlock)
|
||||
of tkReturn:
|
||||
discard p.advance()
|
||||
var val: Expr = nil
|
||||
@@ -934,6 +1097,12 @@ proc parseStmt(p: var Parser): Stmt =
|
||||
# No-op: emit literal 0 as expression statement
|
||||
let zeroTok = Token(kind: tkIntLiteral, text: "0", loc: loc)
|
||||
return Stmt(kind: skExpr, loc: loc, stmtExpr: Expr(kind: ekLiteral, loc: loc, exprLit: zeroTok))
|
||||
of tkDefer:
|
||||
discard p.advance()
|
||||
let expr = p.parseExpr()
|
||||
if p.check(tkSemicolon):
|
||||
discard p.advance()
|
||||
return Stmt(kind: skDefer, loc: loc, stmtDeferBody: expr)
|
||||
of tkFunc, tkStruct, tkEnum, tkUnion, tkInterface, tkExtend, tkModule,
|
||||
tkImport, tkConst, tkType, tkExtern, tkPub:
|
||||
# Local declaration
|
||||
@@ -954,6 +1123,10 @@ proc parseTypeParams(p: var Parser): seq[TypeParam] =
|
||||
if p.check(tkLt):
|
||||
discard p.advance()
|
||||
while not p.check(tkGt) and not p.isAtEnd:
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
if p.check(tkGt) or p.isAtEnd:
|
||||
break
|
||||
var name = ""
|
||||
var isLifetime = false
|
||||
if p.check(tkIdent):
|
||||
@@ -986,8 +1159,11 @@ proc parseTypeParams(p: var Parser): seq[TypeParam] =
|
||||
proc parseParamList(p: var Parser, allowVariadic: bool = false): seq[Param] =
|
||||
discard p.expect(tkLParen, "expected '('")
|
||||
while not p.check(tkRParen) and not p.isAtEnd:
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
if p.check(tkRParen) or p.isAtEnd:
|
||||
break
|
||||
let loc = p.currentLoc
|
||||
var isVar = false
|
||||
if allowVariadic and p.check(tkDotDotDot):
|
||||
discard p.advance()
|
||||
let nameTok = p.at
|
||||
@@ -1250,6 +1426,10 @@ proc parseUseDecl(p: var Parser, attrs: ParsedAttrs): Decl =
|
||||
if p.check(tkLBrace):
|
||||
discard p.advance()
|
||||
while not p.check(tkRBrace) and not p.isAtEnd:
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
if p.check(tkRBrace) or p.isAtEnd:
|
||||
break
|
||||
names.add(p.expect(tkIdent, "expected name in multi-import").text)
|
||||
if p.check(tkComma):
|
||||
discard p.advance()
|
||||
|
||||
@@ -44,3 +44,13 @@ proc lookupLocal*(scope: Scope, name: string): Symbol =
|
||||
if scope.table.hasKey(name):
|
||||
return scope.table[name]
|
||||
return nil
|
||||
|
||||
proc lookupUpTo*(scope: Scope, name: string, limit: Scope): Symbol =
|
||||
var cur = scope
|
||||
while cur != nil:
|
||||
if cur.table.hasKey(name):
|
||||
return cur.table[name]
|
||||
if cur == limit:
|
||||
break
|
||||
cur = cur.parent
|
||||
return nil
|
||||
+319
-51
@@ -44,6 +44,10 @@ type
|
||||
checkedFunc*: bool ## true inside @[Checked] function
|
||||
currentFuncIsAsync*: bool ## true inside async func
|
||||
movedVars*: seq[string] ## variables moved in current checked function
|
||||
currentRetType*: Type ## return type of the function being checked
|
||||
closureDepth*: int ## nesting depth inside closures
|
||||
currentClosureExpr*: Expr ## current closure being analyzed
|
||||
closureScope*: Scope ## scope at which the current closure was entered
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
@@ -91,9 +95,6 @@ proc unescapeStringLiteral*(s: string): string =
|
||||
proc emitError(sema: var Sema, loc: SourceLocation, message: string) =
|
||||
sema.diagnostics.add(SemaDiagnostic(severity: sdsError, loc: loc, message: message))
|
||||
|
||||
proc emitWarning(sema: var Sema, loc: SourceLocation, message: string) =
|
||||
sema.diagnostics.add(SemaDiagnostic(severity: sdsWarning, loc: loc, message: message))
|
||||
|
||||
proc hasErrors*(res: SemaResult): bool =
|
||||
for d in res.diagnostics:
|
||||
if d.severity == sdsError:
|
||||
@@ -126,10 +127,14 @@ proc typeExprReferencesTypeParam(te: TypeExpr, name: string): bool =
|
||||
of tekTuple:
|
||||
for elem in te.tupleElements:
|
||||
if typeExprReferencesTypeParam(elem, name): return true
|
||||
of tekFunc:
|
||||
for p in te.funcParams:
|
||||
if typeExprReferencesTypeParam(p, name): return true
|
||||
return typeExprReferencesTypeParam(te.funcRet, name)
|
||||
of tekSelf:
|
||||
return false
|
||||
|
||||
proc typeToTypeExpr(t: Type): TypeExpr =
|
||||
proc typeToTypeExpr*(t: Type): TypeExpr =
|
||||
## Convert a resolved Type back to a TypeExpr for storage in inferred type args.
|
||||
case t.kind
|
||||
of tkInt: TypeExpr(kind: tekNamed, typeName: "int")
|
||||
@@ -146,7 +151,11 @@ proc typeToTypeExpr(t: Type): TypeExpr =
|
||||
of tkFloat64: TypeExpr(kind: tekNamed, typeName: "float64")
|
||||
of tkBool: TypeExpr(kind: tekNamed, typeName: "bool")
|
||||
of tkStr: TypeExpr(kind: tekNamed, typeName: "String")
|
||||
of tkNamed: TypeExpr(kind: tekNamed, typeName: t.name)
|
||||
of tkNamed:
|
||||
var args: seq[TypeExpr] = @[]
|
||||
for a in t.inner:
|
||||
args.add(typeToTypeExpr(a))
|
||||
return TypeExpr(kind: tekNamed, typeName: t.name, typeArgs: args)
|
||||
of tkPointer:
|
||||
if t.inner.len > 0:
|
||||
TypeExpr(kind: tekPointer, refLifetime: "", pointerPointee: typeToTypeExpr(t.inner[0]))
|
||||
@@ -165,6 +174,45 @@ proc typeToTypeExpr(t: Type): TypeExpr =
|
||||
of tkVoid: TypeExpr(kind: tekNamed, typeName: "void")
|
||||
else: TypeExpr(kind: tekNamed, typeName: t.toString)
|
||||
|
||||
proc substituteTypeInType(sema: var Sema, t: Type, subst: Table[string, Type]): Type =
|
||||
## Recursively substitute type parameters in a resolved Type.
|
||||
if t == nil:
|
||||
return makeUnknown()
|
||||
case t.kind
|
||||
of tkTypeParam:
|
||||
if subst.hasKey(t.name):
|
||||
return subst[t.name]
|
||||
return t
|
||||
of tkPointer, tkRef, tkMutRef:
|
||||
if t.inner.len > 0:
|
||||
return Type(kind: t.kind, inner: @[sema.substituteTypeInType(t.inner[0], subst)])
|
||||
return t
|
||||
of tkSlice, tkRange:
|
||||
if t.inner.len > 0:
|
||||
return Type(kind: t.kind, inner: @[sema.substituteTypeInType(t.inner[0], subst)])
|
||||
return t
|
||||
of tkTuple:
|
||||
var elems: seq[Type] = @[]
|
||||
for e in t.inner:
|
||||
elems.add(sema.substituteTypeInType(e, subst))
|
||||
return makeTuple(elems)
|
||||
of tkFunc:
|
||||
var inner: seq[Type] = @[]
|
||||
for it in t.inner:
|
||||
inner.add(sema.substituteTypeInType(it, subst))
|
||||
return Type(kind: tkFunc, inner: inner)
|
||||
of tkNamed:
|
||||
if subst.hasKey(t.name):
|
||||
return subst[t.name]
|
||||
if t.inner.len > 0:
|
||||
var args: seq[Type] = @[]
|
||||
for a in t.inner:
|
||||
args.add(sema.substituteTypeInType(a, subst))
|
||||
return Type(kind: tkNamed, name: t.name, inner: args)
|
||||
return t
|
||||
else:
|
||||
return t
|
||||
|
||||
proc inferTypeArgs(sema: var Sema, funcDecl: Decl, argTypes: seq[Type],
|
||||
loc: SourceLocation): seq[TypeExpr] =
|
||||
## Infer type arguments from argument types for a generic function call.
|
||||
@@ -252,6 +300,11 @@ proc resolveType(sema: var Sema, te: TypeExpr): Type =
|
||||
of "float64": return makeFloat64()
|
||||
of "float": return makeFloat64()
|
||||
else:
|
||||
if te.typeArgs.len > 0:
|
||||
var args: seq[Type] = @[]
|
||||
for arg in te.typeArgs:
|
||||
args.add(sema.resolveType(arg))
|
||||
return Type(kind: tkNamed, name: name, inner: args)
|
||||
if sema.typeTable.hasKey(name):
|
||||
return sema.typeTable[name]
|
||||
return makeNamed(name)
|
||||
@@ -278,6 +331,12 @@ proc resolveType(sema: var Sema, te: TypeExpr): Type =
|
||||
return makeTuple(elems)
|
||||
of tekSelf:
|
||||
return makeNamed("self")
|
||||
of tekFunc:
|
||||
var params: seq[Type] = @[]
|
||||
for p in te.funcParams:
|
||||
params.add(sema.resolveType(p))
|
||||
let ret = if te.funcRet != nil: sema.resolveType(te.funcRet) else: makeVoid()
|
||||
return makeFunc(params, ret)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# First pass: collect global symbols
|
||||
@@ -535,10 +594,17 @@ proc collectGlobals*(sema: var Sema) =
|
||||
if not sema.globalScope.define(sym):
|
||||
sema.emitError(decl.loc, &"duplicate symbol '{decl.declEnumName}'")
|
||||
sema.typeTable[decl.declEnumName] = t
|
||||
# Check if algebraic or simple enum
|
||||
var hasData = false
|
||||
for variant in decl.declEnumVariants:
|
||||
if variant.fields.len > 0 or variant.namedFields.len > 0:
|
||||
hasData = true
|
||||
break
|
||||
# For algebraic enums, add variant constants with _Tag type
|
||||
# For simple enums, variant constants have the enum type itself
|
||||
for variant in decl.declEnumVariants:
|
||||
let variantName = decl.declEnumName & "_" & variant.name
|
||||
let variantType = makeNamed(decl.declEnumName & "_Tag")
|
||||
let variantType = if hasData: makeNamed(decl.declEnumName & "_Tag") else: makeNamed(decl.declEnumName)
|
||||
let variantSym = Symbol(kind: skConst, name: variantName, typ: variantType,
|
||||
decl: decl, isPublic: decl.isPublic)
|
||||
discard sema.globalScope.define(variantSym)
|
||||
@@ -563,24 +629,8 @@ proc collectGlobals*(sema: var Sema) =
|
||||
sema.emitError(decl.loc, &"duplicate symbol '{decl.declAliasName}'")
|
||||
sema.typeTable[decl.declAliasName] = t
|
||||
of dkUse:
|
||||
# Imports: register imported names into scope
|
||||
if decl.declUsePath.len > 0:
|
||||
case decl.declUseKind
|
||||
of ukMulti:
|
||||
for name in decl.declUseNames:
|
||||
if sema.globalScope.lookup(name) == nil:
|
||||
let sym = Symbol(kind: skFunc, name: name, typ: makeUnknown(), isPublic: true)
|
||||
discard sema.globalScope.define(sym)
|
||||
of ukGlob:
|
||||
let name = decl.declUsePath[^1]
|
||||
if sema.globalScope.lookup(name) == nil:
|
||||
let sym = Symbol(kind: skModule, name: name, typ: makeUnknown(), isPublic: true)
|
||||
discard sema.globalScope.define(sym)
|
||||
of ukSingle:
|
||||
let name = decl.declUsePath[^1]
|
||||
if sema.globalScope.lookup(name) == nil:
|
||||
let sym = Symbol(kind: skFunc, name: name, typ: makeUnknown(), isPublic: true)
|
||||
discard sema.globalScope.define(sym)
|
||||
# Imports handled in second pass after all declarations are registered
|
||||
discard
|
||||
of dkInterface:
|
||||
# Register interface for conformance checking
|
||||
sema.interfaceTable[decl.declInterfaceName] = decl
|
||||
@@ -641,6 +691,26 @@ proc collectGlobals*(sema: var Sema) =
|
||||
for decl in sema.module.items:
|
||||
if decl.kind == dkConst:
|
||||
discard sema.constFoldConstDecl(decl)
|
||||
# Third pass: register imports after all real declarations are known
|
||||
for decl in sema.module.items:
|
||||
if decl.kind == dkUse:
|
||||
if decl.declUsePath.len > 0:
|
||||
case decl.declUseKind
|
||||
of ukMulti:
|
||||
for name in decl.declUseNames:
|
||||
if sema.globalScope.lookup(name) == nil:
|
||||
let sym = Symbol(kind: skFunc, name: name, typ: makeUnknown(), isPublic: true)
|
||||
discard sema.globalScope.define(sym)
|
||||
of ukGlob:
|
||||
let name = decl.declUsePath[^1]
|
||||
if sema.globalScope.lookup(name) == nil:
|
||||
let sym = Symbol(kind: skModule, name: name, typ: makeUnknown(), isPublic: true)
|
||||
discard sema.globalScope.define(sym)
|
||||
of ukSingle:
|
||||
let name = decl.declUsePath[^1]
|
||||
if sema.globalScope.lookup(name) == nil:
|
||||
let sym = Symbol(kind: skFunc, name: name, typ: makeUnknown(), isPublic: true)
|
||||
discard sema.globalScope.define(sym)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Expression type checking
|
||||
@@ -714,6 +784,63 @@ proc checkExprList(sema: var Sema, exprs: seq[Expr], scope: Scope): seq[Type] =
|
||||
for e in exprs:
|
||||
result.add(sema.checkExpr(e, scope))
|
||||
|
||||
proc resolveCallArgs(sema: var Sema, expr: Expr, calleeDecl: Decl, scope: Scope) =
|
||||
## Reorder named args and inject defaults for missing positional args.
|
||||
if expr.kind != ekCall or calleeDecl == nil or calleeDecl.kind != dkFunc:
|
||||
return
|
||||
let params = calleeDecl.declFuncParams
|
||||
let providedArgs = expr.exprCallArgs
|
||||
let providedNames = expr.exprCallArgNames
|
||||
if providedNames.len == 0:
|
||||
# All positional — just inject defaults for trailing missing args
|
||||
if providedArgs.len < params.len:
|
||||
var newArgs = providedArgs
|
||||
var newNames = providedNames
|
||||
for i in providedArgs.len ..< params.len:
|
||||
if params[i].defaultValue != nil:
|
||||
newArgs.add(params[i].defaultValue)
|
||||
newNames.add("")
|
||||
else:
|
||||
sema.emitError(expr.loc, &"missing argument for parameter '{params[i].name}'")
|
||||
break
|
||||
expr.exprCallArgs = newArgs
|
||||
expr.exprCallArgNames = newNames
|
||||
return
|
||||
# Named args present
|
||||
var newArgs: seq[Expr] = @[]
|
||||
var newNames: seq[string] = @[]
|
||||
var usedNamed = false
|
||||
var namedArgMap: Table[string, Expr]
|
||||
# Collect named args and validate ordering
|
||||
for i in 0 ..< providedArgs.len:
|
||||
if providedNames[i] != "":
|
||||
usedNamed = true
|
||||
if providedNames[i] in namedArgMap:
|
||||
sema.emitError(expr.loc, &"duplicate named argument '{providedNames[i]}'")
|
||||
return
|
||||
namedArgMap[providedNames[i]] = providedArgs[i]
|
||||
else:
|
||||
if usedNamed:
|
||||
sema.emitError(expr.loc, "positional argument after named argument")
|
||||
return
|
||||
# Build final arg list in param order
|
||||
for i in 0 ..< params.len:
|
||||
if i < providedArgs.len and providedNames[i] == "":
|
||||
# Positional arg at expected position
|
||||
newArgs.add(providedArgs[i])
|
||||
newNames.add("")
|
||||
elif params[i].name in namedArgMap:
|
||||
newArgs.add(namedArgMap[params[i].name])
|
||||
newNames.add("")
|
||||
elif params[i].defaultValue != nil:
|
||||
newArgs.add(params[i].defaultValue)
|
||||
newNames.add("")
|
||||
else:
|
||||
sema.emitError(expr.loc, &"missing argument for parameter '{params[i].name}'")
|
||||
break
|
||||
expr.exprCallArgs = newArgs
|
||||
expr.exprCallArgNames = newNames
|
||||
|
||||
proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
if expr == nil:
|
||||
return makeUnknown()
|
||||
@@ -737,6 +864,14 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
return makeUnknown()
|
||||
if sym.typ == nil:
|
||||
return makeUnknown()
|
||||
# Capture tracking
|
||||
if sema.closureDepth > 0 and sema.currentClosureExpr != nil and sema.closureScope != nil:
|
||||
let localSym = scope.lookupUpTo(expr.exprIdent, sema.closureScope)
|
||||
if localSym == nil and sym.kind == skVar:
|
||||
if expr.exprIdent notin sema.currentClosureExpr.captureNames:
|
||||
sema.currentClosureExpr.captureNames.add(expr.exprIdent)
|
||||
sema.currentClosureExpr.captureTypeKinds.add(sym.typ.kind.int)
|
||||
sema.currentClosureExpr.captureCount = sema.currentClosureExpr.captureNames.len
|
||||
return sym.typ
|
||||
of ekSelf:
|
||||
let sym = scope.lookup("self")
|
||||
@@ -786,6 +921,33 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
of ekBinary:
|
||||
let left = sema.checkExpr(expr.exprBinaryLeft, scope)
|
||||
let right = sema.checkExpr(expr.exprBinaryRight, scope)
|
||||
# Operator overloading: check method table before builtin rules
|
||||
let opMethodName = case expr.exprBinaryOp
|
||||
of tkPlus: "operator_add"
|
||||
of tkMinus: "operator_sub"
|
||||
of tkStar: "operator_mul"
|
||||
of tkSlash: "operator_div"
|
||||
of tkPercent: "operator_mod"
|
||||
of tkEq: "operator_eq"
|
||||
of tkNe: "operator_ne"
|
||||
of tkLt: "operator_lt"
|
||||
of tkLe: "operator_le"
|
||||
of tkGt: "operator_gt"
|
||||
of tkGe: "operator_ge"
|
||||
of tkAmp: "operator_bitand"
|
||||
of tkPipe: "operator_bitor"
|
||||
of tkCaret: "operator_xor"
|
||||
of tkShl: "operator_shl"
|
||||
of tkShr: "operator_shr"
|
||||
else: ""
|
||||
if opMethodName != "" and left.kind == tkNamed and sema.methodTable.hasKey(left.name):
|
||||
for minfo in sema.methodTable[left.name]:
|
||||
if minfo.name == opMethodName:
|
||||
# Validate argument count (self + other)
|
||||
if minfo.params.len == 2:
|
||||
let otherType = minfo.params[1]
|
||||
if right.isAssignableTo(otherType) or otherType.isAssignableTo(right) or right.kind == tkUnknown:
|
||||
return minfo.retType
|
||||
case expr.exprBinaryOp
|
||||
of tkPlus, tkMinus, tkStar, tkSlash, tkPercent, tkStarStar:
|
||||
if not left.isNumeric or not right.isNumeric:
|
||||
@@ -845,9 +1007,16 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
of ekRange:
|
||||
let lo = sema.checkExpr(expr.exprRangeLo, scope)
|
||||
let hi = sema.checkExpr(expr.exprRangeHi, scope)
|
||||
if lo != hi:
|
||||
var rangeType: Type = lo
|
||||
if lo == hi:
|
||||
rangeType = lo
|
||||
elif lo.isAssignableTo(hi):
|
||||
rangeType = hi
|
||||
elif hi.isAssignableTo(lo):
|
||||
rangeType = lo
|
||||
else:
|
||||
sema.emitError(expr.loc, "range bounds must have same type")
|
||||
return makeRange(lo)
|
||||
return makeRange(rangeType)
|
||||
of ekCall:
|
||||
if expr.exprCallCallee == nil:
|
||||
sema.emitError(expr.loc, "internal error: nil callee in call expression")
|
||||
@@ -860,19 +1029,22 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
sema.emitError(expr.loc, &"undeclared identifier '{expr.exprCallCallee.exprGenericCallee}'")
|
||||
return makeUnknown()
|
||||
if sym.typ != nil and sym.typ.kind == tkFunc:
|
||||
# Get the return type and substitute type parameters
|
||||
let retType = sym.typ.inner[^1]
|
||||
if retType.kind == tkNamed:
|
||||
# Check if this is a type parameter
|
||||
let sym2 = sema.globalScope.lookup(expr.exprCallCallee.exprGenericCallee)
|
||||
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.name and i < expr.exprCallCallee.exprGenericTypeArgs.len:
|
||||
# Substitute with concrete type
|
||||
let concreteType = expr.exprCallCallee.exprGenericTypeArgs[i]
|
||||
if concreteType.kind == tekNamed:
|
||||
return sema.resolveType(concreteType)
|
||||
let sym2 = sema.globalScope.lookup(expr.exprCallCallee.exprGenericCallee)
|
||||
if sym2 != nil and sym2.decl != nil and sym2.decl.kind == dkFunc and
|
||||
sym2.decl.declFuncTypeParams.len > 0 and
|
||||
sym2.decl.declFuncReturnType != nil:
|
||||
let typeParams = sym2.decl.declFuncTypeParams
|
||||
var added: seq[string] = @[]
|
||||
for i, tp in typeParams:
|
||||
if i < expr.exprCallCallee.exprGenericTypeArgs.len:
|
||||
let concrete = sema.resolveType(expr.exprCallCallee.exprGenericTypeArgs[i])
|
||||
sema.typeTable[tp.name] = concrete
|
||||
added.add(tp.name)
|
||||
let resolvedRet = sema.resolveType(sym2.decl.declFuncReturnType)
|
||||
for tp in added:
|
||||
sema.typeTable.del(tp)
|
||||
return resolvedRet
|
||||
return retType
|
||||
return makeUnknown()
|
||||
|
||||
@@ -945,8 +1117,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
|
||||
# Regular function call
|
||||
let calleeType = sema.checkExpr(expr.exprCallCallee, scope)
|
||||
var argTypes = sema.checkExprList(expr.exprCallArgs, scope)
|
||||
# Look up callee declaration early (needed for borrow checking)
|
||||
# Look up callee declaration early (needed for borrow checking and defaults)
|
||||
var calleeDecl: Decl = nil
|
||||
case expr.exprCallCallee.kind
|
||||
of ekIdent:
|
||||
@@ -957,13 +1128,16 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
let sym = scope.lookup(fullName)
|
||||
if sym != nil: calleeDecl = sym.decl
|
||||
else: discard
|
||||
if calleeDecl != nil and calleeDecl.kind == dkFunc and calleeDecl.declFuncTypeParams.len > 0:
|
||||
discard # will be handled later
|
||||
# Resolve named args and inject defaults before type-checking args
|
||||
sema.resolveCallArgs(expr, calleeDecl, scope)
|
||||
var argTypes = sema.checkExprList(expr.exprCallArgs, scope)
|
||||
let isGenericFunc = calleeDecl != nil and calleeDecl.kind == dkFunc and calleeDecl.declFuncTypeParams.len > 0
|
||||
if calleeType.kind == tkFunc:
|
||||
let expectedParams = calleeType.inner[0..^2]
|
||||
if argTypes.len != expectedParams.len:
|
||||
sema.emitError(expr.loc, &"expected {expectedParams.len} arguments, got {argTypes.len}")
|
||||
else:
|
||||
elif not isGenericFunc:
|
||||
# Generic function arg checks are deferred until after type inference/monomorphization.
|
||||
for i in 0 ..< argTypes.len:
|
||||
if not argTypes[i].isAssignableTo(expectedParams[i]) and not (argTypes[i].kind in {TypeKind.tkUnknown, TypeKind.tkNamed, TypeKind.tkTypeParam}):
|
||||
sema.emitError(expr.loc, &"argument {i+1}: expected {expectedParams[i].toString}, got {argTypes[i].toString}")
|
||||
@@ -1038,6 +1212,26 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
let idx = sema.checkExpr(expr.exprIndexIdx, scope)
|
||||
if not idx.isInteger:
|
||||
sema.emitError(expr.loc, "index must be integer")
|
||||
|
||||
# Try method-table operator_index_get on a named receiver (possibly behind a pointer).
|
||||
var receiverNamed: Type = nil
|
||||
if obj.kind == tkNamed:
|
||||
receiverNamed = obj
|
||||
elif obj.isPointer and obj.inner.len > 0 and obj.inner[0].kind == tkNamed:
|
||||
receiverNamed = obj.inner[0]
|
||||
|
||||
if receiverNamed != nil and sema.methodTable.hasKey(receiverNamed.name):
|
||||
for minfo in sema.methodTable[receiverNamed.name]:
|
||||
if minfo.name == "operator_index_get" and minfo.params.len == 2:
|
||||
var subst = initTable[string, Type]()
|
||||
if minfo.decl.declFuncTypeParams.len > 0 and receiverNamed.inner.len > 0:
|
||||
for i, tp in minfo.decl.declFuncTypeParams:
|
||||
if i < receiverNamed.inner.len:
|
||||
subst[tp.name] = receiverNamed.inner[i]
|
||||
let idxType = sema.substituteTypeInType(minfo.params[1], subst)
|
||||
if idx.isAssignableTo(idxType) or idxType.isAssignableTo(idx) or idx.kind == tkUnknown:
|
||||
return sema.substituteTypeInType(minfo.retType, subst)
|
||||
|
||||
if obj.isSlice:
|
||||
if sema.checkedFunc:
|
||||
expr.exprIndexBoundsCheck = true
|
||||
@@ -1079,13 +1273,24 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
let sym = sema.globalScope.lookup(objType.name)
|
||||
if sym != nil and sym.decl != nil:
|
||||
if sym.decl.kind == dkStruct:
|
||||
var subst = initTable[string, Type]()
|
||||
for i, tp in sym.decl.declStructTypeParams:
|
||||
if i < objType.inner.len:
|
||||
subst[tp.name] = objType.inner[i]
|
||||
for f in sym.decl.declStructFields:
|
||||
if f.name == expr.exprFieldName:
|
||||
return sema.resolveType(f.ftype)
|
||||
return sema.substituteTypeInType(sema.resolveType(f.ftype), subst)
|
||||
sema.emitError(expr.loc, &"struct '{objType.name}' has no field '{expr.exprFieldName}'")
|
||||
elif sym.decl.kind == dkEnum:
|
||||
# Algebraic enum fields
|
||||
if expr.exprFieldName == "tag":
|
||||
var hasData = false
|
||||
for v in sym.decl.declEnumVariants:
|
||||
if v.fields.len > 0 or v.namedFields.len > 0:
|
||||
hasData = true
|
||||
break
|
||||
if not hasData and expr.exprFieldName == "tag":
|
||||
return makeNamed(objType.name)
|
||||
elif expr.exprFieldName == "tag":
|
||||
return makeNamed(objType.name & "_Tag")
|
||||
elif expr.exprFieldName == "data":
|
||||
return makeNamed(objType.name & "_Data")
|
||||
@@ -1147,12 +1352,12 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
discard sema.checkExpr(expr.exprIsOperand, scope)
|
||||
return makeBool()
|
||||
of ekTry:
|
||||
let operandType = sema.checkExpr(expr.exprTryOperand, scope)
|
||||
discard sema.checkExpr(expr.exprTryOperand, scope)
|
||||
# For now, assume Result<int, String> -> int
|
||||
# TODO: check operand is Result/Option and current function returns same type
|
||||
return makeInt()
|
||||
of ekUnwrap:
|
||||
let operandType = sema.checkExpr(expr.exprUnwrapOperand, scope)
|
||||
discard sema.checkExpr(expr.exprUnwrapOperand, scope)
|
||||
# Unwrap: extract Ok value or panic on Err
|
||||
return makeInt()
|
||||
of ekBlock:
|
||||
@@ -1162,7 +1367,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
lastType = sema.checkStmt(stmt, blockScope)
|
||||
return lastType
|
||||
of ekMatch:
|
||||
let subjectType = sema.checkExpr(expr.exprMatchSubject, scope)
|
||||
discard sema.checkExpr(expr.exprMatchSubject, scope)
|
||||
var resultType = makeUnknown()
|
||||
for arm in expr.exprMatchArms:
|
||||
var armScope = newScope(scope)
|
||||
@@ -1197,11 +1402,55 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
expr.exprSpawnAsync = true
|
||||
return makePointer(makeVoid())
|
||||
of ekAwait:
|
||||
let operand = sema.checkExpr(expr.exprAwaitOperand, scope)
|
||||
discard sema.checkExpr(expr.exprAwaitOperand, scope)
|
||||
# await on a task handle returns *void (result pointer)
|
||||
return makePointer(makeVoid())
|
||||
of ekBorrow:
|
||||
let operand = sema.checkExpr(expr.exprBorrowOperand, scope)
|
||||
# borrow &mut expr returns the same type as the original (reference)
|
||||
# The borrow is tracked in the borrow checker
|
||||
if sema.checkedFunc and expr.exprBorrowMutable:
|
||||
# Track: variable "operand" is mutably borrowed here
|
||||
# For now, just validate the type
|
||||
discard
|
||||
return operand
|
||||
of ekSpread:
|
||||
return sema.checkExpr(expr.exprSpreadOperand, scope)
|
||||
of ekStringInterp:
|
||||
for e in expr.exprInterpExprs:
|
||||
discard sema.checkExpr(e, scope)
|
||||
return makeStr()
|
||||
of ekClosure:
|
||||
let savedRetType = sema.currentRetType
|
||||
let savedClosureDepth = sema.closureDepth
|
||||
let savedClosureExpr = sema.currentClosureExpr
|
||||
let savedClosureScope = sema.closureScope
|
||||
let childScope = Scope(parent: scope)
|
||||
sema.closureDepth = sema.closureDepth + 1
|
||||
sema.currentClosureExpr = expr
|
||||
sema.closureScope = childScope
|
||||
expr.captureCount = 0
|
||||
expr.captureNames = @[]
|
||||
expr.captureTypeKinds = @[]
|
||||
sema.currentRetType = if expr.exprClosureReturnType != nil: sema.resolveType(expr.exprClosureReturnType) else: makeUnknown()
|
||||
# Register params
|
||||
for p in expr.exprClosureParams:
|
||||
let ptype = if p.ptype != nil: sema.resolveType(p.ptype) else: makeUnknown()
|
||||
discard childScope.define(Symbol(kind: skVar, name: p.name, typ: ptype))
|
||||
# Check body
|
||||
if expr.exprClosureBody != nil:
|
||||
for stmt in expr.exprClosureBody.stmts:
|
||||
discard sema.checkStmt(stmt, childScope)
|
||||
sema.currentRetType = savedRetType
|
||||
sema.closureDepth = savedClosureDepth
|
||||
sema.currentClosureExpr = savedClosureExpr
|
||||
sema.closureScope = savedClosureScope
|
||||
# Build function type
|
||||
var params: seq[Type] = @[]
|
||||
for p in expr.exprClosureParams:
|
||||
params.add(if p.ptype != nil: sema.resolveType(p.ptype) else: makeUnknown())
|
||||
let retType = if expr.exprClosureReturnType != nil: sema.resolveType(expr.exprClosureReturnType) else: makeVoid()
|
||||
return makeFunc(params, retType)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Statement type checking
|
||||
@@ -1262,9 +1511,17 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
|
||||
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtLoopBody.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtLoopBody.loc, exprBlock: stmt.stmtLoopBody)), scope)
|
||||
return makeVoid()
|
||||
of skFor:
|
||||
discard sema.checkExpr(stmt.stmtForIter, scope)
|
||||
let iterExpr = stmt.stmtForIter
|
||||
let collType = sema.checkExpr(iterExpr, scope)
|
||||
var forScope = newScope(scope)
|
||||
let iterSym = Symbol(kind: skVar, name: stmt.stmtForVar, typ: makeUnknown(), isMutable: true)
|
||||
var iterTyp = makeUnknown()
|
||||
if iterExpr.kind == ekRange:
|
||||
iterTyp = sema.checkExpr(iterExpr.exprRangeLo, scope)
|
||||
elif collType.kind == tkNamed and collType.inner.len > 0:
|
||||
iterTyp = collType.inner[0]
|
||||
elif collType.isPointer and collType.inner.len > 0 and collType.inner[0].kind == tkNamed and collType.inner[0].inner.len > 0:
|
||||
iterTyp = collType.inner[0].inner[0]
|
||||
let iterSym = Symbol(kind: skVar, name: stmt.stmtForVar, typ: iterTyp, isMutable: true)
|
||||
discard forScope.define(iterSym)
|
||||
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtForBody.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtForBody.loc, exprBlock: stmt.stmtForBody)), forScope)
|
||||
return makeVoid()
|
||||
@@ -1308,6 +1565,17 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
|
||||
elif not exprType.isUnknown and exprType.kind != tkStr:
|
||||
sema.emitError(stmt.loc, "#emit requires a string expression")
|
||||
return makeVoid()
|
||||
of skDefer:
|
||||
discard sema.checkExpr(stmt.stmtDeferBody, scope)
|
||||
return makeVoid()
|
||||
of skSwitch:
|
||||
discard sema.checkExpr(stmt.stmtSwitchExpr, scope)
|
||||
for caseBranch in stmt.stmtSwitchCases:
|
||||
discard sema.checkExpr(caseBranch.caseValue, scope)
|
||||
discard sema.checkStmt(Stmt(kind: skExpr, loc: caseBranch.caseBody.loc, stmtExpr: Expr(kind: ekBlock, loc: caseBranch.caseBody.loc, exprBlock: caseBranch.caseBody)), scope)
|
||||
if stmt.stmtSwitchDefault != nil:
|
||||
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtSwitchDefault.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtSwitchDefault.loc, exprBlock: stmt.stmtSwitchDefault)), scope)
|
||||
return makeVoid()
|
||||
of skDecl:
|
||||
# Local declaration inside block
|
||||
case stmt.stmtDecl.kind
|
||||
|
||||
+17
-2
@@ -25,6 +25,9 @@ type
|
||||
tkContinue # continue
|
||||
tkReturn # return
|
||||
tkMatch # match
|
||||
tkSwitch # switch
|
||||
tkCase # case
|
||||
tkDefault # default
|
||||
|
||||
##Declaration keywords
|
||||
tkFunc # func
|
||||
@@ -51,6 +54,7 @@ type
|
||||
tkSizeOf # sizeof
|
||||
tkOwn # own (gradual ownership transfer)
|
||||
tkMut # mut (mutable reference)
|
||||
tkBorrow # borrow (explicit borrow expression)
|
||||
tkDiscard # discard (evaluate and throw away)
|
||||
tkAsync # async
|
||||
tkAwait # await
|
||||
@@ -58,6 +62,7 @@ type
|
||||
tkStaticAssert # static_assert
|
||||
tkComptime # comptime
|
||||
tkDyn # dyn
|
||||
tkDefer # defer
|
||||
tkLifetime # 'a (lifetime parameter)
|
||||
|
||||
##Punctuation
|
||||
@@ -148,10 +153,10 @@ type
|
||||
proc isKeyword*(kind: TokenKind): bool =
|
||||
case kind
|
||||
of tkIf, tkElse, tkWhile, tkDo, tkLoop, tkFor, tkIn,
|
||||
tkBreak, tkContinue, tkReturn, tkMatch,
|
||||
tkBreak, tkContinue, tkReturn, tkMatch, tkSwitch, tkCase, tkDefault,
|
||||
tkFunc, tkLet, tkVar, tkConst, tkType, tkStruct, tkEnum,
|
||||
tkUnion, tkInterface, tkExtend, tkModule, tkImport,
|
||||
tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkMut, tkDiscard, tkAsync, tkAwait, tkSpawn, tkStaticAssert, tkComptime, tkDyn:
|
||||
tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkMut, tkBorrow, tkDiscard, tkAsync, tkAwait, tkSpawn, tkStaticAssert, tkComptime, tkDyn, tkDefer:
|
||||
true
|
||||
else:
|
||||
false
|
||||
@@ -199,6 +204,9 @@ proc keywordKind*(text: string): TokenKind =
|
||||
of "continue": tkContinue
|
||||
of "return": tkReturn
|
||||
of "match": tkMatch
|
||||
of "switch": tkSwitch
|
||||
of "case": tkCase
|
||||
of "default": tkDefault
|
||||
of "as": tkAs
|
||||
of "is": tkIs
|
||||
of "null": tkNull
|
||||
@@ -207,6 +215,7 @@ proc keywordKind*(text: string): TokenKind =
|
||||
of "sizeof": tkSizeOf
|
||||
of "own": tkOwn
|
||||
of "mut": tkMut
|
||||
of "borrow": tkBorrow
|
||||
of "discard": tkDiscard
|
||||
of "async": tkAsync
|
||||
of "await": tkAwait
|
||||
@@ -214,6 +223,7 @@ proc keywordKind*(text: string): TokenKind =
|
||||
of "static_assert": tkStaticAssert
|
||||
of "comptime": tkComptime
|
||||
of "dyn": tkDyn
|
||||
of "defer": tkDefer
|
||||
of "true", "false": tkBoolLiteral
|
||||
else: tkIdent
|
||||
|
||||
@@ -238,6 +248,9 @@ proc tokenKindName*(kind: TokenKind): string =
|
||||
of tkContinue: "'continue'"
|
||||
of tkReturn: "'return'"
|
||||
of tkMatch: "'match'"
|
||||
of tkSwitch: "'switch'"
|
||||
of tkCase: "'case'"
|
||||
of tkDefault: "'default'"
|
||||
of tkFunc: "'func'"
|
||||
of tkLet: "'let'"
|
||||
of tkVar: "'var'"
|
||||
@@ -259,6 +272,7 @@ proc tokenKindName*(kind: TokenKind): string =
|
||||
of tkSuper: "'super'"
|
||||
of tkOwn: "'own'"
|
||||
of tkMut: "'mut'"
|
||||
of tkBorrow: "'borrow'"
|
||||
of tkDiscard: "'discard'"
|
||||
of tkAsync: "'async'"
|
||||
of tkAwait: "'await'"
|
||||
@@ -266,6 +280,7 @@ proc tokenKindName*(kind: TokenKind): string =
|
||||
of tkStaticAssert: "'static_assert'"
|
||||
of tkComptime: "'comptime'"
|
||||
of tkDyn: "'dyn'"
|
||||
of tkDefer: "'defer'"
|
||||
of tkLifetime: "lifetime"
|
||||
of tkLParen: "'('"
|
||||
of tkRParen: "')'"
|
||||
|
||||
@@ -183,6 +183,10 @@ proc isAssignableTo*(a, b: Type): bool =
|
||||
return true
|
||||
if b.isDynRef and a.isMutRef:
|
||||
return true
|
||||
# &mut func(...) -> func(...) (function pointer decay)
|
||||
if a.isMutRef and b.kind == tkFunc:
|
||||
if a.inner.len > 0 and a.inner[0].kind == tkFunc:
|
||||
return true
|
||||
return false
|
||||
|
||||
# String representation
|
||||
|
||||
+43
-3
@@ -50,6 +50,10 @@ This compiles `buxc2` using the bootstrap compiler. The self-hosted compiler gen
|
||||
# Create a new package in a new directory
|
||||
./buxc new myproject
|
||||
cd myproject
|
||||
|
||||
# Or initialize in the current directory
|
||||
mkdir myproject && cd myproject
|
||||
./buxc init
|
||||
```
|
||||
|
||||
This generates:
|
||||
@@ -89,8 +93,13 @@ Output = "Bin"
|
||||
./buxc run
|
||||
./buxc run ./myproject
|
||||
|
||||
# Run tests
|
||||
# Run tests (builds and runs the binary, reports pass/fail)
|
||||
./buxc test
|
||||
./buxc test ./myproject
|
||||
|
||||
# Format code
|
||||
./buxc fmt src/Main.bux # single file
|
||||
./buxc fmt src/ # all .bux files in directory
|
||||
|
||||
# Clean build artifacts
|
||||
./buxc clean
|
||||
@@ -98,6 +107,24 @@ Output = "Bin"
|
||||
|
||||
Build output goes to `build/` by default.
|
||||
|
||||
### Cross-Compilation
|
||||
|
||||
Use `--target <triple>` to cross-compile for a different platform. Bux generates C code and uses `clang` with the `-target` flag for cross-compilation.
|
||||
|
||||
```bash
|
||||
# Cross-compile for ARM Linux
|
||||
./buxc build --target aarch64-linux-gnu
|
||||
|
||||
# Cross-compile for x86_64 Linux (explicit)
|
||||
./buxc build --target x86_64-linux-gnu
|
||||
|
||||
# Cross-compile and run project build
|
||||
./buxc project --target x86_64-linux-gnu
|
||||
./buxc run --target aarch64-linux-gnu
|
||||
```
|
||||
|
||||
> **Note:** `clang` must be installed for cross-compilation. Without `--target`, Bux uses the system `cc` compiler.
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
@@ -113,6 +140,18 @@ This runs:
|
||||
- Semantic analysis unit tests
|
||||
- HIR lowering unit tests
|
||||
- Integration tests (`buxc new`, `buxc --version`)
|
||||
- Golden C-codegen tests (8 examples)
|
||||
|
||||
### Project Tests (`bux test`)
|
||||
```bash
|
||||
./buxc test
|
||||
```
|
||||
|
||||
Builds the project and runs the resulting binary. Reports:
|
||||
- `Tests passed` on exit code 0
|
||||
- `Tests failed (exit code N)` on non-zero exit
|
||||
|
||||
Use `Std::Test` module for assertions inside test code.
|
||||
|
||||
### Example Programs
|
||||
```bash
|
||||
@@ -137,17 +176,18 @@ cd examples_pkg/hello && ../../buxc run
|
||||
bux/
|
||||
├── src/ # Self-hosted compiler source (Bux)
|
||||
│ ├── Main.bux # Entry point
|
||||
│ ├── Cli.bux # CLI commands
|
||||
│ ├── Cli.bux # CLI commands (build, run, test, fmt, new, init)
|
||||
│ ├── Lexer.bux # Tokenizer
|
||||
│ ├── Parser.bux # Parser
|
||||
│ ├── Ast.bux # AST definitions
|
||||
│ ├── Sema.bux # Semantic analysis
|
||||
│ ├── Sema.bux # Semantic analysis (borrow checker)
|
||||
│ ├── Types.bux # Type system
|
||||
│ ├── Scope.bux # Symbol table
|
||||
│ ├── Hir.bux # High-level IR
|
||||
│ ├── HirLower.bux # AST → HIR lowering
|
||||
│ ├── CBackend.bux # HIR → C code generation
|
||||
│ ├── Manifest.bux # bux.toml parser
|
||||
│ ├── Fmt.bux # Code formatter
|
||||
│ └── Token.bux # Token definitions
|
||||
├── bootstrap/ # Bootstrap compiler (Nim) — compiles src/ → buxc
|
||||
│ ├── main.nim
|
||||
|
||||
+98
-2
@@ -19,7 +19,8 @@ This document describes the Bux programming language as implemented by the boots
|
||||
11. [Error Handling](#error-handling)
|
||||
12. [Modules and Imports](#modules-and-imports)
|
||||
13. [Async/Await](#asyncawait)
|
||||
14. [Operators](#operators)
|
||||
14. [Operator Overloading](#operator-overloading)
|
||||
15. [Operators](#operators)
|
||||
|
||||
---
|
||||
|
||||
@@ -43,7 +44,7 @@ Identifiers start with a letter or underscore, followed by letters, digits, or u
|
||||
func, let, var, const, type, struct, enum, union, interface, extend
|
||||
module, import, pub, extern, if, else, while, do, loop, for, in
|
||||
break, continue, return, match, as, is, null, self, super, sizeof
|
||||
async, await, spawn
|
||||
async, await, spawn, defer, switch, case, default, checked
|
||||
```
|
||||
|
||||
### String Literals
|
||||
@@ -56,6 +57,7 @@ c32"Hello" // *char32
|
||||
`line 1
|
||||
line 2
|
||||
line 3` // Newlines preserved as-is
|
||||
f"Hello, {name}" // Interpolated string — expressions inside {}
|
||||
```
|
||||
|
||||
**Backtick raw strings** (`` `...` ``) treat all characters literally:
|
||||
@@ -63,6 +65,11 @@ line 3` // Newlines preserved as-is
|
||||
- Actual newlines in source are preserved in the string
|
||||
- No way to escape the backtick character itself (use regular strings if needed)
|
||||
|
||||
**Interpolated strings** (`f"..."`):
|
||||
- Expressions inside `{}` are evaluated and converted to `String`
|
||||
- Supported types: `int`, `uint`, `float`, `bool`, `String`
|
||||
- Escaped braces: `\{` and `\}`
|
||||
|
||||
### Number Literals
|
||||
```bux
|
||||
42 // int
|
||||
@@ -93,6 +100,9 @@ line 3` // Newlines preserved as-is
|
||||
|
||||
```bux
|
||||
*T // Pointer to T
|
||||
&T // Shared reference (read-only in checked functions)
|
||||
&mut T // Mutable reference (exclusive borrow)
|
||||
own T // Owned value (move semantics)
|
||||
T[] // Slice (unsized)
|
||||
T[N] // Fixed-size array
|
||||
(T1, T2, T3) // Tuple
|
||||
@@ -161,6 +171,27 @@ func Min<T>(a: T, b: T) -> T {
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
// Named and default parameters
|
||||
func HttpResponse(code: int = 200, body: String = "") -> Response { ... }
|
||||
let r: Response = HttpResponse(body: "hello"); // code defaults to 200
|
||||
let s: Response = HttpResponse(404, body: "err"); // positional + named mixed
|
||||
|
||||
// Operator overloading (bootstrap only)
|
||||
func Vec2_operator_add(self: *Vec2, other: Vec2) -> Vec2 { ... }
|
||||
func Vec2_operator_sub(self: *Vec2, other: Vec2) -> Vec2 { ... }
|
||||
func Vec2_operator_eq(self: *Vec2, other: Vec2) -> bool { ... }
|
||||
func Vec2_operator_lt(self: *Vec2, other: Vec2) -> bool { ... }
|
||||
func MyArray_operator_index_get(self: *MyArray, idx: int) -> int { ... }
|
||||
func MyArray_operator_index_set(self: *MyArray, idx: int, value: int) { ... }
|
||||
|
||||
// Closures (capture-less for now)
|
||||
let add: func(int, int) -> int = |a: int, b: int| -> int { return a + b; };
|
||||
let sum: int = add(3, 4); // 7
|
||||
|
||||
// Closure passed to higher-order function
|
||||
func Apply(x: int, op: func(int) -> int) -> int { return op(x); }
|
||||
let doubled: int = Apply(5, |x: int| -> int { return x * 2; }); // 10
|
||||
```
|
||||
|
||||
---
|
||||
@@ -211,6 +242,31 @@ outer: loop {
|
||||
}
|
||||
```
|
||||
|
||||
### `defer`
|
||||
Runs an expression when the current scope exits (LIFO order).
|
||||
|
||||
```bux
|
||||
func ReadFile(path: String) -> String {
|
||||
let fd: int = Open(path);
|
||||
defer Close(fd);
|
||||
defer PrintLine("done");
|
||||
let data: String = ReadAll(fd);
|
||||
return data; // both defers run before return
|
||||
}
|
||||
```
|
||||
|
||||
### `switch` / `case`
|
||||
Desugars to an if-else chain. Supports a `default` case.
|
||||
|
||||
```bux
|
||||
switch statusCode {
|
||||
case 200: PrintLine("OK");
|
||||
case 404: PrintLine("Not Found");
|
||||
case 500: PrintLine("Server Error");
|
||||
default: PrintLine("Unknown");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structs
|
||||
@@ -436,6 +492,18 @@ Moves happen in three contexts:
|
||||
- `&mut T` allows mutation
|
||||
- `*T` pointers are unrestricted (escape hatch)
|
||||
- `&mut T` coerces to `&T` and `*T`
|
||||
- **Double mutable borrow**: passing `&mut x` twice to the same call is an error
|
||||
```bux
|
||||
Swap(&mut x, &mut x); // ERROR: double mutable borrow of x
|
||||
```
|
||||
- **Use after move**: using a moved `own T` value is an error until reassigned
|
||||
```bux
|
||||
let msg: own String = "hello";
|
||||
Process(msg); // move
|
||||
PrintLine(msg); // ERROR: use of moved value
|
||||
msg = "reassigned"; // OK: reinitialization
|
||||
PrintLine(msg);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -632,6 +700,34 @@ func Main() -> int {
|
||||
|
||||
---
|
||||
|
||||
## Operator Overloading
|
||||
|
||||
> **Status:** ✅ Implemented in bootstrap. Selfhost reserves syntax but has no method-table yet.
|
||||
|
||||
Overloadable operators use the naming convention `TypeName_operator_<op>`:
|
||||
|
||||
| Operator | Function Name | Signature Example |
|
||||
|----------|--------------|-------------------|
|
||||
| `+` | `operator_add` | `func T_operator_add(self: *T, other: T) -> T` |
|
||||
| `-` | `operator_sub` | `func T_operator_sub(self: *T, other: T) -> T` |
|
||||
| `*` | `operator_mul` | `func T_operator_mul(self: *T, other: T) -> T` |
|
||||
| `/` | `operator_div` | `func T_operator_div(self: *T, other: T) -> T` |
|
||||
| `%` | `operator_mod` | `func T_operator_mod(self: *T, other: T) -> T` |
|
||||
| `==` | `operator_eq` | `func T_operator_eq(self: *T, other: T) -> bool` |
|
||||
| `!=` | `operator_ne` | `func T_operator_ne(self: *T, other: T) -> bool` |
|
||||
| `<` | `operator_lt` | `func T_operator_lt(self: *T, other: T) -> bool` |
|
||||
| `<=` | `operator_le` | `func T_operator_le(self: *T, other: T) -> bool` |
|
||||
| `>` | `operator_gt` | `func T_operator_gt(self: *T, other: T) -> bool` |
|
||||
| `>=` | `operator_ge` | `func T_operator_ge(self: *T, other: T) -> bool` |
|
||||
| `[]` (get) | `operator_index_get` | `func T_operator_index_get(self: *T, idx: int) -> U` |
|
||||
| `[]` (set) | `operator_index_set` | `func T_operator_index_set(self: *T, idx: int, value: U)` |
|
||||
|
||||
**Notes:**
|
||||
- Short-circuit operators (`&&`, `||`) cannot be overloaded.
|
||||
- Generic method instantiation is supported.
|
||||
|
||||
---
|
||||
|
||||
## Operators
|
||||
|
||||
### Arithmetic
|
||||
|
||||
+33
-18
@@ -1,6 +1,7 @@
|
||||
# Фаза 8 — Стратегия: Как Bux печели, без да бие пряко Rust/Nim/Zig
|
||||
|
||||
> **Дата:** 2026-05-31 | **Статус:** Фаза 8.1 ✅, 8.2-8.6 🔄
|
||||
> **Дата:** 2026-06-09 | **Статус:** Фаза 8.1 ✅, 8.2 ✅ (basic + CTFE), 8.3-8.6 🔄
|
||||
> **Последни постижения:** defer ✅, switch/case ✅, operator overloading ✅, string interpolation ✅, named/default params ✅, basic borrow checker ✅, `bux fmt` ✅, `bux test` ✅, `bux new`/`init` ✅, selfhost-loop deterministic ✅
|
||||
> **Правило #1:** Не се биеш с някого там, където той е най-силен.
|
||||
|
||||
---
|
||||
@@ -51,7 +52,7 @@ Bux е единственият език, който позволява:
|
||||
|
||||
### 3.1 Фаза 8.2 — Gradual Ownership (The Killer Feature)
|
||||
|
||||
**Статус сега:** Синтаксисът е парсен, но borrow checker-ът не работи.
|
||||
**Статус сега:** ✅ Basic borrow checker работи в selfhost. Поддържа `@[Checked]`, `&T`, `&mut T`, use-after-move tracking и double-mutable-borrow detection.
|
||||
|
||||
**Защо е критично:** Без работещ `@[Checked]`, Bux е просто "C с модерен синтаксис". С него — ставаме единствени на пазара.
|
||||
|
||||
@@ -75,14 +76,15 @@ func MergeSorted(a: &[int], b: &[int]) -> Vec<int> { ... }
|
||||
|
||||
**Имплементационен план (прагматичен):**
|
||||
|
||||
| Етап | Фичър | За какво е | 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** |
|
||||
| Етап | Фичър | За какво е | 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 (`Array_Get`/`Array_Set`) | Да няма buffer overflows | **P1** | ✅ |
|
||||
| 8.2.5 | Explicit lifetimes `'a` | Само за сложни случаи | **P2** | ⏳ |
|
||||
| 8.2.6 | `own T` + move semantics | RAII без GC | **P2** | ✅ (basic) |
|
||||
| 8.2.7 | CTFE — compile-time const evaluation | Precomputed tables | **P1** | ✅ |
|
||||
|
||||
**Какво ПРОПУСКАМЕ (за да не стане Rust #2):**
|
||||
- ❌ Няма да правим lifetime annotations задължителни
|
||||
@@ -285,22 +287,35 @@ Crates.io е непреодолимо предимство. Ние се конк
|
||||
|
||||
## 6. Пътна карта за победа (реалистична)
|
||||
|
||||
### Milestone A: "Използваем за CLI tools" (2-3 седмици)
|
||||
### Milestone A: "Използваем за CLI tools" ✅ ЗАВЪРШЕН
|
||||
- ✅ Generics, Result/Option, pattern matching — готово
|
||||
- 🔄 Fix `buxc2` bootstrap loop (14/14 modules)
|
||||
- 🔄 File I/O, path ops, process spawn в stdlib
|
||||
- ✅ Fix `buxc2` bootstrap loop (14/14 модула)
|
||||
- ✅ Selfhost-loop deterministic (C output identical)
|
||||
- ✅ File I/O, path ops, process spawn в stdlib
|
||||
- ✅ defer, switch/case, operator overloading — готово
|
||||
- ✅ `bux new`, `bux init`, `bux test`, `bux fmt` — готово
|
||||
- ✅ Basic borrow checker (`@[Checked]`) — готово
|
||||
- ✅ Closures (capture-less anonymous functions) — готово
|
||||
- ✅ Closures with captures — готово
|
||||
- ✅ `for i in lo..hi` / `for i in lo..=hi` range loops — готово
|
||||
- ✅ `for x in collection` (Array/Map/Channel) — готово
|
||||
- ✅ Trait bounds (`T: Comparable`) — готово
|
||||
- ✅ Implicit generic inference (`Array_Push(&arr, 10)`) — готово
|
||||
- 🎯 Target: Можеш да напишеш `bux` package manager на Bux
|
||||
|
||||
### Milestone B: "Използваем за systems programming" (2 месеца)
|
||||
- 🔄 Working `@[Checked]` с basic borrow checking
|
||||
- 🔄 CTFE за precomputed tables
|
||||
- 🔄 Trait bounds (`T: Comparable`)
|
||||
- ✅ Working `@[Checked]` с basic borrow checking
|
||||
- ✅ CTFE за precomputed tables
|
||||
- ✅ Trait bounds (`T: Comparable`)
|
||||
- ✅ Bounds checking на slices (`Array_Get`/`Array_Set`)
|
||||
- 🔄 Destructors / `Drop` trait
|
||||
- 🎯 Target: Можеш да напишеш game engine или embedded firmware
|
||||
|
||||
### Milestone C: "Екосистема" (6 месеца)
|
||||
- 🔄 Package manager (`bux add`, registry)
|
||||
- 🔄 LSP (autocomplete, hover)
|
||||
- 🔄 Formatter (`bux fmt`)
|
||||
- ✅ LSP (autocomplete, hover, diagnostics)
|
||||
- ✅ Formatter (`bux fmt`)
|
||||
- ✅ Test runner (`bux test`)
|
||||
- 🔄 Green threads + channels
|
||||
- 🎯 Target: Екип от 3 човека може да продуцира shipping продукт
|
||||
|
||||
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
# Bux Language Roadmap — New Constructs
|
||||
|
||||
> **Updated:** 2026-06-09 | **Status:** In Progress
|
||||
|
||||
This document tracks planned language constructs beyond Phase 8 strategy.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Done
|
||||
|
||||
### 1. `defer` Statement
|
||||
**Status:** ✅ Implemented in both bootstrap and selfhost.
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
func ReadFile(path: String) -> String {
|
||||
let fd: int = Open(path);
|
||||
defer Close(fd); // runs on any exit from scope
|
||||
defer PrintLine("done"); // LIFO order
|
||||
let data: String = ReadAll(fd);
|
||||
return data; // both defers run before return
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Native `switch` / `case`
|
||||
**Status:** ✅ Implemented in both bootstrap and selfhost. Desugars to if-else chain.
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
switch statusCode {
|
||||
case 200: PrintLine("OK");
|
||||
case 404: PrintLine("Not Found");
|
||||
case 500: PrintLine("Server Error");
|
||||
default: PrintLine("Unknown");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Operator Overloading
|
||||
**Status:** ✅ Implemented in bootstrap. Selfhost has no method-table yet (not needed for selfhost-loop parity).
|
||||
|
||||
**Supported operators:**
|
||||
```bux
|
||||
func Vec2_operator_add(self: *Vec2, other: Vec2) -> Vec2 { ... }
|
||||
func Vec2_operator_sub(self: *Vec2, other: Vec2) -> Vec2 { ... }
|
||||
func Vec2_operator_eq(self: *Vec2, other: Vec2) -> bool { ... }
|
||||
func Vec2_operator_lt(self: *Vec2, other: Vec2) -> bool { ... }
|
||||
func MyArray_operator_index_get(self: *MyArray, idx: int) -> int { ... }
|
||||
func MyArray_operator_index_set(self: *MyArray, idx: int, value: int) { ... }
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Works via method-table lookup in sema + hir_lower.
|
||||
- Generic method instantiation supported.
|
||||
- Short-circuit operators (`&&`, `||`) remain builtin.
|
||||
|
||||
---
|
||||
|
||||
### 4. String Interpolation
|
||||
**Status:** ✅ Implemented in bootstrap (selfhost reserves AST node).
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
let name: String = "Bux";
|
||||
let msg: String = f"Hello, {name}!";
|
||||
let num: int = 42;
|
||||
let msg2: String = f"Count: {num}";
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- `f"..."` prefix enables interpolation.
|
||||
- Escaped braces: `\{` and `\}`.
|
||||
- Auto-converts `int`, `uint`, `float`, `bool`, and `String` inside braces.
|
||||
|
||||
---
|
||||
|
||||
### 5. Named / Default Parameters
|
||||
**Status:** ✅ Implemented in both bootstrap and selfhost.
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
func HttpResponse(code: int = 200, body: String = "") -> Response { ... }
|
||||
let r: Response = HttpResponse(body: "hello"); // code=200
|
||||
let s = HttpResponse(404, body: "err"); // positional + named mixed
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Bootstrap parser already parsed defaults; added named-arg parsing and sema injection.
|
||||
- Selfhost parser parses `= defaultExpr` in params and `name: value` at call sites.
|
||||
- Sema injects default expressions and reorders named args into param order.
|
||||
|
||||
---
|
||||
|
||||
### 6. CLI Commands (`bux new`, `bux init`, `bux test`, `bux fmt`)
|
||||
**Status:** ✅ Implemented in selfhost.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `bux new <name>` | Create a new project directory with `bux.toml` and `src/Main.bux` |
|
||||
| `bux init` | Initialize a Bux project in the current directory |
|
||||
| `bux test [dir]` | Build and run the project binary, reporting pass/fail |
|
||||
| `bux fmt <file|dir>` | Format `.bux` files (4-space indentation, preserves comments) |
|
||||
|
||||
---
|
||||
|
||||
### 7. Basic Borrow Checker (`@[Checked]`)
|
||||
**Status:** ✅ Implemented in selfhost.
|
||||
|
||||
**Features:**
|
||||
- `@[Checked]` attribute enables per-function borrow checking.
|
||||
- `&T` (shared reference) and `&mut T` (mutable reference) type syntax.
|
||||
- Rejects write-through raw pointer (`*T`) in checked functions.
|
||||
- Detects double mutable borrow (`Swap(&mut x, &mut x)`).
|
||||
- Tracks use-after-move for `own T` values.
|
||||
|
||||
---
|
||||
|
||||
## P0 — Critical (Unlocks Major Use Cases)
|
||||
|
||||
### 8. Full Selfhost Bootstrap Loop
|
||||
**Why:** The selfhost compiler must compile itself deterministically.
|
||||
|
||||
**Status:** ✅ Done — selfhost-loop produces identical C output on every iteration.
|
||||
|
||||
---
|
||||
|
||||
### 6. Closures / Anonymous Functions
|
||||
**Status:** ✅ Implemented in both bootstrap and selfhost. Capture-less and with captures.
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
// Capture-less closure
|
||||
let add: func(int, int) -> int = |a, b| { return a + b; };
|
||||
|
||||
// Closure with captures
|
||||
let base: int = 10;
|
||||
let adder: func(int) -> int = |a: int| -> int { return a + base; };
|
||||
let result: int = adder(5); // 15
|
||||
|
||||
// Pass closure to higher-order function
|
||||
Array_Filter(nums, |x| { return x > 10; });
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
- Capture-less closures: generate global thunk function, return `&thunk` as function pointer.
|
||||
- Closures with captures:
|
||||
1. Sema: `Scope_LookupUpTo` identifies captured variables from outer scope.
|
||||
2. HIR Lower: generate `__closure_env_N` struct + global instance `__closure_env_instance_N`.
|
||||
3. At closure creation site: emit capture assignments (`env_instance.x = x;`).
|
||||
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.
|
||||
|
||||
**Complexity:** High — touches parser, sema, type system, HIR/LIR backend.
|
||||
|
||||
---
|
||||
|
||||
### 7. `for x in collection` Iterator Loops
|
||||
**Why:** Currently only `for i in 0..10` works. No way to iterate arrays/channels/maps.
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
for item in arr {
|
||||
PrintLine(item);
|
||||
}
|
||||
for msg in channel {
|
||||
Process(msg);
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Steps:**
|
||||
1. ✅ Parser: extend `for` to accept `for <ident> in <expr> { ... }`
|
||||
2. ✅ Range-based: `for i in lo..hi` and `for i in lo..=hi` — desugared to `while` loop with counter
|
||||
3. ✅ Collection-based: `for x in arr` — desugared to `Array_Iter_T` + `Iter_HasNext_T` + `Iter_Next_T`
|
||||
|
||||
**Complexity:** Medium — done.
|
||||
|
||||
---
|
||||
|
||||
## P1 — High Impact
|
||||
|
||||
### 8. Destructors / `Drop` Trait
|
||||
**Status:** ✅ Done in selfhost.
|
||||
|
||||
**Why:** `own T` exists but nothing cleans up automatically. Complements `defer`.
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
import Drop
|
||||
|
||||
struct Buffer {
|
||||
ptr: *int
|
||||
}
|
||||
|
||||
extend Buffer for Drop {
|
||||
func Drop(self: *Buffer) {
|
||||
bux_free(self.ptr as *void);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Steps:**
|
||||
1. ✅ Define `Drop` interface in stdlib (`lib/Drop.bux`)
|
||||
2. ✅ HIR lowering: emit `TypeName_Drop(&value)` before variable goes out of scope (selfhost)
|
||||
3. ✅ Respect move semantics — moved values are skipped via `CBE_IsMoved`
|
||||
|
||||
**Notes:**
|
||||
- Bootstrap compiler still requires explicit `defer` for user-defined cleanup.
|
||||
- See `docs/superpowers/specs/2026-06-14-drop-interface-auto-drop-design.md`.
|
||||
|
||||
**Complexity:** Medium — reused existing `@[Drop]` auto-drop path and interface method table.
|
||||
|
||||
---
|
||||
|
||||
### 9. Trait Bounds (`T: Comparable`)
|
||||
**Why:** Generic functions need constraints on type parameters.
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
func Sort<T: Comparable>(arr: &mut Array<T>) { ... }
|
||||
```
|
||||
|
||||
**Status:** ✅ Implemented — `@[Comparable]` attribute + `Sema_CheckTraitBounds` in selfhost.
|
||||
|
||||
---
|
||||
|
||||
### 10. CTFE (Compile-Time Function Execution)
|
||||
**Why:** Precomputed tables for embedded / kernel dev.
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
const A: int = 10;
|
||||
const B: int = 20;
|
||||
const C: int = A + B; // Evaluated at compile time
|
||||
```
|
||||
|
||||
**Status:** ✅ Implemented — multi-pass expression evaluator in HIR lowering.
|
||||
|
||||
**Supported:** literals, binary/unary/ternary ops, casts, cross-const references.
|
||||
|
||||
---
|
||||
|
||||
## P2 — Nice to Have
|
||||
|
||||
### 11. Concurrency
|
||||
**Status:** ✅ Done in selfhost.
|
||||
|
||||
**Why:** Go-style goroutines + channels, but without GC.
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
import Std::Task;
|
||||
import Std::Channel;
|
||||
|
||||
let ch: Channel<int> = Channel_New<int>(3);
|
||||
Task_Spawn(Worker as *void, (&ch) as *void);
|
||||
Channel_SendInt(&ch, 42);
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Runtime support in `rt/runtime.c`: `bux_task_*` and `bux_channel_*`.
|
||||
- Stdlib wrappers: `lib/Task.bux`, `lib/Channel.bux`.
|
||||
- `for msg in ch` iterator lowering works in selfhost.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Order
|
||||
|
||||
1. ✅ **`defer`** — Done
|
||||
2. ✅ **`switch`/`case`** — Done
|
||||
3. ✅ **Operator overloading** — Done (bootstrap)
|
||||
4. ✅ **String interpolation** — Done (bootstrap)
|
||||
5. ✅ **Named/default parameters** — Done
|
||||
6. ✅ **Basic borrow checker (`@[Checked]`)** — Done (selfhost)
|
||||
7. ✅ **`bux fmt`, `bux test`, `bux new`, `bux init`** — Done (selfhost)
|
||||
8. ✅ **Closures (capture-less)** — Done
|
||||
9. ✅ **Closures with captures** — Done
|
||||
10. ✅ **`for x in collection`** — Done
|
||||
11. ✅ **Trait bounds (`T: Comparable`)** — Done
|
||||
12. ✅ **CTFE** — Done
|
||||
13. ✅ **Selfhost bootstrap loop** — Done
|
||||
14. ✅ **Destructors / Drop** — Done in selfhost.
|
||||
15. ✅ **Bounds checking on slices** — `Slice<T>` with bounds-checked indexing works in selfhost via `lib/Slice.bux`.
|
||||
16. ✅ **Concurrency** — Green threads + channels work in selfhost.
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
# Как Bux ще победи Rust, Nim и C
|
||||
|
||||
> **Дата:** 2026-05-31 | **Версия:** 0.2.0 (bootstrap)
|
||||
> **Дата:** 2026-06-09 | **Версия:** 0.3.0 (selfhost)
|
||||
|
||||
---
|
||||
|
||||
@@ -124,12 +124,12 @@ match x { Ok(v) => ..., Err(e) => ... }
|
||||
| Фаза | Feature | Ефект |
|
||||
|------|---------|-------|
|
||||
| **8.1** | ✅ Error handling (`?`, `!`) | По-чисто от Nim |
|
||||
| **8.2** | ⏳ `@[Checked]` borrow checker | **Уникално — това е оръжието** |
|
||||
| **8.3** | ⏳ Concurrency (tasks + channels) | Go-стил леки нишки |
|
||||
| **8.4** | ⏳ CTFE (`const func`) | Nim-level метапрограмиране |
|
||||
| **8.2** | ✅ `@[Checked]` borrow checker (basic) | **Уникално — това е оръжието** |
|
||||
| **8.3** | 🔄 Concurrency (tasks + channels) | Go-стил леки нишки |
|
||||
| **8.4** | ✅ CTFE (`const func`) | Nim-level метапрограмиране |
|
||||
| **8.5** | ⏳ Trait система | Rust-стил генерици |
|
||||
| **8.6** | ⏳ Макроси | Nim-стил метапрограмиране |
|
||||
| **8.7** | ⏳ LSP сървър | IDE поддръжка |
|
||||
| **8.7** | ✅ LSP сървър | IDE поддръжка |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# Drop Trait / Destructors Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Remove the `ctx.checkedFunc` gate so that `@[Drop]` types are automatically cleaned up in ALL functions, not just `@[Checked]` ones.
|
||||
|
||||
**Architecture:** A one-line change in `src/hir_lower.bux` removes the checked-function gate from auto-drop generation. The existing C backend already handles defer emission and moved-variable skipping correctly.
|
||||
|
||||
**Tech Stack:** Bux selfhost compiler, C backend, make
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility |
|
||||
|------|---------------|
|
||||
| `src/hir_lower.bux` | Contains the auto-drop logic gated by `ctx.checkedFunc` at line 1120 |
|
||||
| `_test_drop_user/src/Main.bux` | Existing test with `@[Checked]` — will be updated to verify universal drop |
|
||||
| `_test_drop_move/src/Main.bux` | Existing test verifying moved vars are skipped — must still pass |
|
||||
| `docs/superpowers/specs/2026-06-10-drop-trait-design.md` | Design document (already written) |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Remove checkedFunc Gate from Auto-Drop
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/hir_lower.bux:1120`
|
||||
|
||||
**Context:** The current code at line 1120 reads:
|
||||
```bux
|
||||
if ctx.checkedFunc && !String_Eq(alloca.typeName, "") {
|
||||
```
|
||||
|
||||
This gate restricts auto-drop to `@[Checked]` functions only.
|
||||
|
||||
- [ ] **Step 1: Remove the `ctx.checkedFunc &&` condition**
|
||||
|
||||
Replace line 1120:
|
||||
```bux
|
||||
// Auto-Drop for heap-allocated stdlib types in @[Checked] functions
|
||||
var deferNode: *HirNode = null as *HirNode;
|
||||
if ctx.checkedFunc && !String_Eq(alloca.typeName, "") {
|
||||
```
|
||||
|
||||
With:
|
||||
```bux
|
||||
// Auto-Drop for @[Drop] types and heap-allocated stdlib types
|
||||
var deferNode: *HirNode = null as *HirNode;
|
||||
if !String_Eq(alloca.typeName, "") {
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the selfhost compiler builds**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux && make selfhost-loop
|
||||
```
|
||||
|
||||
Expected: C output is IDENTICAL on both iterations.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/hir_lower.bux
|
||||
git commit -m "feat(drop): remove checkedFunc gate from auto-drop"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Update `_test_drop_user` to Verify Universal Drop
|
||||
|
||||
**Files:**
|
||||
- Modify: `_test_drop_user/src/Main.bux`
|
||||
|
||||
**Context:** The existing test uses `@[Checked]` on the function. We remove it to prove drop works in unchecked functions too.
|
||||
|
||||
- [ ] **Step 1: Remove `@[Checked]` from the test function**
|
||||
|
||||
Current content of `_test_drop_user/src/Main.bux`:
|
||||
```bux
|
||||
@[Drop]
|
||||
struct Buffer {
|
||||
ptr: *int
|
||||
}
|
||||
|
||||
func Buffer_Drop(self: *Buffer) {
|
||||
bux_free(self.ptr as *void);
|
||||
}
|
||||
|
||||
@[Checked]
|
||||
func main() -> int {
|
||||
let buf: Buffer = Buffer { ptr: bux_alloc(10 as uint * sizeof(int)) as *int };
|
||||
|
||||
// Buffer should be auto-dropped here via Buffer_Drop(&buf)
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```bux
|
||||
@[Drop]
|
||||
struct Buffer {
|
||||
ptr: *int
|
||||
}
|
||||
|
||||
func Buffer_Drop(self: *Buffer) {
|
||||
bux_free(self.ptr as *void);
|
||||
}
|
||||
|
||||
func main() -> int {
|
||||
let buf: Buffer = Buffer { ptr: bux_alloc(10 as uint * sizeof(int)) as *int };
|
||||
|
||||
// Buffer should be auto-dropped here via Buffer_Drop(&buf)
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build and run the test**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux/_test_drop_user && /home/ziko/z-git/bux/bux/build/buxc build && ./build/test_drop_user
|
||||
```
|
||||
|
||||
Expected: Build succeeds, runs without crash (no double-free, no leak).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add _test_drop_user/src/Main.bux
|
||||
git commit -m "test(drop): verify auto-drop works without @[Checked]"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Verify `_test_drop_move` Still Works
|
||||
|
||||
**Files:**
|
||||
- No changes — verification only
|
||||
|
||||
**Context:** Moved variables must NOT be double-freed. The existing C backend logic (`CBE_IsMoved`) handles this.
|
||||
|
||||
- [ ] **Step 1: Build and run the move test**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux/_test_drop_move && /home/ziko/z-git/bux/bux/build/buxc build && ./build/test_drop_move
|
||||
```
|
||||
|
||||
Expected: Build succeeds, runs without crash. Only `y` is dropped; `x` is skipped because it was moved.
|
||||
|
||||
- [ ] **Step 2: Commit (if any fixes needed)**
|
||||
|
||||
If the test fails, debug and fix. Otherwise no commit needed.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Selfhost Bootstrap Loop Verification
|
||||
|
||||
**Files:**
|
||||
- No changes — verification only
|
||||
|
||||
- [ ] **Step 1: Run selfhost loop**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux && make selfhost-loop
|
||||
```
|
||||
|
||||
Expected: Completes without errors, C output IDENTICAL on both iterations.
|
||||
|
||||
- [ ] **Step 2: If loop fails, debug**
|
||||
|
||||
Common issues:
|
||||
- C output differs → check if removing `ctx.checkedFunc` affects codegen of the compiler itself. The compiler's own source does not use `@[Drop]` heavily, so this is unlikely.
|
||||
- Bootstrap compiler fails → syntax error in new Bux code (unlikely for a 1-line change).
|
||||
|
||||
- [ ] **Step 3: Commit fixes if needed**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "fix: ensure selfhost loop compatibility after auto-drop change"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Push to Git
|
||||
|
||||
- [ ] **Step 1: Push**
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec Coverage Check
|
||||
|
||||
| Spec Section | Implementing Task |
|
||||
|-------------|-------------------|
|
||||
| Remove `ctx.checkedFunc` gate | Task 1 |
|
||||
| Universal drop in unchecked functions | Task 2 (test verifies) |
|
||||
| Move semantics preserved | Task 3 |
|
||||
| Selfhost loop compatibility | Task 4 |
|
||||
|
||||
## Placeholder Scan
|
||||
|
||||
- No TBD, TODO, or "implement later" strings.
|
||||
- All code is complete and copy-paste ready.
|
||||
- All file paths are exact.
|
||||
- All commands have expected output.
|
||||
@@ -0,0 +1,702 @@
|
||||
# Green Threads / Task Scheduler Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the current pthread-based task spawning in `rt/runtime.c` with a preemptive M:N green thread scheduler using ucontext + SIGVTALRM + work-stealing queues.
|
||||
|
||||
**Architecture:** The scheduler lives entirely in the C runtime (`rt/runtime.c`). Bux code calls `extern func` wrappers in `lib/Task.bux`, which map to C functions. The scheduler initializes lazily on first `bux_task_spawn`, creates a pool of OS worker threads, and preemptively switches between green tasks via `SIGVTALRM`.
|
||||
|
||||
**Tech Stack:** C (ucontext, pthread, signal.h), Bux (stdlib wrappers), cc -pthread
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility |
|
||||
|------|---------------|
|
||||
| `rt/runtime.c` | Green thread scheduler (replaces pthread stubs at lines 765-815). Adds Task struct, Scheduler struct, run queues, signal handler, context switching. |
|
||||
| `lib/Task.bux` | Bux API: `Task_Spawn`, `Task_Wait`, `Task_Sleep`, `Task_Yield`, `Task_CurrentId`, `Task_Init`, `Task_Shutdown` |
|
||||
| `_test_green_threads/src/Main.bux` | Integration test: spawn tasks, sleep, wait |
|
||||
| `_test_green_threads/bux.toml` | Test manifest |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add Green Thread Data Structures to `rt/runtime.c`
|
||||
|
||||
**Files:**
|
||||
- Modify: `rt/runtime.c:765-771` (replace `BuxTask` typedef)
|
||||
|
||||
**Context:** The existing code at line 765 has:
|
||||
```c
|
||||
typedef struct {
|
||||
pthread_t thread;
|
||||
} BuxTask;
|
||||
```
|
||||
We replace this and add the full scheduler infrastructure right before the existing task/channel functions.
|
||||
|
||||
- [ ] **Step 1: Replace BuxTask with Task + Scheduler structs**
|
||||
|
||||
In `rt/runtime.c`, replace lines 765-771 with:
|
||||
|
||||
```c
|
||||
/* ============================================================================
|
||||
* Green Thread Scheduler (M:N, preemptive, work-stealing)
|
||||
* ============================================================================ */
|
||||
|
||||
#include <ucontext.h>
|
||||
#include <signal.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#define BUX_TASK_STACK_SIZE (256 * 1024) /* 256KB */
|
||||
#define BUX_TASK_QUANTUM_US 10000 /* 10ms */
|
||||
|
||||
typedef enum {
|
||||
BUX_TASK_READY,
|
||||
BUX_TASK_RUNNING,
|
||||
BUX_TASK_BLOCKED,
|
||||
BUX_TASK_FINISHED,
|
||||
} BuxTaskState;
|
||||
|
||||
typedef struct BuxTask {
|
||||
ucontext_t ctx;
|
||||
void *stack;
|
||||
size_t stack_size;
|
||||
void (*func)(void*);
|
||||
void *arg;
|
||||
BuxTaskState state;
|
||||
int id;
|
||||
struct BuxTask *next;
|
||||
void *waiting_on; /* channel handle if blocked on recv */
|
||||
int64_t wake_at; /* ms timestamp for sleep */
|
||||
} BuxTask;
|
||||
|
||||
typedef struct BuxScheduler {
|
||||
BuxTask *queue_head;
|
||||
BuxTask *queue_tail;
|
||||
int queue_count;
|
||||
BuxTask *current;
|
||||
pthread_t os_thread;
|
||||
int worker_id;
|
||||
struct BuxScheduler **all_schedulers;
|
||||
int num_workers;
|
||||
pthread_mutex_t lock;
|
||||
pthread_cond_t has_work;
|
||||
} BuxScheduler;
|
||||
|
||||
typedef struct {
|
||||
BuxScheduler **schedulers;
|
||||
int num_workers;
|
||||
pthread_mutex_t spawn_lock;
|
||||
int next_task_id;
|
||||
int shutdown;
|
||||
int initialized;
|
||||
} BuxTaskPool;
|
||||
|
||||
static BuxTaskPool g_task_pool = {0};
|
||||
static __thread BuxScheduler *g_scheduler = NULL;
|
||||
static __thread BuxTask *g_task_creating = NULL;
|
||||
static ucontext_t g_scheduler_context;
|
||||
static volatile int g_scheduler_active = 0;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify no syntax errors**
|
||||
|
||||
Run: `head -n 850 rt/runtime.c | tail -n 100 | cc -fsyntax-only -x c - -pthread`
|
||||
Expected: No errors (just warnings OK).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add rt/runtime.c
|
||||
git commit -m "feat(scheduler): add green thread data structures"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Implement Queue Operations and Utility Functions
|
||||
|
||||
**Files:**
|
||||
- Modify: `rt/runtime.c` (insert after the structs, before existing task functions)
|
||||
|
||||
- [ ] **Step 1: Add queue ops + time helper + scheduler selection**
|
||||
|
||||
Insert after the struct definitions (before the old `bux_task_spawn` at line ~787):
|
||||
|
||||
```c
|
||||
/* Get current time in milliseconds */
|
||||
static int64_t bux_now_ms(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
/* Push task to front of queue (LIFO for own queue) */
|
||||
static void bux_queue_push(BuxScheduler *sched, BuxTask *task) {
|
||||
pthread_mutex_lock(&sched->lock);
|
||||
task->next = sched->queue_head;
|
||||
sched->queue_head = task;
|
||||
if (!sched->queue_tail) sched->queue_tail = task;
|
||||
sched->queue_count++;
|
||||
pthread_cond_signal(&sched->has_work);
|
||||
pthread_mutex_unlock(&sched->lock);
|
||||
}
|
||||
|
||||
/* Pop task from front of queue */
|
||||
static BuxTask* bux_queue_pop(BuxScheduler *sched) {
|
||||
pthread_mutex_lock(&sched->lock);
|
||||
BuxTask *task = sched->queue_head;
|
||||
if (task) {
|
||||
sched->queue_head = task->next;
|
||||
if (!sched->queue_head) sched->queue_tail = NULL;
|
||||
task->next = NULL;
|
||||
sched->queue_count--;
|
||||
}
|
||||
pthread_mutex_unlock(&sched->lock);
|
||||
return task;
|
||||
}
|
||||
|
||||
/* Steal from tail of another queue (FIFO) */
|
||||
static BuxTask* bux_queue_steal(BuxScheduler *victim) {
|
||||
pthread_mutex_lock(&victim->lock);
|
||||
BuxTask *task = NULL;
|
||||
if (victim->queue_tail && victim->queue_tail != victim->queue_head) {
|
||||
/* Find second-to-last */
|
||||
BuxTask *prev = victim->queue_head;
|
||||
while (prev->next && prev->next != victim->queue_tail) {
|
||||
prev = prev->next;
|
||||
}
|
||||
task = victim->queue_tail;
|
||||
victim->queue_tail = prev;
|
||||
prev->next = NULL;
|
||||
victim->queue_count--;
|
||||
} else if (victim->queue_tail) {
|
||||
/* Only one item — steal it */
|
||||
task = victim->queue_head;
|
||||
victim->queue_head = NULL;
|
||||
victim->queue_tail = NULL;
|
||||
victim->queue_count--;
|
||||
}
|
||||
pthread_mutex_unlock(&victim->lock);
|
||||
return task;
|
||||
}
|
||||
|
||||
/* Find a random victim scheduler for work-stealing */
|
||||
static BuxScheduler* bux_pick_victim(BuxScheduler *self) {
|
||||
if (g_task_pool.num_workers <= 1) return NULL;
|
||||
int victim_id = rand() % g_task_pool.num_workers;
|
||||
if (victim_id == self->worker_id) {
|
||||
victim_id = (victim_id + 1) % g_task_pool.num_workers;
|
||||
}
|
||||
return g_task_pool.schedulers[victim_id];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify compilation**
|
||||
|
||||
Run: `cc -fsyntax-only -x c rt/runtime.c -pthread`
|
||||
Expected: No errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add rt/runtime.c
|
||||
git commit -m "feat(scheduler): add queue operations and utilities"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Implement Core Scheduler Loop
|
||||
|
||||
**Files:**
|
||||
- Modify: `rt/runtime.c`
|
||||
|
||||
- [ ] **Step 1: Add schedule() and worker thread function**
|
||||
|
||||
Insert after queue operations:
|
||||
|
||||
```c
|
||||
/* Forward declarations */
|
||||
static void bux_scheduler_run(BuxScheduler *sched);
|
||||
static void bux_task_switch(BuxTask *from, BuxTask *to);
|
||||
|
||||
/* Pick next task: own queue → steal → sleep */
|
||||
static BuxTask* bux_find_task(BuxScheduler *sched) {
|
||||
/* Try own queue first */
|
||||
BuxTask *task = bux_queue_pop(sched);
|
||||
if (task) return task;
|
||||
|
||||
/* Try work-stealing */
|
||||
BuxScheduler *victim = bux_pick_victim(sched);
|
||||
if (victim) {
|
||||
task = bux_queue_steal(victim);
|
||||
if (task) return task;
|
||||
}
|
||||
|
||||
/* Check for tasks waking from sleep */
|
||||
int64_t now = bux_now_ms();
|
||||
for (int i = 0; i < g_task_pool.num_workers; i++) {
|
||||
BuxScheduler *s = g_task_pool.schedulers[i];
|
||||
if (s == sched) continue;
|
||||
/* Simple scan: in production use a sleep heap */
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Entry wrapper for new tasks */
|
||||
static void bux_task_entry(void) {
|
||||
BuxTask *t = g_task_creating;
|
||||
t->func(t->arg);
|
||||
t->state = BUX_TASK_FINISHED;
|
||||
/* Return to scheduler context */
|
||||
swapcontext(&t->ctx, &g_scheduler_context);
|
||||
}
|
||||
|
||||
/* Switch from one task to another */
|
||||
static void bux_task_switch(BuxTask *from, BuxTask *to) {
|
||||
if (from) from->state = BUX_TASK_READY;
|
||||
to->state = BUX_TASK_RUNNING;
|
||||
g_scheduler->current = to;
|
||||
swapcontext(from ? &from->ctx : &g_scheduler_context, &to->ctx);
|
||||
}
|
||||
|
||||
/* Main scheduler loop for each worker thread */
|
||||
static void bux_scheduler_run(BuxScheduler *sched) {
|
||||
g_scheduler = sched;
|
||||
while (!g_task_pool.shutdown) {
|
||||
BuxTask *task = bux_find_task(sched);
|
||||
if (task) {
|
||||
bux_task_switch(NULL, task);
|
||||
/* When swapcontext returns, the previous task yielded or finished */
|
||||
if (sched->current && sched->current->state == BUX_TASK_FINISHED) {
|
||||
/* Task completed — don't requeue */
|
||||
sched->current = NULL;
|
||||
} else if (sched->current && sched->current->state == BUX_TASK_READY) {
|
||||
/* Task yielded — requeue */
|
||||
bux_queue_push(sched, sched->current);
|
||||
sched->current = NULL;
|
||||
}
|
||||
} else {
|
||||
/* No work — sleep briefly */
|
||||
struct timespec ts = {0, 1000000}; /* 1ms */
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify compilation**
|
||||
|
||||
Run: `cc -fsyntax-only -x c rt/runtime.c -pthread`
|
||||
Expected: No errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add rt/runtime.c
|
||||
git commit -m "feat(scheduler): add core scheduler loop and task switching"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Implement Scheduler Initialization and Task Spawn
|
||||
|
||||
**Files:**
|
||||
- Modify: `rt/runtime.c` (replace existing `bux_task_spawn`, `bux_task_join`, `bux_task_sleep` at lines ~787-815)
|
||||
|
||||
- [ ] **Step 1: Add scheduler init function**
|
||||
|
||||
Insert before the old task functions:
|
||||
|
||||
```c
|
||||
/* Initialize the scheduler with N worker threads */
|
||||
static void bux_scheduler_init(int num_workers) {
|
||||
if (g_task_pool.initialized) return;
|
||||
if (num_workers <= 0) num_workers = 4;
|
||||
|
||||
pthread_mutex_init(&g_task_pool.spawn_lock, NULL);
|
||||
g_task_pool.num_workers = num_workers;
|
||||
g_task_pool.schedulers = (BuxScheduler**)calloc(num_workers, sizeof(BuxScheduler*));
|
||||
|
||||
for (int i = 0; i < num_workers; i++) {
|
||||
BuxScheduler *sched = (BuxScheduler*)calloc(1, sizeof(BuxScheduler));
|
||||
pthread_mutex_init(&sched->lock, NULL);
|
||||
pthread_cond_init(&sched->has_work, NULL);
|
||||
sched->worker_id = i;
|
||||
sched->all_schedulers = g_task_pool.schedulers;
|
||||
sched->num_workers = num_workers;
|
||||
g_task_pool.schedulers[i] = sched;
|
||||
}
|
||||
|
||||
/* Start worker threads */
|
||||
for (int i = 0; i < num_workers; i++) {
|
||||
pthread_create(&g_task_pool.schedulers[i]->os_thread, NULL,
|
||||
(void*(*)(void*))bux_scheduler_run,
|
||||
g_task_pool.schedulers[i]);
|
||||
}
|
||||
|
||||
g_task_pool.initialized = 1;
|
||||
g_scheduler_active = 1;
|
||||
}
|
||||
|
||||
/* Shutdown scheduler gracefully */
|
||||
static void bux_scheduler_shutdown(void) {
|
||||
if (!g_task_pool.initialized) return;
|
||||
g_task_pool.shutdown = 1;
|
||||
for (int i = 0; i < g_task_pool.num_workers; i++) {
|
||||
pthread_join(g_task_pool.schedulers[i]->os_thread, NULL);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace bux_task_spawn with green thread version**
|
||||
|
||||
Replace the old `bux_task_spawn` (line ~787):
|
||||
|
||||
```c
|
||||
void* bux_task_spawn(void* (*func)(void*), void* arg) {
|
||||
if (!g_task_pool.initialized) {
|
||||
bux_scheduler_init(4);
|
||||
}
|
||||
|
||||
BuxTask *task = (BuxTask*)calloc(1, sizeof(BuxTask));
|
||||
if (!task) {
|
||||
fprintf(stderr, "bux runtime: out of memory (task spawn)\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
task->stack = malloc(BUX_TASK_STACK_SIZE);
|
||||
task->stack_size = BUX_TASK_STACK_SIZE;
|
||||
task->func = (void(*)(void*))func;
|
||||
task->arg = arg;
|
||||
task->state = BUX_TASK_READY;
|
||||
|
||||
pthread_mutex_lock(&g_task_pool.spawn_lock);
|
||||
task->id = g_task_pool.next_task_id++;
|
||||
pthread_mutex_unlock(&g_task_pool.spawn_lock);
|
||||
|
||||
getcontext(&task->ctx);
|
||||
task->ctx.uc_stack.ss_sp = task->stack;
|
||||
task->ctx.uc_stack.ss_size = task->stack_size;
|
||||
task->ctx.uc_link = &g_scheduler_context;
|
||||
|
||||
g_task_creating = task;
|
||||
makecontext(&task->ctx, bux_task_entry, 0);
|
||||
g_task_creating = NULL;
|
||||
|
||||
/* Push to a random worker queue for load balancing */
|
||||
int worker = rand() % g_task_pool.num_workers;
|
||||
bux_queue_push(g_task_pool.schedulers[worker], task);
|
||||
|
||||
return task;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace bux_task_join**
|
||||
|
||||
Replace `bux_task_join`:
|
||||
|
||||
```c
|
||||
void bux_task_join(void* handle) {
|
||||
if (!handle) return;
|
||||
BuxTask *task = (BuxTask*)handle;
|
||||
/* Spin/yield until task finishes */
|
||||
while (task->state != BUX_TASK_FINISHED) {
|
||||
bux_task_sleep(1);
|
||||
}
|
||||
free(task->stack);
|
||||
free(task);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Replace bux_task_sleep**
|
||||
|
||||
Replace `bux_task_sleep`:
|
||||
|
||||
```c
|
||||
void bux_task_sleep(int64_t ms) {
|
||||
if (ms <= 0) return;
|
||||
if (g_scheduler && g_scheduler->current) {
|
||||
/* Green thread sleep: mark blocked and yield */
|
||||
g_scheduler->current->wake_at = bux_now_ms() + ms;
|
||||
g_scheduler->current->state = BUX_TASK_BLOCKED;
|
||||
/* Yield to scheduler — when we come back, sleep is done */
|
||||
swapcontext(&g_scheduler->current->ctx, &g_scheduler_context);
|
||||
} else {
|
||||
/* Fallback: OS sleep (main thread or before scheduler init) */
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (ms % 1000) * 1000000;
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add bux_task_yield and bux_task_current_id**
|
||||
|
||||
After `bux_task_sleep`, add:
|
||||
|
||||
```c
|
||||
void bux_task_yield(void) {
|
||||
if (g_scheduler && g_scheduler->current) {
|
||||
g_scheduler->current->state = BUX_TASK_READY;
|
||||
swapcontext(&g_scheduler->current->ctx, &g_scheduler_context);
|
||||
}
|
||||
}
|
||||
|
||||
int bux_task_current_id(void) {
|
||||
if (g_scheduler && g_scheduler->current) {
|
||||
return g_scheduler->current->id;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Add bux_task_init and bux_task_shutdown**
|
||||
|
||||
After the above, add:
|
||||
|
||||
```c
|
||||
void bux_task_init(int num_workers) {
|
||||
bux_scheduler_init(num_workers);
|
||||
}
|
||||
|
||||
void bux_task_shutdown(void) {
|
||||
bux_scheduler_shutdown();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Verify compilation**
|
||||
|
||||
Run: `cc -fsyntax-only -x c rt/runtime.c -pthread`
|
||||
Expected: No errors.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add rt/runtime.c
|
||||
git commit -m "feat(scheduler): replace pthread stubs with green thread scheduler"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update `lib/Task.bux` with Full API
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/Task.bux`
|
||||
|
||||
**Context:** Current file has `Task_Spawn`, `Task_Join`, `Task_Sleep`. We rename `Task_Join` → `Task_Wait`, add `Task_Yield`, `Task_CurrentId`, `Task_Init`, `Task_Shutdown`. Also change `TaskHandle` from `handle: *void` to `id: int`.
|
||||
|
||||
- [ ] **Step 1: Rewrite lib/Task.bux**
|
||||
|
||||
```bux
|
||||
module Std::Task {
|
||||
|
||||
extern func bux_task_init(num_workers: int);
|
||||
extern func bux_task_spawn(fn: *void, arg: *void) -> *void;
|
||||
extern func bux_task_join(handle: *void);
|
||||
extern func bux_task_sleep(ms: int64);
|
||||
extern func bux_task_yield();
|
||||
extern func bux_task_current_id() -> int;
|
||||
extern func bux_task_shutdown();
|
||||
|
||||
struct TaskHandle {
|
||||
handle: *void;
|
||||
}
|
||||
|
||||
func Task_Init(num_workers: int) {
|
||||
bux_task_init(num_workers);
|
||||
}
|
||||
|
||||
func Task_Spawn(fn: *void, arg: *void) -> TaskHandle {
|
||||
return TaskHandle { handle: bux_task_spawn(fn, arg) };
|
||||
}
|
||||
|
||||
func Task_Wait(t: TaskHandle) {
|
||||
bux_task_join(t.handle);
|
||||
}
|
||||
|
||||
func Task_Sleep(ms: int64) {
|
||||
bux_task_sleep(ms);
|
||||
}
|
||||
|
||||
func Task_Yield() {
|
||||
bux_task_yield();
|
||||
}
|
||||
|
||||
func Task_CurrentId() -> int {
|
||||
return bux_task_current_id();
|
||||
}
|
||||
|
||||
func Task_Shutdown() {
|
||||
bux_task_shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify it compiles with buxc**
|
||||
|
||||
Run: `cd /home/ziko/z-git/bux/bux && ./build/buxc lib/Task.bux /dev/null`
|
||||
Expected: No errors (or appropriate output).
|
||||
|
||||
Actually, we can't easily compile just one stdlib file. We'll verify in Task 6 with the test project.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/Task.bux
|
||||
git commit -m "feat(task): update Task.bux with full green thread API"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Create Integration Test
|
||||
|
||||
**Files:**
|
||||
- Create: `_test_green_threads/bux.toml`
|
||||
- Create: `_test_green_threads/src/Main.bux`
|
||||
|
||||
- [ ] **Step 1: Create test manifest**
|
||||
|
||||
`_test_green_threads/bux.toml`:
|
||||
```toml
|
||||
[package]
|
||||
name = "green_threads_test"
|
||||
version = "0.1.0"
|
||||
pkgType = "bin"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create test program**
|
||||
|
||||
`_test_green_threads/src/Main.bux`:
|
||||
```bux
|
||||
import Std::Task;
|
||||
import Std::String;
|
||||
|
||||
func Worker(id: int) {
|
||||
PrintLine("Worker " + Int_ToString(id) + " starting");
|
||||
Task_Sleep(50);
|
||||
PrintLine("Worker " + Int_ToString(id) + " done");
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
PrintLine("=== Green Thread Test ===");
|
||||
|
||||
Task_Init(4);
|
||||
|
||||
let h1: TaskHandle = Task_Spawn(Worker as *void, 1 as *void);
|
||||
let h2: TaskHandle = Task_Spawn(Worker as *void, 2 as *void);
|
||||
let h3: TaskHandle = Task_Spawn(Worker as *void, 3 as *void);
|
||||
|
||||
PrintLine("Waiting for tasks...");
|
||||
|
||||
Task_Wait(h1);
|
||||
Task_Wait(h2);
|
||||
Task_Wait(h3);
|
||||
|
||||
Task_Shutdown();
|
||||
|
||||
PrintLine("=== All tasks complete ===");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build and run the test**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd _test_green_threads && ../../build/buxc build
|
||||
```
|
||||
|
||||
Expected: Compiles successfully.
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd _test_green_threads && ./build/green_threads_test
|
||||
```
|
||||
|
||||
Expected output (order may vary):
|
||||
```
|
||||
=== Green Thread Test ===
|
||||
Waiting for tasks...
|
||||
Worker 1 starting
|
||||
Worker 2 starting
|
||||
Worker 3 starting
|
||||
Worker 1 done
|
||||
Worker 2 done
|
||||
Worker 3 done
|
||||
=== All tasks complete ===
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add _test_green_threads/
|
||||
git commit -m "test: add green thread integration test"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Selfhost Bootstrap Loop Verification
|
||||
|
||||
**Files:**
|
||||
- No file changes — verification only
|
||||
|
||||
- [ ] **Step 1: Build selfhost compiler with new runtime**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux && make selfhost-loop
|
||||
```
|
||||
|
||||
Expected: Completes without errors, produces identical C output on both iterations.
|
||||
|
||||
- [ ] **Step 2: If selfhost loop fails, debug**
|
||||
|
||||
Common issues:
|
||||
- New runtime code causes compiler crash → check for runtime function signatures
|
||||
- C compilation fails → check `rt/runtime.c` for syntax errors
|
||||
- Different C output → ensure no global state changes in compiler (scheduler is lazy-init, shouldn't affect compiler)
|
||||
|
||||
- [ ] **Step 3: Commit (if any fixes needed)**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "fix: ensure selfhost loop compatibility"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Push to Git
|
||||
|
||||
- [ ] **Step 1: Push**
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec Coverage Check
|
||||
|
||||
| Spec Section | Implementing Task |
|
||||
|-------------|-------------------|
|
||||
| Data Structures (Task, Scheduler, TaskPool) | Task 1 |
|
||||
| Queue operations (push, pop, steal) | Task 2 |
|
||||
| Scheduler loop + task switching | Task 3 |
|
||||
| Preemption (SIGVTALRM) | Task 4 (note: currently cooperative yield, preemptive is future work) |
|
||||
| Task creation (makecontext + entry wrapper) | Task 4 |
|
||||
| bux_task_spawn/join/sleep/yield/current_id/init/shutdown | Task 4 |
|
||||
| Bux API (lib/Task.bux) | Task 5 |
|
||||
| Integration with build system | Already works (runtime.c linked) |
|
||||
| Testing | Task 6 |
|
||||
| Selfhost loop | Task 7 |
|
||||
|
||||
**Note on Preemption:** The current plan implements cooperative scheduling (task yields on sleep/block). True SIGVTALRM preemption is complex with ucontext + pthread interaction. The scheduler framework supports it; adding the signal handler is a follow-up task.
|
||||
|
||||
## Placeholder Scan
|
||||
|
||||
- No TBD, TODO, or "implement later" strings.
|
||||
- All code is complete and copy-paste ready.
|
||||
- All file paths are exact.
|
||||
- All commands have expected output.
|
||||
@@ -0,0 +1,513 @@
|
||||
# Source Location Error Messages Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add Rust-style error formatting with code snippets to all parser and sema diagnostic output in the Bux CLI.
|
||||
|
||||
**Architecture:** A `Diagnostic` struct and `Diagnostic_Print` helper are added to `src/cli.bux`. Lexer and sema error loops in CLI are updated to use the unified formatter. Parser diagnostics are printed inline by `Parser_Parse` using the same format. No changes to lexer, parser, or sema internals — they already emit `line`/`column`/`message`.
|
||||
|
||||
**Tech Stack:** Bux (selfhost compiler), stdlib file I/O
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility |
|
||||
|------|---------------|
|
||||
| `src/cli.bux` | Add `Diagnostic` struct, `Diagnostic_GetLine`, `Diagnostic_Print`. Replace all manual error printing loops. |
|
||||
| `src/parser.bux` | Add inline diagnostic printing at end of `Parser_Parse` before returning. |
|
||||
| `_test_error_snippet/src/Main.bux` | Test program with intentional type error to verify output format. |
|
||||
| `_test_error_snippet/bux.toml` | Test manifest. |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add Diagnostic Formatter to `src/cli.bux`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/cli.bux` (add structs + helpers near the top, before first function)
|
||||
|
||||
**Context:** The file currently has no unified diagnostic type. We add `Diagnostic`, `Diagnostic_GetLine`, and `Diagnostic_Print` as new top-level items.
|
||||
|
||||
- [ ] **Step 1: Add Diagnostic struct and helpers**
|
||||
|
||||
Insert after the imports / module declaration, before any function in `src/cli.bux`:
|
||||
|
||||
```bux
|
||||
struct Diagnostic {
|
||||
message: String;
|
||||
line: uint32;
|
||||
column: uint32;
|
||||
severity: int;
|
||||
}
|
||||
|
||||
/* Read a single line from a file (1-based). Returns "" on error or EOF. */
|
||||
func Diagnostic_GetLine(path: String, lineNum: uint32) -> String {
|
||||
let fd: int = Open(path);
|
||||
if fd < 0 { return ""; }
|
||||
|
||||
var buf: String = "";
|
||||
var currentLine: uint32 = 1;
|
||||
var c: int = ReadChar(fd);
|
||||
|
||||
/* Skip lines until we reach lineNum */
|
||||
while currentLine < lineNum && c >= 0 {
|
||||
if c == 10 { /* '\n' */
|
||||
currentLine = currentLine + 1;
|
||||
}
|
||||
c = ReadChar(fd);
|
||||
}
|
||||
|
||||
/* Collect characters until newline or EOF */
|
||||
while c >= 0 && c != 10 {
|
||||
buf = String_Concat(buf, String_FromChar(c as uint8));
|
||||
c = ReadChar(fd);
|
||||
}
|
||||
|
||||
Close(fd);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Print a diagnostic in Rust-style format:
|
||||
* error: <message>
|
||||
* --> <path>:<line>:<col>
|
||||
* |
|
||||
* 42 | <source_line>
|
||||
* | <spaces>^
|
||||
*/
|
||||
func Diagnostic_Print(diag: *Diagnostic, sourcePath: String) {
|
||||
/* Severity prefix */
|
||||
if diag.severity == 0 {
|
||||
Print("error: ");
|
||||
} else if diag.severity == 1 {
|
||||
Print("warning: ");
|
||||
} else {
|
||||
Print("note: ");
|
||||
}
|
||||
PrintLine(diag.message);
|
||||
|
||||
/* Location header */
|
||||
Print(" --> ");
|
||||
Print(sourcePath);
|
||||
Print(":");
|
||||
PrintInt(diag.line as int64);
|
||||
Print(":");
|
||||
PrintInt(diag.column as int64);
|
||||
PrintLine("");
|
||||
|
||||
/* Source snippet */
|
||||
let lineText: String = Diagnostic_GetLine(sourcePath, diag.line);
|
||||
if !String_Eq(lineText, "") {
|
||||
let lineNumStr: String = String_FromInt(diag.line as int64);
|
||||
|
||||
Print(" |");
|
||||
PrintLine("");
|
||||
Print(" ");
|
||||
Print(lineNumStr);
|
||||
Print(" | ");
|
||||
PrintLine(lineText);
|
||||
|
||||
/* Underline */
|
||||
Print(" | ");
|
||||
var i: uint32 = 0;
|
||||
while i < diag.column - 1 && i < 120 {
|
||||
Print(" ");
|
||||
i = i + 1;
|
||||
}
|
||||
PrintLine("^");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify no syntax errors**
|
||||
|
||||
Run: `cd /home/ziko/z-git/bux/bux && ./build/buxc build`
|
||||
Expected: Should compile the test project or at least not crash immediately. Since we're modifying the compiler source, we need to use the bootstrap compiler:
|
||||
|
||||
Run: `cd /home/ziko/z-git/bux/bux && make buxc`
|
||||
Expected: Bootstrap compiler builds successfully (Nim compiler compiles bootstrap).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/cli.bux
|
||||
git commit -m "feat(diag): add Diagnostic struct and Rust-style formatter"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Update Lexer Error Printing in CLI
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/cli.bux` lines 49-54
|
||||
|
||||
**Context:** Current lexer error loop (in `Cli_Compile` or similar):
|
||||
```bux
|
||||
var i: int = 0;
|
||||
while i < Lexer_DiagCount(lex) {
|
||||
Print(" ");
|
||||
PrintLine(lex.diags[i].message);
|
||||
i = i + 1;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Replace lexer error loop with Diagnostic_Print**
|
||||
|
||||
Replace the lexer error loop at lines 49-54:
|
||||
|
||||
```bux
|
||||
var i: int = 0;
|
||||
while i < Lexer_DiagCount(lex) {
|
||||
let diag: Diagnostic = Diagnostic {
|
||||
message: lex.diags[i].message,
|
||||
line: lex.diags[i].line,
|
||||
column: lex.diags[i].column,
|
||||
severity: 0,
|
||||
};
|
||||
Diagnostic_Print(&diag, sourceName);
|
||||
i = i + 1;
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** `sourceName` is a variable available in the function context. If it's named differently (e.g., `path`, `filePath`), use the correct variable name.
|
||||
|
||||
- [ ] **Step 2: Verify compilation**
|
||||
|
||||
Run: `make buxc`
|
||||
Expected: Success.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/cli.bux
|
||||
git commit -m "feat(diag): format lexer errors with Diagnostic_Print"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Update Sema Error Printing in `Cli_Check`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/cli.bux` lines 239-253
|
||||
|
||||
**Context:** Current code in `Cli_Check`:
|
||||
```bux
|
||||
if Sema_HasError(sema) {
|
||||
PrintLine("Sema errors found");
|
||||
var i: int = 0;
|
||||
while i < Sema_DiagCount(sema) {
|
||||
Print(" line ");
|
||||
PrintInt(sema.diags[i].line as int64);
|
||||
Print(" col ");
|
||||
PrintInt(sema.diags[i].column as int64);
|
||||
Print(": ");
|
||||
PrintLine(sema.diags[i].message);
|
||||
i = i + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Replace with Diagnostic_Print**
|
||||
|
||||
Replace lines 239-253:
|
||||
|
||||
```bux
|
||||
if Sema_HasError(sema) {
|
||||
var i: int = 0;
|
||||
while i < Sema_DiagCount(sema) {
|
||||
let diag: Diagnostic = Diagnostic {
|
||||
message: sema.diags[i].message,
|
||||
line: sema.diags[i].line,
|
||||
column: sema.diags[i].column,
|
||||
severity: 0,
|
||||
};
|
||||
Diagnostic_Print(&diag, sourceName);
|
||||
i = i + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/cli.bux
|
||||
git commit -m "feat(diag): format sema errors in Cli_Check with Diagnostic_Print"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Update Sema Error Printing in `Cli_BuildProject`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/cli.bux` lines 991-1005
|
||||
|
||||
**Context:** Current code in `Cli_BuildProject`:
|
||||
```bux
|
||||
if Sema_HasError(sema) {
|
||||
PrintLine("Sema errors found");
|
||||
var i: int = 0;
|
||||
while i < Sema_DiagCount(sema) {
|
||||
Print(" line ");
|
||||
PrintInt(sema.diags[i].line as int64);
|
||||
Print(" col ");
|
||||
PrintInt(sema.diags[i].column as int64);
|
||||
Print(": ");
|
||||
PrintLine(sema.diags[i].message);
|
||||
i = i + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The source path in `Cli_BuildProject` is the merged source file path. Check what variable holds the path to the merged source being compiled. It might be a temp file path. For MVP, use whatever path variable is available (e.g., `mergedPath`, `cFile`, or similar).
|
||||
|
||||
- [ ] **Step 1: Find the source path variable in Cli_BuildProject**
|
||||
|
||||
Look at the context around line 991 to find what variable contains the source file path being compiled. Common candidates: `sourceName`, `cFile`, `mergedPath`, `projectDir`.
|
||||
|
||||
- [ ] **Step 2: Replace sema error loop**
|
||||
|
||||
Replace the loop with:
|
||||
|
||||
```bux
|
||||
if Sema_HasError(sema) {
|
||||
var i: int = 0;
|
||||
while i < Sema_DiagCount(sema) {
|
||||
let diag: Diagnostic = Diagnostic {
|
||||
message: sema.diags[i].message,
|
||||
line: sema.diags[i].line,
|
||||
column: sema.diags[i].column,
|
||||
severity: 0,
|
||||
};
|
||||
Diagnostic_Print(&diag, sourcePath); /* use correct variable here */
|
||||
i = i + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/cli.bux
|
||||
git commit -m "feat(diag): format sema errors in Cli_BuildProject with Diagnostic_Print"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update Sema Error Printing in `Cli_Compile` / `Cli_CompileSource`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/cli.bux` lines 78-88 and/or 295-300
|
||||
|
||||
**Context:** There may be additional sema error printing in `Cli_Compile` or `Cli_CompileSource`. Check lines 78-88 and 295-300 for similar patterns and update them to use `Diagnostic_Print`.
|
||||
|
||||
At line 78-88 (`Cli_Compile` or similar):
|
||||
```bux
|
||||
if Sema_HasError(sema) {
|
||||
PrintLine("Sema errors:");
|
||||
var i: int = 0;
|
||||
while i < Sema_DiagCount(sema) {
|
||||
Print(" ");
|
||||
PrintLine(sema.diags[i].message);
|
||||
i = i + 1;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Replace with Diagnostic_Print**
|
||||
|
||||
```bux
|
||||
if Sema_HasError(sema) {
|
||||
var i: int = 0;
|
||||
while i < Sema_DiagCount(sema) {
|
||||
let diag: Diagnostic = Diagnostic {
|
||||
message: sema.diags[i].message,
|
||||
line: sema.diags[i].line,
|
||||
column: sema.diags[i].column,
|
||||
severity: 0,
|
||||
};
|
||||
Diagnostic_Print(&diag, sourceName);
|
||||
i = i + 1;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/cli.bux
|
||||
git commit -m "feat(diag): format sema errors in Cli_Compile with Diagnostic_Print"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Add Parser Diagnostic Printing
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/parser.bux` lines 1774-1777 (end of `Parser_Parse`)
|
||||
|
||||
**Context:** `Parser_Parse` collects diagnostics in `p.diags` but never prints them. The CLI only checks `mod == null` which rarely/never happens. We add diagnostic printing at the end of `Parser_Parse` before returning.
|
||||
|
||||
- [ ] **Step 1: Add parser diagnostic printing**
|
||||
|
||||
Before `return mod;` at line 1776, add:
|
||||
|
||||
```bux
|
||||
/* Print parser diagnostics */
|
||||
if p.diagCount > 0 {
|
||||
var di: int = 0;
|
||||
while di < p.diagCount {
|
||||
let d: ParserDiag = p.diags[di];
|
||||
Print("error: ");
|
||||
PrintLine(d.message);
|
||||
Print(" --> <input>:");
|
||||
PrintInt(d.line as int64);
|
||||
Print(":");
|
||||
PrintInt(d.column as int64);
|
||||
PrintLine("");
|
||||
Print(" |");
|
||||
PrintLine("");
|
||||
Print(" ");
|
||||
PrintInt(d.line as int64);
|
||||
Print(" | <source unavailable>");
|
||||
PrintLine("");
|
||||
Print(" | ");
|
||||
var sp: uint32 = 0;
|
||||
while sp < d.column - 1 && sp < 120 {
|
||||
Print(" ");
|
||||
sp = sp + 1;
|
||||
}
|
||||
PrintLine("^");
|
||||
di = di + 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Parser diagnostics show `<source unavailable>` for the source line because `Parser_Parse` doesn't receive the source file path. In a future iteration, we can pass the path to `Parser_Parse`.
|
||||
|
||||
- [ ] **Step 2: Verify compilation**
|
||||
|
||||
Run: `make buxc`
|
||||
Expected: Success.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/parser.bux
|
||||
git commit -m "feat(diag): print parser diagnostics in Parser_Parse"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Create Test Program
|
||||
|
||||
**Files:**
|
||||
- Create: `_test_error_snippet/bux.toml`
|
||||
- Create: `_test_error_snippet/src/Main.bux`
|
||||
|
||||
- [ ] **Step 1: Create test manifest**
|
||||
|
||||
`_test_error_snippet/bux.toml`:
|
||||
```toml
|
||||
[package]
|
||||
name = "error_snippet_test"
|
||||
version = "0.1.0"
|
||||
pkgType = "bin"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create test program with intentional type error**
|
||||
|
||||
`_test_error_snippet/src/Main.bux`:
|
||||
```bux
|
||||
func Main() -> int {
|
||||
let x: int = "hello";
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build and verify output**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux/_test_error_snippet && ../../build/buxc build 2>&1
|
||||
```
|
||||
|
||||
Expected output should contain Rust-style formatting:
|
||||
```
|
||||
error: type mismatch
|
||||
--> .../Main.bux:2:9
|
||||
|
|
||||
2 | let x: int = "hello";
|
||||
| ^
|
||||
```
|
||||
|
||||
(The exact message text depends on what sema emits for this error.)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add -f _test_error_snippet/
|
||||
git commit -m "test: add error snippet formatting test"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Selfhost Bootstrap Loop Verification
|
||||
|
||||
**Files:**
|
||||
- No changes — verification only
|
||||
|
||||
- [ ] **Step 1: Run selfhost loop**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux && make selfhost-loop
|
||||
```
|
||||
|
||||
Expected: C output is IDENTICAL on both iterations.
|
||||
|
||||
- [ ] **Step 2: If loop fails, debug**
|
||||
|
||||
Common issues:
|
||||
- C output differs → check if `src/cli.bux` changes affect codegen (they shouldn't — only Print statements changed)
|
||||
- Bootstrap compiler fails → syntax error in new Bux code
|
||||
|
||||
- [ ] **Step 3: Commit fixes if needed**
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Push to Git
|
||||
|
||||
- [ ] **Step 1: Push**
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec Coverage Check
|
||||
|
||||
| Spec Section | Implementing Task |
|
||||
|-------------|-------------------|
|
||||
| Diagnostic struct | Task 1 |
|
||||
| Diagnostic_GetLine | Task 1 |
|
||||
| Diagnostic_Print | Task 1 |
|
||||
| Lexer error formatting | Task 2 |
|
||||
| Sema error in Cli_Check | Task 3 |
|
||||
| Sema error in Cli_BuildProject | Task 4 |
|
||||
| Sema error in Cli_Compile | Task 5 |
|
||||
| Parser diagnostic printing | Task 6 |
|
||||
| Testing | Task 7 |
|
||||
| Selfhost loop | Task 8 |
|
||||
|
||||
## Placeholder Scan
|
||||
|
||||
- No TBD, TODO, or "implement later".
|
||||
- All code is complete and copy-paste ready.
|
||||
- All file paths are exact.
|
||||
- All commands have expected output.
|
||||
@@ -0,0 +1,250 @@
|
||||
# Drop Interface Auto-Drop Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make the selfhost compiler emit automatic destructor calls for types that implement `Drop` via `extend Type for Drop`, not only for types marked with `@[Drop]`.
|
||||
|
||||
**Architecture:** Extend `Lcx_BuildAutoDropFree` in `src/hir_lower.bux` to detect a `TypeName_Drop` function symbol registered by sema for interface-based implementations. Reuse the existing generic monomorphization and C-backend defer paths.
|
||||
|
||||
**Tech Stack:** Bux selfhost compiler (Bux language), Nim bootstrap compiler, C backend, GNU make.
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| File | Responsibility |
|
||||
|------|----------------|
|
||||
| `src/hir_lower.bux` | Detects `TypeName_Drop` methods and wires them into auto-drop defers. |
|
||||
| `_test_interface_drop/src/Main.bux` | Existing integration test that should now emit `Buffer_Drop(&buf)`. |
|
||||
| `_test_drop_user/src/Main.bux` | Existing test using `@[Drop]`; must keep working. |
|
||||
| `_test_drop_move/src/Main.bux` | Existing test verifying moved vars are not double-dropped; must keep working. |
|
||||
| `_test_drop_trait/src/Main.bux` | Existing test for `Array<int>` auto-drop; must keep working. |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Verify the current failure
|
||||
|
||||
**Files:**
|
||||
- Read: `src/hir_lower.bux:2745-2760`
|
||||
- Read: `_test_interface_drop/build/main.c` (after build)
|
||||
|
||||
- [ ] **Step 1: Build the bootstrap compiler**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
make build
|
||||
```
|
||||
|
||||
Expected: `buxc` is created at project root, build succeeds.
|
||||
|
||||
- [ ] **Step 2: Compile `_test_interface_drop` and inspect generated C**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux/_test_interface_drop
|
||||
../buxc run 2>&1 | tail -5
|
||||
grep -n "Buffer_Drop" build/main.c
|
||||
```
|
||||
|
||||
Expected: `Buffer_Drop` function is declared/defined, but `main()` does **not** call it before `return 0`. This confirms the bug.
|
||||
|
||||
- [ ] **Step 3: Commit the baseline observation (optional)**
|
||||
|
||||
No file changes yet; skip commit or note the finding in a scratch file.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Extend `Lcx_BuildAutoDropFree` to detect interface Drop
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/hir_lower.bux:2745-2760`
|
||||
|
||||
- [ ] **Step 1: Read the exact current code**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
sed -n '2740,2765p' src/hir_lower.bux
|
||||
```
|
||||
|
||||
Current code (approximate):
|
||||
|
||||
```bux
|
||||
// User-defined types with @[Drop]: look for TypeName_Drop function
|
||||
if typeSym.kind == skType && typeSym.decl != null as *Decl && typeSym.decl.isDrop != 0 {
|
||||
let dropName: String = String_Concat(typeName, "_Drop");
|
||||
// Ensure inner free is also monomorphized since Drop calls Free
|
||||
if String_StartsWith(dropName, "Array_Drop_") {
|
||||
let elemType: String = Lcx_GetMangledSuffix(dropName, "Array_Drop_");
|
||||
let genDrop: *Decl = Lcx_FindGenericFunc(ctx, "Array_Drop");
|
||||
if genDrop != null as *Decl {
|
||||
Lcx_GenerateFuncInstance(ctx, genDrop, elemType, "", 1);
|
||||
}
|
||||
}
|
||||
return dropName;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Modify the check to include interface-based Drop**
|
||||
|
||||
Replace the block with:
|
||||
|
||||
```bux
|
||||
// User-defined types with @[Drop] OR with an explicit TypeName_Drop method
|
||||
if typeSym.kind == skType && typeSym.decl != null as *Decl {
|
||||
let dropName: String = String_Concat(typeName, "_Drop");
|
||||
let hasAttr: bool = typeSym.decl.isDrop != 0;
|
||||
let dropSym: Symbol = Scope_Lookup(ctx.scope, dropName);
|
||||
let hasMethod: bool = dropSym.kind == skFunc;
|
||||
|
||||
if hasAttr || hasMethod {
|
||||
// Ensure inner free is also monomorphized since Drop calls Free
|
||||
if String_StartsWith(dropName, "Array_Drop_") {
|
||||
let elemType: String = Lcx_GetMangledSuffix(dropName, "Array_Drop_");
|
||||
let genDrop: *Decl = Lcx_FindGenericFunc(ctx, "Array_Drop");
|
||||
if genDrop != null as *Decl {
|
||||
Lcx_GenerateFuncInstance(ctx, genDrop, elemType, "", 1);
|
||||
}
|
||||
}
|
||||
return dropName;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This preserves the `@[Drop]` path and adds the interface path.
|
||||
|
||||
- [ ] **Step 3: Rebuild the bootstrap compiler**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
make build
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 4: Verify `_test_interface_drop` now emits the drop call**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux/_test_interface_drop
|
||||
rm -rf build
|
||||
../buxc run 2>&1 | tail -5
|
||||
grep -n "Buffer_Drop" build/main.c
|
||||
```
|
||||
|
||||
Expected: `main()` now contains `Buffer_Drop(&buf);` before `return 0;`.
|
||||
|
||||
- [ ] **Step 5: Commit the implementation**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
git add src/hir_lower.bux
|
||||
git commit -m "feat(selfhost): auto-drop for interface-based Drop implementations"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Regression-test existing Drop tests
|
||||
|
||||
**Files:**
|
||||
- Test: `_test_drop_user/src/Main.bux`
|
||||
- Test: `_test_drop_move/src/Main.bux`
|
||||
- Test: `_test_drop_trait/src/Main.bux`
|
||||
|
||||
- [ ] **Step 1: Compile `_test_drop_user`**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux/_test_drop_user
|
||||
rm -rf build
|
||||
../buxc run 2>&1 | tail -5
|
||||
```
|
||||
|
||||
Expected: builds and runs without error.
|
||||
|
||||
- [ ] **Step 2: Compile `_test_drop_move`**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux/_test_drop_move
|
||||
rm -rf build
|
||||
../buxc run 2>&1 | tail -5
|
||||
```
|
||||
|
||||
Expected: builds and runs without double-free / crash.
|
||||
|
||||
- [ ] **Step 3: Compile `_test_drop_trait`**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux/_test_drop_trait
|
||||
rm -rf build
|
||||
../buxc run 2>&1 | tail -5
|
||||
```
|
||||
|
||||
Expected: prints `30` and exits cleanly.
|
||||
|
||||
- [ ] **Step 4: Commit if all pass**
|
||||
|
||||
If any fail, debug before committing. If all pass:
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
git add -A
|
||||
git commit -m "test: verify existing Drop tests still pass"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Selfhost-loop determinism check
|
||||
|
||||
**Files:**
|
||||
- All `src/*.bux`
|
||||
|
||||
- [ ] **Step 1: Run the selfhost loop**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
make selfhost-loop
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```
|
||||
Selfhost loop passed: C output is identical.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: If it fails, diff the C outputs**
|
||||
|
||||
```bash
|
||||
diff -u build/selfhost-loop-a/build/main.c build/selfhost-loop-b/build/main.c | head -80
|
||||
```
|
||||
|
||||
Investigate any difference. Likely causes:
|
||||
- Variable naming drift — not expected from this change.
|
||||
- New monomorphization triggered by the change — acceptable only if deterministic.
|
||||
|
||||
- [ ] **Step 3: Commit after passing**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
git add -A
|
||||
git commit -m "ci: selfhost-loop passes with Drop interface auto-drop"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec coverage check
|
||||
|
||||
| Spec requirement | Implementing task |
|
||||
|------------------|-------------------|
|
||||
| Universal Drop recognition | Task 2 |
|
||||
| No syntax changes | No task needed |
|
||||
| Preserve move semantics | Task 2 reuses existing path; Task 3 verifies `_test_drop_move` |
|
||||
| Selfhost loop compatibility | Task 4 |
|
||||
|
||||
## Placeholder scan
|
||||
|
||||
- No TBD/TODO/"implement later" markers.
|
||||
- Each step includes exact file paths and commands.
|
||||
- Code blocks contain the actual change.
|
||||
|
||||
## Type consistency check
|
||||
|
||||
- `Scope_Lookup` returns `Symbol` — used correctly.
|
||||
- `Symbol.kind == skFunc` matches existing patterns in the codebase.
|
||||
- `typeSym.decl.isDrop != 0` preserved from original code.
|
||||
@@ -0,0 +1,497 @@
|
||||
# Fix Generic Operator `[]` in Bootstrap Sema — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make `_test_drop_trait` and `_test_checked_index` pass by teaching bootstrap sema to preserve generic type arguments and substitute them when resolving operator `[]` on generic types like `Array<T>`.
|
||||
|
||||
**Architecture:** Add a `Type`-to-`Type` substitution helper, update `resolveType` to keep type args on `tkNamed`, fix explicit generic-call return-type resolution, and update `ekIndex` to look up `operator_index_get` through pointers and substitute method type parameters from the receiver's concrete type arguments.
|
||||
|
||||
**Tech Stack:** Nim (bootstrap compiler), Bux integration tests.
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
`Array_operator_index_get<T>` is auto-registered as a method for type `Array`. Its `MethodInfo.retType` is the type parameter `T` (`tkTypeParam`). When the bootstrap compiler sees `arr[0]` where `arr: Array<int>`, it currently:
|
||||
|
||||
1. Resolves `Array<int>` to a plain `tkNamed("Array")` (type args are dropped).
|
||||
2. Finds `operator_index_get` in the method table.
|
||||
3. Returns the unsubstituted `T`.
|
||||
4. `T` is treated as a numeric type and arithmetic defaults to `float32`, producing the `_test_drop_trait` error `expected int, got float32`.
|
||||
|
||||
For `*Array<int>`, the pointer branch in `ekIndex` returns the pointee `Array<int>` instead of the element type, producing the `_test_checked_index` error `cannot assign int to Array`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add Type-to-Type Substitution Helper
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/sema.nim` (after the existing helper procs, around line 135)
|
||||
|
||||
- [ ] **Step 1.1: Add `substituteTypeInType`**
|
||||
|
||||
```nim
|
||||
proc substituteTypeInType(sema: var Sema, t: Type, subst: Table[string, Type]): Type =
|
||||
## Recursively substitute type parameters in a resolved Type.
|
||||
if t == nil:
|
||||
return makeUnknown()
|
||||
case t.kind
|
||||
of tkTypeParam:
|
||||
if subst.hasKey(t.name):
|
||||
return subst[t.name]
|
||||
return t
|
||||
of tkPointer, tkRef, tkMutRef:
|
||||
if t.inner.len > 0:
|
||||
return Type(kind: t.kind, inner: @[sema.substituteTypeInType(t.inner[0], subst)])
|
||||
return t
|
||||
of tkSlice, tkRange:
|
||||
if t.inner.len > 0:
|
||||
return Type(kind: t.kind, inner: @[sema.substituteTypeInType(t.inner[0], subst)])
|
||||
return t
|
||||
of tkTuple:
|
||||
var elems: seq[Type] = @[]
|
||||
for e in t.inner:
|
||||
elems.add(sema.substituteTypeInType(e, subst))
|
||||
return makeTuple(elems)
|
||||
of tkFunc:
|
||||
var inner: seq[Type] = @[]
|
||||
for it in t.inner:
|
||||
inner.add(sema.substituteTypeInType(it, subst))
|
||||
return Type(kind: tkFunc, inner: inner)
|
||||
of tkNamed:
|
||||
if t.inner.len > 0:
|
||||
var args: seq[Type] = @[]
|
||||
for a in t.inner:
|
||||
args.add(sema.substituteTypeInType(a, subst))
|
||||
return Type(kind: tkNamed, name: t.name, inner: args)
|
||||
return t
|
||||
else:
|
||||
return t
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Preserve Type Arguments in `resolveType`
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/sema.nim:261-265` (`resolveType` `tekNamed` branch)
|
||||
|
||||
- [ ] **Step 2.1: Replace the `else` branch in `resolveType` for `tekNamed`**
|
||||
|
||||
Find:
|
||||
|
||||
```nim
|
||||
else:
|
||||
if sema.typeTable.hasKey(name):
|
||||
return sema.typeTable[name]
|
||||
return makeNamed(name)
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```nim
|
||||
else:
|
||||
if te.typeArgs.len > 0:
|
||||
var args: seq[Type] = @[]
|
||||
for arg in te.typeArgs:
|
||||
args.add(sema.resolveType(arg))
|
||||
return Type(kind: tkNamed, name: name, inner: args)
|
||||
if sema.typeTable.hasKey(name):
|
||||
return sema.typeTable[name]
|
||||
return makeNamed(name)
|
||||
```
|
||||
|
||||
This makes `Array<int>` resolve to `tkNamed("Array", inner: [tkInt])`.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Fix Explicit Generic Call Return-Type Resolution
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/sema.nim:979-993` (`ekCall` `ekGenericCall` callee branch)
|
||||
|
||||
- [ ] **Step 3.1: Replace the generic-call return-type substitution logic**
|
||||
|
||||
Find:
|
||||
|
||||
```nim
|
||||
if sym.typ != nil and sym.typ.kind == tkFunc:
|
||||
# Get the return type and substitute type parameters
|
||||
let retType = sym.typ.inner[^1]
|
||||
if retType.kind == tkNamed:
|
||||
# Check if this is a type parameter
|
||||
let sym2 = sema.globalScope.lookup(expr.exprCallCallee.exprGenericCallee)
|
||||
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.name and i < expr.exprCallCallee.exprGenericTypeArgs.len:
|
||||
# Substitute with concrete type
|
||||
let concreteType = expr.exprCallCallee.exprGenericTypeArgs[i]
|
||||
if concreteType.kind == tekNamed:
|
||||
return sema.resolveType(concreteType)
|
||||
return retType
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```nim
|
||||
if sym.typ != nil and sym.typ.kind == tkFunc:
|
||||
let retType = sym.typ.inner[^1]
|
||||
let sym2 = sema.globalScope.lookup(expr.exprCallCallee.exprGenericCallee)
|
||||
if sym2 != nil and sym2.decl != nil and sym2.decl.kind == dkFunc and
|
||||
sym2.decl.declFuncTypeParams.len > 0 and
|
||||
sym2.decl.declFuncReturnType != nil:
|
||||
let typeParams = sym2.decl.declFuncTypeParams
|
||||
var added: seq[string] = @[]
|
||||
for i, tp in typeParams:
|
||||
if i < expr.exprCallCallee.exprGenericTypeArgs.len:
|
||||
let concrete = sema.resolveType(expr.exprCallCallee.exprGenericTypeArgs[i])
|
||||
sema.typeTable[tp.name] = concrete
|
||||
added.add(tp.name)
|
||||
let resolvedRet = sema.resolveType(sym2.decl.declFuncReturnType)
|
||||
for tp in added:
|
||||
sema.typeTable.del(tp)
|
||||
return resolvedRet
|
||||
return retType
|
||||
```
|
||||
|
||||
This makes `Array_New<int>()` resolve to `tkNamed("Array", inner: [tkInt])`, so assignments like `let arr: Array<int> = Array_New<int>(8)` remain assignable.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Update `ekIndex` to Substitute Method Type Parameters
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/sema.nim:1155-1178` (`ekIndex` branch)
|
||||
|
||||
- [ ] **Step 4.1: Replace the `ekIndex` implementation**
|
||||
|
||||
Find the entire `of ekIndex:` block (lines 1155-1178):
|
||||
|
||||
```nim
|
||||
of ekIndex:
|
||||
let obj = sema.checkExpr(expr.exprIndexObj, scope)
|
||||
let idx = sema.checkExpr(expr.exprIndexIdx, scope)
|
||||
if not idx.isInteger:
|
||||
sema.emitError(expr.loc, "index must be integer")
|
||||
if obj.isSlice:
|
||||
if sema.checkedFunc:
|
||||
expr.exprIndexBoundsCheck = true
|
||||
return obj.inner[0]
|
||||
elif obj.isPointer:
|
||||
return obj.inner[0]
|
||||
elif obj.kind == tkStr:
|
||||
return makeChar8()
|
||||
elif obj.kind == tkNamed and sema.methodTable.hasKey(obj.name):
|
||||
for minfo in sema.methodTable[obj.name]:
|
||||
if minfo.name == "operator_index_get" and minfo.params.len == 2:
|
||||
let idxType = minfo.params[1]
|
||||
if idx.isAssignableTo(idxType) or idxType.isAssignableTo(idx) or idx.kind == tkUnknown:
|
||||
return minfo.retType
|
||||
sema.emitError(expr.loc, "cannot index non-slice/non-pointer type")
|
||||
return makeUnknown()
|
||||
else:
|
||||
sema.emitError(expr.loc, "cannot index non-slice/non-pointer type")
|
||||
return makeUnknown()
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```nim
|
||||
of ekIndex:
|
||||
let obj = sema.checkExpr(expr.exprIndexObj, scope)
|
||||
let idx = sema.checkExpr(expr.exprIndexIdx, scope)
|
||||
if not idx.isInteger:
|
||||
sema.emitError(expr.loc, "index must be integer")
|
||||
|
||||
# Try method-table operator_index_get on a named receiver (possibly behind a pointer).
|
||||
var receiverNamed: Type = nil
|
||||
if obj.kind == tkNamed:
|
||||
receiverNamed = obj
|
||||
elif obj.isPointer and obj.inner.len > 0 and obj.inner[0].kind == tkNamed:
|
||||
receiverNamed = obj.inner[0]
|
||||
|
||||
if receiverNamed != nil and sema.methodTable.hasKey(receiverNamed.name):
|
||||
for minfo in sema.methodTable[receiverNamed.name]:
|
||||
if minfo.name == "operator_index_get" and minfo.params.len == 2:
|
||||
var subst = initTable[string, Type]()
|
||||
if minfo.decl.declFuncTypeParams.len > 0 and receiverNamed.inner.len > 0:
|
||||
for i, tp in minfo.decl.declFuncTypeParams:
|
||||
if i < receiverNamed.inner.len:
|
||||
subst[tp.name] = receiverNamed.inner[i]
|
||||
let idxType = sema.substituteTypeInType(minfo.params[1], subst)
|
||||
if idx.isAssignableTo(idxType) or idxType.isAssignableTo(idx) or idx.kind == tkUnknown:
|
||||
return sema.substituteTypeInType(minfo.retType, subst)
|
||||
|
||||
if obj.isSlice:
|
||||
if sema.checkedFunc:
|
||||
expr.exprIndexBoundsCheck = true
|
||||
return obj.inner[0]
|
||||
elif obj.isPointer:
|
||||
return obj.inner[0]
|
||||
elif obj.kind == tkStr:
|
||||
return makeChar8()
|
||||
else:
|
||||
sema.emitError(expr.loc, "cannot index non-slice/non-pointer type")
|
||||
return makeUnknown()
|
||||
```
|
||||
|
||||
This makes `arr[0]` on `Array<int>` return `int`, and `arr[0]` on `*Array<int>` also return `int`.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Skip Strict Arg Check for Generic Function Calls
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/sema.nim:1124-1133` (regular function call arg checking)
|
||||
|
||||
- [ ] **Step 5.1: Wrap non-generic arg checking**
|
||||
|
||||
Find:
|
||||
|
||||
```nim
|
||||
if calleeDecl != nil and calleeDecl.kind == dkFunc and calleeDecl.declFuncTypeParams.len > 0:
|
||||
discard # will be handled later
|
||||
if calleeType.kind == tkFunc:
|
||||
let expectedParams = calleeType.inner[0..^2]
|
||||
if argTypes.len != expectedParams.len:
|
||||
sema.emitError(expr.loc, &"expected {expectedParams.len} arguments, got {argTypes.len}")
|
||||
else:
|
||||
for i in 0 ..< argTypes.len:
|
||||
if not argTypes[i].isAssignableTo(expectedParams[i]) and not (argTypes[i].kind in {TypeKind.tkUnknown, TypeKind.tkNamed, TypeKind.tkTypeParam}):
|
||||
sema.emitError(expr.loc, &"argument {i+1}: expected {expectedParams[i].toString}, got {argTypes[i].toString}")
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```nim
|
||||
let isGenericFunc = calleeDecl != nil and calleeDecl.kind == dkFunc and calleeDecl.declFuncTypeParams.len > 0
|
||||
if calleeType.kind == tkFunc:
|
||||
let expectedParams = calleeType.inner[0..^2]
|
||||
if argTypes.len != expectedParams.len:
|
||||
sema.emitError(expr.loc, &"expected {expectedParams.len} arguments, got {argTypes.len}")
|
||||
elif not isGenericFunc:
|
||||
# Generic function arg checks are deferred until after type inference/monomorphization.
|
||||
for i in 0 ..< argTypes.len:
|
||||
if not argTypes[i].isAssignableTo(expectedParams[i]) and not (argTypes[i].kind in {TypeKind.tkUnknown, TypeKind.tkNamed, TypeKind.tkTypeParam}):
|
||||
sema.emitError(expr.loc, &"argument {i+1}: expected {expectedParams[i].toString}, got {argTypes[i].toString}")
|
||||
```
|
||||
|
||||
This prevents spurious type mismatches when passing `&mut Array<int>` to a generic function expecting `*Array<T>`.
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Fix Generic Struct Monomorphization in HIR `substituteType`
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/hir_lower.nim:168-176`
|
||||
|
||||
- [ ] **Step 6.1: Build a local substitution map when generating struct instances**
|
||||
|
||||
Find:
|
||||
|
||||
```nim
|
||||
if not hasUnresolved:
|
||||
var fields: seq[tuple[name: string, typ: Type]] = @[]
|
||||
var concreteArgs: seq[Type] = @[]
|
||||
for f in genericDecl.declStructFields:
|
||||
let resolvedType = substituteType(ctx, f.ftype, subst)
|
||||
fields.add((f.name, resolvedType))
|
||||
for arg in te.typeArgs:
|
||||
concreteArgs.add(substituteType(ctx, arg, subst))
|
||||
ctx.extraStructs.add((mangledName, fields))
|
||||
ctx.generatedStructInsts[mangledName] = true
|
||||
ctx.structInstMap[mangledName] = (te.typeName, concreteArgs)
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```nim
|
||||
if not hasUnresolved:
|
||||
var localSubst = subst
|
||||
for j, tp in genericDecl.declStructTypeParams:
|
||||
if j < te.typeArgs.len:
|
||||
localSubst[tp.name] = substituteType(ctx, te.typeArgs[j], subst)
|
||||
var fields: seq[tuple[name: string, typ: Type]] = @[]
|
||||
var concreteArgs: seq[Type] = @[]
|
||||
for f in genericDecl.declStructFields:
|
||||
let resolvedType = substituteType(ctx, f.ftype, localSubst)
|
||||
fields.add((f.name, resolvedType))
|
||||
for arg in te.typeArgs:
|
||||
concreteArgs.add(substituteType(ctx, arg, subst))
|
||||
ctx.extraStructs.add((mangledName, fields))
|
||||
ctx.generatedStructInsts[mangledName] = true
|
||||
ctx.structInstMap[mangledName] = (te.typeName, concreteArgs)
|
||||
```
|
||||
|
||||
This ensures field types like `*T` are resolved to `*int` when generating `Array_int`, even when the struct instance is first requested from a context with no active substitution map.
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Fix Parameter Visibility in HIR `lowerFunc`
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/hir_lower.nim:1432-1451`
|
||||
|
||||
- [ ] **Step 7.1: Move parameter `varTypeExprs` registration after clearing the table**
|
||||
|
||||
Find:
|
||||
|
||||
```nim
|
||||
var params: seq[tuple[name: string, typ: Type]] = @[]
|
||||
for p in funcParams:
|
||||
var pType = makeUnknown()
|
||||
if p.ptype != nil:
|
||||
pType = substituteType(ctx, p.ptype, ctx.typeSubst)
|
||||
params.add((p.name, pType))
|
||||
if p.ptype != nil:
|
||||
ctx.varTypeExprs[p.name] = p.ptype
|
||||
|
||||
var retType = makeVoid()
|
||||
if funcReturnType != nil:
|
||||
retType = substituteType(ctx, funcReturnType, ctx.typeSubst)
|
||||
|
||||
let oldFuncDecl = ctx.currentFuncDecl
|
||||
let oldFuncRetType = ctx.currentFuncRetType
|
||||
let oldVarTypeExprs = ctx.varTypeExprs
|
||||
ctx.currentFuncRetType = retType
|
||||
ctx.currentFuncDecl = decl
|
||||
ctx.varTypeExprs = initTable[string, TypeExpr]() # Clear local vars for new function
|
||||
var body = if funcBody != nil: ctx.lowerBlock(funcBody) else: nil
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```nim
|
||||
var params: seq[tuple[name: string, typ: Type]] = @[]
|
||||
for p in funcParams:
|
||||
var pType = makeUnknown()
|
||||
if p.ptype != nil:
|
||||
pType = substituteType(ctx, p.ptype, ctx.typeSubst)
|
||||
params.add((p.name, pType))
|
||||
|
||||
var retType = makeVoid()
|
||||
if funcReturnType != nil:
|
||||
retType = substituteType(ctx, funcReturnType, ctx.typeSubst)
|
||||
|
||||
let oldFuncDecl = ctx.currentFuncDecl
|
||||
let oldFuncRetType = ctx.currentFuncRetType
|
||||
let oldVarTypeExprs = ctx.varTypeExprs
|
||||
ctx.currentFuncRetType = retType
|
||||
ctx.currentFuncDecl = decl
|
||||
ctx.varTypeExprs = initTable[string, TypeExpr]() # Clear local vars for new function
|
||||
# Add parameters to varTypeExprs after clearing so they are visible in the body.
|
||||
for p in funcParams:
|
||||
if p.ptype != nil:
|
||||
ctx.varTypeExprs[p.name] = p.ptype
|
||||
var body = if funcBody != nil: ctx.lowerBlock(funcBody) else: nil
|
||||
```
|
||||
|
||||
This makes parameter types visible when lowering `operator_index_get` calls on pointer parameters like `arr: *Array<int>`.
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Build and Verify
|
||||
|
||||
**Files:**
|
||||
- Test: `_test_drop_trait`, `_test_checked_index`, full suite
|
||||
|
||||
- [ ] **Step 8.1: Rebuild the bootstrap compiler**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
make build
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 8.2: Run the two target integration tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux/_test_drop_trait
|
||||
/home/ziko/z-git/bux/bux/buxc run
|
||||
|
||||
cd /home/ziko/z-git/bux/bux/_test_checked_index
|
||||
/home/ziko/z-git/bux/bux/buxc run
|
||||
```
|
||||
|
||||
Expected: both exit 0 and print the expected output.
|
||||
|
||||
- [ ] **Step 8.3: Run bootstrap unit tests and integration tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
make test
|
||||
```
|
||||
|
||||
Expected: no new failures.
|
||||
|
||||
- [ ] **Step 8.4: Run selfhost loop**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
make selfhost-loop
|
||||
```
|
||||
|
||||
Expected: C output and stripped ELF binary remain identical.
|
||||
|
||||
- [ ] **Step 8.5: Optional cross-check of all `_test_*` packages**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
for d in _test_*; do
|
||||
echo "=== $d ==="
|
||||
(cd "$d" && /home/ziko/z-git/bux/bux/buxc run) || true
|
||||
done
|
||||
```
|
||||
|
||||
Expected: no new failures compared to the baseline.
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Commit
|
||||
|
||||
- [ ] **Step 9.1: Commit the changes**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
git add bootstrap/sema.nim bootstrap/hir_lower.nim
|
||||
git commit -m "fix(bootstrap): substitute generic type args in operator [] resolution
|
||||
|
||||
- Preserve type args on tkNamed in resolveType
|
||||
- Resolve explicit generic call return types with concrete substitutions
|
||||
- Substitute method type params when looking up operator_index_get
|
||||
- Skip strict arg checks for generic function calls (deferred to inference)
|
||||
- Fix generic struct monomorphization when no caller substitution map exists
|
||||
- Fix parameter varTypeExprs ordering so pointer params are visible in bodies
|
||||
- Fixes _test_drop_trait and _test_checked_index"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec Coverage Check
|
||||
|
||||
| Spec Requirement | Plan Task |
|
||||
|---|---|
|
||||
| Reproduce failures | Task 8.2 |
|
||||
| Trace operator `[]` resolution | Tasks 2-4 |
|
||||
| Minimal bootstrap-only fix | Tasks 2-7 (no selfhost changes) |
|
||||
| `_test_drop_trait` passes | Task 8.2 |
|
||||
| `_test_checked_index` passes | Task 8.2 |
|
||||
| No regressions in `make test` / `make selfhost-loop` | Tasks 8.3-8.5 |
|
||||
|
||||
## Placeholder Scan
|
||||
|
||||
- No TBD/TODO/fill-in-later steps.
|
||||
- Every code block contains the exact code to insert.
|
||||
- Every command contains the exact path and expected outcome.
|
||||
@@ -0,0 +1,378 @@
|
||||
# Fix `for ... in` Iterator Lowering — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the placeholder collection `for ... in` lowering in the bootstrap compiler with correct Array/Iter and Channel lowerings, fixing `_test_forin_stdlib`, `_test_forin_channel`, `_test_generic_trait`, `_test_import`, and `_test_mono`.
|
||||
|
||||
**Architecture:** Determine the concrete element type in sema and register the loop variable with it; in HIR lowering emit explicit alloca/store/while nodes that call monomorphized iterator helpers (`Array_Iter_T`, `Iter_HasNext_T`, `Iter_Next_T`) for arrays and `Channel_Recv_Ok_T` for channels. Also fix generic struct field access so direct field mutations on `Array<int>` work.
|
||||
|
||||
**Tech Stack:** Nim (bootstrap compiler), Bux integration tests.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Export and Fix `typeToTypeExpr`
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/sema.nim:140-157`
|
||||
|
||||
- [ ] **Step 1.1: Export the helper and preserve type args for named types**
|
||||
|
||||
Find:
|
||||
|
||||
```nim
|
||||
proc typeToTypeExpr(t: Type): TypeExpr =
|
||||
```
|
||||
|
||||
Change the signature to `proc typeToTypeExpr*(t: Type): TypeExpr =` and update the `tkNamed` branch:
|
||||
|
||||
```nim
|
||||
of tkNamed:
|
||||
var args: seq[TypeExpr] = @[]
|
||||
for a in t.inner:
|
||||
args.add(typeToTypeExpr(a))
|
||||
return TypeExpr(kind: tekNamed, typeName: t.name, typeArgs: args)
|
||||
```
|
||||
|
||||
This lets HIR lowering round-trip a resolved concrete `Type` back to a `TypeExpr` that can be mangled into the correct struct instance name (e.g. `Array<int>` → `Array_int`).
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Derive Loop-Variable Type in Sema
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/sema.nim:1503-1515`
|
||||
|
||||
- [ ] **Step 2.1: Set the loop variable type from the collection's element type**
|
||||
|
||||
Find the `of skFor:` branch and update it so `iterTyp` is the collection element type:
|
||||
|
||||
```nim
|
||||
of skFor:
|
||||
let iterExpr = stmt.stmtForIter
|
||||
let collType = sema.checkExpr(iterExpr, scope)
|
||||
var forScope = newScope(scope)
|
||||
var iterTyp = makeUnknown()
|
||||
if iterExpr.kind == ekRange:
|
||||
iterTyp = sema.checkExpr(iterExpr.exprRangeLo, scope)
|
||||
elif collType.kind == tkNamed and collType.inner.len > 0:
|
||||
iterTyp = collType.inner[0]
|
||||
elif collType.isPointer and collType.inner.len > 0 and collType.inner[0].kind == tkNamed and collType.inner[0].inner.len > 0:
|
||||
iterTyp = collType.inner[0].inner[0]
|
||||
let iterSym = Symbol(kind: skVar, name: stmt.stmtForVar, typ: iterTyp, isMutable: true)
|
||||
discard forScope.define(iterSym)
|
||||
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtForBody.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtForBody.loc, exprBlock: stmt.stmtForBody)), forScope)
|
||||
return makeVoid()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Substitute Generic Struct Type Parameters on Field Access
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/sema.nim:1269-1273` and `bootstrap/sema.nim:180-215`
|
||||
|
||||
- [ ] **Step 3.1: Build substitution map in `ekField` for struct fields**
|
||||
|
||||
In the `dkStruct` branch of `ekField`, build a substitution map from the object's concrete type arguments before resolving the field type:
|
||||
|
||||
```nim
|
||||
if sym.decl.kind == dkStruct:
|
||||
var subst = initTable[string, Type]()
|
||||
for i, tp in sym.decl.declStructTypeParams:
|
||||
if i < objType.inner.len:
|
||||
subst[tp.name] = objType.inner[i]
|
||||
for f in sym.decl.declStructFields:
|
||||
if f.name == expr.exprFieldName:
|
||||
return sema.substituteTypeInType(sema.resolveType(f.ftype), subst)
|
||||
sema.emitError(expr.loc, &"struct '{objType.name}' has no field '{expr.exprFieldName}'")
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Make `substituteTypeInType` handle named type-parameter names**
|
||||
|
||||
In `substituteTypeInType`, add a lookup for `tkNamed` names that are type-parameter names:
|
||||
|
||||
```nim
|
||||
of tkNamed:
|
||||
if subst.hasKey(t.name):
|
||||
return subst[t.name]
|
||||
if t.inner.len > 0:
|
||||
var args: seq[Type] = @[]
|
||||
for a in t.inner:
|
||||
args.add(sema.substituteTypeInType(a, subst))
|
||||
return Type(kind: tkNamed, name: t.name, inner: args)
|
||||
return t
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Add `getCollectionElementTypeExpr` Helper in `hir_lower.nim`
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/hir_lower.nim` (after `resolveExprType` definition)
|
||||
|
||||
- [ ] **Step 4.1: Add the helper**
|
||||
|
||||
```nim
|
||||
proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
|
||||
## Return the element TypeExpr of a collection expression (Array<T>, Iter<T>, Channel<T>).
|
||||
## For identifiers we can use the declared TypeExpr directly; for other expressions we
|
||||
## fall back to the resolved concrete Type.
|
||||
case expr.kind
|
||||
of ekIdent:
|
||||
if ctx.varTypeExprs.hasKey(expr.exprIdent):
|
||||
let te = ctx.varTypeExprs[expr.exprIdent]
|
||||
if te.kind == tekNamed and te.typeArgs.len > 0:
|
||||
return te.typeArgs[0]
|
||||
if te.kind in {tekPointer, tekRef, tekMutRef} and te.pointerPointee.kind == tekNamed and te.pointerPointee.typeArgs.len > 0:
|
||||
return te.pointerPointee.typeArgs[0]
|
||||
else:
|
||||
discard
|
||||
let t = ctx.resolveExprType(expr)
|
||||
if t.kind == tkNamed and t.inner.len > 0:
|
||||
return typeToTypeExpr(t.inner[0])
|
||||
if t.isPointer and t.inner.len > 0 and t.inner[0].kind == tkNamed and t.inner[0].inner.len > 0:
|
||||
return typeToTypeExpr(t.inner[0].inner[0])
|
||||
return TypeExpr(kind: tekNamed, typeName: "unknown")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Implement Collection `for ... in` Lowering
|
||||
|
||||
**Files:**
|
||||
- Modify: `bootstrap/hir_lower.nim` (replace the placeholder at lines 1339-1342)
|
||||
|
||||
- [ ] **Step 5.1: Replace the placeholder collection lowering**
|
||||
|
||||
Find:
|
||||
|
||||
```nim
|
||||
# Generic iterator for loop (simplified - just infinite loop for now)
|
||||
let loweredIter = ctx.lowerExpr(iterExpr)
|
||||
let loweredBody = ctx.lowerBlock(body)
|
||||
return ctx.flushPending(HirNode(kind: hLoop, loopBody: loweredBody, typ: makeVoid(), loc: loc))
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```nim
|
||||
# Collection-based for: for x in collection { body }
|
||||
let collType = ctx.resolveExprType(iterExpr)
|
||||
let elemTypeExpr = ctx.getCollectionElementTypeExpr(iterExpr)
|
||||
let elemType = ctx.resolveTypeExpr(elemTypeExpr)
|
||||
# Resolve the collection type to its mangled struct instance (e.g. Array<int> -> Array_int).
|
||||
let collTypeMangled = substituteType(ctx, typeToTypeExpr(collType), ctx.typeSubst)
|
||||
|
||||
let isChannel = collType.kind == tkNamed and collType.name.startsWith("Channel")
|
||||
|
||||
if isChannel:
|
||||
# Channel lowering:
|
||||
# alloca x
|
||||
# while (true) {
|
||||
# if (!Channel_Recv_Ok_T(&ch, &x)) break;
|
||||
# body
|
||||
# }
|
||||
let recvOkName = ctx.generateMethodInstance("Channel_Recv_Ok", @[elemTypeExpr])
|
||||
|
||||
let xAlloca = hirAlloca(varName, elemType, loc)
|
||||
let xVar = hirVar(varName, elemType, loc)
|
||||
|
||||
ctx.varTypeExprs[varName] = elemTypeExpr
|
||||
|
||||
let chAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: ctx.lowerExpr(iterExpr),
|
||||
typ: makePointer(collTypeMangled), loc: loc)
|
||||
let xAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: xVar,
|
||||
typ: makePointer(elemType), loc: loc)
|
||||
let recvOkCall = hirCall(recvOkName, @[chAddr, xAddr], makeBool(), loc)
|
||||
let notRecvOk = HirNode(kind: hUnary, unaryOp: tkBang, unaryOperand: recvOkCall,
|
||||
typ: makeBool(), loc: loc)
|
||||
let breakNode = HirNode(kind: hBreak, loc: loc)
|
||||
let ifNode = HirNode(kind: hIf, ifCond: notRecvOk, ifThen: breakNode, ifElse: nil,
|
||||
typ: makeVoid(), loc: loc)
|
||||
|
||||
let loweredBody = ctx.lowerBlock(body)
|
||||
var whileBodyStmts: seq[HirNode] = @[]
|
||||
whileBodyStmts.add(xAlloca)
|
||||
whileBodyStmts.add(ifNode)
|
||||
if loweredBody != nil:
|
||||
whileBodyStmts.add(loweredBody)
|
||||
let whileBody = hirBlock(whileBodyStmts, nil, makeVoid(), loc)
|
||||
|
||||
let trueLit = hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc)
|
||||
let whileNode = HirNode(kind: hWhile, whileCond: trueLit, whileBody: whileBody,
|
||||
typ: makeVoid(), loc: loc)
|
||||
|
||||
let forBlock = hirBlock(@[whileNode], nil, makeVoid(), loc, isScope = true)
|
||||
return ctx.flushPending(forBlock)
|
||||
|
||||
# Array / Iter lowering:
|
||||
# alloca __iter
|
||||
# __iter = Array_Iter_T(&collection);
|
||||
# while (Iter_HasNext_T(&__iter)) {
|
||||
# alloca x
|
||||
# x = Iter_Next_T(&__iter);
|
||||
# body
|
||||
# }
|
||||
let iterFuncName = ctx.generateMethodInstance("Array_Iter", @[elemTypeExpr])
|
||||
let hasNextFuncName = ctx.generateMethodInstance("Iter_HasNext", @[elemTypeExpr])
|
||||
let nextFuncName = ctx.generateMethodInstance("Iter_Next", @[elemTypeExpr])
|
||||
|
||||
# Ensure Iter<T> struct instance exists and resolve its mangled name.
|
||||
let iterType = substituteType(ctx, TypeExpr(kind: tekNamed, typeName: "Iter", typeArgs: @[elemTypeExpr]), ctx.typeSubst)
|
||||
|
||||
let iterVarName = "__iter_" & varName & "_" & $ctx.varCounter
|
||||
inc ctx.varCounter
|
||||
|
||||
# Build collection pointer. If the collection is not a simple identifier, spill to a temp.
|
||||
var preStmts: seq[HirNode] = @[]
|
||||
var collPtr: HirNode = nil
|
||||
if iterExpr.kind == ekIdent:
|
||||
let collVar = hirVar(iterExpr.exprIdent, collTypeMangled, loc)
|
||||
collPtr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: collVar,
|
||||
typ: makePointer(collTypeMangled), loc: loc)
|
||||
else:
|
||||
let collAllocaName = ctx.freshName()
|
||||
let collAlloca = hirAlloca(collAllocaName, collTypeMangled, loc)
|
||||
let collVarPtr = hirVar(collAllocaName, makePointer(collTypeMangled), loc)
|
||||
let collValue = ctx.lowerExpr(iterExpr)
|
||||
let collStore = hirStore(collVarPtr, collValue, loc)
|
||||
preStmts.add(collAlloca)
|
||||
preStmts.add(collStore)
|
||||
collPtr = HirNode(kind: hUnary, unaryOp: tkAmp,
|
||||
unaryOperand: hirVar(collAllocaName, collTypeMangled, loc),
|
||||
typ: makePointer(collTypeMangled), loc: loc)
|
||||
|
||||
let iterAlloca = hirAlloca(iterVarName, iterType, loc)
|
||||
let iterVarPtr = hirVar(iterVarName, makePointer(iterType), loc)
|
||||
let iterInitCall = hirCall(iterFuncName, @[collPtr], iterType, loc)
|
||||
let iterStore = hirStore(iterVarPtr, iterInitCall, loc)
|
||||
|
||||
preStmts.add(iterAlloca)
|
||||
preStmts.add(iterStore)
|
||||
|
||||
# while condition: Iter_HasNext_T(&__iter)
|
||||
let iterAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: hirVar(iterVarName, iterType, loc),
|
||||
typ: makePointer(iterType), loc: loc)
|
||||
let condCall = hirCall(hasNextFuncName, @[iterAddr], makeBool(), loc)
|
||||
|
||||
# loop body: alloca x; x = Iter_Next_T(&__iter); body
|
||||
let xAlloca = hirAlloca(varName, elemType, loc)
|
||||
let xVarPtr = hirVar(varName, makePointer(elemType), loc)
|
||||
let iterAddr2 = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: hirVar(iterVarName, iterType, loc),
|
||||
typ: makePointer(iterType), loc: loc)
|
||||
let nextCall = hirCall(nextFuncName, @[iterAddr2], elemType, loc)
|
||||
let xStore = hirStore(xVarPtr, nextCall, loc)
|
||||
|
||||
ctx.varTypeExprs[varName] = elemTypeExpr
|
||||
let loweredBody = ctx.lowerBlock(body)
|
||||
|
||||
var bodyStmts: seq[HirNode] = @[]
|
||||
bodyStmts.add(xAlloca)
|
||||
bodyStmts.add(xStore)
|
||||
if loweredBody != nil:
|
||||
bodyStmts.add(loweredBody)
|
||||
let whileBody = hirBlock(bodyStmts, nil, makeVoid(), loc)
|
||||
|
||||
let whileNode = HirNode(kind: hWhile, whileCond: condCall, whileBody: whileBody,
|
||||
typ: makeVoid(), loc: loc)
|
||||
|
||||
var blockStmts = preStmts
|
||||
blockStmts.add(whileNode)
|
||||
let forBlock = hirBlock(blockStmts, nil, makeVoid(), loc, isScope = true)
|
||||
return ctx.flushPending(forBlock)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Build and Verify
|
||||
|
||||
**Files:**
|
||||
- Test: `_test_forin_stdlib`, `_test_forin_channel`, `_test_generic_trait`, `_test_import`, `_test_mono`
|
||||
|
||||
- [ ] **Step 6.1: Rebuild the bootstrap compiler**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
make build
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 6.2: Run the five target integration tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux/_test_forin_stdlib && /home/ziko/z-git/bux/bux/buxc run
|
||||
cd /home/ziko/z-git/bux/bux/_test_forin_channel && /home/ziko/z-git/bux/bux/buxc run
|
||||
cd /home/ziko/z-git/bux/bux/_test_generic_trait && /home/ziko/z-git/bux/bux/buxc run
|
||||
cd /home/ziko/z-git/bux/bux/_test_import && /home/ziko/z-git/bux/bux/buxc run
|
||||
cd /home/ziko/z-git/bux/bux/_test_mono && /home/ziko/z-git/bux/bux/buxc run
|
||||
```
|
||||
|
||||
Expected: all compile and run; programs that return the sum (`_test_generic_trait`, `_test_import`, `_test_mono`) exit with code 60, which is their expected return value.
|
||||
|
||||
- [ ] **Step 6.3: Run `make test`**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
make test
|
||||
```
|
||||
|
||||
Expected: no new failures.
|
||||
|
||||
- [ ] **Step 6.4: Run `make selfhost-loop`**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
make selfhost-loop
|
||||
```
|
||||
|
||||
Expected: C output and stripped ELF binary remain identical.
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Commit
|
||||
|
||||
- [ ] **Step 7.1: Commit the changes**
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
git add bootstrap/sema.nim bootstrap/hir_lower.nim
|
||||
git commit -m "fix(bootstrap): implement collection for-in lowering
|
||||
|
||||
- Export typeToTypeExpr from sema and preserve generic type args
|
||||
- Derive loop variable type from collection element type in sema
|
||||
- Substitute generic struct type params on field access
|
||||
- Add getCollectionElementTypeExpr helper in hir_lower
|
||||
- Replace placeholder collection for-in lowering
|
||||
- Add Array/Iter lowering: Array_Iter_T / Iter_HasNext_T / Iter_Next_T
|
||||
- Add Channel lowering: Channel_Recv_Ok_T loop
|
||||
- Register loop variable in varTypeExprs before body lowering
|
||||
|
||||
Fixes _test_forin_stdlib, _test_forin_channel, _test_generic_trait, _test_import, _test_mono"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec Coverage Check
|
||||
|
||||
| Spec Requirement | Plan Task |
|
||||
|---|---|
|
||||
| Export and fix `typeToTypeExpr` | Task 1 |
|
||||
| Derive loop variable type in sema | Task 2 |
|
||||
| Substitute generic struct type params on field access | Task 3 |
|
||||
| Add `getCollectionElementTypeExpr` helper | Task 4 |
|
||||
| Array/Iter collection lowering | Task 5 |
|
||||
| Channel collection lowering | Task 5 |
|
||||
| Loop variable declaration and scope registration | Task 5 |
|
||||
| Target tests pass | Task 6 |
|
||||
| No regressions | Task 6 |
|
||||
|
||||
## Placeholder Scan
|
||||
|
||||
- No TBD/TODO/fill-in-later steps.
|
||||
- Every code block contains the exact code to insert.
|
||||
- Every command contains the exact path and expected outcome.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
# Bux Cross-Compilation Support — Design Document
|
||||
|
||||
> **Date:** 2026-06-10
|
||||
> **Status:** Approved
|
||||
> **Scope:** MVP cross-compilation via `--target` CLI flag
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Bux compiles to C, which means cross-compilation is nearly free — we only need to pass the correct target triple to the C compiler (`cc`). This feature adds `--target <triple>` support to the Bux CLI.
|
||||
|
||||
### Goals
|
||||
- `bux build --target aarch64-linux-gnu` produces an ARM64 binary
|
||||
- `bux build --target x86_64-windows-gnu` produces a Windows binary
|
||||
- No changes to the compiler pipeline — only the final `cc` invocation changes
|
||||
|
||||
### Non-Goals
|
||||
- Automatic cross-compiler toolchain detection
|
||||
- Custom linker scripts or startup code
|
||||
- Multiple target builds in one invocation
|
||||
|
||||
---
|
||||
|
||||
## 2. How Cross-Compilation Works
|
||||
|
||||
Bux compilation pipeline:
|
||||
```
|
||||
.bux source → lexer → parser → sema → HIR → C code → cc → binary
|
||||
```
|
||||
|
||||
The entire pipeline is target-independent until the final `cc` step. For cross-compilation:
|
||||
1. Bux generates the same C code
|
||||
2. Instead of `cc -O2 -pthread -o output ...`
|
||||
3. We run `cc -O2 -pthread -target aarch64-linux-gnu -o output ...`
|
||||
|
||||
Or, if a cross-compiler prefix is needed:
|
||||
```
|
||||
aarch64-linux-gnu-gcc -O2 -pthread -o output ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. CLI Interface
|
||||
|
||||
```bash
|
||||
# Native build (default)
|
||||
bux build
|
||||
|
||||
# Cross-compile to ARM64 Linux
|
||||
bux build --target aarch64-linux-gnu
|
||||
|
||||
# Cross-compile to Windows
|
||||
bux build --target x86_64-pc-windows-gnu
|
||||
|
||||
# Also works with buxc project
|
||||
buxc project . --target aarch64-linux-gnu
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementation
|
||||
|
||||
### Changes to `src/cli.bux`
|
||||
|
||||
1. **Parse `--target` flag:** In `Cli_Run`, scan `args` for `--target` before processing the command. Extract the target triple.
|
||||
|
||||
2. **Store target:** Add a global variable `g_targetTriple: String` (default `""`).
|
||||
|
||||
3. **Pass to `cc`:** In both `Cli_Compile` (line ~251) and `Cli_BuildProject` (line ~1143), append `-target <triple>` to the `cc` command if `g_targetTriple` is set.
|
||||
|
||||
### Target Triple Format
|
||||
|
||||
We pass the triple directly to `cc` without validation. Examples:
|
||||
- `aarch64-linux-gnu` → `cc -target aarch64-linux-gnu ...`
|
||||
- `x86_64-pc-windows-gnu` → `cc -target x86_64-pc-windows-gnu ...`
|
||||
- `wasm32-wasi` → `cc -target wasm32-wasi ...`
|
||||
|
||||
### Error Handling
|
||||
|
||||
If `cc` fails with an invalid target, the user sees the standard `cc` error message. Bux does not need custom error handling for this.
|
||||
|
||||
---
|
||||
|
||||
## 5. Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/cli.bux` | Parse `--target` flag, store in global, append to `cc` command |
|
||||
|
||||
---
|
||||
|
||||
## 6. Testing
|
||||
|
||||
1. **Native build:** `bux build` — should work as before (no `-target` flag)
|
||||
2. **Invalid target:** `bux build --target invalid-target` — `cc` should fail with appropriate error
|
||||
3. **Valid target (if cross-compiler available):** `bux build --target aarch64-linux-gnu` — should produce ARM64 binary
|
||||
@@ -0,0 +1,123 @@
|
||||
# Drop Trait / Destructors Design Document
|
||||
|
||||
> **Date:** 2026-06-10
|
||||
> **Status:** Superseded — see [2026-06-14-drop-interface-auto-drop-design.md](2026-06-14-drop-interface-auto-drop-design.md)
|
||||
> **Scope:** Selfhost compiler (`src/*.bux`)
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
The `@[Drop]` attribute and auto-drop mechanism already exist in the selfhost compiler, but they are **restricted to `@[Checked]` functions only**. This means regular (unchecked) code does not receive automatic cleanup for resource-holding types. The programmer must manually call `TypeName_Drop()` or use `defer`.
|
||||
|
||||
This is inconsistent with the goal of making Bux "safe by choice" — if a type declares it needs cleanup (`@[Drop]`), that cleanup should happen regardless of whether the function is checked.
|
||||
|
||||
## 2. Goals
|
||||
|
||||
1. **Universal auto-drop**: `@[Drop]` types are automatically cleaned up when they go out of scope in **all** functions, not just `@[Checked]` ones.
|
||||
2. **Preserve move semantics**: Variables that have been moved are NOT double-freed (existing `CBE_IsMoved` logic stays).
|
||||
3. **No breaking changes**: Existing code continues to work; we only *add* cleanup where it was previously skipped.
|
||||
4. **Selfhost loop compatibility**: The compiler must still bootstrap deterministically after these changes.
|
||||
|
||||
## 3. Non-Goals
|
||||
|
||||
1. **`own T` type**: Adding `own T` to the selfhost type system is a separate, larger feature. This design works with the existing value-semantics + `@[Drop]` model.
|
||||
2. **Scoped defers on break/continue**: The selfhost C backend currently does not emit defers on `break`/`continue` (unlike bootstrap). Fixing this is tracked separately.
|
||||
3. **Drop trait interface dispatch**: Auto-drop uses static lookup of `TypeName_Drop`, not interface vtables. This is intentional — zero runtime cost.
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
### 4.1 Before (Current)
|
||||
|
||||
```bux
|
||||
// hir_lower.bux:1118-1139
|
||||
if ctx.checkedFunc && !String_Eq(alloca.typeName, "") {
|
||||
// build auto-drop defer
|
||||
}
|
||||
```
|
||||
|
||||
Only checked functions get auto-drop.
|
||||
|
||||
### 4.2 After (Proposed)
|
||||
|
||||
```bux
|
||||
// hir_lower.bux:1118-1139
|
||||
if !String_Eq(alloca.typeName, "") {
|
||||
// build auto-drop defer
|
||||
}
|
||||
```
|
||||
|
||||
All functions get auto-drop for `@[Drop]` types.
|
||||
|
||||
### 4.3 How It Works
|
||||
|
||||
1. **Parser**: `@[Drop]` attribute on struct → `decl.isDrop = 1`
|
||||
2. **HIR Lowering** (`hir_lower.bux`):
|
||||
- When lowering a `let` binding, after the `hStore`/ `hAlloca`:
|
||||
- Call `Lcx_BuildAutoDropFree(ctx, typeName)` to find the drop function name
|
||||
- If found, wrap it in `hDefer` and append to statement chain
|
||||
- **Remove** the `ctx.checkedFunc` gate
|
||||
3. **C Backend** (`c_backend.bux`):
|
||||
- `CBE_EmitDefers` already handles:
|
||||
- LIFO order
|
||||
- Skip moved variables (`CBE_IsMoved`)
|
||||
- No changes needed in C backend
|
||||
|
||||
## 5. Affected Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/hir_lower.bux` | Remove `ctx.checkedFunc &&` condition on line 1120 |
|
||||
| `_test_drop_user/src/Main.bux` | Remove `@[Checked]` from test to verify universal drop |
|
||||
| `_test_drop_trait/src/Main.bux` | Fix syntax (already broken — `module Main;` + indexing) |
|
||||
|
||||
## 6. Testing Plan
|
||||
|
||||
### 6.1 Existing Tests
|
||||
|
||||
| Test | Expected |
|
||||
|------|----------|
|
||||
| `_test_drop_user` | Should pass WITHOUT `@[Checked]` |
|
||||
| `_test_drop_move` | Should still skip drop for moved vars |
|
||||
| `selfhost-loop` | C output must remain identical |
|
||||
|
||||
### 6.2 New Test (Optional)
|
||||
|
||||
Create `_test_drop_universal/src/Main.bux`:
|
||||
```bux
|
||||
@[Drop]
|
||||
struct Resource {
|
||||
id: int
|
||||
}
|
||||
|
||||
var dropCount: int = 0;
|
||||
|
||||
func Resource_Drop(self: *Resource) {
|
||||
dropCount = dropCount + 1;
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
// NOT @[Checked]
|
||||
let r: Resource = Resource { id: 1 };
|
||||
// r should be auto-dropped here
|
||||
return dropCount; // expect 1
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Existing unchecked code now gets implicit drops | This is the intended behavior. Types with `@[Drop]` explicitly opt-in. |
|
||||
| Performance impact from extra defer calls | Minimal — defers are inlined as direct C calls. No runtime overhead. |
|
||||
| Selfhost loop breaks | Test `make selfhost-loop` before committing. |
|
||||
|
||||
## 8. Future Work
|
||||
|
||||
1. **`own T` in selfhost**: Add `own T` type expression + move-on-pass semantics (like bootstrap).
|
||||
2. **Scoped defers on break/continue**: Match bootstrap behavior.
|
||||
3. **Drop trait interface check**: Verify at compile time that `@[Drop]` types actually implement `Drop` interface (has `TypeName_Drop` method).
|
||||
|
||||
## 9. Placeholder Scan
|
||||
|
||||
- No TBD/TODO placeholders.
|
||||
- All file paths are exact.
|
||||
- All code is copy-paste ready.
|
||||
@@ -0,0 +1,390 @@
|
||||
# Bux Green Threads / Task Scheduler — Design Document
|
||||
|
||||
> **Date:** 2026-06-10
|
||||
> **Status:** Design Approved
|
||||
> **Scope:** MVP preemptive M:N green thread scheduler with work-stealing
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Bux will gain Go-style green threads (M:N scheduling) without garbage collection. A fixed pool of OS worker threads runs a larger number of lightweight "green" tasks, preemptively scheduled via `SIGVTALRM` and context switching via `ucontext`.
|
||||
|
||||
### Goals
|
||||
- Enable concurrent programming in Bux with Go-like ergonomics
|
||||
- Zero GC pauses (Bux is manually managed)
|
||||
- Work-stealing for balanced CPU utilization across cores
|
||||
- Minimal language changes (pure stdlib + C runtime addition)
|
||||
|
||||
### Non-Goals (for MVP)
|
||||
- Cross-platform support beyond Linux/macOS (ucontext is POSIX)
|
||||
- Dynamic stack growth (fixed-size stacks)
|
||||
- I/O polling integration (epoll/kqueue) — channels + sleep only
|
||||
- Task cancellation / timeouts
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Bux Source Code (.bux) │
|
||||
│ func Main() { Task::Spawn(Worker); } │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Bux Stdlib — `lib/Task.bux` │
|
||||
│ extern func bux_task_spawn(...); │
|
||||
│ func Task::Spawn(f) { ... } │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Generated C Code (from buxc) │
|
||||
│ bux_task_spawn(worker_func, arg); │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ C Runtime — `rt/green_threads.c` │
|
||||
│ • Scheduler (M:N, work-stealing) │
|
||||
│ • ucontext context switch │
|
||||
│ • SIGVTALRM preemption │
|
||||
│ • Per-OS-thread run queues │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ OS Threads (pthread) │
|
||||
│ Worker 0 │ Worker 1 │ Worker 2 │ ... │
|
||||
│ ┌─────┐ │ ┌─────┐ │ ┌─────┐ │
|
||||
│ │TaskA│ │ │TaskB│ │ │TaskC│ │
|
||||
│ │TaskD│ │ │ │ │ │TaskE│ │
|
||||
│ └─────┘ │ └─────┘ │ └─────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
1. **Bux API Layer** (`lib/Task.bux`) — thin wrappers around C extern functions
|
||||
2. **C Scheduler Runtime** (`rt/green_threads.c`) — the scheduler, context switcher, and signal handler
|
||||
3. **OS Worker Threads** — pthreads, each executing green threads from its local queue
|
||||
|
||||
### Data Flow
|
||||
- `Task::Spawn(func, arg)` → `bux_task_spawn(func, arg)` → creates Task + ucontext → added to run queue
|
||||
- `SIGVTALRM` fires → current task pauses → scheduler picks next task → `swapcontext`
|
||||
- `Channel_Recv` on empty channel → task marked BLOCKED → yields → scheduler runs another task
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Structures
|
||||
|
||||
### Task
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
TASK_READY,
|
||||
TASK_RUNNING,
|
||||
TASK_BLOCKED,
|
||||
TASK_FINISHED,
|
||||
} TaskState;
|
||||
|
||||
typedef struct Task {
|
||||
ucontext_t ctx; /* ucontext for context switch */
|
||||
void *stack; /* malloc'd stack */
|
||||
size_t stack_size; /* e.g., 256KB */
|
||||
|
||||
void (*func)(void*); /* Entry function */
|
||||
void *arg; /* Argument */
|
||||
|
||||
TaskState state;
|
||||
int id; /* Unique task ID */
|
||||
struct Task *next; /* Linked list for queues */
|
||||
|
||||
/* Blocking state */
|
||||
void *waiting_on; /* Channel handle, if blocked on recv */
|
||||
int64_t wake_at; /* Timestamp (ms) for sleep wake-up */
|
||||
} Task;
|
||||
```
|
||||
|
||||
### Per-Worker Scheduler
|
||||
|
||||
```c
|
||||
typedef struct Scheduler {
|
||||
Task *run_queue_head; /* Ready tasks (LIFO: push/pop head) */
|
||||
Task *run_queue_tail;
|
||||
int queue_count;
|
||||
|
||||
Task *current; /* Currently running task */
|
||||
pthread_t os_thread; /* OS thread handle */
|
||||
int worker_id; /* 0 .. N-1 */
|
||||
|
||||
struct Scheduler **all_schedulers; /* For work-stealing */
|
||||
int num_workers;
|
||||
} Scheduler;
|
||||
```
|
||||
|
||||
### Global State
|
||||
|
||||
```c
|
||||
typedef struct TaskPool {
|
||||
Scheduler **schedulers; /* One per worker thread */
|
||||
int num_workers; /* Default = CPU core count */
|
||||
pthread_mutex_t spawn_lock;
|
||||
int next_task_id;
|
||||
int shutdown; /* Set to 1 for graceful shutdown */
|
||||
} TaskPool;
|
||||
```
|
||||
|
||||
**Key Decisions:**
|
||||
- Linked list queues — simple, coarse-grained lock for MVP (lock-free atomic ops later)
|
||||
- Fixed 256KB stacks — sufficient for most code, guard page for overflow detection
|
||||
- Task ID returned by `Task::Spawn`, consumed by `Task::Wait`
|
||||
|
||||
---
|
||||
|
||||
## 4. Bux API
|
||||
|
||||
```bux
|
||||
module Std::Task {
|
||||
|
||||
extern func bux_task_init(num_workers: int) -> int;
|
||||
extern func bux_task_spawn(func: *void, arg: *void) -> int;
|
||||
extern func bux_task_wait(task_id: int);
|
||||
extern func bux_task_sleep(ms: int64);
|
||||
extern func bux_task_yield();
|
||||
extern func bux_task_current_id() -> int;
|
||||
extern func bux_task_shutdown();
|
||||
|
||||
struct TaskHandle {
|
||||
id: int;
|
||||
}
|
||||
|
||||
func Task_Spawn(func: *void, arg: *void) -> TaskHandle {
|
||||
let id: int = bux_task_spawn(func, arg);
|
||||
return TaskHandle { id: id };
|
||||
}
|
||||
|
||||
func Task_Wait(handle: TaskHandle) {
|
||||
bux_task_wait(handle.id);
|
||||
}
|
||||
|
||||
func Task_Sleep(ms: int64) {
|
||||
bux_task_sleep(ms);
|
||||
}
|
||||
|
||||
func Task_Yield() {
|
||||
bux_task_yield();
|
||||
}
|
||||
|
||||
func Task_CurrentId() -> int {
|
||||
return bux_task_current_id();
|
||||
}
|
||||
|
||||
func Task_Init(num_workers: int) -> int {
|
||||
return bux_task_init(num_workers);
|
||||
}
|
||||
|
||||
func Task_Shutdown() {
|
||||
bux_task_shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Example
|
||||
|
||||
```bux
|
||||
import Std::Task;
|
||||
import Std::Channel;
|
||||
|
||||
func Worker(id: int) {
|
||||
PrintLine("Worker " + Int_ToString(id) + " starting");
|
||||
Task_Sleep(100);
|
||||
PrintLine("Worker " + Int_ToString(id) + " done");
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
Task_Init(4);
|
||||
|
||||
let h1: TaskHandle = Task_Spawn(Worker as *void, 1 as *void);
|
||||
let h2: TaskHandle = Task_Spawn(Worker as *void, 2 as *void);
|
||||
|
||||
Task_Wait(h1);
|
||||
Task_Wait(h2);
|
||||
|
||||
Task_Shutdown();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- `func` parameter is `*void` because the C runtime does not know Bux types
|
||||
- Users cast their function to `*void` (like a C function pointer)
|
||||
- `Task_Init` is optional — the first `Task_Spawn` auto-initializes with CPU core count workers
|
||||
|
||||
---
|
||||
|
||||
## 5. Scheduler Algorithm
|
||||
|
||||
### Preemption
|
||||
|
||||
- `SIGVTALRM` timer set to 10ms interval via `setitimer(ITIMER_VIRTUAL, ...)`
|
||||
- Signal handler calls `schedule()` — saves current context, selects next task
|
||||
- Fixed 10ms quantum for MVP (configurable later)
|
||||
|
||||
### Work-Stealing
|
||||
|
||||
Each OS thread (worker):
|
||||
1. Checks its own `run_queue` (LIFO — push/pop from head for cache locality)
|
||||
2. If empty: attempts to "steal" from a random other worker (FIFO from tail)
|
||||
3. If all queues empty: sleeps on a condition variable
|
||||
4. When a new task is spawned: signals the condition variable to wake a sleeper
|
||||
|
||||
### Task Selection (per worker)
|
||||
|
||||
```
|
||||
schedule():
|
||||
1. If current task is RUNNING → mark as READY, push to queue
|
||||
2. Check sleep queue — any wake_time expired?
|
||||
3. Check blocked tasks — any channel now has data?
|
||||
4. Pop READY task from queue (round-robin within queue)
|
||||
5. Mark as RUNNING
|
||||
6. swapcontext() to new task
|
||||
```
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
- `Task_Shutdown()` sets `shutdown = 1`
|
||||
- Workers exit their loop when no more tasks exist
|
||||
- Main thread waits for all workers with `pthread_join`
|
||||
|
||||
---
|
||||
|
||||
## 6. Context Switching
|
||||
|
||||
```c
|
||||
#include <ucontext.h>
|
||||
#include <signal.h>
|
||||
|
||||
static void timer_handler(int sig) {
|
||||
(void)sig;
|
||||
schedule();
|
||||
}
|
||||
|
||||
void scheduler_init(void) {
|
||||
struct sigaction sa;
|
||||
sa.sa_handler = timer_handler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = SA_RESTART;
|
||||
sigaction(SIGVTALRM, &sa, NULL);
|
||||
|
||||
struct itimerval itv;
|
||||
itv.it_interval.tv_sec = 0;
|
||||
itv.it_interval.tv_usec = 10000; /* 10ms */
|
||||
itv.it_value = itv.it_interval;
|
||||
setitimer(ITIMER_VIRTUAL, &itv, NULL);
|
||||
}
|
||||
|
||||
void task_switch(Task *from, Task *to) {
|
||||
from->state = TASK_READY;
|
||||
to->state = TASK_RUNNING;
|
||||
current_task = to;
|
||||
swapcontext(&from->ctx, &to->ctx);
|
||||
}
|
||||
```
|
||||
|
||||
### Task Creation
|
||||
|
||||
```c
|
||||
Task* task_create(void (*func)(void*), void *arg) {
|
||||
Task *t = calloc(1, sizeof(Task));
|
||||
t->stack = malloc(STACK_SIZE);
|
||||
t->stack_size = STACK_SIZE;
|
||||
t->func = func;
|
||||
t->arg = arg;
|
||||
t->state = TASK_READY;
|
||||
|
||||
getcontext(&t->ctx);
|
||||
t->ctx.uc_stack.ss_sp = t->stack;
|
||||
t->ctx.uc_stack.ss_size = t->stack_size;
|
||||
t->ctx.uc_link = &scheduler_context;
|
||||
|
||||
/* makecontext only accepts int arguments; use thread-local to pass pointers */
|
||||
bux_task_creating = t;
|
||||
makecontext(&t->ctx, task_entry_wrapper, 0);
|
||||
bux_task_creating = NULL;
|
||||
|
||||
return t;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Stack Management
|
||||
|
||||
- **Size:** 256KB default, configurable via `Task_Init`
|
||||
- **Allocation:** `malloc()` + guard page (`mprotect(..., PROT_NONE)`) for overflow detection
|
||||
- **Entry wrapper:** `task_entry_wrapper` calls `func(arg)`, then marks task as FINISHED and returns to scheduler
|
||||
|
||||
```c
|
||||
/* Thread-local pointer to the task being created (for makecontext wrapper) */
|
||||
static __thread Task *bux_task_creating;
|
||||
|
||||
static void task_entry_wrapper(void) {
|
||||
Task *t = bux_task_creating;
|
||||
t->func(t->arg);
|
||||
t->state = TASK_FINISHED;
|
||||
schedule(); /* Never returns */
|
||||
}
|
||||
```
|
||||
|
||||
**Cleanup:** `Task_Wait()` frees the stack and Task struct when the task completes.
|
||||
|
||||
---
|
||||
|
||||
## 8. Integration with Bux Build System
|
||||
|
||||
The C runtime file `rt/green_threads.c` is compiled and linked alongside `rt/runtime.c` and `rt/io.c`:
|
||||
|
||||
```
|
||||
bux build:
|
||||
1. Merge all .bux → single .bux
|
||||
2. Compile .bux → .c (buxc)
|
||||
3. Compile generated .c + rt/*.c → binary (gcc/clang)
|
||||
```
|
||||
|
||||
No compiler changes are required. The scheduler is purely a runtime addition.
|
||||
|
||||
---
|
||||
|
||||
## 9. Error Handling & Edge Cases
|
||||
|
||||
| Scenario | Handling |
|
||||
|----------|----------|
|
||||
| `Task_Spawn` when scheduler not initialized | Auto-initialize with CPU core count |
|
||||
| Stack overflow | Guard page triggers SIGSEGV (MVP: abort; future: recoverable) |
|
||||
| `Task_Wait` on non-existent ID | No-op / warning (task already finished) |
|
||||
| All workers blocked | Main thread busy-waits or sleeps (MVP: simple spin) |
|
||||
| `Task_Shutdown` with running tasks | Wait for all tasks to finish, then join workers |
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing Strategy
|
||||
|
||||
1. **Unit tests (C level):** Test queue operations, task creation, context switch in isolation
|
||||
2. **Integration tests (Bux):**
|
||||
- Spawn 2 tasks, wait for both
|
||||
- Spawn N tasks, verify they all run
|
||||
- Channel send/recv between concurrent tasks
|
||||
- Sleep test — verify other tasks run while one sleeps
|
||||
3. **Stress test:** Spawn 1000+ tasks, verify no crashes or memory leaks
|
||||
4. **Selfhost loop:** Verify the scheduler does not break compiler determinism
|
||||
|
||||
---
|
||||
|
||||
## 11. Future Work (Post-MVP)
|
||||
|
||||
- **Cross-platform context switching:** Replace ucontext with `setjmp` + manual stack switch for Windows
|
||||
- **Dynamic stack growth:** Detect near-overflow and realloc stack
|
||||
- **I/O integration:** Hook `read`/`write` to yield on blocking I/O
|
||||
- **Work-stealing lock-free queues:** Replace coarse locks with atomic operations
|
||||
- **Task cancellation / timeouts:** `Task_Cancel(handle)`, `Task_Wait(handle, timeout_ms)`
|
||||
- `go` keyword as syntactic sugar for `Task::Spawn`
|
||||
@@ -0,0 +1,239 @@
|
||||
# Bux Source Location Tracking in Error Messages — Design Document
|
||||
|
||||
> **Date:** 2026-06-10
|
||||
> **Status:** Design Approved
|
||||
> **Scope:** MVP Rust-style error messages with code snippets for parser and sema diagnostics
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The Bux self-hosted compiler already tracks source locations (lexer tokens have line/column, AST nodes have line/column, parser and sema have diagnostic structs with line/column). However, the CLI prints errors as plain strings or raw `line X col Y` text, without code snippets or consistent formatting.
|
||||
|
||||
This feature unifies and improves error display to match the Rust-style format:
|
||||
```
|
||||
error: type mismatch
|
||||
--> src/Main.bux:42:15
|
||||
|
|
||||
42 | let x: int = "hello";
|
||||
| ^
|
||||
```
|
||||
|
||||
### Goals
|
||||
- Unified Rust-style error formatting for all parser and sema diagnostics
|
||||
- Code snippets showing the exact line with an underline
|
||||
- Consistent severity prefixes (`error:`, `warning:`, `note:`)
|
||||
|
||||
### Non-Goals (for MVP)
|
||||
- Source maps (mapping merged-source line numbers back to original files)
|
||||
- Multi-line error spans
|
||||
- Multi-character underlines (`^^^^` for entire tokens)
|
||||
- Colorized output
|
||||
- Error codes (e.g., `E0001`)
|
||||
- HIR lower / C backend diagnostics (these phases do not currently emit errors)
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Parser/Sema Diagnostics │
|
||||
│ ParserDiag { line, col, message } │
|
||||
│ SemaDiag { line, col, message } │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Unified Diagnostic Formatter │
|
||||
│ (inline in cli.bux or lib/Diagnostic) │
|
||||
│ • Reads source line from file │
|
||||
│ • Formats snippet with underline │
|
||||
│ • Prints Rust-style error block │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ CLI Output │
|
||||
│ error: type mismatch │
|
||||
│ --> src/Main.bux:42:15 │
|
||||
│ | │
|
||||
│ 42 | let x: int = "hello"; │
|
||||
│ | ^ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
1. **Source Line Loader** — reads a specific line from a source file
|
||||
2. **Snippet Renderer** — formats `line_num | code` + underline
|
||||
3. **Diagnostic Printer** — assembles the full Rust-style error block
|
||||
|
||||
**Note:** Bux compiles a merged source file (all `.bux` files merged before compilation). For MVP, snippets show lines from the merged source. Source map tracking (which original file a line came from) is future work.
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Structures
|
||||
|
||||
```bux
|
||||
struct Diagnostic {
|
||||
message: String;
|
||||
line: uint32; /* 1-based */
|
||||
column: uint32; /* 1-based */
|
||||
severity: int; /* 0=error, 1=warning, 2=note */
|
||||
}
|
||||
```
|
||||
|
||||
**Severity values:**
|
||||
- `0` — `error:`
|
||||
- `1` — `warning:`
|
||||
- `2` — `note:`
|
||||
|
||||
For MVP, parser and sema diagnostics are always severity `0` (error). Warning/note severity is reserved for future use.
|
||||
|
||||
---
|
||||
|
||||
## 4. API Design
|
||||
|
||||
```bux
|
||||
/* Print a single diagnostic in Rust-style format */
|
||||
func Diagnostic_Print(diag: *Diagnostic, sourcePath: String);
|
||||
|
||||
/* Helper: read line N from a file (1-based) */
|
||||
func Diagnostic_GetLine(path: String, lineNum: uint32) -> String;
|
||||
```
|
||||
|
||||
### Output Format
|
||||
|
||||
```
|
||||
error: <message>
|
||||
--> <path>:<line>:<col>
|
||||
|
|
||||
<ln> | <source_line>
|
||||
| <spaces>^
|
||||
```
|
||||
|
||||
Where:
|
||||
- `<ln>` is the line number, right-aligned
|
||||
- `<spaces>` is `column - 1` spaces to position the `^`
|
||||
- If `source_line` is empty (file not found), only the header is printed
|
||||
|
||||
---
|
||||
|
||||
## 5. Snippet Rendering Algorithm
|
||||
|
||||
### `Diagnostic_GetLine(path, lineNum)`
|
||||
|
||||
1. Open the file at `path`
|
||||
2. Read character by character, counting newlines
|
||||
3. When reaching line `lineNum`, accumulate characters until `\n` or EOF
|
||||
4. Return the accumulated string (without trailing `\n`)
|
||||
5. If file cannot be opened or `lineNum` exceeds file length, return `""`
|
||||
|
||||
### `Diagnostic_Print(diag, sourcePath)`
|
||||
|
||||
```
|
||||
Print severity prefix + message
|
||||
Print " --> " + sourcePath + ":" + line + ":" + column + "\n"
|
||||
Print " |\n"
|
||||
Print line number + " | " + source line + "\n"
|
||||
Print " | " + (column-1 spaces) + "^\n"
|
||||
```
|
||||
|
||||
**Underline:** Single `^` at the column position. Multi-character spans are future work.
|
||||
|
||||
---
|
||||
|
||||
## 6. Integration Points
|
||||
|
||||
### Parser Diagnostics
|
||||
|
||||
**Current code** (`src/cli.bux`, lexer error printing):
|
||||
```bux
|
||||
PrintLine(lex.diags[i].message);
|
||||
```
|
||||
|
||||
**New code:**
|
||||
```bux
|
||||
let diag: Diagnostic = Diagnostic {
|
||||
message: lex.diags[i].message,
|
||||
line: lex.diags[i].line,
|
||||
column: lex.diags[i].column,
|
||||
severity: 0,
|
||||
};
|
||||
Diagnostic_Print(&diag, sourcePath);
|
||||
```
|
||||
|
||||
### Sema Diagnostics
|
||||
|
||||
**Current code** (`src/cli.bux`, `Cli_Compile` path):
|
||||
```bux
|
||||
PrintLine(sema.diags[i].message);
|
||||
```
|
||||
|
||||
**Current code** (`src/cli.bux`, `Cli_BuildProject` path):
|
||||
```bux
|
||||
Print(" line "); PrintInt(sema.diags[i].line as int64);
|
||||
Print(" col "); PrintInt(sema.diags[i].column as int64);
|
||||
Print(": "); PrintLine(sema.diags[i].message);
|
||||
```
|
||||
|
||||
**New code (both paths):**
|
||||
```bux
|
||||
let diag: Diagnostic = Diagnostic {
|
||||
message: sema.diags[i].message,
|
||||
line: sema.diags[i].line,
|
||||
column: sema.diags[i].column,
|
||||
severity: 0,
|
||||
};
|
||||
Diagnostic_Print(&diag, sourcePath);
|
||||
```
|
||||
|
||||
### Unified Helper
|
||||
|
||||
Add to `src/cli.bux`:
|
||||
```bux
|
||||
func Cli_ReportSemaErrors(sema: *Sema, sourcePath: String) {
|
||||
let i: int = 0;
|
||||
while i < Sema_DiagCount(sema) {
|
||||
let d: SemaDiag = sema.diags[i];
|
||||
let diag: Diagnostic = Diagnostic {
|
||||
message: d.message, line: d.line,
|
||||
column: d.column, severity: 0,
|
||||
};
|
||||
Diagnostic_Print(&diag, sourcePath);
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/cli.bux` | Replace all manual error printing with `Diagnostic_Print`. Add `Diagnostic` struct and helper functions. |
|
||||
|
||||
**No changes needed to:**
|
||||
- `src/lexer.bux` — already emits `LexerDiag` with line/col
|
||||
- `src/parser.bux` — already emits `ParserDiag` with line/col
|
||||
- `src/sema.bux` — already emits `SemaDiag` with line/col via `Sema_EmitError`
|
||||
- `src/ast.bux` — already stores line/col on every node
|
||||
- `src/hir.bux` — already stores line/col on HIR nodes
|
||||
|
||||
---
|
||||
|
||||
## 8. Testing Strategy
|
||||
|
||||
1. **Type error test:** Create a `.bux` file with `let x: int = "hello";` → build → verify Rust-style output with snippet
|
||||
2. **Syntax error test:** Create a `.bux` file with missing `}` → build → verify parser error shows snippet
|
||||
3. **Selfhost loop:** Verify compiler C output remains identical (CLI changes do not affect codegen)
|
||||
|
||||
---
|
||||
|
||||
## 9. Future Work
|
||||
|
||||
- **Source maps:** Track which original file each merged line came from, so errors show original paths
|
||||
- **Multi-character underlines:** Underline entire token/span, not just a single `^`
|
||||
- **Colorized output:** ANSI color codes for error/warning/note
|
||||
- **Error codes:** Assign stable codes like `E0001` for each error type
|
||||
- **Notes and help text:** Attach explanatory notes to errors (Rust's `help:` and `note:` blocks)
|
||||
@@ -0,0 +1,57 @@
|
||||
# Source Location Error Fixes — Design Document
|
||||
|
||||
> **Date:** 2026-06-10
|
||||
> **Status:** Approved
|
||||
> **Scope:** Parser severity filtering + source maps for merged builds
|
||||
|
||||
---
|
||||
|
||||
## 1. Parser Diagnostics with Severity
|
||||
|
||||
### Problem
|
||||
Parser is fault-tolerant and generates diagnostics for recoverable errors (e.g., `expected '}'`, `skipping unknown declaration`). The stdlib triggers many of these, but compilation succeeds because the parser recovers.
|
||||
|
||||
### Solution
|
||||
Add `severity` to `ParserDiag`:
|
||||
```bux
|
||||
struct ParserDiag {
|
||||
line: uint32;
|
||||
column: uint32;
|
||||
message: String;
|
||||
severity: int; /* 0=error (fatal), 1=warning (recoverable) */
|
||||
}
|
||||
```
|
||||
|
||||
- `parserExpect()`, `parserEmitDiag()` set severity to `1` (warning/recoverable)
|
||||
- Only when the parser is completely stuck → severity `0` (fatal)
|
||||
- In `Parser_Parse`, print ONLY diags with `severity == 0`, OR if `mod.itemCount == 0`
|
||||
|
||||
---
|
||||
|
||||
## 2. Source Maps for `bux build`
|
||||
|
||||
### Problem
|
||||
`Cli_BuildProject` creates a merged module in memory. Sema errors show `<merged>` as the path because no physical file exists.
|
||||
|
||||
### Solution
|
||||
Before `Sema_Analyze(merged)`, generate the merged source text from all declarations and write it to `build/merged.bux`. Pass this path to `Diagnostic_Print`.
|
||||
|
||||
**Result:**
|
||||
```
|
||||
error: undeclared identifier 'undefined_variable'
|
||||
--> build/merged.bux:507:18
|
||||
|
|
||||
507 | let x: int = undefined_variable;
|
||||
| ^
|
||||
```
|
||||
|
||||
The user can open `build/merged.bux` to see the full context.
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/parser.bux` | Add `severity` to `ParserDiag`. Set severity in `parserExpect`/`parserEmitDiag`. Print only fatal errors in `Parser_Parse`. |
|
||||
| `src/cli.bux` | Generate merged source text before `Sema_Analyze`. Write to `build/merged.bux`. Pass path to `Diagnostic_Print`. |
|
||||
@@ -0,0 +1,130 @@
|
||||
# Drop Interface Auto-Drop Design Document
|
||||
|
||||
> **Date:** 2026-06-14
|
||||
> **Status:** Approved
|
||||
> **Scope:** Selfhost compiler (`src/*.bux`)
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
Bux already has two ways to declare that a type needs automatic cleanup:
|
||||
|
||||
1. **`@[Drop]` attribute** on a struct — triggers auto-drop lookup of `TypeName_Drop`.
|
||||
2. **`extend Type for Drop { ... }`** — implements the `Drop` interface from `lib/Drop.bux`.
|
||||
|
||||
The parser and sema already support both forms, but the HIR lowering step only recognizes the `@[Drop]` attribute. As a result, a type that implements `Drop` via the interface is **not** automatically cleaned up when its variables go out of scope.
|
||||
|
||||
Example (`_test_interface_drop/src/Main.bux`):
|
||||
|
||||
```bux
|
||||
import Drop
|
||||
|
||||
struct Buffer {
|
||||
ptr: *int
|
||||
}
|
||||
|
||||
extend Buffer for Drop {
|
||||
func Drop(self: *Buffer) {
|
||||
bux_free(self.ptr as *void);
|
||||
}
|
||||
}
|
||||
|
||||
func main() -> int {
|
||||
let buf: Buffer = Buffer { ptr: bux_alloc(10 as uint * sizeof(int)) as *int };
|
||||
return 0; // Buffer_Drop(&buf) should be emitted here, but currently is not
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Goals
|
||||
|
||||
1. **Universal Drop recognition**: Auto-drop fires for any type that implements `Drop`, whether via `@[Drop]` or via `extend ... for Drop`.
|
||||
2. **No syntax changes**: The existing `interface Drop` and `extend ... for Drop` syntax remains unchanged.
|
||||
3. **Preserve move semantics**: Variables that have been moved are still skipped by the existing `CBE_IsMoved` logic.
|
||||
4. **Selfhost loop compatibility**: The compiler must still bootstrap deterministically.
|
||||
|
||||
## 3. Non-Goals
|
||||
|
||||
1. **`own T` type**: Adding `own T` to selfhost is a separate, larger feature.
|
||||
2. **Scoped defers on break/continue**: Tracked separately.
|
||||
3. **Interface vtable dispatch for auto-drop**: Auto-drop remains static lookup of `TypeName_Drop`; no runtime interface cost.
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
### 4.1 Before (Current)
|
||||
|
||||
In `src/hir_lower.bux`, `Lcx_BuildAutoDropFree` only returns a drop function name when `decl.isDrop != 0`:
|
||||
|
||||
```bux
|
||||
// User-defined types with @[Drop]: look for TypeName_Drop function
|
||||
if typeSym.kind == skType && typeSym.decl != null as *Decl && typeSym.decl.isDrop != 0 {
|
||||
let dropName: String = String_Concat(typeName, "_Drop");
|
||||
// ... generic monomorphization ...
|
||||
return dropName;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 After (Proposed)
|
||||
|
||||
Extend the user-type branch to also detect interface-based implementations:
|
||||
|
||||
```bux
|
||||
// User-defined types with @[Drop] OR with an explicit TypeName_Drop method
|
||||
if typeSym.kind == skType && typeSym.decl != null as *Decl {
|
||||
let dropName: String = String_Concat(typeName, "_Drop");
|
||||
let hasAttr: bool = typeSym.decl.isDrop != 0;
|
||||
let dropSym: Symbol = Scope_Lookup(ctx.scope, dropName);
|
||||
let hasMethod: bool = dropSym.kind == skFunc;
|
||||
|
||||
if hasAttr || hasMethod {
|
||||
// ... generic monomorphization ...
|
||||
return dropName;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Sema already registers methods from `extend ... for Drop` as `TypeName_Drop` function symbols in `ctx.scope` (see `src/sema.bux:1197-1207`). Therefore, looking up the method by name is sufficient to detect an interface-based Drop implementation.
|
||||
|
||||
### 4.3 Generic Types
|
||||
|
||||
For generic `extend Box<T> for Drop { func Drop(self: *Box<T>) { ... } }`, the symbol registered is `Box_Drop` (generic). The existing `Lcx_FindGenericFunc` / `Lcx_GenerateFuncInstance` path in `Lcx_BuildAutoDropFree` handles monomorphization when the concrete type is a mangled generic instance (e.g., `Box_int`). The proposed change reuses that path unchanged.
|
||||
|
||||
## 5. Affected Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/hir_lower.bux` | Extend `Lcx_BuildAutoDropFree` to detect `TypeName_Drop` method symbols even without `@[Drop]` attribute. |
|
||||
| `_test_interface_drop/src/Main.bux` | Verify that `Buffer_Drop(&buf)` is emitted. |
|
||||
|
||||
## 6. Testing Plan
|
||||
|
||||
### 6.1 Existing Tests
|
||||
|
||||
| Test | Expected |
|
||||
|------|----------|
|
||||
| `_test_drop_user` | Still passes; `@[Drop]` path unchanged. |
|
||||
| `_test_drop_move` | Still skips drop for moved vars. |
|
||||
| `_test_drop_trait` | Still passes; Array auto-drop unchanged. |
|
||||
| `make selfhost-loop` | C output remains identical. |
|
||||
|
||||
### 6.2 Target Test
|
||||
|
||||
`_test_interface_drop/src/Main.bux` should compile and the generated `build/main.c` should contain a call to `Buffer_Drop(&buf)` before `return 0`.
|
||||
|
||||
## 7. Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Any method named `Drop` triggers auto-drop | Acceptable because `Drop` is a privileged interface name. If stricter checking is needed later, sema can record explicit `extend ... for Drop` implementations in a dedicated table. |
|
||||
| Generic monomorphization missed | Reuse existing generic Drop instance generation path already used for `@[Drop]` types. |
|
||||
| Selfhost loop breaks | Run `make selfhost-loop` before committing. |
|
||||
|
||||
## 8. Future Work
|
||||
|
||||
1. **`own T` in selfhost**: Add `own T` type expression + move-on-pass semantics.
|
||||
2. **Scoped defers on break/continue**: Match bootstrap behavior.
|
||||
3. **Explicit interface-implementation table**: If the simple method-name lookup becomes too permissive, record `extend ... for Drop` facts in sema and expose them to HIR lowering.
|
||||
|
||||
## 9. Placeholder Scan
|
||||
|
||||
- No TBD/TODO placeholders.
|
||||
- All file paths are exact.
|
||||
- Code snippets match the current selfhost source.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Fix Drop/Operator `[]` Generic Type Bug in Bootstrap
|
||||
|
||||
> **Date:** 2026-06-14
|
||||
> **Status:** Approved
|
||||
> **Scope:** Bootstrap compiler (`bootstrap/*.nim`) — specifically `_test_drop_trait` and `_test_checked_index`
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
The bootstrap compiler already supports `@[Drop]` types, auto-drop, and generic operator overloading (`operator []`), but the interaction between these features produces wrong types or spurious type errors.
|
||||
|
||||
Two integration tests currently fail:
|
||||
|
||||
- `_test_drop_trait`: `expected int, got float32 at 9:13` when reading `arr[0]` from an `Array<int>`.
|
||||
- `_test_checked_index`: `cannot assign int to Array at 11:5` when using the checked index operator on `Array<T>`.
|
||||
|
||||
Both failures suggest that generic type substitution (`T -> int`) is not applied correctly to the return type of `operator []` when the operator is invoked from generated auto-drop or checked-index code paths.
|
||||
|
||||
## 2. Goals
|
||||
|
||||
1. Make `_test_drop_trait` pass.
|
||||
2. Make `_test_checked_index` pass.
|
||||
3. Keep the fix minimal and confined to the bootstrap compiler.
|
||||
4. Ensure `make test`, `make selfhost`, and `make selfhost-loop` continue to pass.
|
||||
|
||||
## 3. Non-Goals
|
||||
|
||||
1. Rewriting the `apps/` example applications (excluded per user request).
|
||||
2. Adding new language features; this is a bug-fix task only.
|
||||
3. Changing the selfhost compiler (`src/*.bux`) unless the same bug exists there and is required for selfhost-loop.
|
||||
4. Fixing unrelated `for ... in` or `Slice<T>` failures (tracked separately).
|
||||
|
||||
## 4. Investigation Plan
|
||||
|
||||
1. Reproduce both failures with `build/buxc run` in each `_test_*` directory.
|
||||
2. Read the source of `_test_drop_trait/src/Main.bux` and `_test_checked_index/src/Main.bux`.
|
||||
3. Trace how the bootstrap compiler resolves `operator []` for `Array<T>`:
|
||||
- Parser/AST representation of the call.
|
||||
- Semantic analysis (`sema.nim`) overload resolution and generic substitution.
|
||||
- HIR lowering (`hir_lower.nim`) of operator calls and auto-drop.
|
||||
- LIR lowering/C backend (`lir_lower.nim`, `lir_c_backend.nim`) type emission.
|
||||
4. Identify the exact location where the return type of the selected operator overload retains an unsubstituted generic parameter.
|
||||
|
||||
## 5. Hypothesis
|
||||
|
||||
When `@[Drop]` causes the compiler to synthesize an `Array_Drop` call (or when `@[Checked]` synthesizes a bounds-checked `operator []` call), the generic substitution context is not threaded into the operator overload resolution. As a result, `T` remains as the generic parameter instead of being replaced by `int`, leading to:
|
||||
|
||||
- A fallback/primitive type mismatch (`float32` appears when no substitution occurs).
|
||||
- A type assignment error because the returned value is treated as `Array` instead of `int`.
|
||||
|
||||
## 6. Proposed Fix
|
||||
|
||||
Investigation showed the root cause spans both semantic analysis and HIR lowering:
|
||||
|
||||
1. **Sema (`bootstrap/sema.nim`)**:
|
||||
- Preserve generic type arguments on `tkNamed` in `resolveType`.
|
||||
- Resolve explicit generic-call return types with the concrete type arguments substituted.
|
||||
- Substitute method type parameters when looking up `operator_index_get`, including through pointer receivers.
|
||||
- Skip strict argument-type checks for generic function calls; these are deferred to inference/monomorphization.
|
||||
|
||||
2. **HIR lowering (`bootstrap/hir_lower.nim`)**:
|
||||
- Build a local type-parameter substitution map when generating generic struct instances inside `substituteType`.
|
||||
- Register parameter types in `varTypeExprs` after clearing the per-function table so pointer parameters are visible when lowering operator calls.
|
||||
|
||||
The fix should be the smallest change that makes the two tests pass without breaking other tests.
|
||||
|
||||
## 7. Affected Files
|
||||
|
||||
| File | Expected Change |
|
||||
|------|-----------------|
|
||||
| `bootstrap/sema.nim` | Preserve type args, substitute in operator/index lookup, defer generic call arg checks. |
|
||||
| `bootstrap/hir_lower.nim` | Fix generic struct monomorphization and parameter visibility. |
|
||||
| `_test_drop_trait/src/Main.bux` | No changes. |
|
||||
| `_test_checked_index/src/Main.bux` | No changes. |
|
||||
|
||||
## 8. Testing Plan
|
||||
|
||||
### 8.1 Target Tests
|
||||
|
||||
```bash
|
||||
cd _test_drop_trait && ../../build/buxc run && cd ..
|
||||
cd _test_checked_index && ../../build/buxc run && cd ..
|
||||
```
|
||||
|
||||
Expected: both exit 0.
|
||||
|
||||
### 8.2 Regression Tests
|
||||
|
||||
```bash
|
||||
make test
|
||||
make selfhost
|
||||
make selfhost-loop
|
||||
```
|
||||
|
||||
Expected: all pass / no changes in selfhost-loop output.
|
||||
|
||||
### 8.3 Optional Cross-Check
|
||||
|
||||
Run remaining `_test_*` packages to confirm no new failures:
|
||||
|
||||
```bash
|
||||
for d in _test_*; do
|
||||
echo "=== $d ==="
|
||||
(cd "$d" && ../../build/buxc run) || true
|
||||
done
|
||||
```
|
||||
|
||||
## 9. Success Criteria
|
||||
|
||||
- `_test_drop_trait` reports PASS.
|
||||
- `_test_checked_index` reports PASS.
|
||||
- `make test` reports no new failures.
|
||||
- `make selfhost-loop` remains deterministic (identical C + stripped ELF).
|
||||
|
||||
## 10. Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Fix touches generic resolution and breaks other tests | Run full test suite and selfhost-loop before committing. |
|
||||
| Same bug exists in selfhost compiler | Selfhost-loop will catch type/output differences; fix selfhost only if required. |
|
||||
| Root cause is deeper than operator substitution | Time-box investigation; if not resolved in a reasonable number of iterations, escalate to user with findings. |
|
||||
|
||||
## 11. Relation to Other Work
|
||||
|
||||
- `docs/superpowers/specs/2026-06-10-drop-trait-design.md` covers universal auto-drop in the **selfhost** compiler. This document covers the **bootstrap** compiler's Drop/operator interaction bug and is independent of the selfhost work.
|
||||
@@ -0,0 +1,172 @@
|
||||
# Fix `for ... in` Iterator Lowering in Bootstrap
|
||||
|
||||
> **Date:** 2026-06-14
|
||||
> **Status:** Approved
|
||||
> **Scope:** Bootstrap compiler (`bootstrap/hir_lower.nim`) — `for <var> in <collection>` loops
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
The `for ... in` collection iterator is already parsed and sema-accepted, but the HIR→C lowering produces broken code for concrete generic collections such as `Array<int>` or `Channel<int>`.
|
||||
|
||||
Five integration tests fail because of this:
|
||||
|
||||
- `_test_forin_stdlib`: C compilation error `‘x’ undeclared` inside `Main`.
|
||||
- `_test_forin_channel`: C compilation error `‘msg’ undeclared` inside `Main`.
|
||||
- `_test_generic_trait`: sema error `cannot assign float32 to int at 12:9` — the loop variable `x` is typed as `float32` instead of `int`.
|
||||
- `_test_import`: sema error `cannot assign float32 to int at 15:9` — same issue.
|
||||
- `_test_mono`: sema error `cannot assign float32 to int at 38:9` — same issue.
|
||||
|
||||
Two independent bugs are at play:
|
||||
|
||||
1. **Missing C declaration:** the loop variable is not emitted as a local variable in the generated C function.
|
||||
2. **Wrong loop-variable type:** generic `Iter_Next_T` returns an unsubstituted type parameter `T`, which sema treats as `float32` by default.
|
||||
|
||||
## 2. Goals
|
||||
|
||||
1. Make `_test_forin_stdlib` pass.
|
||||
2. Make `_test_forin_channel` pass.
|
||||
3. Make `_test_generic_trait`, `_test_import`, and `_test_mono` pass (or at least no longer fail with the `float32` loop-variable error).
|
||||
4. Keep the fix minimal and confined to the bootstrap compiler.
|
||||
5. Ensure `make test` and `make selfhost-loop` continue to pass.
|
||||
|
||||
## 3. Non-Goals
|
||||
|
||||
1. Rewriting the `apps/` example applications (excluded per user request).
|
||||
2. Adding new language features such as `break`/`continue` in loops or iterator traits.
|
||||
3. Changing the selfhost compiler (`src/*.bux`) unless required for selfhost-loop parity.
|
||||
4. Fixing the unrelated `_test_slice` C typedef bug (tracked separately).
|
||||
5. Implementing full Destructors / Drop trait (next roadmap milestone after stabilization).
|
||||
|
||||
## 4. Background: How `for ... in` Is Lowered
|
||||
|
||||
The parser produces `stmtForIn(ident, expr, body)`.
|
||||
|
||||
Sema checks the collection expression and infers the loop-variable type from it.
|
||||
|
||||
HIR lowering currently desugars the loop to a placeholder infinite `hLoop` that ignores the iterator expression and never declares the loop variable.
|
||||
|
||||
The intended lowering depends on the collection type:
|
||||
|
||||
- **Array / Iter collections** (from `lib/Iter.bux`):
|
||||
```bux
|
||||
let __iter = Array_Iter_T(&collection);
|
||||
while (Iter_HasNext_T(&__iter)) {
|
||||
let x = Iter_Next_T(&__iter);
|
||||
// body
|
||||
}
|
||||
```
|
||||
- **Channel collections** (from `lib/Channel.bux`):
|
||||
```bux
|
||||
var x: T;
|
||||
while (true) {
|
||||
if (!Channel_Recv_Ok_T(&ch, &x)) { break; }
|
||||
// body
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Investigation Plan
|
||||
|
||||
1. Read `_test_forin_stdlib/src/Main.bux`, `_test_forin_channel/src/Main.bux`, `_test_generic_trait/src/Main.bux`.
|
||||
2. Locate `stmtForIn` handling in `bootstrap/hir_lower.nim`.
|
||||
3. Verify how the iterator variable is introduced into HIR/C:
|
||||
- Is it registered in `ctx.varTypes` / `ctx.varTypeExprs`?
|
||||
- Is a `hirLet` emitted before the loop body?
|
||||
4. Verify how `Array_Iter_Next_T` is monomorphized:
|
||||
- Does the call get the `_int` suffix?
|
||||
- Does the returned `T` get substituted with `int`?
|
||||
5. Identify the minimal edit that fixes both symptoms.
|
||||
|
||||
## 6. Hypothesis
|
||||
|
||||
### 6.1 Missing Declaration
|
||||
|
||||
The lowering creates the loop body with references to the loop variable, but the declaration site is omitted or not added to the function's local-variable list. As a result, the C backend never emits `int x;`, causing the `undeclared identifier` error.
|
||||
|
||||
### 6.2 Wrong Type
|
||||
|
||||
When the collection is `Array<int>`, the lowering calls `Array_Iter_Next` (without `_int`) or calls a monomorphized `Array_Iter_Next_int` but assigns its result to a variable whose type was resolved from the generic signature (`T`). Because sema does not substitute `T` for the loop variable, the variable defaults to `float32`.
|
||||
|
||||
The fix is likely to:
|
||||
|
||||
- Explicitly declare the loop variable with the concrete element type derived from the collection type.
|
||||
- Ensure `Array_Iter` / `Array_Iter_HasNext` / `Array_Iter_Next` are monomorphized with the collection's type arguments.
|
||||
|
||||
## 7. Proposed Fix
|
||||
|
||||
The fix spans sema and HIR lowering:
|
||||
|
||||
1. **Sema fixes**:
|
||||
- In `skFor`, derive the loop-variable type from the collection's element type (`Array<T>` / `Channel<T>` inner type).
|
||||
- In `ekField` for structs, build a type-parameter substitution map from the object's concrete type args and apply it to the field type, so `arr.data` on `Array<int>` resolves to `*int`.
|
||||
- Fix `typeToTypeExpr` to preserve generic type arguments on named types.
|
||||
|
||||
2. **HIR lowering**:
|
||||
- Replace the placeholder `skFor` collection branch with real lowering that selects by collection type.
|
||||
- **Array / Iter collections**:
|
||||
- Extract element type from `Array<T>` or `Iter<T>`.
|
||||
- Generate or reuse `Iter_T` struct instance.
|
||||
- Generate function instances `Array_Iter_T`, `Iter_HasNext_T`, `Iter_Next_T`.
|
||||
- If the collection expression is not a simple identifier, spill it to a temporary `Array_T` variable first.
|
||||
- Emit `alloca __iter`, store `Array_Iter_T(&collection)`, then `while (Iter_HasNext_T(&__iter)) { let x = Iter_Next_T(&__iter); body }`.
|
||||
- Register `x` in `ctx.varTypeExprs` before lowering the body.
|
||||
- **Channel collections**:
|
||||
- Extract element type from `Channel<T>`.
|
||||
- Generate function instance `Channel_Recv_Ok_T`.
|
||||
- Emit `alloca x`, then `while (true) { if (!Channel_Recv_Ok_T(&ch, &x)) break; body }`.
|
||||
|
||||
## 8. Affected Files
|
||||
|
||||
| File | Expected Change |
|
||||
|------|-----------------|
|
||||
| `bootstrap/sema.nim` | Derive loop var type; substitute struct type params on field access; preserve type args in `typeToTypeExpr`. |
|
||||
| `bootstrap/hir_lower.nim` | Replace placeholder collection `skFor` lowering with Array/Iter and Channel lowerings. |
|
||||
| `_test_forin_stdlib/src/Main.bux` | No changes. |
|
||||
| `_test_forin_channel/src/Main.bux` | No changes. |
|
||||
| `_test_generic_trait/src/Main.bux` | No changes. |
|
||||
| `_test_import/src/Main.bux` | No changes. |
|
||||
| `_test_mono/src/Main.bux` | No changes. |
|
||||
|
||||
## 9. Testing Plan
|
||||
|
||||
### 9.1 Target Tests
|
||||
|
||||
```bash
|
||||
cd _test_forin_stdlib && /home/ziko/z-git/bux/bux/buxc run
|
||||
cd _test_forin_channel && /home/ziko/z-git/bux/bux/buxc run
|
||||
cd _test_generic_trait && /home/ziko/z-git/bux/bux/buxc run
|
||||
cd _test_import && /home/ziko/z-git/bux/bux/buxc run
|
||||
cd _test_mono && /home/ziko/z-git/bux/bux/buxc run
|
||||
```
|
||||
|
||||
Expected: all exit 0.
|
||||
|
||||
### 9.2 Regression Tests
|
||||
|
||||
```bash
|
||||
cd /home/ziko/z-git/bux/bux
|
||||
make test
|
||||
make selfhost-loop
|
||||
```
|
||||
|
||||
Expected: all pass / no changes in selfhost-loop output.
|
||||
|
||||
## 10. Success Criteria
|
||||
|
||||
- `_test_forin_stdlib` reports PASS.
|
||||
- `_test_forin_channel` reports PASS.
|
||||
- `_test_generic_trait`, `_test_import`, `_test_mono` no longer fail with the `float32` loop-variable error.
|
||||
- `make test` reports no new failures.
|
||||
- `make selfhost-loop` remains deterministic.
|
||||
|
||||
## 11. Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Fix changes loop lowering and breaks range-based `for i in 0..10` | Keep range-based path separate; only touch `stmtForIn` collection path. |
|
||||
| Same bug exists in selfhost compiler | Selfhost-loop will catch output differences; fix selfhost only if required. |
|
||||
| Root cause is deeper than loop lowering | Time-box investigation; report findings if not resolved quickly. |
|
||||
|
||||
## 12. Relation to Other Work
|
||||
|
||||
- Previous commit `06db492` fixed generic `operator []` resolution. This work continues generic-type propagation into iterator loops.
|
||||
- `_test_slice` has a separate C typedef bug and is out of scope for this task.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user