Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8c21609fd | |||
| f21002d258 | |||
| a23388cd0d | |||
| d387853c11 | |||
| 4f583eaec1 | |||
| a538e05041 | |||
| 869fb9c7d3 | |||
| 497e01d537 | |||
| 4df12bda99 | |||
| ea5ba70528 | |||
| a0bed68cbf | |||
| 6b954f28e0 | |||
| c5b6ba3494 | |||
| b8a920b235 | |||
| f0890e13f8 | |||
| 76ea9cc083 | |||
| 44ecbd6873 | |||
| 6f950bcbfb | |||
| ec5984762b | |||
| db7ba1dff2 | |||
| e87985e879 | |||
| 1f8289059a | |||
| d517c62380 | |||
| fa1521a71e |
@@ -33,3 +33,9 @@ _test_*/
|
|||||||
# Log files
|
# Log files
|
||||||
*.log
|
*.log
|
||||||
.nim_runtime/
|
.nim_runtime/
|
||||||
|
|
||||||
|
# Local build artifacts
|
||||||
|
buxc_debug
|
||||||
|
bootstrap/main
|
||||||
|
tools/bux-lsp
|
||||||
|
benches/*.out
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ BUILD_DIR := build
|
|||||||
# Project-local nimcache so CI can cache compiles (default is ~/.cache/nim).
|
# Project-local nimcache so CI can cache compiles (default is ~/.cache/nim).
|
||||||
NIMFLAGS ?= --nimcache:nimcache
|
NIMFLAGS ?= --nimcache:nimcache
|
||||||
|
|
||||||
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 ownership_checked ownership_release drop_early_return lifetime_elision ctfe ctfe_crc async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic generic_infer_hof struct_tuple_pat match_block nested_patterns match_guards pattern_shadow move_field move_field_partial move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt macro_tt_raw macro_type collections_extra
|
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 try_generic ownership ownership_checked ownership_release drop_early_return lifetime_elision ctfe ctfe_crc async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic generic_infer_hof struct_tuple_pat match_block nested_patterns match_guards pattern_shadow move_field move_field_partial move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt macro_tt_raw macro_type macro_type_generic macro_op_paste collections_extra generic_enum switch is_operator
|
||||||
|
|
||||||
# Platform smoke (macOS CI): full EXAMPLES still runs on Linux.
|
# Platform smoke (macOS CI): full EXAMPLES still runs on Linux.
|
||||||
EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt macro_tt_raw ctfe_crc
|
EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt macro_tt_raw ctfe_crc
|
||||||
|
|
||||||
.PHONY: all build dev debug test clean clean-all test-examples test-examples-smoke selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs bench test-apps test-dwarf test-selfhost-smoke test-unit test-linux-targets ensure-buxc
|
.PHONY: all build dev debug test clean clean-all test-examples test-examples-smoke selfhost test-golden test-errors test-stdlib selfhost-loop lsp vscode vscode-package fmt-check docs bench test-apps test-dwarf test-selfhost-smoke test-unit test-linux-targets test-freestanding ensure-buxc
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ debug: dev
|
|||||||
@echo "Debug binary: buxc_debug"
|
@echo "Debug binary: buxc_debug"
|
||||||
|
|
||||||
# Full local / sequential suite (same coverage as split CI jobs combined).
|
# Full local / sequential suite (same coverage as split CI jobs combined).
|
||||||
test: build fmt-check test-examples test-errors test-stdlib test-registry test-dwarf test-drop-move test-linux-targets test-apps test-selfhost-smoke test-unit
|
test: build fmt-check test-examples test-errors test-stdlib test-registry test-dwarf test-drop-move test-linux-targets test-freestanding test-apps test-selfhost-smoke test-unit
|
||||||
|
|
||||||
# Nim unit tests + tiny CLI smoke (needs Nim + buxc).
|
# Nim unit tests + tiny CLI smoke (needs Nim + buxc).
|
||||||
test-unit: ensure-buxc
|
test-unit: ensure-buxc
|
||||||
@@ -140,6 +140,12 @@ test-errors: ensure-buxc
|
|||||||
@chmod +x tests/error_golden/run.sh
|
@chmod +x tests/error_golden/run.sh
|
||||||
@tests/error_golden/run.sh ./$(OUT)
|
@tests/error_golden/run.sh ./$(OUT)
|
||||||
|
|
||||||
|
.PHONY: test-error-recovery
|
||||||
|
test-error-recovery: ensure-buxc selfhost
|
||||||
|
@echo "=== Selfhost multi-error recovery test ==="
|
||||||
|
@chmod +x _test_error_recovery/run.sh
|
||||||
|
@_test_error_recovery/run.sh
|
||||||
|
|
||||||
test-stdlib: ensure-buxc
|
test-stdlib: ensure-buxc
|
||||||
@echo "=== Stdlib golden tests ==="
|
@echo "=== Stdlib golden tests ==="
|
||||||
@chmod +x tests/stdlib_golden/run.sh
|
@chmod +x tests/stdlib_golden/run.sh
|
||||||
@@ -189,6 +195,19 @@ lsp: tools/bux-lsp
|
|||||||
tools/bux-lsp: tools/lsp_server.nim bootstrap/*.nim
|
tools/bux-lsp: tools/lsp_server.nim bootstrap/*.nim
|
||||||
cd tools && $(NIM) c -d:release --opt:size --path:../bootstrap -o:bux-lsp lsp_server.nim
|
cd tools && $(NIM) c -d:release --opt:size --path:../bootstrap -o:bux-lsp lsp_server.nim
|
||||||
|
|
||||||
|
# VS Code extension (syntax + LSP client). Requires Node.js/npm.
|
||||||
|
.PHONY: vscode vscode-package
|
||||||
|
vscode: lsp
|
||||||
|
@cd vscode && npm install --silent && npm run compile
|
||||||
|
@echo "VS Code extension compiled → vscode/out/"
|
||||||
|
@echo " Dev: open vscode/ in VS Code and press F5"
|
||||||
|
@echo " Or: code --install-extension \$$(pwd)/vscode (after npm i && compile)"
|
||||||
|
@echo " LSP: tools/bux-lsp (auto-discovered)"
|
||||||
|
|
||||||
|
vscode-package: vscode
|
||||||
|
@cd vscode && npx --yes @vscode/vsce package --no-dependencies
|
||||||
|
@echo "VSIX: vscode/bux-lang-$$(node -p \"require('./vscode/package.json').version\").vsix"
|
||||||
|
|
||||||
.PHONY: test-lsp
|
.PHONY: test-lsp
|
||||||
test-lsp: lsp
|
test-lsp: lsp
|
||||||
@echo "=== LSP unit (locals / inference) ==="
|
@echo "=== LSP unit (locals / inference) ==="
|
||||||
@@ -196,6 +215,12 @@ test-lsp: lsp
|
|||||||
@echo "=== LSP hover smoke ==="
|
@echo "=== LSP hover smoke ==="
|
||||||
@chmod +x tools/smoke_lsp_hover.sh
|
@chmod +x tools/smoke_lsp_hover.sh
|
||||||
@tools/smoke_lsp_hover.sh
|
@tools/smoke_lsp_hover.sh
|
||||||
|
@echo "=== LSP diagnostics (error underlines) smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_diagnostics.sh
|
||||||
|
@tools/smoke_lsp_diagnostics.sh
|
||||||
|
@echo "=== LSP formatting smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_formatting.sh
|
||||||
|
@tools/smoke_lsp_formatting.sh
|
||||||
@echo "=== LSP references / rename smoke ==="
|
@echo "=== LSP references / rename smoke ==="
|
||||||
@chmod +x tools/smoke_lsp_rename.sh
|
@chmod +x tools/smoke_lsp_rename.sh
|
||||||
@tools/smoke_lsp_rename.sh
|
@tools/smoke_lsp_rename.sh
|
||||||
@@ -272,6 +297,13 @@ test-linux-targets: ensure-buxc
|
|||||||
@chmod +x tools/smoke_linux_targets.sh
|
@chmod +x tools/smoke_linux_targets.sh
|
||||||
@tools/smoke_linux_targets.sh
|
@tools/smoke_linux_targets.sh
|
||||||
|
|
||||||
|
# post-1.0 — freestanding runtime (-ffreestanding object + BUX_RUNTIME=freestanding package)
|
||||||
|
.PHONY: test-freestanding
|
||||||
|
test-freestanding: ensure-buxc
|
||||||
|
@echo "=== Freestanding runtime smoke ==="
|
||||||
|
@chmod +x tools/smoke_freestanding.sh
|
||||||
|
@tools/smoke_freestanding.sh
|
||||||
|
|
||||||
# Session 78 — Nexus HTTPS (self-signed) smoke
|
# Session 78 — Nexus HTTPS (self-signed) smoke
|
||||||
.PHONY: test-nexus-tls
|
.PHONY: test-nexus-tls
|
||||||
test-nexus-tls: ensure-buxc
|
test-nexus-tls: ensure-buxc
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
# Bux Programming Language — Roadmap to v1.0.0
|
# Bux Programming Language — Roadmap to v1.0.0
|
||||||
|
|
||||||
> **Version:** 0.3.1 (2026-06-06)
|
> **Historical document.** Language **v1.0.0** shipped 2026-07-27 (tag `v1.0.0`).
|
||||||
|
> For current status use: [`README.md`](README.md), [`docs/LanguageRef.md`](docs/LanguageRef.md),
|
||||||
|
> [`docs/RELEASE_v1.0.0.md`](docs/RELEASE_v1.0.0.md), [`docs/QUALITY_PLAN.md`](docs/QUALITY_PLAN.md).
|
||||||
|
>
|
||||||
|
> **Version (this file):** frozen archive of pre-1.0 planning
|
||||||
> **Bootstrap:** Nim (`bootstrap/`) — compiles `src/` → `buxc`
|
> **Bootstrap:** Nim (`bootstrap/`) — compiles `src/` → `buxc`
|
||||||
> **Self-host:** Bux (`src/`) — compiles via `buxc` → `buxc2`
|
> **Self-host:** Bux (`src/`) — compiles via `buxc` → `buxc2`
|
||||||
> **Target:** Bux v1.0.0 — fully self-hosting, gradual ownership, tooling
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
> **Status:** **v1.0.0** — language freeze. Bootstrap (`buxc`, Nim) and self-hosted (`buxc2`, Bux) both compile `.bux` → C → native binary.
|
> **Status:** **v1.0.2** — language freeze (1.0) + patch fixes. Bootstrap (`buxc`, Nim) and self-hosted (`buxc2`, Bux) both compile `.bux` → C → native binary.
|
||||||
> **Selfhost loop:** deterministic C codegen + ELF verified.
|
> **Selfhost loop:** deterministic C codegen + ELF verified.
|
||||||
> **Gradual Ownership:** `@[Checked]` borrow checker, `@[Release]` zero-cost mode, `borrow &mut` expressions.
|
> **Gradual Ownership:** `@[Checked]` borrow checker, `@[Release]` zero-cost mode, `borrow &mut` expressions.
|
||||||
> **Closures:** multi-instance capturing closures via fat function pointers (`BuxFn { code, env }`) in both compilers.
|
> **Closures:** multi-instance capturing closures via fat function pointers (`BuxFn { code, env }`) in both compilers.
|
||||||
@@ -247,7 +247,7 @@ func Main() -> int {
|
|||||||
| **Package Manager** | `bux add`, `bux install`, `bux.lock`, path + git deps |
|
| **Package Manager** | `bux add`, `bux install`, `bux.lock`, path + git deps |
|
||||||
| **Cross-Compilation** | `--target <triple>` via clang (e.g. `aarch64-linux-gnu`) |
|
| **Cross-Compilation** | `--target <triple>` via clang (e.g. `aarch64-linux-gnu`) |
|
||||||
| **Diagnostics** | Rust-style snippets, multi-char underlines, `= help:` hints |
|
| **Diagnostics** | Rust-style snippets, multi-char underlines, `= help:` hints |
|
||||||
| **Tooling** | `bux new/build/run/test/check/fmt/doc`, LSP 0.4.0 (locals + inferred lets) |
|
| **Tooling** | `bux new/build/run/test/check/fmt/doc`, **LSP 0.17** (live error squiggles, hover, rename, hierarchies), **VS Code** extension (`vscode/`) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -284,26 +284,43 @@ bux/
|
|||||||
├── tests/ # 🧪 Unit tests (Nim)
|
├── tests/ # 🧪 Unit tests (Nim)
|
||||||
├── examples/ # Example programs
|
├── examples/ # Example programs
|
||||||
├── apps/ # Real-world applications
|
├── apps/ # Real-world applications
|
||||||
├── docs/ # Documentation
|
├── docs/ # Documentation (LanguageRef = v1.0 normative)
|
||||||
|
├── tools/ # bux-lsp, smoke scripts, benches
|
||||||
|
├── vscode/ # VS Code extension (syntax + LSP client)
|
||||||
├── README.md
|
├── README.md
|
||||||
├── PLAN.md # Roadmap to v1.0.0
|
├── PLAN.md # Historical roadmap (pre-1.0)
|
||||||
└── Makefile
|
└── Makefile
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Language Server & VS Code
|
||||||
|
|
||||||
|
**Yes — Bux has an LSP** (`bux-lsp` **0.17**): **live error underlines**, hover, go-to-def, rename, refs, call/type hierarchy. Docs: [`docs/LSP.md`](docs/LSP.md).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make lsp # build tools/bux-lsp (stdio JSON-RPC — any editor)
|
||||||
|
make test-lsp # smoke suite
|
||||||
|
make vscode # VS Code extension (syntax + client; auto-finds tools/bux-lsp)
|
||||||
|
# Open the repo in VS Code, or F5 from vscode/
|
||||||
|
# Settings: bux.lsp.path, bux.lsp.enabled · vscode/README.md
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
| Doc | Description |
|
| Doc | Description |
|
||||||
|-----|-------------|
|
|-----|-------------|
|
||||||
| [`docs/LanguageRef.md`](docs/LanguageRef.md) | Language reference |
|
| [`docs/LanguageRef.md`](docs/LanguageRef.md) | **Normative** language reference (v1.0) |
|
||||||
| [`docs/Stdlib.md`](docs/Stdlib.md) | Standard library API |
|
| [`docs/Stdlib.md`](docs/Stdlib.md) | Standard library API |
|
||||||
| [`docs/BuildAndTest.md`](docs/BuildAndTest.md) | Build, test, and tooling |
|
| [`docs/BuildAndTest.md`](docs/BuildAndTest.md) | Build, test, cross, tooling |
|
||||||
| [`docs/QUALITY_PLAN.md`](docs/QUALITY_PLAN.md) | Roadmap toward a “good” v1.0 |
|
|
||||||
| [`docs/Packages.md`](docs/Packages.md) | Package manager + registry |
|
| [`docs/Packages.md`](docs/Packages.md) | Package manager + registry |
|
||||||
| [`docs/SEMVER.md`](docs/SEMVER.md) | Versioning policy |
|
| [`docs/SEMVER.md`](docs/SEMVER.md) | Semver policy (**active** post-1.0) |
|
||||||
| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Feature status (constructs) |
|
| [`docs/RELEASE_v1.0.0.md`](docs/RELEASE_v1.0.0.md) | v1.0.0 freeze notes |
|
||||||
| [`PLAN.md`](PLAN.md) | Long-form phase plan |
|
| [`docs/RELEASE_v1.0.2.md`](docs/RELEASE_v1.0.2.md) | v1.0.2 patch (stdlib grow + checked div) |
|
||||||
|
| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Language construct status |
|
||||||
|
| [`docs/QUALITY_PLAN.md`](docs/QUALITY_PLAN.md) | Session history + path to v1.0 (archive) |
|
||||||
|
| [`vscode/README.md`](vscode/README.md) | VS Code extension install & settings |
|
||||||
|
| [`PLAN.md`](PLAN.md) | Historical phase plan (pre-1.0) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "call_args"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
extern func PrintInt(n: int);
|
||||||
|
extern func PrintLine(s: String);
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
PrintInt("hello");
|
||||||
|
PrintLine(42);
|
||||||
|
PrintInt(true);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "error_recovery"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression: buxc2 must report ALL independent semantic errors in one run.
|
||||||
|
set -euo pipefail
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ROOT="$(cd "$HERE/.." && pwd)"
|
||||||
|
BUXC2="$ROOT/build/selfhost/build/buxc2"
|
||||||
|
export BUX_STDLIB="$ROOT/lib"
|
||||||
|
|
||||||
|
if [[ ! -x "$BUXC2" ]]; then
|
||||||
|
echo "=== building selfhost (buxc2) ==="
|
||||||
|
(cd "$ROOT" && make selfhost)
|
||||||
|
fi
|
||||||
|
if [[ ! -x "$BUXC2" ]]; then
|
||||||
|
echo "error: buxc2 not found at $BUXC2" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
out="$(cd "$HERE" && "$BUXC2" check src/Main.bux 2>&1 || true)"
|
||||||
|
echo "$out"
|
||||||
|
|
||||||
|
grep -q "cannot assign String to int" <<<"$out"
|
||||||
|
grep -q "cannot assign int to bool" <<<"$out"
|
||||||
|
grep -q "undeclared identifier 'undefined_variable'" <<<"$out"
|
||||||
|
|
||||||
|
echo "PASS: all independent semantic errors reported in one run"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
func Main() -> int {
|
||||||
|
let x: int = "hello";
|
||||||
|
let y: bool = 42;
|
||||||
|
let z: int = undefined_variable;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "parse_recovery"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
func Good() -> int {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bad() -> int {
|
||||||
|
let x: int = ;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
func AlsoBad() -> int {
|
||||||
|
let y: int = 5 + ;
|
||||||
|
let z: int = 6;
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
func AlsoGood() -> int {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
@@ -462,6 +462,7 @@ type
|
|||||||
of dkEnum:
|
of dkEnum:
|
||||||
declEnumName*: string
|
declEnumName*: string
|
||||||
declEnumBaseType*: TypeExpr
|
declEnumBaseType*: TypeExpr
|
||||||
|
declEnumTypeParams*: seq[TypeParam]
|
||||||
declEnumVariants*: seq[EnumVariant]
|
declEnumVariants*: seq[EnumVariant]
|
||||||
of dkUnion:
|
of dkUnion:
|
||||||
declUnionName*: string
|
declUnionName*: string
|
||||||
|
|||||||
+11
-1
@@ -256,6 +256,12 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
|
|||||||
of hBinary:
|
of hBinary:
|
||||||
let left = be.emitExpr(node.binaryLeft)
|
let left = be.emitExpr(node.binaryLeft)
|
||||||
let right = be.emitExpr(node.binaryRight)
|
let right = be.emitExpr(node.binaryRight)
|
||||||
|
# Checked integer / and % (float keeps raw C operators)
|
||||||
|
let isFloat = node.typ != nil and node.typ.kind in {tkFloat32, tkFloat64}
|
||||||
|
if node.binaryOp == tkSlash and not isFloat:
|
||||||
|
return &"bux_div_i64((int64_t)({left}), (int64_t)({right}))"
|
||||||
|
if node.binaryOp == tkPercent and not isFloat:
|
||||||
|
return &"bux_mod_i64((int64_t)({left}), (int64_t)({right}))"
|
||||||
let op = operatorToC(node.binaryOp)
|
let op = operatorToC(node.binaryOp)
|
||||||
return &"({left} {op} {right})"
|
return &"({left} {op} {right})"
|
||||||
|
|
||||||
@@ -346,7 +352,8 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
|
|||||||
return &"(({typ}){operand})"
|
return &"(({typ}){operand})"
|
||||||
|
|
||||||
of hIs:
|
of hIs:
|
||||||
return "true" # TODO: proper type checking
|
# Should be desugared in hir_lower; keep false if any residual hIs remains
|
||||||
|
return "false"
|
||||||
|
|
||||||
of hSizeOf:
|
of hSizeOf:
|
||||||
let typ = typeToC(be, node.sizeOfType)
|
let typ = typeToC(be, node.sizeOfType)
|
||||||
@@ -657,6 +664,9 @@ proc emitModule*(be: var CBackend, module: HirModule): string =
|
|||||||
be.emitLine("#include <stdbool.h>")
|
be.emitLine("#include <stdbool.h>")
|
||||||
be.emitLine("#include <string.h>")
|
be.emitLine("#include <string.h>")
|
||||||
be.emitLine("")
|
be.emitLine("")
|
||||||
|
be.emitLine("extern int64_t bux_div_i64(int64_t a, int64_t b);")
|
||||||
|
be.emitLine("extern int64_t bux_mod_i64(int64_t a, int64_t b);")
|
||||||
|
be.emitLine("")
|
||||||
|
|
||||||
# Pre-collect slice types so we can emit forward declarations early
|
# Pre-collect slice types so we can emit forward declarations early
|
||||||
let sliceTypes = collectSliceTypes(module)
|
let sliceTypes = collectSliceTypes(module)
|
||||||
|
|||||||
+9
-4
@@ -16,6 +16,7 @@ type
|
|||||||
RuntimeFlavor* = enum
|
RuntimeFlavor* = enum
|
||||||
rfFull ## rt/runtime.c — POSIX + OpenSSL
|
rfFull ## rt/runtime.c — POSIX + OpenSSL
|
||||||
rfMinimal ## rt/runtime_minimal.c — thin, static/container/embed friendly
|
rfMinimal ## rt/runtime_minimal.c — thin, static/container/embed friendly
|
||||||
|
rfFreestanding ## rt/runtime_freestanding.c — no-libc research spike
|
||||||
rfWin ## rt/runtime_win.c — Windows/MinGW (historical)
|
rfWin ## rt/runtime_win.c — Windows/MinGW (historical)
|
||||||
|
|
||||||
GlobalOptions* = object
|
GlobalOptions* = object
|
||||||
@@ -63,7 +64,7 @@ Registry / toolchain env:
|
|||||||
BUX_REGISTRY_INSECURE=1 Allow self-signed HTTPS registry (dev/smoke)
|
BUX_REGISTRY_INSECURE=1 Allow self-signed HTTPS registry (dev/smoke)
|
||||||
BUX_CFLAGS Extra flags appended to the C compiler line
|
BUX_CFLAGS Extra flags appended to the C compiler line
|
||||||
BUX_CC C compiler binary (overrides --target pick)
|
BUX_CC C compiler binary (overrides --target pick)
|
||||||
BUX_RUNTIME full|minimal|thin|embed|win (default: full on Unix)
|
BUX_RUNTIME full|minimal|thin|embed|freestanding|win (default: full on Unix)
|
||||||
BUX_STATIC=1 Same as --static
|
BUX_STATIC=1 Same as --static
|
||||||
|
|
||||||
Global options:
|
Global options:
|
||||||
@@ -136,8 +137,10 @@ proc resolveRuntimeFlavor(opts: GlobalOptions): RuntimeFlavor =
|
|||||||
case e
|
case e
|
||||||
of "full", "posix":
|
of "full", "posix":
|
||||||
return rfFull
|
return rfFull
|
||||||
of "minimal", "thin", "embed", "embedded", "freestanding":
|
of "minimal", "thin", "embed", "embedded":
|
||||||
return rfMinimal
|
return rfMinimal
|
||||||
|
of "freestanding", "bare", "nolibc":
|
||||||
|
return rfFreestanding
|
||||||
of "win", "windows":
|
of "win", "windows":
|
||||||
return rfWin
|
return rfWin
|
||||||
of "":
|
of "":
|
||||||
@@ -159,10 +162,12 @@ proc runtimeFileName(flavor: RuntimeFlavor): string =
|
|||||||
case flavor
|
case flavor
|
||||||
of rfFull: "runtime.c"
|
of rfFull: "runtime.c"
|
||||||
of rfMinimal: "runtime_minimal.c"
|
of rfMinimal: "runtime_minimal.c"
|
||||||
|
of rfFreestanding: "runtime_freestanding.c"
|
||||||
of rfWin: "runtime_win.c"
|
of rfWin: "runtime_win.c"
|
||||||
|
|
||||||
proc isThinRuntime(flavor: RuntimeFlavor): bool =
|
proc isThinRuntime(flavor: RuntimeFlavor): bool =
|
||||||
flavor in {rfMinimal, rfWin}
|
## Thin = no OpenSSL / pthread link needs
|
||||||
|
flavor in {rfMinimal, rfFreestanding, rfWin}
|
||||||
|
|
||||||
proc findOnPath(bin: string): bool =
|
proc findOnPath(bin: string): bool =
|
||||||
## True if `bin` resolves as an executable on PATH (or is an absolute path).
|
## True if `bin` resolves as an executable on PATH (or is an absolute path).
|
||||||
@@ -1345,7 +1350,7 @@ proc cmdDoc*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
proc cmdVersion*(args: seq[string], opts: GlobalOptions): int =
|
proc cmdVersion*(args: seq[string], opts: GlobalOptions): int =
|
||||||
echo "bux 1.0.0 (bootstrap)"
|
echo "bux 1.0.2 (bootstrap)"
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
proc runCli*(args: seq[string]): int =
|
proc runCli*(args: seq[string]): int =
|
||||||
|
|||||||
+457
-31
@@ -18,6 +18,9 @@ type
|
|||||||
generatedStructInsts*: Table[string, bool] # Track generated struct instantiations
|
generatedStructInsts*: Table[string, bool] # Track generated struct instantiations
|
||||||
extraStructs*: seq[tuple[name: string, fields: seq[tuple[name: string, typ: Type]]]]
|
extraStructs*: seq[tuple[name: string, fields: seq[tuple[name: string, typ: Type]]]]
|
||||||
structInstMap*: Table[string, tuple[baseName: string, typeArgs: seq[Type]]] # Mangled name -> base + args
|
structInstMap*: Table[string, tuple[baseName: string, typeArgs: seq[Type]]] # Mangled name -> base + args
|
||||||
|
genericEnums*: Table[string, Decl] # Generic enum declarations
|
||||||
|
generatedEnumInsts*: Table[string, bool] # Track generated enum instantiations
|
||||||
|
extraEnums*: seq[tuple[name: string, variants: seq[HirEnumVariant]]]
|
||||||
genericFuncs*: Table[string, Decl] # Generic function declarations
|
genericFuncs*: Table[string, Decl] # Generic function declarations
|
||||||
generatedFuncInsts*: Table[string, bool] # Track generated function instantiations
|
generatedFuncInsts*: Table[string, bool] # Track generated function instantiations
|
||||||
extraFuncs*: seq[HirFunc] # Monomorphized generic methods
|
extraFuncs*: seq[HirFunc] # Monomorphized generic methods
|
||||||
@@ -38,6 +41,8 @@ type
|
|||||||
## Locals whose value was moved into another owner (struct field, let, return).
|
## Locals whose value was moved into another owner (struct field, let, return).
|
||||||
## Auto-Drop is skipped for these (session 37 — field-move ownership).
|
## Auto-Drop is skipped for these (session 37 — field-move ownership).
|
||||||
movedOutLocals*: HashSet[string]
|
movedOutLocals*: HashSet[string]
|
||||||
|
## Current let/var init type (for inferring generic enum concrete names in struct inits)
|
||||||
|
currentInitTypeExpr*: TypeExpr
|
||||||
## Partial field moves: local → dotted paths moved out by value
|
## Partial field moves: local → dotted paths moved out by value
|
||||||
## (e.g. "items", "inner.items" for nested `a.b.c` — session 70/73).
|
## (e.g. "items", "inner.items" for nested `a.b.c` — session 70/73).
|
||||||
## When parent Type_Drop is skipped, remaining droppable fields still Drop.
|
## When parent Type_Drop is skipped, remaining droppable fields still Drop.
|
||||||
@@ -876,6 +881,9 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx =
|
|||||||
result.generatedStructInsts = initTable[string, bool]()
|
result.generatedStructInsts = initTable[string, bool]()
|
||||||
result.extraStructs = @[]
|
result.extraStructs = @[]
|
||||||
result.structInstMap = initTable[string, tuple[baseName: string, typeArgs: seq[Type]]]()
|
result.structInstMap = initTable[string, tuple[baseName: string, typeArgs: seq[Type]]]()
|
||||||
|
result.genericEnums = initTable[string, Decl]()
|
||||||
|
result.generatedEnumInsts = initTable[string, bool]()
|
||||||
|
result.extraEnums = @[]
|
||||||
result.genericFuncs = initTable[string, Decl]()
|
result.genericFuncs = initTable[string, Decl]()
|
||||||
result.generatedFuncInsts = initTable[string, bool]()
|
result.generatedFuncInsts = initTable[string, bool]()
|
||||||
result.extraFuncs = @[]
|
result.extraFuncs = @[]
|
||||||
@@ -970,6 +978,56 @@ proc substituteType(ctx: var LowerCtx, te: TypeExpr, subst: Table[string, Type])
|
|||||||
ctx.generatedStructInsts[mangledName] = true
|
ctx.generatedStructInsts[mangledName] = true
|
||||||
ctx.structInstMap[mangledName] = (te.typeName, concreteArgs)
|
ctx.structInstMap[mangledName] = (te.typeName, concreteArgs)
|
||||||
return makeNamed(mangledName)
|
return makeNamed(mangledName)
|
||||||
|
if te.typeArgs.len > 0 and ctx.genericEnums.hasKey(te.typeName):
|
||||||
|
var suffix = ""
|
||||||
|
for i, arg in te.typeArgs:
|
||||||
|
if i > 0: suffix.add("_")
|
||||||
|
let argType = substituteType(ctx, arg, subst)
|
||||||
|
suffix.add(argType.toString)
|
||||||
|
let mangledName = te.typeName & "_" & suffix
|
||||||
|
if not ctx.generatedEnumInsts.hasKey(mangledName):
|
||||||
|
let genericDecl = ctx.genericEnums[te.typeName]
|
||||||
|
var hasUnresolved = false
|
||||||
|
for arg in te.typeArgs:
|
||||||
|
let argType = substituteType(ctx, arg, subst)
|
||||||
|
for tp in genericDecl.declEnumTypeParams:
|
||||||
|
if argType.kind == tkNamed and argType.name == tp.name:
|
||||||
|
hasUnresolved = true
|
||||||
|
break
|
||||||
|
if hasUnresolved: break
|
||||||
|
if not hasUnresolved:
|
||||||
|
var localSubst = subst
|
||||||
|
for j, tp in genericDecl.declEnumTypeParams:
|
||||||
|
if j < te.typeArgs.len:
|
||||||
|
localSubst[tp.name] = substituteType(ctx, te.typeArgs[j], subst)
|
||||||
|
var variants: seq[HirEnumVariant] = @[]
|
||||||
|
for v in genericDecl.declEnumVariants:
|
||||||
|
var fields: seq[Type] = @[]
|
||||||
|
for f in v.fields:
|
||||||
|
fields.add(if f != nil: substituteType(ctx, f, localSubst) else: makeUnknown())
|
||||||
|
var namedFields: seq[tuple[name: string, typ: Type]] = @[]
|
||||||
|
for nf in v.namedFields:
|
||||||
|
let fType = if nf.ftype != nil: substituteType(ctx, nf.ftype, localSubst) else: makeUnknown()
|
||||||
|
namedFields.add((nf.name, fType))
|
||||||
|
variants.add(HirEnumVariant(name: v.name, fields: fields, namedFields: namedFields))
|
||||||
|
if fields.len > 1:
|
||||||
|
var nestedFields: seq[tuple[name: string, typ: Type]] = @[]
|
||||||
|
for i, ft in fields:
|
||||||
|
nestedFields.add((v.name & "_" & $i, ft))
|
||||||
|
let nestedName = mangledName & "_" & v.name & "_Payload"
|
||||||
|
ctx.extraStructs.add((nestedName, nestedFields))
|
||||||
|
elif namedFields.len > 0:
|
||||||
|
var nestedFields: seq[tuple[name: string, typ: Type]] = @[]
|
||||||
|
for nf in namedFields:
|
||||||
|
nestedFields.add((nf.name, nf.typ))
|
||||||
|
let nestedName = mangledName & "_" & v.name & "_Payload"
|
||||||
|
ctx.extraStructs.add((nestedName, nestedFields))
|
||||||
|
ctx.extraEnums.add((mangledName, variants))
|
||||||
|
ctx.generatedEnumInsts[mangledName] = true
|
||||||
|
var concreteArgs: seq[Type] = @[]
|
||||||
|
for arg in te.typeArgs: concreteArgs.add(ctx.resolveTypeExpr(arg))
|
||||||
|
ctx.structInstMap[mangledName] = (te.typeName, concreteArgs)
|
||||||
|
return makeNamed(mangledName)
|
||||||
return ctx.resolveTypeExpr(te)
|
return ctx.resolveTypeExpr(te)
|
||||||
of tekOwn:
|
of tekOwn:
|
||||||
return substituteType(ctx, te.pointerPointee, subst)
|
return substituteType(ctx, te.pointerPointee, subst)
|
||||||
@@ -1029,6 +1087,56 @@ proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type =
|
|||||||
ctx.generatedStructInsts[mangledName] = true
|
ctx.generatedStructInsts[mangledName] = true
|
||||||
ctx.structInstMap[mangledName] = (te.typeName, concreteArgs)
|
ctx.structInstMap[mangledName] = (te.typeName, concreteArgs)
|
||||||
return makeNamed(mangledName)
|
return makeNamed(mangledName)
|
||||||
|
if te.typeArgs.len > 0 and ctx.genericEnums.hasKey(te.typeName):
|
||||||
|
var suffix = ""
|
||||||
|
for i, arg in te.typeArgs:
|
||||||
|
if i > 0: suffix.add("_")
|
||||||
|
let argType = ctx.resolveTypeExpr(arg)
|
||||||
|
suffix.add(argType.toString)
|
||||||
|
let mangledName = te.typeName & "_" & suffix
|
||||||
|
if not ctx.generatedEnumInsts.hasKey(mangledName):
|
||||||
|
let genericDecl = ctx.genericEnums[te.typeName]
|
||||||
|
var hasUnresolved = false
|
||||||
|
for arg in te.typeArgs:
|
||||||
|
let argType = ctx.resolveTypeExpr(arg)
|
||||||
|
for tp in genericDecl.declEnumTypeParams:
|
||||||
|
if argType.kind == tkNamed and argType.name == tp.name:
|
||||||
|
hasUnresolved = true
|
||||||
|
break
|
||||||
|
if hasUnresolved: break
|
||||||
|
if not hasUnresolved:
|
||||||
|
var subst = initTable[string, Type]()
|
||||||
|
for j, tp in genericDecl.declEnumTypeParams:
|
||||||
|
if j < te.typeArgs.len:
|
||||||
|
subst[tp.name] = ctx.resolveTypeExpr(te.typeArgs[j])
|
||||||
|
var variants: seq[HirEnumVariant] = @[]
|
||||||
|
for v in genericDecl.declEnumVariants:
|
||||||
|
var fields: seq[Type] = @[]
|
||||||
|
for f in v.fields:
|
||||||
|
fields.add(if f != nil: substituteType(ctx, f, subst) else: makeUnknown())
|
||||||
|
var namedFields: seq[tuple[name: string, typ: Type]] = @[]
|
||||||
|
for nf in v.namedFields:
|
||||||
|
let fType = if nf.ftype != nil: substituteType(ctx, nf.ftype, subst) else: makeUnknown()
|
||||||
|
namedFields.add((nf.name, fType))
|
||||||
|
variants.add(HirEnumVariant(name: v.name, fields: fields, namedFields: namedFields))
|
||||||
|
if fields.len > 1:
|
||||||
|
var nestedFields: seq[tuple[name: string, typ: Type]] = @[]
|
||||||
|
for i, ft in fields:
|
||||||
|
nestedFields.add((v.name & "_" & $i, ft))
|
||||||
|
let nestedName = mangledName & "_" & v.name & "_Payload"
|
||||||
|
ctx.extraStructs.add((nestedName, nestedFields))
|
||||||
|
elif namedFields.len > 0:
|
||||||
|
var nestedFields: seq[tuple[name: string, typ: Type]] = @[]
|
||||||
|
for nf in namedFields:
|
||||||
|
nestedFields.add((nf.name, nf.typ))
|
||||||
|
let nestedName = mangledName & "_" & v.name & "_Payload"
|
||||||
|
ctx.extraStructs.add((nestedName, nestedFields))
|
||||||
|
ctx.extraEnums.add((mangledName, variants))
|
||||||
|
ctx.generatedEnumInsts[mangledName] = true
|
||||||
|
var concreteArgs2: seq[Type] = @[]
|
||||||
|
for arg in te.typeArgs: concreteArgs2.add(ctx.resolveTypeExpr(arg))
|
||||||
|
ctx.structInstMap[mangledName] = (te.typeName, concreteArgs2)
|
||||||
|
return makeNamed(mangledName)
|
||||||
case te.typeName
|
case te.typeName
|
||||||
of "void": return makeVoid()
|
of "void": return makeVoid()
|
||||||
of "bool": return makeBool()
|
of "bool": return makeBool()
|
||||||
@@ -1171,23 +1279,39 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
|||||||
# Check if this is a _Data union field access
|
# Check if this is a _Data union field access
|
||||||
if objType.name.endsWith("_Data"):
|
if objType.name.endsWith("_Data"):
|
||||||
let enumName = objType.name[0..^6]
|
let enumName = objType.name[0..^6]
|
||||||
let enumSym = ctx.globalScope.lookup(enumName)
|
var enumSym = ctx.globalScope.lookup(enumName)
|
||||||
|
var enumDecl: Decl = nil
|
||||||
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
|
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
|
||||||
for variant in enumSym.decl.declEnumVariants:
|
enumDecl = enumSym.decl
|
||||||
|
elif ctx.structInstMap.hasKey(enumName):
|
||||||
|
let (baseName, typeArgs) = ctx.structInstMap[enumName]
|
||||||
|
let baseSym = ctx.globalScope.lookup(baseName)
|
||||||
|
if baseSym != nil and baseSym.decl != nil and baseSym.decl.kind == dkEnum:
|
||||||
|
enumDecl = baseSym.decl
|
||||||
|
if enumDecl != nil:
|
||||||
|
var subst = initTable[string, Type]()
|
||||||
|
if ctx.structInstMap.hasKey(enumName):
|
||||||
|
var ti: int = 0
|
||||||
|
for tp in enumDecl.declEnumTypeParams:
|
||||||
|
if ti < ctx.structInstMap[enumName][1].len:
|
||||||
|
subst[tp.name] = ctx.structInstMap[enumName][1][ti]
|
||||||
|
ti = ti + 1
|
||||||
|
for variant in enumDecl.declEnumVariants:
|
||||||
for i, f in variant.fields:
|
for i, f in variant.fields:
|
||||||
let fieldName = variant.name & "_" & $i
|
let fieldName = variant.name & "_" & $i
|
||||||
if fieldName == expr.exprFieldName:
|
if fieldName == expr.exprFieldName:
|
||||||
return ctx.resolveTypeExpr(f)
|
return substituteType(ctx, f, subst)
|
||||||
for nf in variant.namedFields:
|
for nf in variant.namedFields:
|
||||||
if nf.name == expr.exprFieldName:
|
if nf.name == expr.exprFieldName:
|
||||||
return ctx.resolveTypeExpr(nf.ftype)
|
return substituteType(ctx, nf.ftype, subst)
|
||||||
var sym = ctx.globalScope.lookup(objType.name)
|
var sym = ctx.globalScope.lookup(objType.name)
|
||||||
var decl = if sym != nil: sym.decl else: nil
|
var decl = if sym != nil: sym.decl else: nil
|
||||||
# If the type is a monomorphized generic struct instance, look up the base
|
# If the type is a monomorphized generic struct instance, look up the base
|
||||||
if decl == nil and ctx.structInstMap.hasKey(objType.name):
|
if decl == nil and ctx.structInstMap.hasKey(objType.name):
|
||||||
let (baseName, typeArgs) = ctx.structInstMap[objType.name]
|
let (baseName, typeArgs) = ctx.structInstMap[objType.name]
|
||||||
let baseSym = ctx.globalScope.lookup(baseName)
|
let baseSym = ctx.globalScope.lookup(baseName)
|
||||||
if baseSym != nil and baseSym.decl != nil and baseSym.decl.kind == dkStruct:
|
if baseSym != nil and baseSym.decl != nil:
|
||||||
|
if baseSym.decl.kind == dkStruct:
|
||||||
decl = baseSym.decl
|
decl = baseSym.decl
|
||||||
var subst = initTable[string, Type]()
|
var subst = initTable[string, Type]()
|
||||||
for i, tp in decl.declStructTypeParams:
|
for i, tp in decl.declStructTypeParams:
|
||||||
@@ -1212,6 +1336,12 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
|||||||
of tekOwn, tekPointer:
|
of tekOwn, tekPointer:
|
||||||
return substituteType(ctx, f.ftype, subst)
|
return substituteType(ctx, f.ftype, subst)
|
||||||
else: return makeUnknown()
|
else: return makeUnknown()
|
||||||
|
elif baseSym.decl.kind == dkEnum:
|
||||||
|
# Generated enum struct: fields are tag and data
|
||||||
|
if expr.exprFieldName == "tag":
|
||||||
|
return makeNamed(objType.name & "_Tag")
|
||||||
|
if expr.exprFieldName == "data":
|
||||||
|
return makeNamed(objType.name & "_Data")
|
||||||
if decl != nil:
|
if decl != nil:
|
||||||
case decl.kind
|
case decl.kind
|
||||||
of dkStruct:
|
of dkStruct:
|
||||||
@@ -1849,6 +1979,16 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
|||||||
let argType = ctx.resolveTypeExpr(targ)
|
let argType = ctx.resolveTypeExpr(targ)
|
||||||
suffix.add(argType.toString)
|
suffix.add(argType.toString)
|
||||||
structName = structName & "_" & suffix
|
structName = structName & "_" & suffix
|
||||||
|
elif ctx.currentInitTypeExpr != nil and ctx.currentInitTypeExpr.kind == tekNamed and
|
||||||
|
ctx.currentInitTypeExpr.typeName == structName and
|
||||||
|
ctx.currentInitTypeExpr.typeArgs.len > 0:
|
||||||
|
# Infer type args from enclosing let/var declaration
|
||||||
|
var suffix = ""
|
||||||
|
for i, targ in ctx.currentInitTypeExpr.typeArgs:
|
||||||
|
if i > 0: suffix.add("_")
|
||||||
|
let argType = ctx.resolveTypeExpr(targ)
|
||||||
|
suffix.add(argType.toString)
|
||||||
|
structName = structName & "_" & suffix
|
||||||
# Simple enum init: EnumName { tag: EnumName_Variant } -> EnumName_Variant
|
# Simple enum init: EnumName { tag: EnumName_Variant } -> EnumName_Variant
|
||||||
var enumDecl: Decl = nil
|
var enumDecl: Decl = nil
|
||||||
let enumSym = ctx.globalScope.lookup(structName)
|
let enumSym = ctx.globalScope.lookup(structName)
|
||||||
@@ -1913,36 +2053,155 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
|||||||
typ: typ, loc: loc)
|
typ: typ, loc: loc)
|
||||||
|
|
||||||
of ekIs:
|
of ekIs:
|
||||||
|
# Desugar `expr is Variant` to a tag comparison so LIR/C backends need no hIs.
|
||||||
|
# Simple enums (no data): subject == Enum_Variant
|
||||||
|
# Algebraic enums: subject.tag == Enum_Variant
|
||||||
let operand = ctx.lowerExpr(expr.exprIsOperand)
|
let operand = ctx.lowerExpr(expr.exprIsOperand)
|
||||||
var isType = makeUnknown()
|
var variantName = ""
|
||||||
if expr.exprIsType != nil and expr.exprIsType.kind == tekNamed:
|
if expr.exprIsType != nil and expr.exprIsType.kind == tekNamed:
|
||||||
isType = makeNamed(expr.exprIsType.typeName)
|
variantName = expr.exprIsType.typeName
|
||||||
return HirNode(kind: hIs, isOperand: operand, isType: isType,
|
var enumName = ""
|
||||||
|
var opType = ctx.resolveExprType(expr.exprIsOperand)
|
||||||
|
# Prefer TypeExpr with type args so generic enums monomorphize (Result_int_String)
|
||||||
|
if expr.exprIsOperand != nil and expr.exprIsOperand.kind == ekIdent:
|
||||||
|
if ctx.varTypeExprs.hasKey(expr.exprIsOperand.exprIdent):
|
||||||
|
let te = ctx.varTypeExprs[expr.exprIsOperand.exprIdent]
|
||||||
|
if te != nil:
|
||||||
|
let resolved = ctx.resolveTypeExpr(te)
|
||||||
|
if resolved != nil and resolved.kind == tkNamed and resolved.name.len > 0:
|
||||||
|
opType = resolved
|
||||||
|
if opType != nil and opType.kind == tkNamed:
|
||||||
|
enumName = opType.name
|
||||||
|
if enumName.len > 0 and variantName.len > 0:
|
||||||
|
var baseName = enumName
|
||||||
|
if ctx.structInstMap.hasKey(enumName):
|
||||||
|
baseName = ctx.structInstMap[enumName].baseName
|
||||||
|
var hasData = ctx.enumHasDataVariants(baseName)
|
||||||
|
if not hasData:
|
||||||
|
hasData = ctx.enumHasDataVariants(enumName)
|
||||||
|
# Monomorphized data enums always have tag+data layout
|
||||||
|
if not hasData and ctx.structInstMap.hasKey(enumName):
|
||||||
|
hasData = true
|
||||||
|
let tagName = enumName & "_" & variantName
|
||||||
|
if hasData:
|
||||||
|
let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: operand, fieldName: "tag",
|
||||||
|
typ: makePointer(makeNamed(enumName & "_Tag")), loc: loc)
|
||||||
|
let tagLoad = HirNode(kind: hLoad, loadPtr: tagField,
|
||||||
|
typ: makeNamed(enumName & "_Tag"), loc: loc)
|
||||||
|
let tagConst = hirVar(tagName, makeNamed(enumName & "_Tag"), loc)
|
||||||
|
return hirBinary(tkEq, tagLoad, tagConst, makeBool(), loc)
|
||||||
|
else:
|
||||||
|
let tagConst = hirVar(tagName, makeNamed(enumName), loc)
|
||||||
|
return hirBinary(tkEq, operand, tagConst, makeBool(), loc)
|
||||||
|
# Non-enum / unresolved: false
|
||||||
|
return HirNode(kind: hLit,
|
||||||
|
litToken: Token(kind: tkBoolLiteral, text: "false", loc: loc),
|
||||||
typ: makeBool(), loc: loc)
|
typ: makeBool(), loc: loc)
|
||||||
|
|
||||||
of ekTry:
|
of ekTry:
|
||||||
let operand = ctx.lowerExpr(expr.exprTryOperand)
|
let operand = ctx.lowerExpr(expr.exprTryOperand)
|
||||||
let operandType = ctx.resolveExprType(expr.exprTryOperand)
|
var operandType = ctx.resolveExprType(expr.exprTryOperand)
|
||||||
|
# Keep original type args for Ok payload (before mangling to Result_T_E)
|
||||||
|
let payloadArgs: seq[Type] =
|
||||||
|
if operandType != nil and operandType.kind == tkNamed: operandType.inner
|
||||||
|
else: @[]
|
||||||
|
|
||||||
var typeName = ""
|
var typeName = ""
|
||||||
var errTag = ""
|
var errTag = ""
|
||||||
var okField = ""
|
var okField = ""
|
||||||
if operandType.kind == tkNamed:
|
if operandType != nil and operandType.kind == tkNamed:
|
||||||
typeName = operandType.name
|
typeName = operandType.name
|
||||||
case typeName
|
else:
|
||||||
of "Result":
|
typeName = "Result"
|
||||||
errTag = "Result_Err"
|
|
||||||
okField = "Ok_0"
|
# Upgrade bare generic enum name to concrete monomorphization.
|
||||||
of "Option":
|
# Sema stores Result/Option without mangled type-args; try needs Result_int_String_Tag.
|
||||||
errTag = "Option_None"
|
if ctx.genericEnums.hasKey(typeName) or typeName == "Result" or typeName == "Option":
|
||||||
|
# Prefer resolving call/ident TypeExpr with type args
|
||||||
|
if expr.exprTryOperand != nil:
|
||||||
|
if expr.exprTryOperand.kind == ekIdent and ctx.varTypeExprs.hasKey(expr.exprTryOperand.exprIdent):
|
||||||
|
let te = ctx.varTypeExprs[expr.exprTryOperand.exprIdent]
|
||||||
|
if te != nil:
|
||||||
|
let resolved = ctx.resolveTypeExpr(te)
|
||||||
|
if resolved != nil and resolved.kind == tkNamed and resolved.name.startsWith(typeName & "_"):
|
||||||
|
typeName = resolved.name
|
||||||
|
elif expr.exprTryOperand.kind == ekCall and expr.exprTryOperand.exprCallCallee != nil and
|
||||||
|
expr.exprTryOperand.exprCallCallee.kind == ekIdent:
|
||||||
|
let calSym = ctx.globalScope.lookup(expr.exprTryOperand.exprCallCallee.exprIdent)
|
||||||
|
if calSym != nil and calSym.decl != nil and calSym.decl.kind == dkFunc and
|
||||||
|
calSym.decl.declFuncReturnType != nil:
|
||||||
|
let resolved = ctx.resolveTypeExpr(calSym.decl.declFuncReturnType)
|
||||||
|
if resolved != nil and resolved.kind == tkNamed and
|
||||||
|
(resolved.name == typeName or resolved.name.startsWith(typeName & "_")):
|
||||||
|
typeName = resolved.name
|
||||||
|
# Enclosing function return type (must match for `?` propagation)
|
||||||
|
var stillBare = typeName == "Result" or typeName == "Option"
|
||||||
|
if stillBare and ctx.currentFuncRetType != nil and
|
||||||
|
ctx.currentFuncRetType.kind == tkNamed:
|
||||||
|
let rn = ctx.currentFuncRetType.name
|
||||||
|
if rn.startsWith(typeName & "_"):
|
||||||
|
typeName = rn
|
||||||
|
stillBare = false
|
||||||
|
# Prefer mangled name from type args: Result + [String,String] → Result_String_String
|
||||||
|
if stillBare and payloadArgs.len > 0 and
|
||||||
|
(typeName == "Result" or typeName == "Option"):
|
||||||
|
var mangled = typeName
|
||||||
|
for a in payloadArgs:
|
||||||
|
mangled = mangled & "_" & a.toString.replace(" ", "").replace("*", "p")
|
||||||
|
typeName = mangled
|
||||||
|
operandType = Type(kind: tkNamed, name: typeName, inner: payloadArgs)
|
||||||
|
|
||||||
|
# Err tag / Ok field from base or concrete name
|
||||||
|
let baseForTags =
|
||||||
|
if ctx.structInstMap.hasKey(typeName): ctx.structInstMap[typeName].baseName
|
||||||
|
elif ctx.genericEnums.hasKey(typeName): typeName
|
||||||
|
else: typeName
|
||||||
|
if baseForTags == "Option" or typeName.startsWith("Option_"):
|
||||||
|
errTag = typeName & "_None"
|
||||||
|
if typeName == "Option": errTag = "Option_None"
|
||||||
okField = "Some_0"
|
okField = "Some_0"
|
||||||
|
elif baseForTags == "Result" or typeName.startsWith("Result_"):
|
||||||
|
errTag = typeName & "_Err"
|
||||||
|
if typeName == "Result": errTag = "Result_Err"
|
||||||
|
okField = "Ok_0"
|
||||||
else:
|
else:
|
||||||
errTag = typeName & "_Err"
|
errTag = typeName & "_Err"
|
||||||
okField = "Ok_0"
|
okField = "Ok_0"
|
||||||
|
|
||||||
|
# Payload type (Ok_0 / Some_0) — must match T of Result<T,E>, not always int
|
||||||
|
var okType = makeInt()
|
||||||
|
if payloadArgs.len >= 1:
|
||||||
|
okType = payloadArgs[0]
|
||||||
else:
|
else:
|
||||||
errTag = "Result_Err"
|
var enumSym = ctx.globalScope.lookup(typeName)
|
||||||
okField = "Ok_0"
|
var enumDecl: Decl = nil
|
||||||
typeName = "Result"
|
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
|
||||||
|
enumDecl = enumSym.decl
|
||||||
|
elif ctx.structInstMap.hasKey(typeName):
|
||||||
|
let (baseName, _) = ctx.structInstMap[typeName]
|
||||||
|
let baseSym = ctx.globalScope.lookup(baseName)
|
||||||
|
if baseSym != nil and baseSym.decl != nil and baseSym.decl.kind == dkEnum:
|
||||||
|
enumDecl = baseSym.decl
|
||||||
|
elif typeName.startsWith("Result_"):
|
||||||
|
let baseSym = ctx.globalScope.lookup("Result")
|
||||||
|
if baseSym != nil and baseSym.decl != nil: enumDecl = baseSym.decl
|
||||||
|
elif typeName.startsWith("Option_"):
|
||||||
|
let baseSym = ctx.globalScope.lookup("Option")
|
||||||
|
if baseSym != nil and baseSym.decl != nil: enumDecl = baseSym.decl
|
||||||
|
if enumDecl != nil:
|
||||||
|
var subst = initTable[string, Type]()
|
||||||
|
if ctx.structInstMap.hasKey(typeName):
|
||||||
|
var ti = 0
|
||||||
|
for tp in enumDecl.declEnumTypeParams:
|
||||||
|
if ti < ctx.structInstMap[typeName][1].len:
|
||||||
|
subst[tp.name] = ctx.structInstMap[typeName][1][ti]
|
||||||
|
inc ti
|
||||||
|
for variant in enumDecl.declEnumVariants:
|
||||||
|
for i, f in variant.fields:
|
||||||
|
let fieldName = variant.name & "_" & $i
|
||||||
|
if fieldName == okField:
|
||||||
|
okType = substituteType(ctx, f, subst)
|
||||||
|
break
|
||||||
|
|
||||||
let tmpName = ctx.freshTryVar()
|
let tmpName = ctx.freshTryVar()
|
||||||
let tmpAlloca = hirAlloca(tmpName, operandType, loc)
|
let tmpAlloca = hirAlloca(tmpName, operandType, loc)
|
||||||
@@ -1966,8 +2225,8 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
|||||||
let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr,
|
let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr,
|
||||||
typ: makeNamed(typeName & "_Data"), loc: loc)
|
typ: makeNamed(typeName & "_Data"), loc: loc)
|
||||||
let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: okField,
|
let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: okField,
|
||||||
typ: makePointer(makeInt()), loc: loc)
|
typ: makePointer(okType), loc: loc)
|
||||||
let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: makeInt(), loc: loc)
|
let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: okType, loc: loc)
|
||||||
|
|
||||||
ctx.pendingStmts.add(tmpAlloca)
|
ctx.pendingStmts.add(tmpAlloca)
|
||||||
ctx.pendingStmts.add(tmpStore)
|
ctx.pendingStmts.add(tmpStore)
|
||||||
@@ -1980,14 +2239,66 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
|||||||
|
|
||||||
var errTag = "Result_Err"
|
var errTag = "Result_Err"
|
||||||
var typeName = "Result"
|
var typeName = "Result"
|
||||||
if operandType.kind == tkNamed:
|
var okField = "Ok_0"
|
||||||
|
if operandType != nil and operandType.kind == tkNamed:
|
||||||
typeName = operandType.name
|
typeName = operandType.name
|
||||||
if typeName == "Option":
|
if typeName == "Option" or typeName.startsWith("Option_"):
|
||||||
errTag = "Option_None"
|
errTag = if typeName == "Option": "Option_None" else: typeName & "_None"
|
||||||
|
okField = "Some_0"
|
||||||
|
elif typeName == "Result":
|
||||||
|
errTag = "Result_Err"
|
||||||
|
else:
|
||||||
|
errTag = typeName & "_Err"
|
||||||
|
|
||||||
|
# Mangle Result + type args when still bare
|
||||||
|
if operandType != nil and operandType.kind == tkNamed and operandType.inner.len > 0 and
|
||||||
|
(typeName == "Result" or typeName == "Option"):
|
||||||
|
var mangled = typeName
|
||||||
|
for a in operandType.inner:
|
||||||
|
mangled = mangled & "_" & a.toString.replace(" ", "").replace("*", "p")
|
||||||
|
typeName = mangled
|
||||||
|
if typeName.startsWith("Option_"):
|
||||||
|
errTag = typeName & "_None"
|
||||||
|
okField = "Some_0"
|
||||||
|
else:
|
||||||
|
errTag = typeName & "_Err"
|
||||||
|
okField = "Ok_0"
|
||||||
|
|
||||||
|
var okType = makeInt()
|
||||||
|
if operandType != nil and operandType.kind == tkNamed and operandType.inner.len >= 1:
|
||||||
|
okType = operandType.inner[0]
|
||||||
|
else:
|
||||||
|
var enumDecl: Decl = nil
|
||||||
|
let enumSym = ctx.globalScope.lookup(
|
||||||
|
if typeName.startsWith("Result_"): "Result"
|
||||||
|
elif typeName.startsWith("Option_"): "Option"
|
||||||
|
else: typeName)
|
||||||
|
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
|
||||||
|
enumDecl = enumSym.decl
|
||||||
|
if enumDecl != nil:
|
||||||
|
var subst = initTable[string, Type]()
|
||||||
|
if ctx.structInstMap.hasKey(typeName):
|
||||||
|
var ti = 0
|
||||||
|
for tp in enumDecl.declEnumTypeParams:
|
||||||
|
if ti < ctx.structInstMap[typeName][1].len:
|
||||||
|
subst[tp.name] = ctx.structInstMap[typeName][1][ti]
|
||||||
|
inc ti
|
||||||
|
elif operandType != nil and operandType.kind == tkNamed:
|
||||||
|
var ti = 0
|
||||||
|
for tp in enumDecl.declEnumTypeParams:
|
||||||
|
if ti < operandType.inner.len:
|
||||||
|
subst[tp.name] = operandType.inner[ti]
|
||||||
|
inc ti
|
||||||
|
for variant in enumDecl.declEnumVariants:
|
||||||
|
for i, f in variant.fields:
|
||||||
|
if variant.name & "_" & $i == okField:
|
||||||
|
okType = substituteType(ctx, f, subst)
|
||||||
|
|
||||||
|
# Same shape as `?`: stack temporary of the Result/Option value (not a pointer).
|
||||||
|
# Typing the var as *Result made C emit `tmp->tag` on a value (invalid).
|
||||||
let tmpName = ctx.freshTryVar()
|
let tmpName = ctx.freshTryVar()
|
||||||
let tmpAlloca = hirAlloca(tmpName, operandType, loc)
|
let tmpAlloca = hirAlloca(tmpName, operandType, loc)
|
||||||
let tmpVar = hirVar(tmpName, makePointer(operandType), loc)
|
let tmpVar = hirVar(tmpName, operandType, loc)
|
||||||
let tmpStore = hirStore(tmpVar, operand, loc)
|
let tmpStore = hirStore(tmpVar, operand, loc)
|
||||||
|
|
||||||
let tagPtr = HirNode(kind: hFieldPtr, fieldPtrBase: tmpVar, fieldName: "tag",
|
let tagPtr = HirNode(kind: hFieldPtr, fieldPtrBase: tmpVar, fieldName: "tag",
|
||||||
@@ -1997,11 +2308,15 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
|||||||
let errConst = hirVar(errTag, makeNamed(typeName & "_Tag"), loc)
|
let errConst = hirVar(errTag, makeNamed(typeName & "_Tag"), loc)
|
||||||
let cond = hirBinary(tkEq, tagLoad, errConst, makeBool(), loc)
|
let cond = hirBinary(tkEq, tagLoad, errConst, makeBool(), loc)
|
||||||
|
|
||||||
# On error: call bux_panic("unwrap failed")
|
# On error: call bux_panic("unwrap failed") then exit (do not continue with garbage)
|
||||||
let panicTok = Token(kind: tkStringLiteral, text: "\"unwrap failed\"", loc: loc)
|
let panicTok = Token(kind: tkStringLiteral, text: "\"unwrap failed\"", loc: loc)
|
||||||
let panicMsg = HirNode(kind: hLit, litToken: panicTok, typ: makeStr(), loc: loc)
|
let panicMsg = HirNode(kind: hLit, litToken: panicTok, typ: makeStr(), loc: loc)
|
||||||
let panicCall = hirCall("bux_panic", @[panicMsg], makeVoid(), loc)
|
let panicCall = hirCall("bux_panic", @[panicMsg], makeVoid(), loc)
|
||||||
let thenBlock = hirBlock(@[panicCall], nil, makeVoid(), loc)
|
let exitLit = HirNode(kind: hLit,
|
||||||
|
litToken: Token(kind: tkIntLiteral, text: "1", loc: loc),
|
||||||
|
typ: makeInt(), loc: loc)
|
||||||
|
let exitCall = hirCall("bux_exit", @[exitLit], makeVoid(), loc)
|
||||||
|
let thenBlock = hirBlock(@[panicCall, exitCall], nil, makeVoid(), loc)
|
||||||
let ifNode = HirNode(kind: hIf, ifCond: cond, ifThen: thenBlock,
|
let ifNode = HirNode(kind: hIf, ifCond: cond, ifThen: thenBlock,
|
||||||
ifElse: nil, typ: makeVoid(), loc: loc)
|
ifElse: nil, typ: makeVoid(), loc: loc)
|
||||||
|
|
||||||
@@ -2010,9 +2325,9 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
|||||||
typ: makePointer(makeNamed(typeName & "_Data")), loc: loc)
|
typ: makePointer(makeNamed(typeName & "_Data")), loc: loc)
|
||||||
let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr,
|
let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr,
|
||||||
typ: makeNamed(typeName & "_Data"), loc: loc)
|
typ: makeNamed(typeName & "_Data"), loc: loc)
|
||||||
let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: "Ok_0",
|
let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: okField,
|
||||||
typ: makePointer(makeInt()), loc: loc)
|
typ: makePointer(okType), loc: loc)
|
||||||
let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: makeInt(), loc: loc)
|
let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: okType, loc: loc)
|
||||||
|
|
||||||
ctx.pendingStmts.add(tmpAlloca)
|
ctx.pendingStmts.add(tmpAlloca)
|
||||||
ctx.pendingStmts.add(tmpStore)
|
ctx.pendingStmts.add(tmpStore)
|
||||||
@@ -2181,7 +2496,9 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
|||||||
of skLet:
|
of skLet:
|
||||||
var initHir: HirNode = nil
|
var initHir: HirNode = nil
|
||||||
if stmt.stmtLetInit != nil:
|
if stmt.stmtLetInit != nil:
|
||||||
|
ctx.currentInitTypeExpr = stmt.stmtLetType
|
||||||
initHir = ctx.lowerExpr(stmt.stmtLetInit)
|
initHir = ctx.lowerExpr(stmt.stmtLetInit)
|
||||||
|
ctx.currentInitTypeExpr = nil
|
||||||
let allocaType = if stmt.stmtLetType != nil:
|
let allocaType = if stmt.stmtLetType != nil:
|
||||||
# Full resolve covers named, pointer, slice, tuple, func, refs, etc.
|
# Full resolve covers named, pointer, slice, tuple, func, refs, etc.
|
||||||
ctx.resolveTypeExpr(stmt.stmtLetType)
|
ctx.resolveTypeExpr(stmt.stmtLetType)
|
||||||
@@ -2809,12 +3126,14 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
|
|||||||
discard
|
discard
|
||||||
|
|
||||||
|
|
||||||
# First pass: collect generic functions and generic structs
|
# First pass: collect generic functions, generic structs, and generic enums
|
||||||
for decl in module.items:
|
for decl in module.items:
|
||||||
if decl.kind == dkFunc and decl.declFuncTypeParams.len > 0:
|
if decl.kind == dkFunc and decl.declFuncTypeParams.len > 0:
|
||||||
ctx.genericFuncs[decl.declFuncName] = decl
|
ctx.genericFuncs[decl.declFuncName] = decl
|
||||||
if decl.kind == dkStruct and decl.declStructTypeParams.len > 0:
|
if decl.kind == dkStruct and decl.declStructTypeParams.len > 0:
|
||||||
ctx.genericStructs[decl.declStructName] = decl
|
ctx.genericStructs[decl.declStructName] = decl
|
||||||
|
if decl.kind == dkEnum and decl.declEnumTypeParams.len > 0:
|
||||||
|
ctx.genericEnums[decl.declEnumName] = decl
|
||||||
if decl.kind == dkImpl and decl.declImplTypeParams.len > 0:
|
if decl.kind == dkImpl and decl.declImplTypeParams.len > 0:
|
||||||
let typeName = decl.declImplTypeName
|
let typeName = decl.declImplTypeName
|
||||||
for methodDecl in decl.declImplMethods:
|
for methodDecl in decl.declImplMethods:
|
||||||
@@ -2864,6 +3183,8 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
|
|||||||
fields.add((f.name, fType))
|
fields.add((f.name, fType))
|
||||||
structs.add((decl.declStructName, fields))
|
structs.add((decl.declStructName, fields))
|
||||||
of dkEnum:
|
of dkEnum:
|
||||||
|
# Skip generic enums — instantiated on demand via generateEnumInstance
|
||||||
|
if decl.declEnumTypeParams.len > 0: continue
|
||||||
var variants: seq[HirEnumVariant] = @[]
|
var variants: seq[HirEnumVariant] = @[]
|
||||||
for v in decl.declEnumVariants:
|
for v in decl.declEnumVariants:
|
||||||
var fields: seq[Type] = @[]
|
var fields: seq[Type] = @[]
|
||||||
@@ -2906,10 +3227,115 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
|
|||||||
for s in ctx.extraStructs:
|
for s in ctx.extraStructs:
|
||||||
structs.add(s)
|
structs.add(s)
|
||||||
|
|
||||||
|
# Add monomorphized generic enums
|
||||||
|
for e in ctx.extraEnums:
|
||||||
|
enums.add(e)
|
||||||
|
|
||||||
# Add monomorphized generic methods
|
# Add monomorphized generic methods
|
||||||
for f in ctx.extraFuncs:
|
for f in ctx.extraFuncs:
|
||||||
funcs.add(f)
|
funcs.add(f)
|
||||||
|
|
||||||
|
# Mangle generic enum tag references in all function bodies
|
||||||
|
proc substEnumName(name: string, ctx: LowerCtx): string =
|
||||||
|
result = name
|
||||||
|
for enumName, _ in ctx.genericEnums:
|
||||||
|
# Check if name IS the generic enum name (bare type reference)
|
||||||
|
if name == enumName:
|
||||||
|
for en in ctx.extraEnums:
|
||||||
|
if en.name.startsWith(enumName & "_"):
|
||||||
|
return en.name
|
||||||
|
# Check if name starts with generic enum name + "_" (tag reference)
|
||||||
|
let prefix = enumName & "_"
|
||||||
|
if name.startsWith(prefix) and name != enumName:
|
||||||
|
let rest = name[prefix.len..^1]
|
||||||
|
# Skip if already a concrete instance (e.g., "Pair_int_String_First")
|
||||||
|
var alreadyConcrete = false
|
||||||
|
for en in ctx.extraEnums:
|
||||||
|
if name.startsWith(en.name & "_") or name == en.name:
|
||||||
|
alreadyConcrete = true
|
||||||
|
break
|
||||||
|
if not alreadyConcrete:
|
||||||
|
for en in ctx.extraEnums:
|
||||||
|
if en.name.startsWith(enumName & "_"):
|
||||||
|
return en.name & "_" & rest
|
||||||
|
|
||||||
|
# Substitute type names in Type fields (Result → Result_int_String,
|
||||||
|
# Result_Tag → Result_int_String_Tag, Result_Data → Result_int_String_Data).
|
||||||
|
proc substEnumType(typ: var Type, ctx: LowerCtx) =
|
||||||
|
if typ == nil: return
|
||||||
|
if typ.kind == tkNamed:
|
||||||
|
for enumName, _ in ctx.genericEnums:
|
||||||
|
if typ.name == enumName:
|
||||||
|
for en in ctx.extraEnums:
|
||||||
|
if en.name.startsWith(enumName & "_"):
|
||||||
|
typ = makeNamed(en.name)
|
||||||
|
return
|
||||||
|
# Suffix forms used by try/field lowering
|
||||||
|
let tagSuffix = enumName & "_Tag"
|
||||||
|
let dataSuffix = enumName & "_Data"
|
||||||
|
if typ.name == tagSuffix or typ.name == dataSuffix:
|
||||||
|
let rest = typ.name[enumName.len + 1 .. ^1] # "Tag" or "Data"
|
||||||
|
for en in ctx.extraEnums:
|
||||||
|
if en.name.startsWith(enumName & "_"):
|
||||||
|
typ = makeNamed(en.name & "_" & rest)
|
||||||
|
return
|
||||||
|
elif typ.kind in {tkPointer, tkRef, tkMutRef, tkSlice} and typ.inner.len > 0:
|
||||||
|
var inner = typ.inner[0]
|
||||||
|
substEnumType(inner, ctx)
|
||||||
|
typ.inner[0] = inner
|
||||||
|
|
||||||
|
proc mangleHirNode(n: HirNode, ctx: LowerCtx) =
|
||||||
|
if n == nil: return
|
||||||
|
# Mangle type annotation on every node (temps for .tag/.data loads)
|
||||||
|
if n.typ != nil:
|
||||||
|
substEnumType(n.typ, ctx)
|
||||||
|
case n.kind
|
||||||
|
of hVar: n.varName = substEnumName(n.varName, ctx)
|
||||||
|
of hStructInit: n.structInitName = substEnumName(n.structInitName, ctx)
|
||||||
|
of hFieldAccess: n.fieldAccessName = substEnumName(n.fieldAccessName, ctx)
|
||||||
|
of hArrowField: n.arrowFieldName = substEnumName(n.arrowFieldName, ctx)
|
||||||
|
of hAlloca: substEnumType(n.allocaType, ctx)
|
||||||
|
of hCast: substEnumType(n.castType, ctx)
|
||||||
|
else: discard
|
||||||
|
# Walk children by variant
|
||||||
|
case n.kind
|
||||||
|
of hUnary: mangleHirNode(n.unaryOperand, ctx)
|
||||||
|
of hBinary: mangleHirNode(n.binaryLeft, ctx); mangleHirNode(n.binaryRight, ctx)
|
||||||
|
of hAssign: mangleHirNode(n.assignTarget, ctx); mangleHirNode(n.assignValue, ctx)
|
||||||
|
of hIf: mangleHirNode(n.ifCond, ctx); mangleHirNode(n.ifThen, ctx); mangleHirNode(n.ifElse, ctx)
|
||||||
|
of hWhile: mangleHirNode(n.whileCond, ctx); mangleHirNode(n.whileBody, ctx)
|
||||||
|
of hLoop: mangleHirNode(n.loopBody, ctx)
|
||||||
|
of hReturn: mangleHirNode(n.returnValue, ctx)
|
||||||
|
of hDefer: mangleHirNode(n.deferBody, ctx)
|
||||||
|
of hLoad: mangleHirNode(n.loadPtr, ctx)
|
||||||
|
of hStore: mangleHirNode(n.storePtr, ctx); mangleHirNode(n.storeValue, ctx)
|
||||||
|
of hFieldPtr: mangleHirNode(n.fieldPtrBase, ctx)
|
||||||
|
of hFieldAccess: mangleHirNode(n.fieldAccessBase, ctx)
|
||||||
|
of hArrowField: mangleHirNode(n.arrowFieldBase, ctx)
|
||||||
|
of hIndexPtr: mangleHirNode(n.indexPtrBase, ctx); mangleHirNode(n.indexPtrIndex, ctx)
|
||||||
|
of hCall:
|
||||||
|
for c in n.callArgs: mangleHirNode(c, ctx)
|
||||||
|
of hCallIndirect:
|
||||||
|
mangleHirNode(n.callIndirectCallee, ctx)
|
||||||
|
for c in n.callIndirectArgs: mangleHirNode(c, ctx)
|
||||||
|
of hCast: mangleHirNode(n.castOperand, ctx)
|
||||||
|
of hSpawn:
|
||||||
|
for c in n.spawnArgs: mangleHirNode(c, ctx)
|
||||||
|
of hBlock:
|
||||||
|
for c in n.blockStmts: mangleHirNode(c, ctx)
|
||||||
|
mangleHirNode(n.blockExpr, ctx)
|
||||||
|
of hStructInit:
|
||||||
|
for sf in n.structInitFields.mitems:
|
||||||
|
mangleHirNode(sf.value, ctx)
|
||||||
|
of hSliceInit:
|
||||||
|
for c in n.sliceInitElements: mangleHirNode(c, ctx)
|
||||||
|
of hSliceIndex:
|
||||||
|
mangleHirNode(n.sliceIndexBase, ctx); mangleHirNode(n.sliceIndexIndex, ctx)
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
for f in mitems(funcs):
|
||||||
|
mangleHirNode(f.body, ctx)
|
||||||
|
|
||||||
# Collect interface info for vtable generation
|
# Collect interface info for vtable generation
|
||||||
var ifaceInfos: seq[tuple[name: string, hasAssocTypes: bool, methods: seq[tuple[name: string, params: seq[Type], ret: Type]]]] = @[]
|
var ifaceInfos: seq[tuple[name: string, hasAssocTypes: bool, methods: seq[tuple[name: string, params: seq[Type], ret: Type]]]] = @[]
|
||||||
for ifaceName, ifaceDecl in sema.interfaceTable:
|
for ifaceName, ifaceDecl in sema.interfaceTable:
|
||||||
|
|||||||
@@ -106,14 +106,17 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
|
|||||||
be.emitLine(&"{v(instr.dst)} = {v(instr.src)};")
|
be.emitLine(&"{v(instr.dst)} = {v(instr.src)};")
|
||||||
|
|
||||||
# ── Arithmetic ──
|
# ── Arithmetic ──
|
||||||
of lirAdd, lirSub, lirMul, lirDiv, lirMod,
|
of lirDiv:
|
||||||
|
# Checked integer division (runtime panics on divisor 0 instead of SIGFPE).
|
||||||
|
be.emitLine(&"{v(instr.dst)} = bux_div_i64((int64_t)({v(instr.src)}), (int64_t)({v(instr.src2)}));")
|
||||||
|
of lirMod:
|
||||||
|
be.emitLine(&"{v(instr.dst)} = bux_mod_i64((int64_t)({v(instr.src)}), (int64_t)({v(instr.src2)}));")
|
||||||
|
of lirAdd, lirSub, lirMul,
|
||||||
lirAnd, lirOr, lirXor, lirShl, lirShr:
|
lirAnd, lirOr, lirXor, lirShl, lirShr:
|
||||||
let op = case instr.kind
|
let op = case instr.kind
|
||||||
of lirAdd: "+"
|
of lirAdd: "+"
|
||||||
of lirSub: "-"
|
of lirSub: "-"
|
||||||
of lirMul: "*"
|
of lirMul: "*"
|
||||||
of lirDiv: "/"
|
|
||||||
of lirMod: "%"
|
|
||||||
of lirAnd: "&"
|
of lirAnd: "&"
|
||||||
of lirOr: "|"
|
of lirOr: "|"
|
||||||
of lirXor: "^"
|
of lirXor: "^"
|
||||||
@@ -629,6 +632,10 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
|
|||||||
be.emitLine("#include <stdbool.h>")
|
be.emitLine("#include <stdbool.h>")
|
||||||
be.emitLine("#include <string.h>")
|
be.emitLine("#include <string.h>")
|
||||||
be.emitLine("")
|
be.emitLine("")
|
||||||
|
# Checked integer div/mod (rt/runtime*.c) — always available
|
||||||
|
be.emitLine("extern int64_t bux_div_i64(int64_t a, int64_t b);")
|
||||||
|
be.emitLine("extern int64_t bux_mod_i64(int64_t a, int64_t b);")
|
||||||
|
be.emitLine("")
|
||||||
|
|
||||||
# Forward struct declarations
|
# Forward struct declarations
|
||||||
for s in module.structs:
|
for s in module.structs:
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ type
|
|||||||
proc emitErr(res: var MacroExpandResult, loc: SourceLocation, msg: string) =
|
proc emitErr(res: var MacroExpandResult, loc: SourceLocation, msg: string) =
|
||||||
res.diagnostics.add(MacroDiagnostic(loc: loc, message: msg))
|
res.diagnostics.add(MacroDiagnostic(loc: loc, message: msg))
|
||||||
|
|
||||||
|
proc isBinaryPasteOp*(k: TokenKind): bool =
|
||||||
|
## Operators allowed as `$op:tt` paste into `$op($a, $b)` → `a OP b`.
|
||||||
|
case k
|
||||||
|
of tkPlus, tkMinus, tkStar, tkSlash, tkPercent, tkStarStar,
|
||||||
|
tkAmp, tkPipe, tkCaret, tkShl, tkShr,
|
||||||
|
tkAmpAmp, tkPipePipe,
|
||||||
|
tkEq, tkNe, tkLt, tkLe, tkGt, tkGe:
|
||||||
|
true
|
||||||
|
else:
|
||||||
|
false
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Deep clone (bootstrap has no Ast_Clone*)
|
# Deep clone (bootstrap has no Ast_Clone*)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -152,9 +163,12 @@ proc cloneExpr*(e: Expr): Expr =
|
|||||||
for a in e.exprCallArgs:
|
for a in e.exprCallArgs:
|
||||||
result.exprCallArgs.add(cloneExpr(a))
|
result.exprCallArgs.add(cloneExpr(a))
|
||||||
of ekGenericCall:
|
of ekGenericCall:
|
||||||
|
var gtas: seq[TypeExpr] = @[]
|
||||||
|
for ta in e.exprGenericTypeArgs:
|
||||||
|
gtas.add(cloneTypeExpr(ta))
|
||||||
result = Expr(kind: ekGenericCall, loc: e.loc,
|
result = Expr(kind: ekGenericCall, loc: e.loc,
|
||||||
exprGenericCallee: e.exprGenericCallee,
|
exprGenericCallee: e.exprGenericCallee,
|
||||||
exprGenericTypeArgs: e.exprGenericTypeArgs)
|
exprGenericTypeArgs: gtas)
|
||||||
of ekIndex:
|
of ekIndex:
|
||||||
result = Expr(kind: ekIndex, loc: e.loc,
|
result = Expr(kind: ekIndex, loc: e.loc,
|
||||||
exprIndexObj: cloneExpr(e.exprIndexObj),
|
exprIndexObj: cloneExpr(e.exprIndexObj),
|
||||||
@@ -165,9 +179,12 @@ proc cloneExpr*(e: Expr): Expr =
|
|||||||
exprFieldObj: cloneExpr(e.exprFieldObj),
|
exprFieldObj: cloneExpr(e.exprFieldObj),
|
||||||
exprFieldName: e.exprFieldName)
|
exprFieldName: e.exprFieldName)
|
||||||
of ekStructInit:
|
of ekStructInit:
|
||||||
|
var stas: seq[TypeExpr] = @[]
|
||||||
|
for ta in e.exprStructInitTypeArgs:
|
||||||
|
stas.add(cloneTypeExpr(ta))
|
||||||
result = Expr(kind: ekStructInit, loc: e.loc,
|
result = Expr(kind: ekStructInit, loc: e.loc,
|
||||||
exprStructInitName: e.exprStructInitName,
|
exprStructInitName: e.exprStructInitName,
|
||||||
exprStructInitTypeArgs: e.exprStructInitTypeArgs,
|
exprStructInitTypeArgs: stas,
|
||||||
exprStructInitFields: @[])
|
exprStructInitFields: @[])
|
||||||
for f in e.exprStructInitFields:
|
for f in e.exprStructInitFields:
|
||||||
result.exprStructInitFields.add((f.name, cloneExpr(f.value)))
|
result.exprStructInitFields.add((f.name, cloneExpr(f.value)))
|
||||||
@@ -895,6 +912,12 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
|
|||||||
of ekRange:
|
of ekRange:
|
||||||
c.exprRangeLo = substExpr(c.exprRangeLo, env, callLoc)
|
c.exprRangeLo = substExpr(c.exprRangeLo, env, callLoc)
|
||||||
c.exprRangeHi = substExpr(c.exprRangeHi, env, callLoc)
|
c.exprRangeHi = substExpr(c.exprRangeHi, env, callLoc)
|
||||||
|
of ekGenericCall:
|
||||||
|
# Array_New<$t>(…) / Foo<$t, $u> — substitute type fragments in type args
|
||||||
|
var gtas: seq[TypeExpr] = @[]
|
||||||
|
for ta in c.exprGenericTypeArgs:
|
||||||
|
gtas.add(substType(ta, env, callLoc))
|
||||||
|
c.exprGenericTypeArgs = gtas
|
||||||
of ekCall:
|
of ekCall:
|
||||||
c.exprCallCallee = substExpr(c.exprCallCallee, env, callLoc)
|
c.exprCallCallee = substExpr(c.exprCallCallee, env, callLoc)
|
||||||
var args: seq[Expr] = @[]
|
var args: seq[Expr] = @[]
|
||||||
@@ -950,12 +973,37 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
|
|||||||
argNames.add("")
|
argNames.add("")
|
||||||
c.exprCallArgs = args
|
c.exprCallArgs = args
|
||||||
c.exprCallArgNames = argNames
|
c.exprCallArgNames = argNames
|
||||||
|
# Operators-only paste: `$op($a, $b)` where `$op:tt` is a bound binary operator
|
||||||
|
# → rebuild as `$a OP $b` (post-1.0).
|
||||||
|
if c.exprCallCallee != nil and c.exprCallCallee.kind == ekMacroTt and
|
||||||
|
c.exprCallCallee.exprMacroTtInner != nil and
|
||||||
|
c.exprCallCallee.exprMacroTtInner.kind == ekLiteral and
|
||||||
|
c.exprCallArgs.len == 2:
|
||||||
|
let opTok = c.exprCallCallee.exprMacroTtInner.exprLit
|
||||||
|
if opTok.kind.isBinaryPasteOp:
|
||||||
|
result = Expr(kind: ekBinary, loc: callLoc,
|
||||||
|
exprBinaryOp: opTok.kind,
|
||||||
|
exprBinaryLeft: c.exprCallArgs[0],
|
||||||
|
exprBinaryRight: c.exprCallArgs[1])
|
||||||
|
return
|
||||||
|
# Callee already unwrapped to op literal by value-position MacroTt splice
|
||||||
|
if c.exprCallCallee != nil and c.exprCallCallee.kind == ekLiteral and
|
||||||
|
c.exprCallArgs.len == 2 and c.exprCallCallee.exprLit.kind.isBinaryPasteOp:
|
||||||
|
result = Expr(kind: ekBinary, loc: callLoc,
|
||||||
|
exprBinaryOp: c.exprCallCallee.exprLit.kind,
|
||||||
|
exprBinaryLeft: c.exprCallArgs[0],
|
||||||
|
exprBinaryRight: c.exprCallArgs[1])
|
||||||
|
return
|
||||||
of ekIndex:
|
of ekIndex:
|
||||||
c.exprIndexObj = substExpr(c.exprIndexObj, env, callLoc)
|
c.exprIndexObj = substExpr(c.exprIndexObj, env, callLoc)
|
||||||
c.exprIndexIdx = substExpr(c.exprIndexIdx, env, callLoc)
|
c.exprIndexIdx = substExpr(c.exprIndexIdx, env, callLoc)
|
||||||
of ekField:
|
of ekField:
|
||||||
c.exprFieldObj = substExpr(c.exprFieldObj, env, callLoc)
|
c.exprFieldObj = substExpr(c.exprFieldObj, env, callLoc)
|
||||||
of ekStructInit:
|
of ekStructInit:
|
||||||
|
var stas: seq[TypeExpr] = @[]
|
||||||
|
for ta in c.exprStructInitTypeArgs:
|
||||||
|
stas.add(substType(ta, env, callLoc))
|
||||||
|
c.exprStructInitTypeArgs = stas
|
||||||
var fields: seq[tuple[name: string, value: Expr]] = @[]
|
var fields: seq[tuple[name: string, value: Expr]] = @[]
|
||||||
for f in c.exprStructInitFields:
|
for f in c.exprStructInitFields:
|
||||||
fields.add((f.name, substExpr(f.value, env, callLoc)))
|
fields.add((f.name, substExpr(f.value, env, callLoc)))
|
||||||
@@ -1247,6 +1295,25 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
|
|||||||
exprMacroTtInner: inner, exprMacroTtGroup: true)
|
exprMacroTtInner: inner, exprMacroTtGroup: true)
|
||||||
result = @[callee, group]
|
result = @[callee, group]
|
||||||
|
|
||||||
|
## Operators-only juxta: single binary arg `1 + 2` matches
|
||||||
|
## `$a:expr, $op:tt, $b:expr` (or juxta without commas).
|
||||||
|
proc juxtaBinarySplit(rule: MacroRule, inArgs: seq[Expr]): seq[Expr] =
|
||||||
|
result = inArgs
|
||||||
|
if inArgs.len != 1 or inArgs[0] == nil: return
|
||||||
|
if inArgs[0].kind != ekBinary: return
|
||||||
|
if rule.frags.len != 3: return
|
||||||
|
if rule.frags[0].isRep or rule.frags[1].isRep or rule.frags[2].isRep: return
|
||||||
|
let k0 = if rule.frags[0].kinds.len > 0: rule.frags[0].kinds[0] else: rule.frags[0].kind
|
||||||
|
let k1 = if rule.frags[1].kinds.len > 0: rule.frags[1].kinds[0] else: rule.frags[1].kind
|
||||||
|
let k2 = if rule.frags[2].kinds.len > 0: rule.frags[2].kinds[0] else: rule.frags[2].kind
|
||||||
|
if k0 != mfkExpr or k1 != mfkTt or k2 != mfkExpr: return
|
||||||
|
if not inArgs[0].exprBinaryOp.isBinaryPasteOp: return
|
||||||
|
let opTok = Token(kind: inArgs[0].exprBinaryOp, text: "", loc: inArgs[0].loc)
|
||||||
|
let lit = Expr(kind: ekLiteral, loc: inArgs[0].loc, exprLit: opTok)
|
||||||
|
let opTt = Expr(kind: ekMacroTt, loc: inArgs[0].loc,
|
||||||
|
exprMacroTtInner: lit, exprMacroTtGroup: false)
|
||||||
|
result = @[inArgs[0].exprBinaryLeft, opTt, inArgs[0].exprBinaryRight]
|
||||||
|
|
||||||
var matched: MacroRule
|
var matched: MacroRule
|
||||||
var env: MacroEnv
|
var env: MacroEnv
|
||||||
var found = false
|
var found = false
|
||||||
@@ -1257,7 +1324,8 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
|
|||||||
var gi = 0
|
var gi = 0
|
||||||
var ai = 0
|
var ai = 0
|
||||||
let useGroups = nReps > 1 and groups.len > 1
|
let useGroups = nReps > 1 and groups.len > 1
|
||||||
let flat = juxtaCallSplit(rule, args)
|
var flat = juxtaCallSplit(rule, args)
|
||||||
|
flat = juxtaBinarySplit(rule, flat)
|
||||||
|
|
||||||
for frag in rule.frags:
|
for frag in rule.frags:
|
||||||
if failed: break
|
if failed: break
|
||||||
|
|||||||
@@ -434,10 +434,22 @@ proc isMacroStmtStart(p: Parser): bool =
|
|||||||
p.peek() in {tkLet, tkVar, tkIf, tkWhile, tkFor, tkLoop, tkMatch, tkReturn,
|
p.peek() in {tkLet, tkVar, tkIf, tkWhile, tkFor, tkLoop, tkMatch, tkReturn,
|
||||||
tkBreak, tkContinue, tkDefer, tkSwitch, tkDo}
|
tkBreak, tkContinue, tkDefer, tkSwitch, tkDo}
|
||||||
|
|
||||||
|
proc isBinaryPasteOpToken(k: TokenKind): bool =
|
||||||
|
## Same set as macroexpand.isBinaryPasteOp — kept local to avoid cycles.
|
||||||
|
case k
|
||||||
|
of tkPlus, tkMinus, tkStar, tkSlash, tkPercent, tkStarStar,
|
||||||
|
tkAmp, tkPipe, tkCaret, tkShl, tkShr,
|
||||||
|
tkAmpAmp, tkPipePipe,
|
||||||
|
tkEq, tkNe, tkLt, tkLe, tkGt, tkGe:
|
||||||
|
true
|
||||||
|
else:
|
||||||
|
false
|
||||||
|
|
||||||
proc parseMacroArg(p: var Parser): Expr =
|
proc parseMacroArg(p: var Parser): Expr =
|
||||||
## Macro call argument:
|
## Macro call argument:
|
||||||
## - statement keywords → ekMacroStmt
|
## - statement keywords → ekMacroStmt
|
||||||
## - `_` / pattern-only starts → ekMacroPat (also `$p:pat` from expr via coerce)
|
## - `_` / pattern-only starts → ekMacroPat (also `$p:pat` from expr via coerce)
|
||||||
|
## - bare binary operators (`+`, `*`, `==`, …) → ekMacroTt (operators-only paste)
|
||||||
## - else expression
|
## - else expression
|
||||||
let loc = p.currentLoc
|
let loc = p.currentLoc
|
||||||
if p.isMacroStmtStart():
|
if p.isMacroStmtStart():
|
||||||
@@ -447,6 +459,20 @@ proc parseMacroArg(p: var Parser): Expr =
|
|||||||
if p.check(tkUnderscore):
|
if p.check(tkUnderscore):
|
||||||
let pat = p.parsePattern()
|
let pat = p.parsePattern()
|
||||||
return Expr(kind: ekMacroPat, loc: loc, exprMacroPat: pat)
|
return Expr(kind: ekMacroPat, loc: loc, exprMacroPat: pat)
|
||||||
|
# Operators-only tt: `apply_op!(+, 1, 2)` / `apply_op!(*, 2, 3)`.
|
||||||
|
# Bare ops are not primary exprs. Unary-capable tokens (`*`, `-`, …) are only
|
||||||
|
# claimed when the next token ends the arg (`,`, `)`, `;`) so `*int` still
|
||||||
|
# parses as a type/expr and juxta `a * b` still works as ekBinary.
|
||||||
|
let pk = p.peek()
|
||||||
|
if isBinaryPasteOpToken(pk):
|
||||||
|
let nxt = p.peek(1)
|
||||||
|
let endsArg = nxt in {tkComma, tkRParen, tkSemicolon, tkEndOfFile, tkNewLine}
|
||||||
|
let unaryCapable = pk in {tkStar, tkMinus, tkBang, tkAmp, tkTilde,
|
||||||
|
tkPlusPlus, tkMinusMinus}
|
||||||
|
if endsArg or not unaryCapable:
|
||||||
|
let tok = p.advance()
|
||||||
|
let lit = Expr(kind: ekLiteral, loc: loc, exprLit: tok)
|
||||||
|
return Expr(kind: ekMacroTt, loc: loc, exprMacroTtInner: lit, exprMacroTtGroup: false)
|
||||||
p.parseExpr()
|
p.parseExpr()
|
||||||
|
|
||||||
proc parseMacroRepExpr(p: var Parser): Expr =
|
proc parseMacroRepExpr(p: var Parser): Expr =
|
||||||
@@ -1404,6 +1430,7 @@ proc parseEnumDecl(p: var Parser, isPublic: bool): Decl =
|
|||||||
let loc = p.currentLoc
|
let loc = p.currentLoc
|
||||||
discard p.expect(tkEnum, "expected 'enum'")
|
discard p.expect(tkEnum, "expected 'enum'")
|
||||||
let name = p.expect(tkIdent, "expected enum name").text
|
let name = p.expect(tkIdent, "expected enum name").text
|
||||||
|
let typeParams = p.parseTypeParams()
|
||||||
var baseType: TypeExpr = nil
|
var baseType: TypeExpr = nil
|
||||||
if p.check(tkColon):
|
if p.check(tkColon):
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
@@ -1444,6 +1471,7 @@ proc parseEnumDecl(p: var Parser, isPublic: bool): Decl =
|
|||||||
discard p.expect(tkRBrace, "expected '}' to close enum")
|
discard p.expect(tkRBrace, "expected '}' to close enum")
|
||||||
return Decl(kind: dkEnum, loc: loc, isPublic: isPublic,
|
return Decl(kind: dkEnum, loc: loc, isPublic: isPublic,
|
||||||
declEnumName: name, declEnumBaseType: baseType,
|
declEnumName: name, declEnumBaseType: baseType,
|
||||||
|
declEnumTypeParams: typeParams,
|
||||||
declEnumVariants: variants)
|
declEnumVariants: variants)
|
||||||
|
|
||||||
proc parseUnionDecl(p: var Parser, isPublic: bool): Decl =
|
proc parseUnionDecl(p: var Parser, isPublic: bool): Decl =
|
||||||
|
|||||||
+57
-7
@@ -1078,6 +1078,58 @@ proc collectGlobals*(sema: var Sema) =
|
|||||||
proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type
|
proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type
|
||||||
proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type
|
proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type
|
||||||
|
|
||||||
|
proc isResultOrOptionName(name: string): bool =
|
||||||
|
name == "Result" or name.startsWith("Result_") or
|
||||||
|
name == "Option" or name.startsWith("Option_")
|
||||||
|
|
||||||
|
proc extractResultOptionPayload*(sema: var Sema, opTy: Type, loc: SourceLocation, opKind: string): Type =
|
||||||
|
## Payload type of `Result`/`Option` for `?` (try) and `!` (unwrap).
|
||||||
|
## Prefer type-args (`Result<T,E>` → T); fall back to Ok/Some field on the enum decl.
|
||||||
|
if opTy == nil or opTy.isUnknown:
|
||||||
|
return makeUnknown()
|
||||||
|
if opTy.kind != tkNamed:
|
||||||
|
sema.emitError(loc, opKind & " requires Result or Option operand")
|
||||||
|
return makeUnknown()
|
||||||
|
let name = opTy.name
|
||||||
|
if not isResultOrOptionName(name):
|
||||||
|
sema.emitError(loc, opKind & " requires Result or Option, got " & opTy.toString)
|
||||||
|
return makeUnknown()
|
||||||
|
# Result<T,E> / Option<T> store payload in .inner
|
||||||
|
if opTy.inner.len >= 1:
|
||||||
|
return opTy.inner[0]
|
||||||
|
# Bare / monomorphized name: look up Ok(T) / Some(T) on the enum decl
|
||||||
|
var enumSym = sema.globalScope.lookup(name)
|
||||||
|
if (enumSym == nil or enumSym.decl == nil or enumSym.decl.kind != dkEnum):
|
||||||
|
if name.startsWith("Result_"):
|
||||||
|
enumSym = sema.globalScope.lookup("Result")
|
||||||
|
elif name.startsWith("Option_"):
|
||||||
|
enumSym = sema.globalScope.lookup("Option")
|
||||||
|
let wantVariant =
|
||||||
|
if name == "Option" or name.startsWith("Option_"): "Some"
|
||||||
|
else: "Ok"
|
||||||
|
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
|
||||||
|
var subst = initTable[string, Type]()
|
||||||
|
# Result_String_String → try to bind type params from mangled suffix when possible
|
||||||
|
if opTy.inner.len == 0 and enumSym.decl.declEnumTypeParams.len > 0 and
|
||||||
|
(name.startsWith("Result_") or name.startsWith("Option_")):
|
||||||
|
let prefix = if name.startsWith("Result_"): "Result_" else: "Option_"
|
||||||
|
let rest = name[prefix.len .. ^1]
|
||||||
|
# Split on '_' is imperfect for nested types; for simple T_E it works
|
||||||
|
let parts = rest.split('_')
|
||||||
|
var pi = 0
|
||||||
|
for tp in enumSym.decl.declEnumTypeParams:
|
||||||
|
if pi < parts.len and parts[pi].len > 0:
|
||||||
|
# Re-resolve simple type names (int, String, …)
|
||||||
|
let te = TypeExpr(kind: tekNamed, typeName: parts[pi])
|
||||||
|
subst[tp.name] = sema.resolveType(te)
|
||||||
|
inc pi
|
||||||
|
for variant in enumSym.decl.declEnumVariants:
|
||||||
|
if variant.name == wantVariant and variant.fields.len > 0:
|
||||||
|
let raw = sema.resolveType(variant.fields[0])
|
||||||
|
return sema.substituteTypeInType(raw, subst)
|
||||||
|
# Unknown payload — don't invent int (breaks String Results)
|
||||||
|
return makeUnknown()
|
||||||
|
|
||||||
proc typeImplements(sema: Sema, t: Type, interfaceName: string): bool =
|
proc typeImplements(sema: Sema, t: Type, interfaceName: string): bool =
|
||||||
## Check if a type implements an interface by verifying all required methods exist.
|
## Check if a type implements an interface by verifying all required methods exist.
|
||||||
if t.isUnknown: return true
|
if t.isUnknown: return true
|
||||||
@@ -1838,14 +1890,12 @@ proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type =
|
|||||||
discard sema.checkExpr(expr.exprIsOperand, scope)
|
discard sema.checkExpr(expr.exprIsOperand, scope)
|
||||||
return makeBool()
|
return makeBool()
|
||||||
of ekTry:
|
of ekTry:
|
||||||
discard sema.checkExpr(expr.exprTryOperand, scope)
|
let opTy = sema.checkExpr(expr.exprTryOperand, scope)
|
||||||
# For now, assume Result<int, String> -> int
|
# Payload of Result/Option; validates operand is Result or Option
|
||||||
# TODO: check operand is Result/Option and current function returns same type
|
return sema.extractResultOptionPayload(opTy, expr.loc, "try operator (`?`)")
|
||||||
return makeInt()
|
|
||||||
of ekUnwrap:
|
of ekUnwrap:
|
||||||
discard sema.checkExpr(expr.exprUnwrapOperand, scope)
|
let opTy = sema.checkExpr(expr.exprUnwrapOperand, scope)
|
||||||
# Unwrap: extract Ok value or panic on Err
|
return sema.extractResultOptionPayload(opTy, expr.loc, "unwrap operator (`!`)")
|
||||||
return makeInt()
|
|
||||||
of ekBlock:
|
of ekBlock:
|
||||||
var blockScope = newScope(scope)
|
var blockScope = newScope(scope)
|
||||||
var lastType = makeVoid()
|
var lastType = makeVoid()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[Package]
|
[Package]
|
||||||
Name = "buxc"
|
Name = "buxc"
|
||||||
Version = "1.0.0"
|
Version = "1.0.2"
|
||||||
Type = "bin"
|
Type = "bin"
|
||||||
|
|
||||||
[Build]
|
[Build]
|
||||||
|
|||||||
+36
-12
@@ -1,5 +1,8 @@
|
|||||||
# Build and Test Guide
|
# Build and Test Guide
|
||||||
|
|
||||||
|
> **Compiler:** **v1.0.0** — `./buxc --version` → `bux 1.0.0 (bootstrap)`
|
||||||
|
> Selfhost: `make selfhost` → `buxc2` reports `Bux 1.0.0 (self-hosting)`.
|
||||||
|
|
||||||
This guide covers building the Bux bootstrap compiler, creating projects, and running tests.
|
This guide covers building the Bux bootstrap compiler, creating projects, and running tests.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -178,13 +181,25 @@ no-libc freestanding firmware.
|
|||||||
| Thin runtime (no pthread/OpenSSL) | ✅ | `rt/runtime_minimal.c` |
|
| Thin runtime (no pthread/OpenSSL) | ✅ | `rt/runtime_minimal.c` |
|
||||||
| Static musl / distroless | ✅ | `make test-musl-static`, Dockerfiles |
|
| Static musl / distroless | ✅ | `make test-musl-static`, Dockerfiles |
|
||||||
| Linux multi-arch cross | ✅ | aarch64 + riscv64 smokes (SKIP without gcc) |
|
| Linux multi-arch cross | ✅ | aarch64 + riscv64 smokes (SKIP without gcc) |
|
||||||
| True freestanding (`-ffreestanding`, no libc) | 🔬 spike | Needs custom alloc, panic, and I/O stubs |
|
| True freestanding (`-ffreestanding`, no libc) | ✅ spike shipped | `rt/runtime_freestanding.c` + `BUX_RUNTIME=freestanding` |
|
||||||
| Cortex-M / qemu-system | 🔬 spike | Same; plus linker scripts and startup |
|
| Cortex-M / qemu-system | 🔬 research | Needs linker scripts, startup, board UART — not in tree |
|
||||||
|
|
||||||
**Practical path today:** build with `BUX_RUNTIME=minimal --static --target …` for
|
**Freestanding runtime** (`rt/runtime_freestanding.c`):
|
||||||
Linux userspace on foreign ISAs; treat bare-metal as a research project that
|
|
||||||
starts from a custom `runtime_freestanding.c` (not shipped) and does **not**
|
- No hosted libc: bump heap (default 256 KiB), freestanding string/arith helpers
|
||||||
import `Std::Net` / `Std::Task` / OpenSSL.
|
- Weak hooks `bux_fs_write` / `bux_fs_halt` for board BSP or host overrides
|
||||||
|
- Net / Task / TLS / FS return failure stubs
|
||||||
|
- Optional `_start` when compiled with `-DBUX_FS_PROVIDE_START` (nostdlib experiments)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-freestanding # -ffreestanding -c + package build exit 42
|
||||||
|
BUX_RUNTIME=freestanding buxc build .
|
||||||
|
# Still uses host crt0 unless you pass -nostdlib / custom start via BUX_CFLAGS
|
||||||
|
```
|
||||||
|
|
||||||
|
**Practical path for cloud/cross:** `BUX_RUNTIME=minimal --static --target …` for
|
||||||
|
Linux userspace. Use `freestanding` only for no-libc research; do **not** import
|
||||||
|
`Std::Net` / `Std::Task` / OpenSSL on that path.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -329,16 +344,25 @@ make docs # writes docs/api/stdlib.md
|
|||||||
Scans `///` line comments (and bootstrap also accepts adjacent `/* */`) immediately
|
Scans `///` line comments (and bootstrap also accepts adjacent `/* */`) immediately
|
||||||
before `func` / `struct` / `enum` / `interface` / `module` declarations.
|
before `func` / `struct` / `enum` / `interface` / `module` declarations.
|
||||||
|
|
||||||
### Language Server (`bux-lsp` 0.4.0)
|
### Language Server (`bux-lsp` 0.17.0)
|
||||||
|
|
||||||
|
Full write-up: **[LSP.md](LSP.md)**. VS Code client: **[../vscode/README.md](../vscode/README.md)**.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make lsp # → tools/bux-lsp
|
make lsp # → tools/bux-lsp
|
||||||
nim r --path:bootstrap tools/test_lsp_locals.nim
|
make test-lsp # unit + all smoke_lsp_*.sh
|
||||||
./tools/smoke_lsp_hover.sh
|
make vscode # compile VS Code extension (npm)
|
||||||
```
|
```
|
||||||
|
|
||||||
Features: diagnostics (`buxc check`), hover, go-to-def, outline, completion.
|
| Area | Status |
|
||||||
**Locals are position-sensitive** (nested scopes / shadowing). **Inferred `let` types**
|
|------|--------|
|
||||||
appear on hover (`let x: int · inferred`).
|
| **Error underlines** | ✅ live on open/**change**/save (in-process lex/parse/sema) |
|
||||||
|
| Hover / definition / completion | ✅ (scoped locals + inferred `let`) |
|
||||||
|
| References / rename | ✅ (fields, variants, methods, import paths) |
|
||||||
|
| Document + workspace symbols | ✅ |
|
||||||
|
| Call hierarchy / type hierarchy / implementation | ✅ |
|
||||||
|
|
||||||
|
Any editor that speaks **LSP over stdio** can run `tools/bux-lsp` directly.
|
||||||
|
|
||||||
### Example Programs
|
### Example Programs
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Bux — План за подобрения (post-v1.0.0)
|
||||||
|
|
||||||
|
> **Дата:** 2026-07-28
|
||||||
|
> **Статус:** Всички приоритетни задачи изпълнени ✅ · follow-up DX/correctness shipped · **v1.0.2** patch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 6 — v1.0.2 runtime correctness (2026-07-28)
|
||||||
|
|
||||||
|
| # | Задача | Файлове |
|
||||||
|
|---|--------|---------|
|
||||||
|
| S.1 | `Array_Push` grow from cap 0 (no segfault) | `lib/Array.bux` |
|
||||||
|
| S.2 | `Map`/`StringMap`/`Set` min cap + auto-rehash (no hang / FPE) | `lib/Map.bux`, `lib/Set.bux` |
|
||||||
|
| S.3 | Integer `/` `%` → `bux_div_i64` / `bux_mod_i64` | `bootstrap/lir_c_backend.nim`, `bootstrap/c_backend.nim`, `src/c_backend.bux` |
|
||||||
|
| S.4 | Version **1.0.2** + golden regressions | `bootstrap/cli.nim`, `src/cli.bux`, `bux.toml`, `tests/stdlib_golden/` |
|
||||||
|
|
||||||
|
**Verified:** stdlib golden PASS; `Array_New(0)+Push` OK; `Map_New(2)+5 inserts` OK; div-by-zero → `bux panic: division by zero`; examples PASS; fmt-check PASS.
|
||||||
|
|
||||||
|
## Сесия 4 — Try payload type + LSP formatting (2026-07-28)
|
||||||
|
|
||||||
|
| # | Задача | Файлове |
|
||||||
|
|---|--------|---------|
|
||||||
|
| F.5 | `?` / `!` вече връщат **Ok/Some payload type** (не винаги `int`) | `bootstrap/sema.nim`, `bootstrap/hir_lower.nim` |
|
||||||
|
| F.6 | Example `try_generic` — `Result<String, String>` + `?` | `examples/try_generic.bux` |
|
||||||
|
| D.6 | LSP **v0.18** `textDocument/formatting` (+ range) = `bux fmt` | `tools/lsp_server.nim`, `tools/smoke_lsp_formatting.sh` |
|
||||||
|
| D.7 | VS Code format-on-save default; docs | `vscode/package.json`, `docs/LSP.md` |
|
||||||
|
|
||||||
|
**Verified:** `try_generic` prints `hello` / `empty name`; `try_operator` still OK; formatting smoke PASS.
|
||||||
|
|
||||||
|
## Сесия 5 — Post-1.0 backlog: macros + freestanding (2026-07-28)
|
||||||
|
|
||||||
|
| # | Задача | Файлове |
|
||||||
|
|---|--------|---------|
|
||||||
|
| M.1 | Generics in `$t:type` + `$t` in `Array_New<$t>` | `bootstrap/macroexpand.nim`, `src/macroexpand.bux` |
|
||||||
|
| M.2 | Operators-only `:tt` paste (`$op($a,$b)` + juxta binary split) | `bootstrap/parser.nim`, `bootstrap/macroexpand.nim` |
|
||||||
|
| M.3 | Examples `macro_type_generic`, `macro_op_paste` | `examples/` |
|
||||||
|
| R.1 | `rt/runtime_freestanding.c` + `BUX_RUNTIME=freestanding` | `rt/`, `bootstrap/cli.nim`, `src/cli.bux` |
|
||||||
|
| R.2 | `make test-freestanding` smoke | `tools/smoke_freestanding.sh` |
|
||||||
|
|
||||||
|
**Verified:** both macro examples PASS; freestanding `-ffreestanding -c` + package exit 42.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Свършено (3 сесии, 13 файла, +608/-96 реда)
|
||||||
|
|
||||||
|
### Сесия 1 — Критични бъгове
|
||||||
|
| # | Задача | Файлове |
|
||||||
|
|---|--------|---------|
|
||||||
|
| B.1 | Грешки при хардкоднати лимити (>8 param/variant/capture) | `src/parser.bux` |
|
||||||
|
| B.2 | `is` оператор — lowering + codegen | `src/hir_lower.bux`, `src/c_backend.bux` |
|
||||||
|
| B.3 | Enum type param парсване (`enum Result<T,E>`) | `src/parser.bux` |
|
||||||
|
| B.4 | `Type_Eq` структурно сравнение | `src/types.bux` |
|
||||||
|
| B.6 | `Iter<T>` safety документация | `lib/Iter.bux` |
|
||||||
|
| B.8 | Generic enum lowering (selfhost) | `src/hir_lower.bux` |
|
||||||
|
| B.9 | Generic enum lowering (Nim bootstrap) | `bootstrap/ast.nim`, `bootstrap/parser.nim`, `bootstrap/hir_lower.nim` |
|
||||||
|
|
||||||
|
### Сесия 2 — Tag/type манглинг
|
||||||
|
| # | Задача | Файлове |
|
||||||
|
|---|--------|---------|
|
||||||
|
| B.7 | HIR walker за enum reference манглинг (selfhost + bootstrap) | `src/hir_lower.bux`, `bootstrap/hir_lower.nim` |
|
||||||
|
| — | generic_enum example + test | `examples/generic_enum.bux`, `Makefile` |
|
||||||
|
|
||||||
|
### Сесия 3 — Data field достъп + Stdlib
|
||||||
|
| # | Задача | Файлове |
|
||||||
|
|---|--------|---------|
|
||||||
|
| B.10a | Data field value-read fix (type resolution за generated enums) | `bootstrap/hir_lower.nim` |
|
||||||
|
| B.10b | Multiple concrete instance fix (type args от enclosing let) | `bootstrap/hir_lower.nim` |
|
||||||
|
| B.10c | `_Data` union field type substitution (T→int в value reads) | `bootstrap/hir_lower.nim` |
|
||||||
|
| B.11 | `Result<T,E>` и `Option<T>` генерични | `lib/Result.bux`, `lib/Option.bux`, `examples/map_remove.bux`, `tests/stdlib_golden/collections/src/Main.bux` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Follow-up fixes (post DeepSeek session)
|
||||||
|
|
||||||
|
| # | Бъг | Фикс |
|
||||||
|
|---|-----|------|
|
||||||
|
| F.1 | `is` → LIR `unhandled hIs` / always false | Desugar to `==` / `.tag ==` in bootstrap + selfhost |
|
||||||
|
| F.2 | `?` + `Result<T,E>` → `Result_Tag` C error | Concrete monomorphized typeName + `_Tag`/`_Data` mangling |
|
||||||
|
| F.3 | `Unwrap` panic continues with garbage | `bux_exit(1)` after panic in Result/Option |
|
||||||
|
| F.4 | Regression example | `examples/is_operator.bux` |
|
||||||
|
|
||||||
|
## Резултат
|
||||||
|
|
||||||
|
- **Всички тестове: 0 FAIL, 0 error**
|
||||||
|
- **Selfhost loop: детерминистичен (C + ELF identical)**
|
||||||
|
- **Generic enum-ите работят end-to-end:**
|
||||||
|
- Парсване с type параметри
|
||||||
|
- Tag проверки (`p.tag == Pair_First`)
|
||||||
|
- Data field достъп (`p.data.First_0` като l-value и r-value)
|
||||||
|
- Множество конкретни инстанции в един файл
|
||||||
|
- `Result<T,E>` и `Option<T>` в stdlib
|
||||||
|
- `is` operator (simple + algebraic enums)
|
||||||
|
- `?` try operator with monomorphized `Result<T,E>`
|
||||||
|
|
||||||
|
## Пример който работи
|
||||||
|
|
||||||
|
```bux
|
||||||
|
enum Pair<T, U> {
|
||||||
|
First(T), Second(U),
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let p: Pair<int, String> = Pair_MakeFirst<int, String>(42);
|
||||||
|
if p.tag == Pair_First {
|
||||||
|
PrintInt(p.data.First_0 as int64); // → 42
|
||||||
|
}
|
||||||
|
let s: Pair<String, int> = Pair_MakeSecond<String, int>(99);
|
||||||
|
if s.tag == Pair_Second {
|
||||||
|
PrintInt(s.data.Second_0 as int64); // → 99
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
# Bux Language Server (`bux-lsp`)
|
||||||
|
|
||||||
|
> **Status:** **v0.18.0** — stdio JSON-RPC 2.0 language server
|
||||||
|
> **Binary:** `tools/bux-lsp` (`make lsp`)
|
||||||
|
> **Editors:** VS Code extension in [`vscode/`](../vscode/README.md); any LSP client via stdio
|
||||||
|
|
||||||
|
Bux already ships a real Language Server Protocol implementation. It is **not**
|
||||||
|
syntax-only: hover and outline use bootstrap semantic analysis when available,
|
||||||
|
**error underlines (red squiggles)** come from **in-process** lex / parse / type-check
|
||||||
|
of the **live editor buffer** on every open, edit, and save, and **Format Document**
|
||||||
|
uses the same indentation engine as `bux fmt`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the repository root
|
||||||
|
make lsp # build → tools/bux-lsp
|
||||||
|
make vscode # optional: compile VS Code client
|
||||||
|
make test-lsp # unit + smoke tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Point your editor at the binary:
|
||||||
|
|
||||||
|
| Client | How |
|
||||||
|
|--------|-----|
|
||||||
|
| **VS Code** | Open the repo (or install `vscode/`). Extension auto-finds `tools/bux-lsp`. Setting: `bux.lsp.path` |
|
||||||
|
| **Neovim** | `vim.lsp.start({ cmd = { "path/to/tools/bux-lsp" }, … })` |
|
||||||
|
| **Helix / Zed / Emacs** | Configure language server command = `bux-lsp` (stdio) |
|
||||||
|
| **Any LSP client** | Spawn `bux-lsp` with no args; speak LSP over stdin/stdout |
|
||||||
|
|
||||||
|
Protocol framing: standard `Content-Length` headers + JSON-RPC 2.0 body.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capabilities (v0.18)
|
||||||
|
|
||||||
|
| Method | Support | Notes |
|
||||||
|
|--------|---------|--------|
|
||||||
|
| `initialize` / `shutdown` / `exit` | ✅ | `serverInfo`: `bux-lsp` 0.18.0 |
|
||||||
|
| `textDocument/didOpen` / `didChange` / `didSave` | ✅ | Full text sync (`textDocumentSync: 1`) |
|
||||||
|
| `textDocument/publishDiagnostics` | ✅ | **Live underlines** on open/change/save (in-process); optional `buxc` merge on open/save |
|
||||||
|
| `textDocument/formatting` | ✅ | Full document — same rules as `bux fmt` (4-space brace indent) |
|
||||||
|
| `textDocument/rangeFormatting` | ✅ | Applies full-file format (indent depends on whole brace structure) |
|
||||||
|
| `textDocument/completion` | ✅ | Trigger: `.` `:` |
|
||||||
|
| `textDocument/hover` | ✅ | Sema types for globals/stdlib; **scoped locals** + inferred `let` |
|
||||||
|
| `textDocument/definition` | ✅ | Go to definition |
|
||||||
|
| `textDocument/references` | ✅ | Find all references |
|
||||||
|
| `textDocument/rename` + `prepareRename` | ✅ | Locals, globals, fields, variants, methods, import path segments |
|
||||||
|
| `textDocument/documentSymbol` | ✅ | Outline |
|
||||||
|
| `workspace/symbol` | ✅ | Fuzzy-ish workspace search |
|
||||||
|
| `textDocument/implementation` | ✅ | Interface → implementing types/methods |
|
||||||
|
| Call hierarchy | ✅ | prepare / incoming / outgoing (funcs + methods + interface dispatch) |
|
||||||
|
| Type hierarchy | ✅ | prepare / supertypes / subtypes (`extend T for I`) |
|
||||||
|
|
||||||
|
### Version history (high level)
|
||||||
|
|
||||||
|
| Ver | Highlights |
|
||||||
|
|-----|------------|
|
||||||
|
| 0.2–0.3 | Diagnostics, hover, go-to-def, outline, real sema enrich |
|
||||||
|
| 0.4 | Position-sensitive locals, inferred `let` types |
|
||||||
|
| 0.5 | References + rename |
|
||||||
|
| 0.6 | `workspace/symbol` |
|
||||||
|
| 0.7–0.10 | Deep rename (fields/variants/methods/paths) |
|
||||||
|
| 0.8–0.11 | Call hierarchy (methods, interface dispatch) |
|
||||||
|
| 0.12–0.14 | Module-path rename, `implementation`, workspace import index |
|
||||||
|
| 0.15–0.16 | Type hierarchy + workspace type-impl index |
|
||||||
|
| 0.17 | **Live buffer diagnostics** (lex/parse/sema) on every edit — red squiggles without `buxc` on PATH |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VS Code
|
||||||
|
|
||||||
|
See **[vscode/README.md](../vscode/README.md)** for install, commands, and settings.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make vscode
|
||||||
|
# Status bar: "Bux" — click to restart server
|
||||||
|
# Commands: Bux: Restart / Stop / Show Output
|
||||||
|
```
|
||||||
|
|
||||||
|
Settings:
|
||||||
|
|
||||||
|
- `bux.lsp.enabled` (default `true`)
|
||||||
|
- `bux.lsp.path` (default `"bux-lsp"`; also searches `tools/bux-lsp`)
|
||||||
|
- `bux-lsp.trace.server` — `off` | `messages` | `verbose`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual smoke (no editor)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After make lsp
|
||||||
|
printf 'Content-Length: 85\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}' \
|
||||||
|
| ./tools/bux-lsp
|
||||||
|
# Expect a JSON result with "capabilities" and serverInfo version 0.17.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error underlines (what students usually want)
|
||||||
|
|
||||||
|
Open a `.bux` file with the LSP running. A type error such as:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Main() -> int {
|
||||||
|
let x: int = "boom"; // red underline under "boom"
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
is reported via `textDocument/publishDiagnostics` as soon as the file is opened or
|
||||||
|
you type (didChange). No save required. Source field: `"bux"` (in-process) or
|
||||||
|
`"buxc"` (optional CLI merge on open/save).
|
||||||
|
|
||||||
|
Automated coverage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-lsp
|
||||||
|
# tools/smoke_lsp_diagnostics.sh ← error underlines
|
||||||
|
# tools/test_lsp_locals.nim
|
||||||
|
# tools/smoke_lsp_hover.sh
|
||||||
|
# tools/smoke_lsp_rename*.sh
|
||||||
|
# tools/smoke_lsp_*hierarchy*.sh
|
||||||
|
# …
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Format Document / format-on-save
|
||||||
|
|
||||||
|
`textDocument/formatting` re-indents the buffer with **4 spaces × brace depth**
|
||||||
|
(identical to `bux fmt` / `bootstrap/fmt.nim`). Idempotent: a clean file yields
|
||||||
|
an empty edit list.
|
||||||
|
|
||||||
|
**VS Code:** the extension defaults `editor.formatOnSave` for `[bux]`. Disable
|
||||||
|
with `"editor.formatOnSave": false` in workspace settings if you prefer manual
|
||||||
|
only. Command palette: **Format Document**.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-lsp # includes tools/smoke_lsp_formatting.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations / not yet
|
||||||
|
|
||||||
|
Honest gaps (so Reddit / issue trackers stay accurate):
|
||||||
|
|
||||||
|
- **No semantic tokens** provider (TextMate grammar handles highlighting in VS Code)
|
||||||
|
- **No code actions / lightbulbs** (quick-fixes)
|
||||||
|
- **No inlay hints**
|
||||||
|
- **rangeFormatting** reformats the whole file (partial selection cannot get correct indent without full brace context)
|
||||||
|
- Completion is useful but not a full IDE IntelliSense engine
|
||||||
|
- Single-process stdio only (no TCP/socket mode)
|
||||||
|
|
||||||
|
PRs welcome: `tools/lsp_server.nim`, tests under `tools/smoke_lsp_*.sh` and `make test-lsp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
| Doc / path | Role |
|
||||||
|
|------------|------|
|
||||||
|
| [`tools/lsp_server.nim`](../tools/lsp_server.nim) | Server implementation |
|
||||||
|
| [`vscode/`](../vscode/) | Official VS Code client |
|
||||||
|
| [`BuildAndTest.md`](BuildAndTest.md) | Build / test matrix |
|
||||||
|
| [`Makefile`](../Makefile) targets `lsp`, `test-lsp`, `vscode` |
|
||||||
+55
-5
@@ -1,6 +1,11 @@
|
|||||||
# Bux Language Reference
|
# Bux Language Reference
|
||||||
|
|
||||||
This document describes the Bux programming language as implemented by the bootstrap compiler.
|
> **Status:** Normative for **v1.0.0** (language freeze).
|
||||||
|
> Describes Bux as implemented by the **bootstrap** (`buxc`, Nim) and **self-hosted** (`buxc2`) compilers.
|
||||||
|
> Behaviour here is the contract for semver after 1.0 — see [`SEMVER.md`](SEMVER.md) and [`RELEASE_v1.0.0.md`](RELEASE_v1.0.0.md).
|
||||||
|
|
||||||
|
This document is the primary language specification. Compiler bugs that contradict
|
||||||
|
it are fixed without a MAJOR version bump; intentional breaking changes require MAJOR.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -992,6 +997,25 @@ func Compute() -> Result {
|
|||||||
```
|
```
|
||||||
|
|
||||||
`?` can be used on `Result` and `Option` types in any expression context.
|
`?` can be used on `Result` and `Option` types in any expression context.
|
||||||
|
The type of `expr?` is the **Ok / Some payload** (`T` in `Result<T,E>` or
|
||||||
|
`Option<T>`), not always `int`. The enclosing function must return a compatible
|
||||||
|
Result/Option so Err/None can propagate.
|
||||||
|
|
||||||
|
```bux
|
||||||
|
// Generic Result — payload type is String
|
||||||
|
func GetName() -> Result<String, String> {
|
||||||
|
return Result_NewOk<String, String>("bux");
|
||||||
|
}
|
||||||
|
func Run() -> Result<String, String> {
|
||||||
|
let n: String = GetName()?; // n: String
|
||||||
|
return Result_NewOk<String, String>(n);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The postfix unwrap operator `expr!` extracts Ok/Some or panics (and exits) on
|
||||||
|
Err/None; its type is likewise the payload type.
|
||||||
|
|
||||||
|
See also `examples/try_operator.bux` and `examples/try_generic.bux`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1307,13 +1331,32 @@ macro! with_acc {
|
|||||||
between fragments) matches a **single call-site argument** that is a call
|
between fragments) matches a **single call-site argument** that is a call
|
||||||
expression: `apply_juxta!(Add(2, 5))` → binds `$f=Add`, `$args` = arg-list
|
expression: `apply_juxta!(Add(2, 5))` → binds `$f=Add`, `$args` = arg-list
|
||||||
group, then `$f($args)` flattens to `Add(2, 5)`.
|
group, then `$f($args)` flattens to `Add(2, 5)`.
|
||||||
- **Type fragments (session 87):**
|
- **Type fragments (session 87+):** named, pointer, and **generic** types
|
||||||
|
(`Array<int>`, `*int`); `$t` substitutes in `sizeof` / cast / let types and
|
||||||
|
monomorph call type args (`Array_New<$t>`).
|
||||||
```bux
|
```bux
|
||||||
macro! size_of {
|
macro! size_of {
|
||||||
( $t:type ) => { sizeof($t) as int }
|
( $t:type ) => { sizeof($t) as int }
|
||||||
}
|
}
|
||||||
|
macro! new_array {
|
||||||
|
( $t:type, $cap:expr ) => { Array_New<$t>($cap) }
|
||||||
|
}
|
||||||
let n: int = size_of!(int);
|
let n: int = size_of!(int);
|
||||||
let p: int = size_of!(*int);
|
let p: int = size_of!(*int);
|
||||||
|
let s: int = size_of!(Array<int>);
|
||||||
|
var a: Array<int> = new_array!(int, 4);
|
||||||
|
```
|
||||||
|
- **Operators-only `:tt` paste:**
|
||||||
|
```bux
|
||||||
|
macro! apply_op {
|
||||||
|
( $op:tt, $a:expr, $b:expr ) => { $op($a, $b) }
|
||||||
|
}
|
||||||
|
macro! flip_op {
|
||||||
|
( $a:expr, $op:tt, $b:expr ) => { $op($b, $a) }
|
||||||
|
}
|
||||||
|
let x: int = apply_op!(+, 3, 4); // 7
|
||||||
|
let y: int = apply_op!(*, 6, 7); // 42
|
||||||
|
let z: int = flip_op!(10 - 3); // -7 (juxta binary split)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Invocation
|
### Invocation
|
||||||
@@ -1410,8 +1453,15 @@ Examples: `examples/macro_hygiene.bux`, `examples/macro_unhygienic.bux`.
|
|||||||
templates (not as a free-standing primary expression).
|
templates (not as a free-standing primary expression).
|
||||||
- Raw delimiter-balanced `tt` covers **tuple** `(a, b)` and **slice lit**
|
- Raw delimiter-balanced `tt` covers **tuple** `(a, b)` and **slice lit**
|
||||||
`[a, b]` groups, plus **juxta call-split** for `$f:ident $args:tt` matching
|
`[a, b]` groups, plus **juxta call-split** for `$f:ident $args:tt` matching
|
||||||
`F(a, b)`. Arbitrary free-form token pastes (operators-only, type-only
|
`F(a, b)`.
|
||||||
without AST) remain out of scope.
|
- **Operators-only paste:** bare binary ops as `:tt` (`+`, `*`, `==`, …) and
|
||||||
|
juxta binary split `$a:expr $op:tt $b:expr` on a single binary arg. Template
|
||||||
|
form `$op($a, $b)` rebuilds `a OP b`. See `examples/macro_op_paste.bux`.
|
||||||
|
- **`:type` generics:** `Array<int>`, `*int`, nested type args; `$t` splices
|
||||||
|
into `sizeof($t)`, casts, and `Array_New<$t>(…)`. See
|
||||||
|
`examples/macro_type.bux`, `examples/macro_type_generic.bux`.
|
||||||
|
- Fully free-form token streams (unparsed soup) remain out of scope.
|
||||||
|
|
||||||
Examples: `examples/macro_tt.bux`, `examples/macro_tt_raw.bux`,
|
Examples: `examples/macro_tt.bux`, `examples/macro_tt_raw.bux`,
|
||||||
`examples/macro_repeat.bux`, `examples/macro_nested.bux`.
|
`examples/macro_repeat.bux`, `examples/macro_nested.bux`,
|
||||||
|
`examples/macro_type_generic.bux`, `examples/macro_op_paste.bux`.
|
||||||
|
|||||||
+3
-2
@@ -1,8 +1,9 @@
|
|||||||
# Bux Package Manager
|
# Bux Package Manager
|
||||||
|
|
||||||
> **Status:** Path + git + **local/file registry** (E.1) + **HTTP(S) index URL** (cached under `~/.bux/cache/`).
|
> **Status (v1.0):** Path + git + **local/file registry** + **HTTP(S) index URL**
|
||||||
|
> (cached under `~/.bux/cache/`). Lockfile + `bux install --locked` for CI.
|
||||||
|
|
||||||
See also: [SEMVER.md](SEMVER.md) for version policy.
|
See also: [SEMVER.md](SEMVER.md) for version policy (active as of language **v1.0.0**).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+16
-4
@@ -94,7 +94,7 @@
|
|||||||
|---|--------|------|--------|
|
|---|--------|------|--------|
|
||||||
| E.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks | ✅ local index + **HTTP(S) URL** cache + file/git + `search` |
|
| E.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks | ✅ local index + **HTTP(S) URL** cache + file/git + `search` |
|
||||||
| E.2 | 3–5 production-quality apps в `apps/` | Showcase | ✅ 4 apps + `make test-apps` smoke (build + CLI) |
|
| E.2 | 3–5 production-quality apps в `apps/` | Showcase | ✅ 4 apps + `make test-apps` smoke (build + CLI) |
|
||||||
| E.3 | Language freeze + semver policy | Trust | ✅ draft `docs/SEMVER.md` |
|
| E.3 | Language freeze + semver policy | Trust | ✅ **v1.0.0** + active `docs/SEMVER.md` |
|
||||||
| E.4 | Debugger/DWARF basics | Systems audience | ✅ `#line`→`.bux` + `-g` / `--release`; `make test-dwarf` |
|
| E.4 | Debugger/DWARF basics | Systems audience | ✅ `#line`→`.bux` + `-g` / `--release`; `make test-dwarf` |
|
||||||
| E.5 | Benchmarks vs C/Zig/Nim (micro + nexus) | Marketing + regression | ✅ micro + C/Nim/Zig twins + `make bench-nexus` (wrk) |
|
| E.5 | Benchmarks vs C/Zig/Nim (micro + nexus) | Marketing + regression | ✅ micro + C/Nim/Zig twins + `make bench-nexus` (wrk) |
|
||||||
|
|
||||||
@@ -1212,7 +1212,7 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Платформен фокус (v0.5 → v1.0)
|
## Платформен фокус (v0.5 → v1.0 ✅)
|
||||||
|
|
||||||
| Ниша | Какво значи за Bux | Статус / посока |
|
| Ниша | Какво значи за Bux | Статус / посока |
|
||||||
|------|--------------------|-----------------|
|
|------|--------------------|-----------------|
|
||||||
@@ -1472,10 +1472,22 @@ bootstrap + **buxc2** `macro_tt_raw` (incl. slice) PASS.
|
|||||||
5. Tag `v1.0.0` after `make test` gate
|
5. Tag `v1.0.0` after `make test` gate
|
||||||
|
|
||||||
**Post-1.0 backlog (MINOR, not freeze blockers):**
|
**Post-1.0 backlog (MINOR, not freeze blockers):**
|
||||||
- Generics in `:type` (`Array<int>`); operators-only tt paste
|
- ~~Generics in `:type` (`Array<int>`); operators-only tt paste~~ ✅ session 5
|
||||||
- `runtime_freestanding.c` + Cortex-M research
|
- ~~`runtime_freestanding.c`~~ ✅ session 5 (Cortex-M / board BSP still research)
|
||||||
- LSP / IDE versioning independent of language MAJOR
|
- LSP / IDE versioning independent of language MAJOR
|
||||||
|
|
||||||
|
### Follow-up (2026-07-28)
|
||||||
|
|
||||||
|
1. **`?` / `!` payload types** — bootstrap no longer hardcodes `int`; Ok/Some type
|
||||||
|
from `Result<T,E>` / enum fields (`bootstrap/sema.nim`, `hir_lower.nim`).
|
||||||
|
2. **Example** `try_generic` — String Result propagation.
|
||||||
|
3. **LSP 0.18** — `textDocument/formatting` (+ range) via `formatSource` / `bux fmt`;
|
||||||
|
VS Code format-on-save default; `tools/smoke_lsp_formatting.sh`.
|
||||||
|
4. **Macros:** generic `:type` + `Array_New<$t>`; operators-only `$op($a,$b)` /
|
||||||
|
juxta binary split (`examples/macro_type_generic`, `macro_op_paste`).
|
||||||
|
5. **Freestanding:** `rt/runtime_freestanding.c`, `BUX_RUNTIME=freestanding`,
|
||||||
|
`make test-freestanding`.
|
||||||
|
|
||||||
### Изрично **не** правим
|
### Изрично **не** правим
|
||||||
|
|
||||||
- Повече Windows examples / Win OpenSSL / Win sockets
|
- Повече Windows examples / Win OpenSSL / Win sockets
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Bux documentation index
|
||||||
|
|
||||||
|
**Language version:** **v1.0.0** (freeze) — tag `v1.0.0`
|
||||||
|
|
||||||
|
| Document | Role |
|
||||||
|
|----------|------|
|
||||||
|
| [LanguageRef.md](LanguageRef.md) | **Normative** language specification |
|
||||||
|
| [Stdlib.md](Stdlib.md) | Standard library overview / API tables |
|
||||||
|
| [BuildAndTest.md](BuildAndTest.md) | Build, test, cross, static, containers |
|
||||||
|
| [LSP.md](LSP.md) | **Language server** (`bux-lsp` 0.17) — live error underlines + editor setup |
|
||||||
|
| [Packages.md](Packages.md) | Manifests, registry, lockfiles |
|
||||||
|
| [SEMVER.md](SEMVER.md) | Versioning policy (active after 1.0) |
|
||||||
|
| [RELEASE_v1.0.0.md](RELEASE_v1.0.0.md) | v1.0.0 release / freeze notes |
|
||||||
|
| [ROADMAP.md](ROADMAP.md) | Language construct checklist |
|
||||||
|
| [QUALITY_PLAN.md](QUALITY_PLAN.md) | Session log + path to v1.0 (archive) |
|
||||||
|
| [api/stdlib.md](api/stdlib.md) | Generated API docs from `///` (via `make docs`) |
|
||||||
|
|
||||||
|
Parent tree: [../README.md](../README.md) · historical plan: [../PLAN.md](../PLAN.md)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Bux v1.0.2 — Patch
|
||||||
|
|
||||||
|
**Date:** 2026-07-28
|
||||||
|
**Tag:** `v1.0.2`
|
||||||
|
|
||||||
|
Patch release on the **v1.0 language freeze**. No language-surface breakage;
|
||||||
|
stdlib + codegen correctness only.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
| Area | Issue | Fix |
|
||||||
|
|------|--------|-----|
|
||||||
|
| `Array_Push` | `Array_New(0)` + push → segfault (`cap * 2 == 0`) | Grow to at least 4 when capacity was 0 (same as `Array_Insert`) |
|
||||||
|
| `Map` / `StringMap` | Full table → infinite loop in open-addressing probe; `cap == 0` → SIGFPE on `%` | Default min cap 8; auto-rehash when load ≥ 50% |
|
||||||
|
| `Set` | Same as Map | Same grow / min-cap policy |
|
||||||
|
| Integer `/` and `%` | Divisor 0 → raw SIGFPE | Emit `bux_div_i64` / `bux_mod_i64` (bootstrap LIR + C backends, selfhost C backend) |
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
./buxc --version # bux 1.0.2 (bootstrap)
|
||||||
|
make test-stdlib test-examples
|
||||||
|
```
|
||||||
+5
-3
@@ -1,9 +1,11 @@
|
|||||||
# Bux Language Roadmap — New Constructs
|
# Bux Language Roadmap — New Constructs
|
||||||
|
|
||||||
> **Updated:** 2026-07-27 | **Status:** ✅ Constructs for v1.0 shipped (language freeze)
|
> **Updated:** 2026-07-27 | **Status:** ✅ Constructs for **v1.0.0** shipped (language freeze)
|
||||||
> Recent: macros (`tt`/`type`/juxta), stdlib daily APIs, Linux/riscv smoke; see `docs/RELEASE_v1.0.0.md`.
|
> Recent: macros (`tt`/`type`/juxta), stdlib daily APIs, Linux/riscv smoke.
|
||||||
|
> Release: [`RELEASE_v1.0.0.md`](RELEASE_v1.0.0.md) · Spec: [`LanguageRef.md`](LanguageRef.md).
|
||||||
|
|
||||||
This document tracks planned language constructs beyond Phase 8 strategy.
|
This document tracks language constructs (historical + status). For the normative
|
||||||
|
spec use LanguageRef; for post-1.0 optional work see QUALITY_PLAN session 88 backlog.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+8
-3
@@ -1,6 +1,11 @@
|
|||||||
# Bux Standard Library
|
# Bux Standard Library
|
||||||
|
|
||||||
The Bux standard library provides core functionality for systems programming. All modules are merged into every compilation, so no explicit linking is needed.
|
> **Status:** Documented API for **v1.0.0**. Sources live under `lib/`; modules are
|
||||||
|
> merged into every compilation (no separate link step for stdlib).
|
||||||
|
|
||||||
|
The standard library targets systems and cloud-on-Linux programming (I/O, collections,
|
||||||
|
net/TLS with full runtime, crypto, tasks/channels). Thin/static builds use
|
||||||
|
`BUX_RUNTIME=minimal` — see [BuildAndTest.md](BuildAndTest.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -280,8 +285,8 @@ func Main() -> int {
|
|||||||
let s: String = String_Replace("hello world", "world", "Bux");
|
let s: String = String_Replace("hello world", "world", "Bux");
|
||||||
PrintLine(s); // "hello Bux"
|
PrintLine(s); // "hello Bux"
|
||||||
|
|
||||||
let fmt: String = String_Format2("{0} + {1} = magic", "Bux", "QBE");
|
let fmt: String = String_Format2("{0} + {1} = magic", "Bux", "C");
|
||||||
PrintLine(fmt); // "Bux + QBE = magic"
|
PrintLine(fmt); // "Bux + C = magic"
|
||||||
|
|
||||||
let found: String = String_Find("hello world", "world");
|
let found: String = String_Find("hello world", "world");
|
||||||
if found as uint != 0 {
|
if found as uint != 0 {
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
# Self-hosted Sema Error Recovery 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 self-hosted Bux compiler (`buxc2`) report all independent semantic errors in a single run instead of stopping after the first one, using a `tyUnknown` error sentinel.
|
||||||
|
|
||||||
|
**Architecture:** Work entirely in the self-hosted compiler (`src/sema.bux`, `src/cli.bux`). Semantic errors are recorded via `Sema_EmitError`; error paths return a `tyUnknown` sentinel (through a new `Sema_EmitExprError` helper) so parent expressions and sibling statements keep being checked. Codegen is still skipped whenever `Sema_HasError` is true.
|
||||||
|
|
||||||
|
**Tech Stack:** Bux (self-hosted compiler source), Bash test fixtures, `make selfhost` (bootstrap `buxc` compiles `src/*.bux` into `build/selfhost/build/buxc2`).
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-07-28-selfhost-sema-error-recovery-design.md`
|
||||||
|
|
||||||
|
**Repo layout facts the engineer must know:**
|
||||||
|
- Self-hosted compiler sources: `src/*.bux` (entry: `src/main.bux`; pipeline wired in `src/cli.bux`).
|
||||||
|
- `make selfhost` copies `src/*.bux` to `build/selfhost/src/`, renames `main.bux` → `Main.bux`, and builds with the bootstrap `./buxc`. Result binary: `build/selfhost/build/buxc2`.
|
||||||
|
- `buxc2 check <dir>` type-checks a package (needs `BUX_STDLIB=<repo>/lib` exported when run from a foreign directory).
|
||||||
|
- Type kind constants live in `src/types.bux` (`tyUnknown = 0`, `tyVoid = 1`, `tyBool = 2`, `tyStr = 9`, `tyInt = 14`, `tyFloat64 = 21`, `tyPointer = 22`, `tyNamed = 26`, `tyFunc = 28`).
|
||||||
|
- `Sema_CheckStmt` / `Sema_CheckExpr` are in `src/sema.bux` (statement checking starts ~line 1425, expression checking ~line 819).
|
||||||
|
- **Do not use `if` as an expression** in `src/*.bux` — the bootstrap parser rejects it (that is exactly what Task 0 fixes in `hir_lower.bux`). Ternary `cond ? a : b` is ALSO unsupported by the bootstrap parser (postfix `?` is always consumed as the try-operator); use statement form instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 0: Unblock `make selfhost` (prerequisite)
|
||||||
|
|
||||||
|
`make selfhost` currently fails because `src/hir_lower.bux:2726` uses `if` as an expression, which the bootstrap parser does not support. Replace it with the supported ternary form. This task exists only so that every later task is verifiable end-to-end with `buxc2`.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/hir_lower.bux:2726`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Verify the failure first**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make selfhost 2>&1 | tail -20`
|
||||||
|
Expected: FAIL — parse error `expected expression` at `src/hir_lower.bux:2726:50` pointing at the `if`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace the if-expression with statement form**
|
||||||
|
|
||||||
|
NOTE (corrected during execution): the bootstrap parser supports NEITHER `if`-expressions NOR ternary `cond ? a : b` — the postfix loop in `bootstrap/parser.nim:794-796` consumes any `?` as the try-operator, making the ternary branch unreachable. Use statement form instead.
|
||||||
|
|
||||||
|
In `src/hir_lower.bux`, change line 2726 from:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
let operandTypeExpr: *TypeExpr = if operandExpr != null as *Expr { operandExpr.refType } else { null as *TypeExpr };
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
var operandTypeExpr: *TypeExpr = null as *TypeExpr;
|
||||||
|
if operandExpr != null as *Expr { operandTypeExpr = operandExpr.refType; }
|
||||||
|
```
|
||||||
|
|
||||||
|
(`operandTypeExpr` is only read afterwards inside the same block, so `var` instead of `let` is semantically safe.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify selfhost now builds**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make selfhost 2>&1 | tail -5`
|
||||||
|
Expected: `=== Self-hosted compiler built successfully ===` and the binary exists:
|
||||||
|
|
||||||
|
Run: `test -x build/selfhost/build/buxc2 && echo buxc2-ok`
|
||||||
|
Expected: `buxc2-ok`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/hir_lower.bux
|
||||||
|
git commit -m "fix: rewrite if-expression as statements in hir_lower (unblocks make selfhost)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add `Sema_EmitExprError` sentinel helper
|
||||||
|
|
||||||
|
Introduce one explicit helper for the "record an error, mark the expression with a `?` type, return `tyUnknown`" pattern, and convert the two existing expression error sites that already return `tyUnknown` (undeclared identifier, `self` outside method) to use it. This locks in the convention the rest of the plan relies on.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/sema.bux` (after line 94; and lines ~852, ~880)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test fixture**
|
||||||
|
|
||||||
|
Create `_test_error_recovery/` (fixture content is finalized in Task 4; for now create a minimal package):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p _test_error_recovery/src
|
||||||
|
cat > _test_error_recovery/bux.toml <<'EOF'
|
||||||
|
[Package]
|
||||||
|
Name = "error_recovery"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
|
EOF
|
||||||
|
cat > _test_error_recovery/src/Main.bux <<'EOF'
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
PrintLine(undefined_variable);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1; echo "exit=$?"`
|
||||||
|
Expected: exit=1 and the output contains `undeclared identifier 'undefined_variable'`. (This passes today; the fixture proves the sentinel path keeps working after the refactor.)
|
||||||
|
|
||||||
|
NOTE (corrected during execution): the self-hosted `check` command takes a FILE path, not a directory (`Cli_Check` reads the file directly), and single-file check mode does NOT resolve `import`s — so `PrintLine` would also appear as undeclared. The final fixture (Task 3) therefore avoids imports entirely.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the helper to `src/sema.bux`**
|
||||||
|
|
||||||
|
Immediately after `Sema_EmitError` (after line 94), insert:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Sema_EmitExprError(sema: *Sema, expr: *Expr, msg: String) -> int {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, msg);
|
||||||
|
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
||||||
|
te.kind = tekNamed;
|
||||||
|
te.typeName = "?";
|
||||||
|
expr.refType = te;
|
||||||
|
return tyUnknown;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Convert the undeclared-identifier site**
|
||||||
|
|
||||||
|
In `Sema_CheckExpr` (`ekIdent` branch, ~line 849-854), change:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
let sym: Symbol = Scope_Lookup(sema.scope, expr.strValue);
|
||||||
|
if sym.kind == 0 && !String_Eq(sym.name, expr.strValue) {
|
||||||
|
let errMsg: String = String_Concat("undeclared identifier '", expr.strValue);
|
||||||
|
let errMsg2: String = String_Concat(errMsg, "'");
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, errMsg2);
|
||||||
|
return tyUnknown;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
let sym: Symbol = Scope_Lookup(sema.scope, expr.strValue);
|
||||||
|
if sym.kind == 0 && !String_Eq(sym.name, expr.strValue) {
|
||||||
|
let errMsg: String = String_Concat("undeclared identifier '", expr.strValue);
|
||||||
|
let errMsg2: String = String_Concat(errMsg, "'");
|
||||||
|
return Sema_EmitExprError(sema, expr, errMsg2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Convert the `self` site**
|
||||||
|
|
||||||
|
In the `ekSelf` branch (~line 877-882), change:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
if sym.kind == 0 && !String_Eq(sym.name, "self") {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, "self outside method");
|
||||||
|
return tyUnknown;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
if sym.kind == 0 && !String_Eq(sym.name, "self") {
|
||||||
|
return Sema_EmitExprError(sema, expr, "self outside method");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rebuild selfhost and re-run the fixture**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make selfhost 2>&1 | tail -3`
|
||||||
|
Expected: `=== Self-hosted compiler built successfully ===`
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1; echo "exit=$?"`
|
||||||
|
Expected: exit=1 and output still contains `undeclared identifier 'undefined_variable'`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/sema.bux
|
||||||
|
git add -f _test_error_recovery/
|
||||||
|
git commit -m "feat(selfhost-sema): add Sema_EmitExprError sentinel helper"
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: `_test_*/` is gitignored, but tracked fixtures exist (e.g. `_test_error_snippet/`), so `git add -f` is the established way to commit a new fixture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Arithmetic errors return the sentinel (no cascading diagnostics)
|
||||||
|
|
||||||
|
Today a failed arithmetic expression emits an error but then returns `tyInt`/`tyFloat64`, which lets follow-on code treat the bad expression as a valid number and produce derived errors. Return `tyUnknown` via the helper instead.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/sema.bux` (`ekBinary` arithmetic tail, ~lines 961-965)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Extend the fixture with a cascade probe**
|
||||||
|
|
||||||
|
Replace `_test_error_recovery/src/Main.bux` with (no imports — single-file check mode does not resolve them):
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Main() -> int {
|
||||||
|
let bad: int = "a" + 1;
|
||||||
|
let z: int = undefined_variable;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1; echo "exit=$?"`
|
||||||
|
Expected BEFORE the fix: exit=1, output contains `arithmetic requires numeric operands` and `undeclared identifier 'undefined_variable'`. (If extra derived errors about `bad` appear, this task removes them in Task 3 interplay; the key assertion is that both independent errors appear in ONE run.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Return the sentinel from the arithmetic error path**
|
||||||
|
|
||||||
|
In `Sema_CheckExpr`, `ekBinary` branch, change:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
if !Sema_IsNumeric(left) || !Sema_IsNumeric(right) {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, "arithmetic requires numeric operands");
|
||||||
|
}
|
||||||
|
if left == tyFloat64 || right == tyFloat64 { return tyFloat64; }
|
||||||
|
return tyInt;
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
if !Sema_IsNumeric(left) || !Sema_IsNumeric(right) {
|
||||||
|
return Sema_EmitExprError(sema, expr, "arithmetic requires numeric operands");
|
||||||
|
}
|
||||||
|
if left == tyFloat64 || right == tyFloat64 { return tyFloat64; }
|
||||||
|
return tyInt;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `Sema_IsNumeric` returns `true` for `tyUnknown` (see `src/sema.bux:181`), so an already-broken operand does not re-trigger this error. That is the intended cascade suppression.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Rebuild and verify both independent errors appear in one run**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make selfhost 2>&1 | tail -3`
|
||||||
|
Expected: `=== Self-hosted compiler built successfully ===`
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1 | tee /tmp/err_recovery.out; echo "exit=$?"`
|
||||||
|
|
||||||
|
Run: `grep -c "arithmetic requires numeric operands" /tmp/err_recovery.out && grep -c "undeclared identifier" /tmp/err_recovery.out`
|
||||||
|
Expected: `1` and `1` (each error reported exactly once, in the same run).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/sema.bux
|
||||||
|
git add -f _test_error_recovery/
|
||||||
|
git commit -m "feat(selfhost-sema): arithmetic errors return tyUnknown sentinel"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Add annotation-vs-initializer assignment checking
|
||||||
|
|
||||||
|
The self-hosted sema currently never checks `let x: T = init` for type agreement (the bootstrap compiler reports `cannot assign String to int`; `buxc2` silently accepts it and lets the C compiler fail later). Add the check in `Sema_CheckStmt` (`skLet`), emitting the same message shape as the bootstrap compiler, and keep registering the variable so checking continues.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/sema.bux` (new strict-numeric predicate after `Sema_IsBool` ~line 190; `skLet` branch ~lines 1443-1462)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Extend the fixture with two assignment errors**
|
||||||
|
|
||||||
|
Replace `_test_error_recovery/src/Main.bux` with:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Main() -> int {
|
||||||
|
let x: int = "hello";
|
||||||
|
let y: bool = 42;
|
||||||
|
let z: int = undefined_variable;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1; echo "exit=$?"`
|
||||||
|
Expected BEFORE the fix: exit=1, output contains `undeclared identifier 'undefined_variable'` but NO `cannot assign` lines (this is the failing assertion the task fixes).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add a strict numeric predicate**
|
||||||
|
|
||||||
|
`Sema_IsNumeric` (`src/sema.bux:181`) deliberately treats `tyNamed`/`tyTypeParam` as numeric (operator overloading). Assignment checking must not. After `Sema_IsBool` (~line 190), insert:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Sema_IsStrictNumeric(kind: int) -> bool {
|
||||||
|
if kind == tyInt8 || kind == tyInt16 || kind == tyInt32 || kind == tyInt64 || kind == tyInt { return true; }
|
||||||
|
if kind == tyUInt8 || kind == tyUInt16 || kind == tyUInt32 || kind == tyUInt64 || kind == tyUInt { return true; }
|
||||||
|
if kind == tyFloat32 || kind == tyFloat64 { return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add a display-name helper for diagnostics**
|
||||||
|
|
||||||
|
After `Sema_IsStrictNumeric`, insert:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Sema_TypeNameForDiag(te: *TypeExpr, kind: int) -> String {
|
||||||
|
if te != null as *TypeExpr {
|
||||||
|
if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
|
||||||
|
return String_Concat(te.pointerPointee.typeName, "*");
|
||||||
|
}
|
||||||
|
if !String_Eq(te.typeName, "") { return te.typeName; }
|
||||||
|
}
|
||||||
|
if kind == tyBool { return "bool"; }
|
||||||
|
if kind == tyStr { return "String"; }
|
||||||
|
if kind == tyInt { return "int"; }
|
||||||
|
if kind == tyInt64 { return "int64"; }
|
||||||
|
if kind == tyUInt { return "uint"; }
|
||||||
|
if kind == tyFloat64 { return "float64"; }
|
||||||
|
if kind == tyPointer { return "*void"; }
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Emit the assignment error in `skLet`**
|
||||||
|
|
||||||
|
In `Sema_CheckStmt`, `skLet` branch, insert the check AFTER the annotation/inference block (after the `} else if stmt.child1 != null as *Expr && stmt.child1.refType != null as *TypeExpr {` block closes, i.e. right before `sym.isMutable = stmt.boolValue;` ~line 1462):
|
||||||
|
|
||||||
|
```bux
|
||||||
|
// Assignment check: annotation vs initializer (skip when either side is unknown)
|
||||||
|
if stmt.refStmtType != null as *TypeExpr && initType != tyUnknown {
|
||||||
|
let annotKind: int = Sema_ResolveType(sema, stmt.refStmtType);
|
||||||
|
if annotKind != tyUnknown {
|
||||||
|
var mismatch: bool = false;
|
||||||
|
if initType == tyNamed && annotKind == tyNamed {
|
||||||
|
// Both named: kinds are equal, compare type names (when available).
|
||||||
|
if stmt.child1 != null as *Expr && stmt.child1.refType != null as *TypeExpr {
|
||||||
|
if !String_Eq(stmt.child1.refType.typeName, "") &&
|
||||||
|
!String_Eq(stmt.child1.refType.typeName, stmt.refStmtType.typeName) {
|
||||||
|
mismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if initType != annotKind {
|
||||||
|
let numericOk: bool = Sema_IsStrictNumeric(initType) && Sema_IsStrictNumeric(annotKind);
|
||||||
|
let initIsPtr: bool = initType == tyPointer || initType == tyStr;
|
||||||
|
let annotIsPtr: bool = annotKind == tyPointer || annotKind == tyStr;
|
||||||
|
let ptrOk: bool = initIsPtr && annotIsPtr;
|
||||||
|
if !numericOk && !ptrOk {
|
||||||
|
mismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mismatch {
|
||||||
|
let gotName: String = Sema_TypeNameForDiag(stmt.child1.refType, initType);
|
||||||
|
let wantName: String = Sema_TypeNameForDiag(stmt.refStmtType, annotKind);
|
||||||
|
let msg: String = String_Concat("cannot assign ",
|
||||||
|
String_Concat(gotName, String_Concat(" to ", wantName)));
|
||||||
|
Sema_EmitError(sema, stmt.line, stmt.column, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Important: the variable is still registered with the annotation type on the next lines (`sym.typeKind = Sema_ResolveType(...)` already ran), so subsequent uses of `x` type-check as the annotated type and do not cascade.
|
||||||
|
|
||||||
|
Known limitation (documented, not fixed here): struct-literal initializers (`ekStructInit`) do not set `expr.refType`, so two different named struct types are only compared by kind; name comparison only applies when `child1.refType` is available.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rebuild and verify all three independent errors appear in one run**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make selfhost 2>&1 | tail -3`
|
||||||
|
Expected: `=== Self-hosted compiler built successfully ===`
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1 | tee /tmp/err_recovery.out; echo "exit=$?"`
|
||||||
|
|
||||||
|
Run: `grep -c "cannot assign String to int" /tmp/err_recovery.out; grep -c "cannot assign int to bool" /tmp/err_recovery.out; grep -c "undeclared identifier 'undefined_variable'" /tmp/err_recovery.out`
|
||||||
|
Expected: `1`, `1`, `1` — three independent errors in a single run.
|
||||||
|
|
||||||
|
Also verify valid code still passes (no false positives). Use an import-free file (check mode does not resolve imports):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /tmp/hello_ok/src
|
||||||
|
cat > /tmp/hello_ok/src/Main.bux <<'EOF'
|
||||||
|
func Main() -> int {
|
||||||
|
let x: int = 5;
|
||||||
|
let y: bool = true;
|
||||||
|
let s: String = "ok";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
BUX_STDLIB=/home/ziko/z-git/bux/bux/lib /home/ziko/z-git/bux/bux/build/selfhost/build/buxc2 check /tmp/hello_ok/src/Main.bux 2>&1; echo "exit=$?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `exit=0` (no errors for valid code).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/sema.bux
|
||||||
|
git add -f _test_error_recovery/
|
||||||
|
git commit -m "feat(selfhost-sema): check annotation vs initializer type in let/var"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Regression runner for multi-error recovery
|
||||||
|
|
||||||
|
Lock the behavior in with a runnable script so regressions are caught by one command.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `_test_error_recovery/run.sh`
|
||||||
|
- Modify: `Makefile` (add `test-error-recovery` target)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the runner script**
|
||||||
|
|
||||||
|
Create `_test_error_recovery/run.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression: buxc2 must report ALL independent semantic errors in one run.
|
||||||
|
set -euo pipefail
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ROOT="$(cd "$HERE/.." && pwd)"
|
||||||
|
BUXC2="$ROOT/build/selfhost/build/buxc2"
|
||||||
|
export BUX_STDLIB="$ROOT/lib"
|
||||||
|
|
||||||
|
if [[ ! -x "$BUXC2" ]]; then
|
||||||
|
echo "=== building selfhost (buxc2) ==="
|
||||||
|
(cd "$ROOT" && make selfhost)
|
||||||
|
fi
|
||||||
|
if [[ ! -x "$BUXC2" ]]; then
|
||||||
|
echo "error: buxc2 not found at $BUXC2" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
out="$(cd "$HERE" && "$BUXC2" check src/Main.bux 2>&1 || true)"
|
||||||
|
echo "$out"
|
||||||
|
|
||||||
|
grep -q "cannot assign String to int" <<<"$out"
|
||||||
|
grep -q "cannot assign int to bool" <<<"$out"
|
||||||
|
grep -q "undeclared identifier 'undefined_variable'" <<<"$out"
|
||||||
|
|
||||||
|
echo "PASS: all independent semantic errors reported in one run"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `chmod +x _test_error_recovery/run.sh`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run it — must pass**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && ./_test_error_recovery/run.sh`
|
||||||
|
Expected: ends with `PASS: all independent semantic errors reported in one run`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the Makefile target**
|
||||||
|
|
||||||
|
In `Makefile`, add after the `test-errors` target block (after line 141):
|
||||||
|
|
||||||
|
```make
|
||||||
|
test-error-recovery: ensure-buxc selfhost
|
||||||
|
@echo "=== Selfhost multi-error recovery test ==="
|
||||||
|
@chmod +x _test_error_recovery/run.sh
|
||||||
|
@_test_error_recovery/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the target**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make test-error-recovery`
|
||||||
|
Expected: `PASS: all independent semantic errors reported in one run`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Negative check — valid code still compiles through buxc2**
|
||||||
|
|
||||||
|
Run: `BUX_STDLIB=/home/ziko/z-git/bux/bux/lib /home/ziko/z-git/bux/bux/build/selfhost/build/buxc2 check /tmp/hello_ok/src/Main.bux && echo "exit=$?"`
|
||||||
|
Expected: `exit=0`
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add Makefile
|
||||||
|
git add -f _test_error_recovery/
|
||||||
|
git commit -m "test: selfhost multi-error recovery regression fixture"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-review notes
|
||||||
|
|
||||||
|
- Spec coverage: sentinel helper (Task 1), cascade suppression (Task 2), statement-level continuation with new assignment diagnostics (Task 3), CLI keeps skipping codegen on `hasError` (existing behavior, asserted by fixtures exiting 1 before codegen), regression test (Task 4). The `Sema_CheckStmt` early-return audit found no statement-level early returns that skip sibling statements, so no Task is needed for that; the `Sema_CheckReturnLifetime` internal `return`s only exit that helper and are correct.
|
||||||
|
- Type consistency: `Sema_EmitExprError(sema: *Sema, expr: *Expr, msg: String) -> int`, `Sema_IsStrictNumeric(kind: int) -> bool`, `Sema_TypeNameForDiag(te: *TypeExpr, kind: int) -> String` are used with the same signatures in every task.
|
||||||
|
- Known blockers/limitations: struct-literal `refType` is not set, so named-vs-named assignment checking is best-effort; bootstrap compiler behavior is unchanged.
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Self-hosted compiler: semantic error recovery
|
||||||
|
|
||||||
|
**Date:** 2026-07-28
|
||||||
|
**Scope:** Self-hosted Bux compiler only (`src/sema.bux`, `src/cli.bux`)
|
||||||
|
**Status:** Approved design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
When the self-hosted compiler performs semantic analysis, it must report **all independent semantic errors** in a single run instead of stopping after the first one. Users should see every type mismatch, undeclared identifier, return-type error, etc., before the compiler exits with failure.
|
||||||
|
|
||||||
|
## Current state
|
||||||
|
|
||||||
|
- `src/sema.bux` already collects diagnostics in `Sema.diags` via `Sema_EmitError` and sets `Sema.hasError`.
|
||||||
|
- `Sema_Analyze` walks every top-level declaration, so errors in different functions are all collected.
|
||||||
|
- However, inside a single function, several `Sema_EmitError` call sites are followed by an early `return` (or by returning `tyUnknown` in a way that aborts the parent expression). This truncates checking of the remaining statements/sub-expressions and hides errors.
|
||||||
|
- `src/cli.bux` correctly prints all collected semantic diagnostics and skips HIR lowering / C codegen when `Sema_HasError` is true.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Implement **expression-level recovery with an error sentinel** (`tyUnknown`):
|
||||||
|
|
||||||
|
- Every error path in `Sema_CheckExpr` must return `tyUnknown` and must not abort the parent check.
|
||||||
|
- Every error path in `Sema_CheckStmt` must record the diagnostic and then continue with the next statement (no early `return` that skips the rest of the block/function).
|
||||||
|
- `tyUnknown` is already treated as numeric-compatible in `Sema_IsNumeric`; audit the other predicates and error sites so `tyUnknown` suppresses cascading errors rather than causing them.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- No changes to the bootstrap Nim compiler (`bootstrap/*.nim`).
|
||||||
|
- No parser recovery in this work item; parser errors still stop the pipeline before semantic analysis.
|
||||||
|
- No "best-effort codegen": if semantic errors exist, HIR lowering and C generation are still skipped.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Sema context
|
||||||
|
|
||||||
|
`Sema` keeps its existing diagnostic storage:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
struct Sema {
|
||||||
|
// ... existing fields ...
|
||||||
|
diagCount: int;
|
||||||
|
diags: *SemaDiag;
|
||||||
|
hasError: bool;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Sema_EmitError` remains the low-level reporter.
|
||||||
|
|
||||||
|
### Error sentinel helper
|
||||||
|
|
||||||
|
Add a small helper for expression-level errors:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Sema_EmitExprError(sema: *Sema, expr: *Expr, msg: String) -> int {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, msg);
|
||||||
|
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
||||||
|
te.kind = tekNamed;
|
||||||
|
te.typeName = "?";
|
||||||
|
expr.refType = te;
|
||||||
|
return tyUnknown;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Callers use it like:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
return Sema_EmitExprError(sema, expr, "undeclared identifier 'foo'");
|
||||||
|
```
|
||||||
|
|
||||||
|
This makes the "record error + return sentinel" pattern explicit and harder to get wrong.
|
||||||
|
|
||||||
|
### Statement-level recovery
|
||||||
|
|
||||||
|
Audit `Sema_CheckStmt`:
|
||||||
|
|
||||||
|
- After any `Sema_EmitError` inside a statement handler, the handler must fall through to the normal `return` at the end of that branch so that the caller (`Sema_CheckBlock` / `Sema_Analyze`) continues with the next statement.
|
||||||
|
- Example: `if` condition not `bool` must still check `then`/`else` blocks before returning.
|
||||||
|
|
||||||
|
### Expression-level recovery
|
||||||
|
|
||||||
|
Audit `Sema_CheckExpr`:
|
||||||
|
|
||||||
|
- Binary/unary/call/index expressions: always check both/all children before deciding whether the parent expression has a valid type.
|
||||||
|
- When a child returns `tyUnknown`, the parent must return `tyUnknown` (or the already-known result type for comparisons) without emitting a second, derived error.
|
||||||
|
- `Sema_IsNumeric` already returns `true` for `tyUnknown`; keep that behavior. Check `Sema_IsBool` and any custom predicates that might emit follow-up errors on `tyUnknown`.
|
||||||
|
|
||||||
|
### CLI behavior
|
||||||
|
|
||||||
|
`src/cli.bux` keeps the current flow:
|
||||||
|
|
||||||
|
1. `Sema_Analyze(mod)`
|
||||||
|
2. If `Sema_HasError(sema)` → print every `sema.diags[i]` with `Diagnostic_Print` and return failure (`""` / exit code 1).
|
||||||
|
3. Only if no errors → `HirLower_LowerModule` → `CBackend_Generate`.
|
||||||
|
|
||||||
|
This guarantees that code generation never runs on an AST with semantic errors.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Source
|
||||||
|
→ Lexer
|
||||||
|
→ Parser
|
||||||
|
→ Macro expand
|
||||||
|
→ Sema_Analyze
|
||||||
|
├─ Sema_CollectGlobals
|
||||||
|
└─ for each func: Sema_CheckStmt / Sema_CheckExpr
|
||||||
|
├─ error → Sema_EmitError / Sema_EmitExprError → tyUnknown → continue
|
||||||
|
└─ ok → normal type
|
||||||
|
→ if hasError: print all diags, exit 1
|
||||||
|
→ else: HirLower → CBackend → cc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling rules
|
||||||
|
|
||||||
|
1. **Never panic/abort** for user-source errors.
|
||||||
|
2. **Never skip** checking the rest of a block because one statement failed.
|
||||||
|
3. **Never emit a cascading error** on an expression whose type is already `tyUnknown`.
|
||||||
|
4. **Do not generate code** when `hasError` is true.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Create `_test_error_recovery/`:
|
||||||
|
|
||||||
|
- `src/Main.bux` contains multiple independent semantic errors, e.g.:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Io::{PrintLine, PrintInt};
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let x: int = "hello";
|
||||||
|
let y: bool = 42;
|
||||||
|
PrintInt(x + y);
|
||||||
|
PrintLine(undefined_variable);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected `buxc2 check` (self-hosted) output: all semantic errors listed in one run.
|
||||||
|
|
||||||
|
### Known blocker
|
||||||
|
|
||||||
|
`make selfhost` currently fails because the bootstrap parser rejects `if` used as an expression in `src/hir_lower.bux:2726`. The changes in this design are in `src/sema.bux` and `src/cli.bux`, so they can be syntax-checked with the bootstrap parser, but the new recovery behavior cannot be exercised end-to-end until that parser gap is fixed.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Changing `Sema_CheckStmt`/`Sema_CheckExpr` control flow may accidentally alter valid-code behavior; keep the diff minimal and only move/eliminate early `return`s that follow error emission.
|
||||||
|
- `tyUnknown` suppression logic relies on existing predicates; missing one predicate may produce noisy cascading diagnostics.
|
||||||
|
- Self-hosted compiler cannot be built right now, so runtime verification of `buxc2` diagnostics is blocked.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// generic_enum.bux — Full test: generic enums, value reads, match, multiple instances
|
||||||
|
|
||||||
|
import Std::Io::{PrintLine, PrintInt};
|
||||||
|
|
||||||
|
enum Pair<T, U> {
|
||||||
|
First(T),
|
||||||
|
Second(U),
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pair_MakeFirst<T, U>(value: T) -> Pair<T, U> {
|
||||||
|
let p: Pair<T, U> = Pair { tag: Pair_First };
|
||||||
|
p.data.First_0 = value;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pair_MakeSecond<T, U>(value: U) -> Pair<T, U> {
|
||||||
|
let p: Pair<T, U> = Pair { tag: Pair_Second };
|
||||||
|
p.data.Second_0 = value;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
// Test 1: tag check + value read
|
||||||
|
let p: Pair<int, String> = Pair_MakeFirst<int, String>(42);
|
||||||
|
if p.tag == Pair_First {
|
||||||
|
Print("First value: ");
|
||||||
|
PrintInt(p.data.First_0 as int64);
|
||||||
|
PrintLine("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: second concrete instance (different types)
|
||||||
|
let s: Pair<String, int> = Pair_MakeSecond<String, int>(99);
|
||||||
|
if s.tag == Pair_Second {
|
||||||
|
Print("Second value: ");
|
||||||
|
PrintInt(s.data.Second_0 as int64);
|
||||||
|
PrintLine("");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// is_operator.bux — `expr is Variant` for simple and algebraic enums
|
||||||
|
import Std::Io::{PrintLine, PrintInt};
|
||||||
|
|
||||||
|
enum Color {
|
||||||
|
Red,
|
||||||
|
Green,
|
||||||
|
Blue,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Box {
|
||||||
|
Val(int),
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let c: Color = Color { tag: Color_Red };
|
||||||
|
if c is Red {
|
||||||
|
PrintLine("color-red");
|
||||||
|
}
|
||||||
|
if c is Blue {
|
||||||
|
PrintLine("color-blue-unexpected");
|
||||||
|
} else {
|
||||||
|
PrintLine("color-not-blue");
|
||||||
|
}
|
||||||
|
|
||||||
|
let b: Box = Box { tag: Box_Val };
|
||||||
|
b.data.Val_0 = 42;
|
||||||
|
if b is Val {
|
||||||
|
Print("box-val=");
|
||||||
|
PrintInt(b.data.Val_0 as int64);
|
||||||
|
PrintLine("");
|
||||||
|
}
|
||||||
|
if b is Empty {
|
||||||
|
PrintLine("box-empty-unexpected");
|
||||||
|
} else {
|
||||||
|
PrintLine("box-not-empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Operators-only `:tt` paste — `$op($a, $b)` → `a OP b`
|
||||||
|
import Std::Io::{PrintLine, PrintInt};
|
||||||
|
import Std::Test::{Test_AssertEqInt, Test_AssertEqBool, Test_Pass};
|
||||||
|
|
||||||
|
// Explicit op as call-site tt: apply_op!(+, 3, 4) → 7
|
||||||
|
macro! apply_op {
|
||||||
|
( $op:tt, $a:expr, $b:expr ) => { $op($a, $b) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Juxta binary split: flip_op!(10 - 3) → 3 - 10
|
||||||
|
macro! flip_op {
|
||||||
|
( $a:expr, $op:tt, $b:expr ) => { $op($b, $a) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let sum: int = apply_op!(+, 3, 4);
|
||||||
|
Test_AssertEqInt(sum, 7);
|
||||||
|
|
||||||
|
let prod: int = apply_op!(*, 6, 7);
|
||||||
|
Test_AssertEqInt(prod, 42);
|
||||||
|
|
||||||
|
let diff: int = flip_op!(10 - 3);
|
||||||
|
Test_AssertEqInt(diff, -7);
|
||||||
|
|
||||||
|
let eq: bool = apply_op!(==, 5, 5);
|
||||||
|
Test_AssertEqBool(eq, true);
|
||||||
|
|
||||||
|
let lt: bool = apply_op!(<, 2, 9);
|
||||||
|
Test_AssertEqBool(lt, true);
|
||||||
|
|
||||||
|
PrintInt(sum);
|
||||||
|
PrintLine("");
|
||||||
|
PrintInt(prod);
|
||||||
|
PrintLine("");
|
||||||
|
PrintInt(diff);
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("PASS macro_op_paste");
|
||||||
|
Test_Pass("macro_op_paste");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Generics in `$t:type` + type-arg splice into Array_New<$t>
|
||||||
|
import Std::Io::{PrintLine, PrintInt};
|
||||||
|
import Std::Array::{Array, Array_New, Array_Push, Array_Get, Array_Len, Array_Free};
|
||||||
|
import Std::Test::{Test_AssertEqInt, Test_Pass};
|
||||||
|
|
||||||
|
macro! size_of {
|
||||||
|
( $t:type ) => { sizeof($t) as int }
|
||||||
|
}
|
||||||
|
|
||||||
|
macro! new_array {
|
||||||
|
( $t:type, $cap:expr ) => {
|
||||||
|
Array_New<$t>($cap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
// Generic type fragment: Array<int>
|
||||||
|
let sz: int = size_of!(Array<int>);
|
||||||
|
PrintInt(sz);
|
||||||
|
PrintLine("");
|
||||||
|
if sz < 8 {
|
||||||
|
PrintLine("FAIL size_of Array<int>");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $t spliced into monomorphized call Array_New<$t>
|
||||||
|
var a: Array<int> = new_array!(int, 4);
|
||||||
|
Array_Push<int>(&a, 10);
|
||||||
|
Array_Push<int>(&a, 20);
|
||||||
|
Test_AssertEqInt(Array_Get<int>(&a, 0), 10);
|
||||||
|
Test_AssertEqInt(Array_Get<int>(&a, 1), 20);
|
||||||
|
Test_AssertEqInt(Array_Len<int>(&a) as int, 2);
|
||||||
|
Array_Free<int>(&a);
|
||||||
|
|
||||||
|
let psz: int = size_of!(*int);
|
||||||
|
if psz != 4 && psz != 8 {
|
||||||
|
PrintLine("FAIL size_of *int");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintLine("PASS macro_type_generic");
|
||||||
|
Test_Pass("macro_type_generic");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+13
-13
@@ -38,21 +38,21 @@ func Main() -> int {
|
|||||||
Set_Free<int>(&s);
|
Set_Free<int>(&s);
|
||||||
|
|
||||||
// --- Result helpers ---
|
// --- Result helpers ---
|
||||||
let ok: Result = Result_NewOk(42);
|
let ok: Result<int, String> = Result_NewOk<int, String>(42);
|
||||||
let err: Result = Result_NewErr("boom");
|
let err: Result<int, String> = Result_NewErr<int, String>("boom");
|
||||||
Test_AssertTrue(Result_IsOk(ok));
|
Test_AssertTrue(Result_IsOk<int, String>(ok));
|
||||||
Test_AssertTrue(Result_IsErr(err));
|
Test_AssertTrue(Result_IsErr<int, String>(err));
|
||||||
Test_AssertEqInt(Result_UnwrapOr(err, -1), -1);
|
Test_AssertEqInt(Result_UnwrapOr<int, String>(err, -1), -1);
|
||||||
let recovered: Result = Result_Or(err, Result_NewOk(7));
|
let recovered: Result<int, String> = Result_Or<int, String>(err, Result_NewOk<int, String>(7));
|
||||||
Test_AssertEqInt(Result_UnwrapOr(recovered, 0), 7);
|
Test_AssertEqInt(Result_UnwrapOr<int, String>(recovered, 0), 7);
|
||||||
Test_AssertTrue(String_Eq(Result_UnwrapErr(err), "boom"));
|
Test_AssertTrue(String_Eq(Result_UnwrapErr<int, String>(err), "boom"));
|
||||||
|
|
||||||
// --- Option helpers ---
|
// --- Option helpers ---
|
||||||
let some: Option = Option_NewSome(5);
|
let some: Option<int> = Option_NewSome<int>(5);
|
||||||
let none: Option = Option_NewNone();
|
let none: Option<int> = Option_NewNone<int>();
|
||||||
Test_AssertTrue(Option_IsSome(some));
|
Test_AssertTrue(Option_IsSome<int>(some));
|
||||||
let o2: Option = Option_Or(none, some);
|
let o2: Option<int> = Option_Or<int>(none, some);
|
||||||
Test_AssertEqInt(Option_UnwrapOr(o2, 0), 5);
|
Test_AssertEqInt(Option_UnwrapOr<int>(o2, 0), 5);
|
||||||
|
|
||||||
PrintLine("map_remove: ok");
|
PrintLine("map_remove: ok");
|
||||||
Test_Pass("map_remove + result helpers");
|
Test_Pass("map_remove + result helpers");
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// switch.bux — Test switch with many cases (>8)
|
||||||
|
|
||||||
|
import Std::Io::{PrintLine, PrintInt};
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
var x: int = 7;
|
||||||
|
|
||||||
|
switch x {
|
||||||
|
case 0:
|
||||||
|
PrintLine("zero");
|
||||||
|
case 1:
|
||||||
|
PrintLine("one");
|
||||||
|
case 2:
|
||||||
|
PrintLine("two");
|
||||||
|
case 3:
|
||||||
|
PrintLine("three");
|
||||||
|
case 4:
|
||||||
|
PrintLine("four");
|
||||||
|
case 5:
|
||||||
|
PrintLine("five");
|
||||||
|
case 6:
|
||||||
|
PrintLine("six");
|
||||||
|
case 7:
|
||||||
|
PrintLine("seven");
|
||||||
|
case 8:
|
||||||
|
PrintLine("eight");
|
||||||
|
case 9:
|
||||||
|
PrintLine("nine");
|
||||||
|
case 10:
|
||||||
|
PrintLine("ten");
|
||||||
|
default:
|
||||||
|
PrintLine("other");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with match at end
|
||||||
|
x = 15;
|
||||||
|
switch x {
|
||||||
|
case 0:
|
||||||
|
PrintLine("a");
|
||||||
|
case 1:
|
||||||
|
PrintLine("b");
|
||||||
|
case 5:
|
||||||
|
PrintLine("c");
|
||||||
|
case 10:
|
||||||
|
PrintLine("d");
|
||||||
|
case 15:
|
||||||
|
PrintLine("fifteen-ok");
|
||||||
|
case 20:
|
||||||
|
PrintLine("e");
|
||||||
|
case 25:
|
||||||
|
PrintLine("f");
|
||||||
|
case 30:
|
||||||
|
PrintLine("g");
|
||||||
|
case 35:
|
||||||
|
PrintLine("h");
|
||||||
|
case 40:
|
||||||
|
PrintLine("i");
|
||||||
|
default:
|
||||||
|
PrintLine("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Try operator `?` with generic Result<T,E> (non-int Ok payload).
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::Result::{Result, Result_NewOk, Result_NewErr};
|
||||||
|
import Std::String::{String_Eq};
|
||||||
|
|
||||||
|
func GetGreeting(name: String) -> Result<String, String> {
|
||||||
|
if String_Eq(name, "") {
|
||||||
|
return Result_NewErr<String, String>("empty name");
|
||||||
|
}
|
||||||
|
return Result_NewOk<String, String>("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
func Greet(name: String) -> Result<String, String> {
|
||||||
|
let g: String = GetGreeting(name)?;
|
||||||
|
return Result_NewOk<String, String>(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
PrintLine("Try generic Result demo:");
|
||||||
|
|
||||||
|
let ok: Result<String, String> = Greet("bux");
|
||||||
|
if ok.tag == Result_Ok {
|
||||||
|
PrintLine(ok.data.Ok_0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let err: Result<String, String> = Greet("");
|
||||||
|
if err.tag == Result_Err {
|
||||||
|
PrintLine(err.data.Err_0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+5
-1
@@ -21,7 +21,11 @@ module Std::Array {
|
|||||||
/// Append `value`, growing capacity if needed.
|
/// Append `value`, growing capacity if needed.
|
||||||
func Array_Push<T>(self: *Array<T>, value: T) {
|
func Array_Push<T>(self: *Array<T>, value: T) {
|
||||||
if self.len >= self.cap {
|
if self.len >= self.cap {
|
||||||
self.cap = self.cap * 2;
|
var newCap: uint = self.cap * 2;
|
||||||
|
if newCap == 0 {
|
||||||
|
newCap = 4;
|
||||||
|
}
|
||||||
|
self.cap = newCap;
|
||||||
self.data = bux_realloc(self.data as *void, self.cap * sizeof(T)) as *T;
|
self.data = bux_realloc(self.data as *void, self.cap * sizeof(T)) as *T;
|
||||||
}
|
}
|
||||||
self.data[self.len] = value;
|
self.data[self.len] = value;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ module Std::Iter {
|
|||||||
|
|
||||||
import Std::Array::*;
|
import Std::Array::*;
|
||||||
|
|
||||||
|
// SAFETY: Iter<T> stores a raw *T pointer to the source array's element buffer.
|
||||||
|
// The iterator MUST NOT outlive the source Array<T>. Modifying the source array
|
||||||
|
// (e.g. Array_Push which may reallocate the buffer) while an iterator is active
|
||||||
|
// will result in a dangling pointer and undefined behavior.
|
||||||
struct Iter<T> {
|
struct Iter<T> {
|
||||||
data: *T,
|
data: *T,
|
||||||
len: uint,
|
len: uint,
|
||||||
|
|||||||
+115
-12
@@ -2,6 +2,8 @@ module Std::Map {
|
|||||||
|
|
||||||
extern func bux_hash_bytes(ptr: *void, size: uint) -> uint;
|
extern func bux_hash_bytes(ptr: *void, size: uint) -> uint;
|
||||||
extern func bux_hash_string(s: String) -> uint;
|
extern func bux_hash_string(s: String) -> uint;
|
||||||
|
extern func bux_alloc(size: uint) -> *void;
|
||||||
|
extern func bux_free(ptr: *void);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Generic Map<K, V> — works with value-type keys (int, float, etc.)
|
// Generic Map<K, V> — works with value-type keys (int, float, etc.)
|
||||||
@@ -20,18 +22,48 @@ module Std::Map {
|
|||||||
len: uint,
|
len: uint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a map with at least `cap` slots (open addressing).
|
||||||
|
/// `cap == 0` defaults to 8 so inserts never hit `% 0` or a full table with no growth room.
|
||||||
func Map_New<K, V>(cap: uint) -> Map<K, V> {
|
func Map_New<K, V>(cap: uint) -> Map<K, V> {
|
||||||
let total: uint = cap * sizeof(MapEntry<K, V>);
|
var c: uint = cap;
|
||||||
|
if c == 0 {
|
||||||
|
c = 8;
|
||||||
|
}
|
||||||
|
let total: uint = c * sizeof(MapEntry<K, V>);
|
||||||
let data: *MapEntry<K, V> = bux_alloc(total) as *MapEntry<K, V>;
|
let data: *MapEntry<K, V> = bux_alloc(total) as *MapEntry<K, V>;
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
while i < cap {
|
while i < c {
|
||||||
data[i].occupied = false;
|
data[i].occupied = false;
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
}
|
}
|
||||||
return Map<K, V> { entries: data, cap: cap, len: 0 };
|
return Map<K, V> { entries: data, cap: c, len: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
func Map_Set<K, V>(m: *Map<K, V>, key: K, value: V) {
|
/// Grow / rehash to `newCap` (must be > 0). Transfers ownership; does not Drop `fresh`.
|
||||||
|
func Map_Rehash<K, V>(m: *Map<K, V>, newCap: uint) {
|
||||||
|
var nc: uint = newCap;
|
||||||
|
if nc == 0 {
|
||||||
|
nc = 8;
|
||||||
|
}
|
||||||
|
var fresh: Map<K, V> = Map_New<K, V>(nc);
|
||||||
|
var i: uint = 0;
|
||||||
|
while i < m.cap {
|
||||||
|
if m.entries[i].occupied {
|
||||||
|
Map_SetInsertOnly<K, V>(&fresh, m.entries[i].key, m.entries[i].value);
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
bux_free(m.entries as *void);
|
||||||
|
m.entries = fresh.entries;
|
||||||
|
m.cap = fresh.cap;
|
||||||
|
m.len = fresh.len;
|
||||||
|
fresh.entries = null as *MapEntry<K, V>;
|
||||||
|
fresh.cap = 0;
|
||||||
|
fresh.len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert assuming free slots exist (no grow). Used by rehash.
|
||||||
|
func Map_SetInsertOnly<K, V>(m: *Map<K, V>, key: K, value: V) {
|
||||||
var keyPtr: *K = &key;
|
var keyPtr: *K = &key;
|
||||||
let hash: uint = bux_hash_bytes(keyPtr as *void, sizeof(K));
|
let hash: uint = bux_hash_bytes(keyPtr as *void, sizeof(K));
|
||||||
var idx: uint = hash % m.cap;
|
var idx: uint = hash % m.cap;
|
||||||
@@ -48,7 +80,23 @@ module Std::Map {
|
|||||||
m.len = m.len + 1;
|
m.len = m.len + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Map_Set<K, V>(m: *Map<K, V>, key: K, value: V) {
|
||||||
|
// Keep load factor under ~50% for open addressing (also handles cap==0).
|
||||||
|
if m.cap == 0 || m.len * 2 >= m.cap {
|
||||||
|
var nc: uint = m.cap * 2;
|
||||||
|
if nc < 8 {
|
||||||
|
nc = 8;
|
||||||
|
}
|
||||||
|
Map_Rehash<K, V>(m, nc);
|
||||||
|
}
|
||||||
|
Map_SetInsertOnly<K, V>(m, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
func Map_Get<K, V>(m: *Map<K, V>, key: K) -> V {
|
func Map_Get<K, V>(m: *Map<K, V>, key: K) -> V {
|
||||||
|
if m.cap == 0 {
|
||||||
|
var zero: V = 0 as V;
|
||||||
|
return zero;
|
||||||
|
}
|
||||||
var keyPtr: *K = &key;
|
var keyPtr: *K = &key;
|
||||||
let hash: uint = bux_hash_bytes(keyPtr as *void, sizeof(K));
|
let hash: uint = bux_hash_bytes(keyPtr as *void, sizeof(K));
|
||||||
var idx: uint = hash % m.cap;
|
var idx: uint = hash % m.cap;
|
||||||
@@ -72,6 +120,9 @@ module Std::Map {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Map_Has<K, V>(m: *Map<K, V>, key: K) -> bool {
|
func Map_Has<K, V>(m: *Map<K, V>, key: K) -> bool {
|
||||||
|
if m.cap == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
var keyPtr: *K = &key;
|
var keyPtr: *K = &key;
|
||||||
let hash: uint = bux_hash_bytes(keyPtr as *void, sizeof(K));
|
let hash: uint = bux_hash_bytes(keyPtr as *void, sizeof(K));
|
||||||
var idx: uint = hash % m.cap;
|
var idx: uint = hash % m.cap;
|
||||||
@@ -97,12 +148,16 @@ module Std::Map {
|
|||||||
if !Map_Has<K, V>(m, key) {
|
if !Map_Has<K, V>(m, key) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var fresh: Map<K, V> = Map_New<K, V>(m.cap);
|
var keepCap: uint = m.cap;
|
||||||
|
if keepCap == 0 {
|
||||||
|
keepCap = 8;
|
||||||
|
}
|
||||||
|
var fresh: Map<K, V> = Map_New<K, V>(keepCap);
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
while i < m.cap {
|
while i < m.cap {
|
||||||
if m.entries[i].occupied {
|
if m.entries[i].occupied {
|
||||||
if m.entries[i].key != key {
|
if m.entries[i].key != key {
|
||||||
Map_Set<K, V>(&fresh, m.entries[i].key, m.entries[i].value);
|
Map_SetInsertOnly<K, V>(&fresh, m.entries[i].key, m.entries[i].value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
@@ -155,17 +210,43 @@ module Std::Map {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StringMap_New<V>(cap: uint) -> StringMap<V> {
|
func StringMap_New<V>(cap: uint) -> StringMap<V> {
|
||||||
let total: uint = cap * sizeof(StringMapEntry<V>);
|
var c: uint = cap;
|
||||||
|
if c == 0 {
|
||||||
|
c = 8;
|
||||||
|
}
|
||||||
|
let total: uint = c * sizeof(StringMapEntry<V>);
|
||||||
let data: *StringMapEntry<V> = bux_alloc(total) as *StringMapEntry<V>;
|
let data: *StringMapEntry<V> = bux_alloc(total) as *StringMapEntry<V>;
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
while i < cap {
|
while i < c {
|
||||||
data[i].occupied = false;
|
data[i].occupied = false;
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
}
|
}
|
||||||
return StringMap<V> { entries: data, cap: cap, len: 0 };
|
return StringMap<V> { entries: data, cap: c, len: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
func StringMap_Set<V>(m: *StringMap<V>, key: String, value: V) {
|
func StringMap_Rehash<V>(m: *StringMap<V>, newCap: uint) {
|
||||||
|
var nc: uint = newCap;
|
||||||
|
if nc == 0 {
|
||||||
|
nc = 8;
|
||||||
|
}
|
||||||
|
var fresh: StringMap<V> = StringMap_New<V>(nc);
|
||||||
|
var i: uint = 0;
|
||||||
|
while i < m.cap {
|
||||||
|
if m.entries[i].occupied {
|
||||||
|
StringMap_SetInsertOnly<V>(&fresh, m.entries[i].key, m.entries[i].value);
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
bux_free(m.entries as *void);
|
||||||
|
m.entries = fresh.entries;
|
||||||
|
m.cap = fresh.cap;
|
||||||
|
m.len = fresh.len;
|
||||||
|
fresh.entries = null as *StringMapEntry<V>;
|
||||||
|
fresh.cap = 0;
|
||||||
|
fresh.len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringMap_SetInsertOnly<V>(m: *StringMap<V>, key: String, value: V) {
|
||||||
let hash: uint = bux_hash_string(key);
|
let hash: uint = bux_hash_string(key);
|
||||||
var idx: uint = hash % m.cap;
|
var idx: uint = hash % m.cap;
|
||||||
while m.entries[idx].occupied {
|
while m.entries[idx].occupied {
|
||||||
@@ -181,7 +262,22 @@ module Std::Map {
|
|||||||
m.len = m.len + 1;
|
m.len = m.len + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StringMap_Set<V>(m: *StringMap<V>, key: String, value: V) {
|
||||||
|
if m.cap == 0 || m.len * 2 >= m.cap {
|
||||||
|
var nc: uint = m.cap * 2;
|
||||||
|
if nc < 8 {
|
||||||
|
nc = 8;
|
||||||
|
}
|
||||||
|
StringMap_Rehash<V>(m, nc);
|
||||||
|
}
|
||||||
|
StringMap_SetInsertOnly<V>(m, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
func StringMap_Get<V>(m: *StringMap<V>, key: String) -> V {
|
func StringMap_Get<V>(m: *StringMap<V>, key: String) -> V {
|
||||||
|
if m.cap == 0 {
|
||||||
|
var zero: V = 0 as V;
|
||||||
|
return zero;
|
||||||
|
}
|
||||||
let hash: uint = bux_hash_string(key);
|
let hash: uint = bux_hash_string(key);
|
||||||
var idx: uint = hash % m.cap;
|
var idx: uint = hash % m.cap;
|
||||||
while m.entries[idx].occupied {
|
while m.entries[idx].occupied {
|
||||||
@@ -203,6 +299,9 @@ module Std::Map {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StringMap_Has<V>(m: *StringMap<V>, key: String) -> bool {
|
func StringMap_Has<V>(m: *StringMap<V>, key: String) -> bool {
|
||||||
|
if m.cap == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let hash: uint = bux_hash_string(key);
|
let hash: uint = bux_hash_string(key);
|
||||||
var idx: uint = hash % m.cap;
|
var idx: uint = hash % m.cap;
|
||||||
while m.entries[idx].occupied {
|
while m.entries[idx].occupied {
|
||||||
@@ -226,12 +325,16 @@ module Std::Map {
|
|||||||
if !StringMap_Has<V>(m, key) {
|
if !StringMap_Has<V>(m, key) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var fresh: StringMap<V> = StringMap_New<V>(m.cap);
|
var keepCap: uint = m.cap;
|
||||||
|
if keepCap == 0 {
|
||||||
|
keepCap = 8;
|
||||||
|
}
|
||||||
|
var fresh: StringMap<V> = StringMap_New<V>(keepCap);
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
while i < m.cap {
|
while i < m.cap {
|
||||||
if m.entries[i].occupied {
|
if m.entries[i].occupied {
|
||||||
if !String_Eq(m.entries[i].key, key) {
|
if !String_Eq(m.entries[i].key, key) {
|
||||||
StringMap_Set<V>(&fresh, m.entries[i].key, m.entries[i].value);
|
StringMap_SetInsertOnly<V>(&fresh, m.entries[i].key, m.entries[i].value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
|
|||||||
+12
-14
@@ -3,46 +3,45 @@ module Std::Option {
|
|||||||
|
|
||||||
extern func bux_exit(code: int);
|
extern func bux_exit(code: int);
|
||||||
|
|
||||||
enum Option {
|
enum Option<T> {
|
||||||
Some(int),
|
Some(T),
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Option_NewSome(value: int) -> Option {
|
func Option_NewSome<T>(value: T) -> Option<T> {
|
||||||
let o: Option = Option { tag: Option_Some };
|
let o: Option<T> = Option { tag: Option_Some };
|
||||||
o.data.Some_0 = value;
|
o.data.Some_0 = value;
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
func Option_NewNone() -> Option {
|
func Option_NewNone<T>() -> Option<T> {
|
||||||
return Option { tag: Option_None };
|
return Option { tag: Option_None };
|
||||||
}
|
}
|
||||||
|
|
||||||
func Option_IsSome(o: Option) -> bool {
|
func Option_IsSome<T>(o: Option<T>) -> bool {
|
||||||
return o.tag == Option_Some;
|
return o.tag == Option_Some;
|
||||||
}
|
}
|
||||||
|
|
||||||
func Option_IsNone(o: Option) -> bool {
|
func Option_IsNone<T>(o: Option<T>) -> bool {
|
||||||
return o.tag == Option_None;
|
return o.tag == Option_None;
|
||||||
}
|
}
|
||||||
|
|
||||||
func Option_Unwrap(o: Option) -> int {
|
func Option_Unwrap<T>(o: Option<T>) -> T {
|
||||||
if o.tag != Option_Some {
|
if o.tag != Option_Some {
|
||||||
PrintLine("panic: unwrap on None");
|
PrintLine("panic: unwrap on None");
|
||||||
return 0;
|
bux_exit(1);
|
||||||
}
|
}
|
||||||
return o.data.Some_0;
|
return o.data.Some_0;
|
||||||
}
|
}
|
||||||
|
|
||||||
func Option_UnwrapOr(o: Option, fallback: int) -> int {
|
func Option_UnwrapOr<T>(o: Option<T>, fallback: T) -> T {
|
||||||
if o.tag == Option_Some {
|
if o.tag == Option_Some {
|
||||||
return o.data.Some_0;
|
return o.data.Some_0;
|
||||||
}
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Unwrap Some or panic with a custom message */
|
func Option_Expect<T>(o: Option<T>, msg: String) -> T {
|
||||||
func Option_Expect(o: Option, msg: String) -> int {
|
|
||||||
if o.tag != Option_Some {
|
if o.tag != Option_Some {
|
||||||
PrintLine(msg);
|
PrintLine(msg);
|
||||||
bux_exit(1);
|
bux_exit(1);
|
||||||
@@ -50,8 +49,7 @@ module Std::Option {
|
|||||||
return o.data.Some_0;
|
return o.data.Some_0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If o is Some return it, otherwise return other */
|
func Option_Or<T>(o: Option<T>, other: Option<T>) -> Option<T> {
|
||||||
func Option_Or(o: Option, other: Option) -> Option {
|
|
||||||
if o.tag == Option_Some {
|
if o.tag == Option_Some {
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-19
@@ -3,48 +3,47 @@ module Std::Result {
|
|||||||
|
|
||||||
extern func bux_exit(code: int);
|
extern func bux_exit(code: int);
|
||||||
|
|
||||||
enum Result {
|
enum Result<T, E> {
|
||||||
Ok(int),
|
Ok(T),
|
||||||
Err(String),
|
Err(E),
|
||||||
}
|
}
|
||||||
|
|
||||||
func Result_NewOk(value: int) -> Result {
|
func Result_NewOk<T, E>(value: T) -> Result<T, E> {
|
||||||
let r: Result = Result { tag: Result_Ok };
|
let r: Result<T, E> = Result { tag: Result_Ok };
|
||||||
r.data.Ok_0 = value;
|
r.data.Ok_0 = value;
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
func Result_NewErr(msg: String) -> Result {
|
func Result_NewErr<T, E>(msg: E) -> Result<T, E> {
|
||||||
let r: Result = Result { tag: Result_Err };
|
let r: Result<T, E> = Result { tag: Result_Err };
|
||||||
r.data.Err_0 = msg;
|
r.data.Err_0 = msg;
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
func Result_IsOk(r: Result) -> bool {
|
func Result_IsOk<T, E>(r: Result<T, E>) -> bool {
|
||||||
return r.tag == Result_Ok;
|
return r.tag == Result_Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
func Result_IsErr(r: Result) -> bool {
|
func Result_IsErr<T, E>(r: Result<T, E>) -> bool {
|
||||||
return r.tag == Result_Err;
|
return r.tag == Result_Err;
|
||||||
}
|
}
|
||||||
|
|
||||||
func Result_Unwrap(r: Result) -> int {
|
func Result_Unwrap<T, E>(r: Result<T, E>) -> T {
|
||||||
if r.tag != Result_Ok {
|
if r.tag != Result_Ok {
|
||||||
PrintLine("panic: unwrap on Err");
|
PrintLine("panic: unwrap on Err");
|
||||||
return 0;
|
bux_exit(1);
|
||||||
}
|
}
|
||||||
return r.data.Ok_0;
|
return r.data.Ok_0;
|
||||||
}
|
}
|
||||||
|
|
||||||
func Result_UnwrapOr(r: Result, fallback: int) -> int {
|
func Result_UnwrapOr<T, E>(r: Result<T, E>, fallback: T) -> T {
|
||||||
if r.tag == Result_Ok {
|
if r.tag == Result_Ok {
|
||||||
return r.data.Ok_0;
|
return r.data.Ok_0;
|
||||||
}
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Unwrap Ok or panic with a custom message */
|
func Result_Expect<T, E>(r: Result<T, E>, msg: String) -> T {
|
||||||
func Result_Expect(r: Result, msg: String) -> int {
|
|
||||||
if r.tag != Result_Ok {
|
if r.tag != Result_Ok {
|
||||||
PrintLine(msg);
|
PrintLine(msg);
|
||||||
bux_exit(1);
|
bux_exit(1);
|
||||||
@@ -52,17 +51,15 @@ module Std::Result {
|
|||||||
return r.data.Ok_0;
|
return r.data.Ok_0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extract Err payload (panics if Ok) */
|
func Result_UnwrapErr<T, E>(r: Result<T, E>) -> E {
|
||||||
func Result_UnwrapErr(r: Result) -> String {
|
|
||||||
if r.tag != Result_Err {
|
if r.tag != Result_Err {
|
||||||
PrintLine("panic: unwrap_err on Ok");
|
PrintLine("panic: unwrap_err on Ok");
|
||||||
return "";
|
bux_exit(1);
|
||||||
}
|
}
|
||||||
return r.data.Err_0;
|
return r.data.Err_0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If r is Ok return it, otherwise return other */
|
func Result_Or<T, E>(r: Result<T, E>, other: Result<T, E>) -> Result<T, E> {
|
||||||
func Result_Or(r: Result, other: Result) -> Result {
|
|
||||||
if r.tag == Result_Ok {
|
if r.tag == Result_Ok {
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-6
@@ -16,18 +16,45 @@ module Std::Set {
|
|||||||
len: uint,
|
len: uint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a set with at least `cap` slots. `cap == 0` defaults to 8.
|
||||||
func Set_New<T>(cap: uint) -> Set<T> {
|
func Set_New<T>(cap: uint) -> Set<T> {
|
||||||
let total: uint = cap * sizeof(SetEntry<T>);
|
var c: uint = cap;
|
||||||
|
if c == 0 {
|
||||||
|
c = 8;
|
||||||
|
}
|
||||||
|
let total: uint = c * sizeof(SetEntry<T>);
|
||||||
let data: *SetEntry<T> = bux_alloc(total) as *SetEntry<T>;
|
let data: *SetEntry<T> = bux_alloc(total) as *SetEntry<T>;
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
while i < cap {
|
while i < c {
|
||||||
data[i].occupied = false;
|
data[i].occupied = false;
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
}
|
}
|
||||||
return Set<T> { entries: data, cap: cap, len: 0 };
|
return Set<T> { entries: data, cap: c, len: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
func Set_Add<T>(s: *Set<T>, value: T) {
|
func Set_Rehash<T>(s: *Set<T>, newCap: uint) {
|
||||||
|
var nc: uint = newCap;
|
||||||
|
if nc == 0 {
|
||||||
|
nc = 8;
|
||||||
|
}
|
||||||
|
var fresh: Set<T> = Set_New<T>(nc);
|
||||||
|
var i: uint = 0;
|
||||||
|
while i < s.cap {
|
||||||
|
if s.entries[i].occupied {
|
||||||
|
Set_AddInsertOnly<T>(&fresh, s.entries[i].value);
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
bux_free(s.entries as *void);
|
||||||
|
s.entries = fresh.entries;
|
||||||
|
s.cap = fresh.cap;
|
||||||
|
s.len = fresh.len;
|
||||||
|
fresh.entries = null as *SetEntry<T>;
|
||||||
|
fresh.cap = 0;
|
||||||
|
fresh.len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Set_AddInsertOnly<T>(s: *Set<T>, value: T) {
|
||||||
var valuePtr: *T = &value;
|
var valuePtr: *T = &value;
|
||||||
let hash: uint = bux_hash_bytes(valuePtr as *void, sizeof(T));
|
let hash: uint = bux_hash_bytes(valuePtr as *void, sizeof(T));
|
||||||
var idx: uint = hash % s.cap;
|
var idx: uint = hash % s.cap;
|
||||||
@@ -43,7 +70,21 @@ module Std::Set {
|
|||||||
s.len = s.len + 1;
|
s.len = s.len + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Set_Add<T>(s: *Set<T>, value: T) {
|
||||||
|
if s.cap == 0 || s.len * 2 >= s.cap {
|
||||||
|
var nc: uint = s.cap * 2;
|
||||||
|
if nc < 8 {
|
||||||
|
nc = 8;
|
||||||
|
}
|
||||||
|
Set_Rehash<T>(s, nc);
|
||||||
|
}
|
||||||
|
Set_AddInsertOnly<T>(s, value);
|
||||||
|
}
|
||||||
|
|
||||||
func Set_Has<T>(s: *Set<T>, value: T) -> bool {
|
func Set_Has<T>(s: *Set<T>, value: T) -> bool {
|
||||||
|
if s.cap == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
var valuePtr: *T = &value;
|
var valuePtr: *T = &value;
|
||||||
let hash: uint = bux_hash_bytes(valuePtr as *void, sizeof(T));
|
let hash: uint = bux_hash_bytes(valuePtr as *void, sizeof(T));
|
||||||
var idx: uint = hash % s.cap;
|
var idx: uint = hash % s.cap;
|
||||||
@@ -70,14 +111,18 @@ module Std::Set {
|
|||||||
if !Set_Has<T>(s, value) {
|
if !Set_Has<T>(s, value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var fresh: Set<T> = Set_New<T>(s.cap);
|
var keepCap: uint = s.cap;
|
||||||
|
if keepCap == 0 {
|
||||||
|
keepCap = 8;
|
||||||
|
}
|
||||||
|
var fresh: Set<T> = Set_New<T>(keepCap);
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
while i < s.cap {
|
while i < s.cap {
|
||||||
if s.entries[i].occupied {
|
if s.entries[i].occupied {
|
||||||
var entryPtr: *T = &s.entries[i].value;
|
var entryPtr: *T = &s.entries[i].value;
|
||||||
var valuePtr: *T = &value;
|
var valuePtr: *T = &value;
|
||||||
if bux_mem_eq(entryPtr as *void, valuePtr as *void, sizeof(T)) == 0 {
|
if bux_mem_eq(entryPtr as *void, valuePtr as *void, sizeof(T)) == 0 {
|
||||||
Set_Add<T>(&fresh, s.entries[i].value);
|
Set_AddInsertOnly<T>(&fresh, s.entries[i].value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
|
|||||||
@@ -0,0 +1,484 @@
|
|||||||
|
/* Bux Runtime — freestanding / bare-metal research spike (post-v1.0)
|
||||||
|
*
|
||||||
|
* Goal: link with `-ffreestanding -nostdlib` without pulling in a hosted libc.
|
||||||
|
* This is **not** a full embedded platform kit (no Cortex-M startup, no
|
||||||
|
* linker scripts, no UART drivers). It is a portable base for:
|
||||||
|
* - compile smoke under -ffreestanding
|
||||||
|
* - no-libc static link experiments (custom `_start`)
|
||||||
|
* - future board BSPs that replace the weak I/O hooks
|
||||||
|
*
|
||||||
|
* Select: BUX_RUNTIME=freestanding (bootstrap + selfhost)
|
||||||
|
*
|
||||||
|
* Memory: fixed bump allocator over a static arena (default 256 KiB).
|
||||||
|
* Override at compile time: -DBUX_FS_HEAP_BYTES=N
|
||||||
|
*
|
||||||
|
* I/O: weak stubs — default print is a no-op; panic spins. Host may override:
|
||||||
|
* void bux_fs_write(const char* s, unsigned n);
|
||||||
|
* void bux_fs_halt(int code);
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if defined(__STDC_HOSTED__) && __STDC_HOSTED__ == 1 && !defined(BUX_FS_FORCE)
|
||||||
|
/* When accidentally compiled as hosted without -ffreestanding, still avoid
|
||||||
|
* libc — we implement everything we need. */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ── Fixed-width types without stdint.h ────────────────────────────────── */
|
||||||
|
typedef signed char bux_i8;
|
||||||
|
typedef unsigned char bux_u8;
|
||||||
|
typedef short bux_i16;
|
||||||
|
typedef unsigned short bux_u16;
|
||||||
|
typedef int bux_i32;
|
||||||
|
typedef unsigned int bux_u32;
|
||||||
|
#if defined(__LP64__) || defined(_WIN64) || defined(__x86_64__) || defined(__aarch64__)
|
||||||
|
typedef long bux_i64;
|
||||||
|
typedef unsigned long bux_u64;
|
||||||
|
typedef unsigned long bux_size;
|
||||||
|
#else
|
||||||
|
typedef long long bux_i64;
|
||||||
|
typedef unsigned long long bux_u64;
|
||||||
|
typedef unsigned int bux_size;
|
||||||
|
#endif
|
||||||
|
typedef bux_u8 bux_bool;
|
||||||
|
#ifndef NULL
|
||||||
|
# define NULL ((void*)0)
|
||||||
|
#endif
|
||||||
|
#ifndef true
|
||||||
|
# define true 1
|
||||||
|
# define false 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ── Heap ─────────────────────────────────────────────────────────────── */
|
||||||
|
#ifndef BUX_FS_HEAP_BYTES
|
||||||
|
# define BUX_FS_HEAP_BYTES (256u * 1024u)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static unsigned char bux_fs_heap[BUX_FS_HEAP_BYTES];
|
||||||
|
static bux_size bux_fs_heap_off = 0;
|
||||||
|
|
||||||
|
/* Weak hooks — board / host may override */
|
||||||
|
__attribute__((weak)) void bux_fs_write(const char* s, unsigned n) {
|
||||||
|
(void)s; (void)n;
|
||||||
|
}
|
||||||
|
__attribute__((weak)) void bux_fs_halt(int code) {
|
||||||
|
(void)code;
|
||||||
|
for (;;) { /* spin */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── CLI args (empty under freestanding) ──────────────────────────────── */
|
||||||
|
int g_argc = 0;
|
||||||
|
char** g_argv = NULL;
|
||||||
|
|
||||||
|
int bux_argc(void) { return g_argc; }
|
||||||
|
char* bux_argv(int index) {
|
||||||
|
(void)index;
|
||||||
|
return (char*)"";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Memory ───────────────────────────────────────────────────────────── */
|
||||||
|
static void bux_fs_zero(void* p, bux_size n) {
|
||||||
|
unsigned char* b = (unsigned char*)p;
|
||||||
|
bux_size i;
|
||||||
|
for (i = 0; i < n; i++) b[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* bux_alloc(bux_size size) {
|
||||||
|
/* 8-byte align */
|
||||||
|
bux_size off = (bux_fs_heap_off + 7u) & ~(bux_size)7u;
|
||||||
|
if (size == 0) size = 1;
|
||||||
|
if (off + size > (bux_size)BUX_FS_HEAP_BYTES) {
|
||||||
|
bux_fs_write("OOM\n", 4);
|
||||||
|
bux_fs_halt(1);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
void* p = &bux_fs_heap[off];
|
||||||
|
bux_fs_heap_off = off + size;
|
||||||
|
bux_fs_zero(p, size);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* bux_realloc(void* ptr, bux_size size) {
|
||||||
|
/* Bump allocator cannot free — allocate fresh + copy if needed */
|
||||||
|
void* n = bux_alloc(size);
|
||||||
|
if (ptr != NULL && n != NULL && size > 0) {
|
||||||
|
unsigned char* d = (unsigned char*)n;
|
||||||
|
unsigned char* s = (unsigned char*)ptr;
|
||||||
|
bux_size i;
|
||||||
|
/* unknown old size — best effort copy of `size` bytes */
|
||||||
|
for (i = 0; i < size; i++) d[i] = s[i];
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bux_free(void* ptr) { (void)ptr; /* bump: no-op */ }
|
||||||
|
|
||||||
|
/* ── Basic I/O / panic ────────────────────────────────────────────────── */
|
||||||
|
void bux_print(const char* s) {
|
||||||
|
if (!s) return;
|
||||||
|
unsigned n = 0;
|
||||||
|
while (s[n]) n++;
|
||||||
|
bux_fs_write(s, n);
|
||||||
|
}
|
||||||
|
void bux_println(const char* s) {
|
||||||
|
bux_print(s ? s : "");
|
||||||
|
bux_fs_write("\n", 1);
|
||||||
|
}
|
||||||
|
void bux_print_int(bux_i64 n) {
|
||||||
|
char buf[32];
|
||||||
|
int i = 0;
|
||||||
|
int neg = 0;
|
||||||
|
if (n < 0) { neg = 1; n = -n; }
|
||||||
|
if (n == 0) { buf[i++] = '0'; }
|
||||||
|
else {
|
||||||
|
while (n > 0 && i < 30) {
|
||||||
|
buf[i++] = (char)('0' + (n % 10));
|
||||||
|
n /= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (neg) buf[i++] = '-';
|
||||||
|
/* reverse */
|
||||||
|
int a = 0, b = i - 1;
|
||||||
|
while (a < b) {
|
||||||
|
char t = buf[a]; buf[a] = buf[b]; buf[b] = t;
|
||||||
|
a++; b--;
|
||||||
|
}
|
||||||
|
bux_fs_write(buf, (unsigned)i);
|
||||||
|
}
|
||||||
|
void bux_print_float(double f) {
|
||||||
|
/* minimal: cast to int for freestanding */
|
||||||
|
bux_print_int((bux_i64)f);
|
||||||
|
}
|
||||||
|
void bux_print_bool(bux_bool b) { bux_print(b ? "true" : "false"); }
|
||||||
|
void bux_print_char(char c) { bux_fs_write(&c, 1); }
|
||||||
|
void bux_panic(const char* msg) {
|
||||||
|
bux_fs_write("PANIC: ", 7);
|
||||||
|
if (msg) {
|
||||||
|
unsigned n = 0;
|
||||||
|
while (msg[n]) n++;
|
||||||
|
bux_fs_write(msg, n);
|
||||||
|
}
|
||||||
|
bux_fs_write("\n", 1);
|
||||||
|
bux_fs_halt(1);
|
||||||
|
}
|
||||||
|
void bux_exit(int code) { bux_fs_halt(code); }
|
||||||
|
void bux_assert(int cond, const char* file, int line, const char* expr) {
|
||||||
|
(void)file; (void)line; (void)expr;
|
||||||
|
if (!cond) bux_panic("assert failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Checked arithmetic ───────────────────────────────────────────────── */
|
||||||
|
bux_i64 bux_div_i64(bux_i64 a, bux_i64 b) {
|
||||||
|
if (b == 0) bux_panic("division by zero");
|
||||||
|
return a / b;
|
||||||
|
}
|
||||||
|
bux_i64 bux_mod_i64(bux_i64 a, bux_i64 b) {
|
||||||
|
if (b == 0) bux_panic("modulo by zero");
|
||||||
|
return a % b;
|
||||||
|
}
|
||||||
|
bux_i64 bux_add_i64_checked(bux_i64 a, bux_i64 b) { return a + b; }
|
||||||
|
bux_i64 bux_sub_i64_checked(bux_i64 a, bux_i64 b) { return a - b; }
|
||||||
|
bux_i64 bux_mul_i64_checked(bux_i64 a, bux_i64 b) { return a * b; }
|
||||||
|
bux_i64 bux_neg_i64_checked(bux_i64 a) { return -a; }
|
||||||
|
|
||||||
|
/* ── Strings (no libc) ────────────────────────────────────────────────── */
|
||||||
|
unsigned int bux_strlen(const char* s) {
|
||||||
|
unsigned int n = 0;
|
||||||
|
if (!s) return 0;
|
||||||
|
while (s[n]) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
int bux_strlen_c(const char* s) { return (int)bux_strlen(s); }
|
||||||
|
int bux_strcmp(const char* a, const char* b) {
|
||||||
|
if (!a) a = "";
|
||||||
|
if (!b) b = "";
|
||||||
|
while (*a && *a == *b) { a++; b++; }
|
||||||
|
return (unsigned char)*a - (unsigned char)*b;
|
||||||
|
}
|
||||||
|
int bux_strncmp(const char* a, const char* b, unsigned int n) {
|
||||||
|
if (!a) a = "";
|
||||||
|
if (!b) b = "";
|
||||||
|
unsigned int i;
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
if (a[i] != b[i] || a[i] == 0) return (unsigned char)a[i] - (unsigned char)b[i];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char* bux_strcpy(char* dest, const char* src) {
|
||||||
|
if (!dest) return NULL;
|
||||||
|
if (!src) { dest[0] = 0; return dest; }
|
||||||
|
char* d = dest;
|
||||||
|
while ((*d++ = *src++)) {}
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
char* bux_strcat(char* dest, const char* src) {
|
||||||
|
if (!dest) return NULL;
|
||||||
|
if (!src) return dest;
|
||||||
|
char* d = dest;
|
||||||
|
while (*d) d++;
|
||||||
|
while ((*d++ = *src++)) {}
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
char* bux_strncpy(char* dest, const char* src, unsigned int n) {
|
||||||
|
if (!dest) return NULL;
|
||||||
|
unsigned int i = 0;
|
||||||
|
if (src) {
|
||||||
|
for (; i < n && src[i]; i++) dest[i] = src[i];
|
||||||
|
}
|
||||||
|
for (; i < n; i++) dest[i] = 0;
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
double bux_str_to_float(const char* s) {
|
||||||
|
(void)s;
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
bux_i64 bux_str_to_int(const char* s) {
|
||||||
|
if (!s) return 0;
|
||||||
|
bux_i64 v = 0;
|
||||||
|
int neg = 0;
|
||||||
|
if (*s == '-') { neg = 1; s++; }
|
||||||
|
while (*s >= '0' && *s <= '9') {
|
||||||
|
v = v * 10 + (*s - '0');
|
||||||
|
s++;
|
||||||
|
}
|
||||||
|
return neg ? -v : v;
|
||||||
|
}
|
||||||
|
const char* bux_strstr(const char* haystack, const char* needle) {
|
||||||
|
if (!haystack || !needle) return NULL;
|
||||||
|
if (!*needle) return haystack;
|
||||||
|
for (const char* h = haystack; *h; h++) {
|
||||||
|
const char* a = h;
|
||||||
|
const char* b = needle;
|
||||||
|
while (*a && *b && *a == *b) { a++; b++; }
|
||||||
|
if (!*b) return h;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
unsigned int bux_str_offset(const char* pos, const char* base) {
|
||||||
|
if (!pos || !base) return 0;
|
||||||
|
return (unsigned int)(pos - base);
|
||||||
|
}
|
||||||
|
int bux_str_contains(const char* haystack, const char* needle) {
|
||||||
|
return bux_strstr(haystack, needle) != NULL;
|
||||||
|
}
|
||||||
|
int bux_str_is_null(const char* s) { return s == NULL; }
|
||||||
|
|
||||||
|
char* bux_str_slice(const char* s, unsigned int start, unsigned int len) {
|
||||||
|
if (!s) s = "";
|
||||||
|
unsigned int sl = bux_strlen(s);
|
||||||
|
if (start > sl) start = sl;
|
||||||
|
if (start + len > sl) len = sl - start;
|
||||||
|
char* out = (char*)bux_alloc(len + 1);
|
||||||
|
unsigned int i;
|
||||||
|
for (i = 0; i < len; i++) out[i] = s[start + i];
|
||||||
|
out[len] = 0;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
static int is_ws(char c) {
|
||||||
|
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
|
||||||
|
}
|
||||||
|
char* bux_str_trim_left(const char* s) {
|
||||||
|
if (!s) s = "";
|
||||||
|
while (*s && is_ws(*s)) s++;
|
||||||
|
return bux_str_slice(s, 0, bux_strlen(s));
|
||||||
|
}
|
||||||
|
char* bux_str_trim_right(const char* s) {
|
||||||
|
if (!s) s = "";
|
||||||
|
unsigned int n = bux_strlen(s);
|
||||||
|
while (n > 0 && is_ws(s[n - 1])) n--;
|
||||||
|
return bux_str_slice(s, 0, n);
|
||||||
|
}
|
||||||
|
char* bux_str_trim(const char* s) {
|
||||||
|
char* a = bux_str_trim_left(s);
|
||||||
|
return bux_str_trim_right(a);
|
||||||
|
}
|
||||||
|
char* bux_int_to_str(bux_i64 n) {
|
||||||
|
char buf[32];
|
||||||
|
int i = 0;
|
||||||
|
int neg = 0;
|
||||||
|
if (n < 0) { neg = 1; n = -n; }
|
||||||
|
if (n == 0) buf[i++] = '0';
|
||||||
|
else while (n > 0 && i < 30) { buf[i++] = (char)('0' + (n % 10)); n /= 10; }
|
||||||
|
if (neg) buf[i++] = '-';
|
||||||
|
int a = 0, b = i - 1;
|
||||||
|
while (a < b) { char t = buf[a]; buf[a] = buf[b]; buf[b] = t; a++; b--; }
|
||||||
|
buf[i] = 0;
|
||||||
|
return bux_str_slice(buf, 0, (unsigned)i);
|
||||||
|
}
|
||||||
|
char* bux_float_to_string(double f) { return bux_int_to_str((bux_i64)f); }
|
||||||
|
|
||||||
|
unsigned int bux_str_split_count(const char* s, const char* delim) {
|
||||||
|
(void)s; (void)delim;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char* bux_str_split_part(const char* s, const char* delim, unsigned int index) {
|
||||||
|
(void)s; (void)delim; (void)index;
|
||||||
|
return (char*)"";
|
||||||
|
}
|
||||||
|
char* bux_str_join2(const char* a, const char* b, const char* sep) {
|
||||||
|
unsigned int na = bux_strlen(a), nb = bux_strlen(b), ns = bux_strlen(sep);
|
||||||
|
char* out = (char*)bux_alloc(na + ns + nb + 1);
|
||||||
|
unsigned int i = 0, j;
|
||||||
|
for (j = 0; j < na; j++) out[i++] = a[j];
|
||||||
|
for (j = 0; j < ns; j++) out[i++] = sep[j];
|
||||||
|
for (j = 0; j < nb; j++) out[i++] = b[j];
|
||||||
|
out[i] = 0;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
char* bux_str_format(const char* fmt, const char* a0, const char* a1, const char* a2, const char* a3) {
|
||||||
|
(void)a1; (void)a2; (void)a3;
|
||||||
|
/* minimal: return fmt or a0 */
|
||||||
|
if (a0 && a0[0]) return bux_str_slice(a0, 0, bux_strlen(a0));
|
||||||
|
if (fmt) return bux_str_slice(fmt, 0, bux_strlen(fmt));
|
||||||
|
return (char*)"";
|
||||||
|
}
|
||||||
|
char* bux_escape_c_string(const char* s, int len) {
|
||||||
|
(void)len;
|
||||||
|
return s ? bux_str_slice(s, 0, bux_strlen(s)) : (char*)"";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* StringBuilder stubs (shape-compatible enough for unused mono) */
|
||||||
|
typedef struct {
|
||||||
|
char* data;
|
||||||
|
unsigned int len;
|
||||||
|
unsigned int cap;
|
||||||
|
} BuxStringBuilder;
|
||||||
|
|
||||||
|
void bux_sb_append(BuxStringBuilder* sb, const char* s) {
|
||||||
|
(void)sb; (void)s;
|
||||||
|
}
|
||||||
|
void bux_sb_append_int(BuxStringBuilder* sb, bux_i64 n) { (void)sb; (void)n; }
|
||||||
|
void bux_sb_append_float(BuxStringBuilder* sb, double f) { (void)sb; (void)f; }
|
||||||
|
void bux_sb_append_char(BuxStringBuilder* sb, char c) { (void)sb; (void)c; }
|
||||||
|
const char* bux_sb_build(BuxStringBuilder* sb) { return sb && sb->data ? sb->data : ""; }
|
||||||
|
void bux_sb_free(BuxStringBuilder* sb) { (void)sb; }
|
||||||
|
|
||||||
|
/* ── FS / path / OS — unavailable ─────────────────────────────────────── */
|
||||||
|
char* bux_read_file(const char* path) { (void)path; return NULL; }
|
||||||
|
int bux_write_file(const char* path, const char* content) {
|
||||||
|
(void)path; (void)content;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int bux_file_exists(const char* path) { (void)path; return 0; }
|
||||||
|
char* bux_path_join(const char* a, const char* b) {
|
||||||
|
return bux_str_join2(a ? a : "", b ? b : "", "/");
|
||||||
|
}
|
||||||
|
char* bux_path_parent(const char* path) {
|
||||||
|
(void)path;
|
||||||
|
return (char*)".";
|
||||||
|
}
|
||||||
|
char* bux_path_ext(const char* path) {
|
||||||
|
(void)path;
|
||||||
|
return (char*)"";
|
||||||
|
}
|
||||||
|
int bux_mkdir_if_needed(const char* path) { (void)path; return -1; }
|
||||||
|
int bux_dir_exists(const char* path) { (void)path; return 0; }
|
||||||
|
char** bux_list_dir(const char* dir, const char* ext, int* out_count) {
|
||||||
|
(void)dir; (void)ext;
|
||||||
|
if (out_count) *out_count = 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Math (integer only; float ops soft-stub) ─────────────────────────── */
|
||||||
|
double bux_sqrt(double x) { return x; }
|
||||||
|
double bux_pow(double x, double y) { (void)y; return x; }
|
||||||
|
bux_i64 bux_abs_i64(bux_i64 x) { return x < 0 ? -x : x; }
|
||||||
|
double bux_abs_f64(double x) { return x < 0 ? -x : x; }
|
||||||
|
bux_i64 bux_min_i64(bux_i64 a, bux_i64 b) { return a < b ? a : b; }
|
||||||
|
bux_i64 bux_max_i64(bux_i64 a, bux_i64 b) { return a > b ? a : b; }
|
||||||
|
double bux_min_f64(double a, double b) { return a < b ? a : b; }
|
||||||
|
double bux_max_f64(double a, double b) { return a > b ? a : b; }
|
||||||
|
|
||||||
|
unsigned int bux_hash_bytes(const void* ptr, bux_size size) {
|
||||||
|
const unsigned char* p = (const unsigned char*)ptr;
|
||||||
|
unsigned int h = 2166136261u;
|
||||||
|
bux_size i;
|
||||||
|
if (!p) return 0;
|
||||||
|
for (i = 0; i < size; i++) {
|
||||||
|
h ^= p[i];
|
||||||
|
h *= 16777619u;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
int bux_mem_eq(const void* a, const void* b, bux_size size) {
|
||||||
|
const unsigned char* x = (const unsigned char*)a;
|
||||||
|
const unsigned char* y = (const unsigned char*)b;
|
||||||
|
bux_size i;
|
||||||
|
if (!x || !y) return x == y;
|
||||||
|
for (i = 0; i < size; i++) if (x[i] != y[i]) return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
unsigned int bux_hash_string(const char* s) {
|
||||||
|
return bux_hash_bytes(s, bux_strlen(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* bux_getenv(const char* name) { (void)name; return ""; }
|
||||||
|
const char* bux_cc_ld_stable(void) { return ""; }
|
||||||
|
int bux_setenv(const char* name, const char* value) {
|
||||||
|
(void)name; (void)value;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const char* bux_getcwd(void) { return "."; }
|
||||||
|
int bux_chdir(const char* path) { (void)path; return -1; }
|
||||||
|
|
||||||
|
void bux_install_stop_handlers(void) {}
|
||||||
|
int bux_should_stop(void) { return 0; }
|
||||||
|
void bux_set_stop_listen_fd(int fd) { (void)fd; }
|
||||||
|
|
||||||
|
bux_i64 bux_time_ms(void) { return 0; }
|
||||||
|
bux_i64 bux_time_us(void) { return 0; }
|
||||||
|
|
||||||
|
/* ── Task / net / crypto — hard stubs ─────────────────────────────────── */
|
||||||
|
int bux_task_spawn(void* fn, void* arg) { (void)fn; (void)arg; return -1; }
|
||||||
|
void bux_task_yield(void) {}
|
||||||
|
void bux_task_sleep_ms(int ms) { (void)ms; }
|
||||||
|
|
||||||
|
int bux_chan_new(int cap) { (void)cap; return -1; }
|
||||||
|
int bux_chan_send(int id, void* msg) { (void)id; (void)msg; return -1; }
|
||||||
|
void* bux_chan_recv(int id) { (void)id; return NULL; }
|
||||||
|
void bux_chan_close(int id) { (void)id; }
|
||||||
|
|
||||||
|
int bux_tcp_listen(int port) { (void)port; return -1; }
|
||||||
|
int bux_tcp_accept(int fd) { (void)fd; return -1; }
|
||||||
|
int bux_tcp_connect(const char* host, int port) {
|
||||||
|
(void)host; (void)port;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int bux_tcp_send(int fd, const void* buf, int n) {
|
||||||
|
(void)fd; (void)buf; (void)n;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int bux_tcp_recv(int fd, void* buf, int n) {
|
||||||
|
(void)fd; (void)buf; (void)n;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
void bux_tcp_close(int fd) { (void)fd; }
|
||||||
|
|
||||||
|
void* bux_tls_server_ctx(const char* cert, const char* key) {
|
||||||
|
(void)cert; (void)key;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
void* bux_tls_server_ctx_ex(const char* cert, const char* key, const char* ca) {
|
||||||
|
(void)cert; (void)key; (void)ca;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
int bux_tls_accept(void* ctx, int fd) { (void)ctx; (void)fd; return -1; }
|
||||||
|
int bux_tls_send(int h, const void* buf, int n) {
|
||||||
|
(void)h; (void)buf; (void)n;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int bux_tls_recv(int h, void* buf, int n) {
|
||||||
|
(void)h; (void)buf; (void)n;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
void bux_tls_close(int h) { (void)h; }
|
||||||
|
const char* bux_tls_error(void) { return "tls unavailable (freestanding)"; }
|
||||||
|
|
||||||
|
/* ── Optional bare `_start` for -nostdlib link experiments ───────────── */
|
||||||
|
#ifdef BUX_FS_PROVIDE_START
|
||||||
|
extern int main(int argc, char** argv);
|
||||||
|
void _start(void) {
|
||||||
|
g_argc = 0;
|
||||||
|
g_argv = NULL;
|
||||||
|
int code = main(0, NULL);
|
||||||
|
bux_fs_halt(code);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
+12
@@ -373,6 +373,16 @@ module Ast {
|
|||||||
// Struct fields (up to 256)
|
// Struct fields (up to 256)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Parser diagnostic (recoverable errors collected during parsing)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct ParserDiag {
|
||||||
|
line: uint32,
|
||||||
|
column: uint32,
|
||||||
|
message: String,
|
||||||
|
severity: int, /* 0=error (fatal), 1=warning (recoverable) */
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Module — AST root
|
// Module — AST root
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -381,6 +391,8 @@ module Ast {
|
|||||||
path: String, // path segments joined
|
path: String, // path segments joined
|
||||||
itemCount: int,
|
itemCount: int,
|
||||||
firstItem: *Decl,
|
firstItem: *Decl,
|
||||||
|
diagCount: int,
|
||||||
|
diags: *ParserDiag,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -996,7 +996,24 @@ module CBackend {
|
|||||||
|
|
||||||
// Binary — always parenthesize so C precedence cannot rewrite the AST.
|
// Binary — always parenthesize so C precedence cannot rewrite the AST.
|
||||||
// Without parens, Mul(Add(a,b), c) emits `a + b * c` (= a+(b*c)) instead of (a+b)*c.
|
// Without parens, Mul(Add(a,b), c) emits `a + b * c` (= a+(b*c)) instead of (a+b)*c.
|
||||||
|
// Integer / and % use checked runtime helpers (panic on zero divisor).
|
||||||
if kind == hBinary {
|
if kind == hBinary {
|
||||||
|
if node.intValue == tkSlash {
|
||||||
|
StringBuilder_Append(&cbe.sb, "bux_div_i64((int64_t)(");
|
||||||
|
CBE_EmitExpr(cbe, node.child1);
|
||||||
|
StringBuilder_Append(&cbe.sb, "), (int64_t)(");
|
||||||
|
CBE_EmitExpr(cbe, node.child2);
|
||||||
|
StringBuilder_Append(&cbe.sb, "))");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if node.intValue == tkPercent {
|
||||||
|
StringBuilder_Append(&cbe.sb, "bux_mod_i64((int64_t)(");
|
||||||
|
CBE_EmitExpr(cbe, node.child1);
|
||||||
|
StringBuilder_Append(&cbe.sb, "), (int64_t)(");
|
||||||
|
CBE_EmitExpr(cbe, node.child2);
|
||||||
|
StringBuilder_Append(&cbe.sb, "))");
|
||||||
|
return;
|
||||||
|
}
|
||||||
StringBuilder_Append(&cbe.sb, "(");
|
StringBuilder_Append(&cbe.sb, "(");
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
CBE_EmitExpr(cbe, node.child1);
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
StringBuilder_Append(&cbe.sb, " ");
|
||||||
@@ -1498,6 +1515,12 @@ module CBackend {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is (type test): should be desugared to hBinary in HIR lowering
|
||||||
|
if kind == hIs {
|
||||||
|
StringBuilder_Append(&cbe.sb, "0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Struct init: ((TypeName){.field = value, ...})
|
// Struct init: ((TypeName){.field = value, ...})
|
||||||
if kind == hStructInit {
|
if kind == hStructInit {
|
||||||
// Field values taken by value → skip auto-Drop of those locals
|
// Field values taken by value → skip auto-Drop of those locals
|
||||||
@@ -2102,6 +2125,8 @@ module CBackend {
|
|||||||
StringBuilder_Append(&cbe.sb, "#include <string.h>\n");
|
StringBuilder_Append(&cbe.sb, "#include <string.h>\n");
|
||||||
StringBuilder_Append(&cbe.sb, "#include <stdio.h>\n");
|
StringBuilder_Append(&cbe.sb, "#include <stdio.h>\n");
|
||||||
StringBuilder_Append(&cbe.sb, "#include <stdlib.h>\n\n");
|
StringBuilder_Append(&cbe.sb, "#include <stdlib.h>\n\n");
|
||||||
|
StringBuilder_Append(&cbe.sb, "extern int64_t bux_div_i64(int64_t a, int64_t b);\n");
|
||||||
|
StringBuilder_Append(&cbe.sb, "extern int64_t bux_mod_i64(int64_t a, int64_t b);\n\n");
|
||||||
// Type aliases
|
// Type aliases
|
||||||
StringBuilder_Append(&cbe.sb, "typedef const char* String;\n");
|
StringBuilder_Append(&cbe.sb, "typedef const char* String;\n");
|
||||||
StringBuilder_Append(&cbe.sb, "typedef unsigned char uint8;\n");
|
StringBuilder_Append(&cbe.sb, "typedef unsigned char uint8;\n");
|
||||||
|
|||||||
+89
-6
@@ -59,10 +59,13 @@ module Cli {
|
|||||||
return "rt/runtime_win.c";
|
return "rt/runtime_win.c";
|
||||||
}
|
}
|
||||||
if String_Eq(env, "minimal") || String_Eq(env, "thin") ||
|
if String_Eq(env, "minimal") || String_Eq(env, "thin") ||
|
||||||
String_Eq(env, "embed") || String_Eq(env, "embedded") ||
|
String_Eq(env, "embed") || String_Eq(env, "embedded") {
|
||||||
String_Eq(env, "freestanding") {
|
|
||||||
return "rt/runtime_minimal.c";
|
return "rt/runtime_minimal.c";
|
||||||
}
|
}
|
||||||
|
if String_Eq(env, "freestanding") || String_Eq(env, "bare") ||
|
||||||
|
String_Eq(env, "nolibc") {
|
||||||
|
return "rt/runtime_freestanding.c";
|
||||||
|
}
|
||||||
if String_Eq(env, "full") || String_Eq(env, "posix") {
|
if String_Eq(env, "full") || String_Eq(env, "posix") {
|
||||||
return "rt/runtime.c";
|
return "rt/runtime.c";
|
||||||
}
|
}
|
||||||
@@ -78,6 +81,7 @@ module Cli {
|
|||||||
|
|
||||||
func Cli_IsThinRuntimePath(rtPath: String) -> bool {
|
func Cli_IsThinRuntimePath(rtPath: String) -> bool {
|
||||||
if String_Contains(rtPath, "runtime_minimal.c") { return true; }
|
if String_Contains(rtPath, "runtime_minimal.c") { return true; }
|
||||||
|
if String_Contains(rtPath, "runtime_freestanding.c") { return true; }
|
||||||
if String_Contains(rtPath, "runtime_win.c") { return true; }
|
if String_Contains(rtPath, "runtime_win.c") { return true; }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -354,6 +358,28 @@ module Cli {
|
|||||||
// Import the compiler pipeline
|
// Import the compiler pipeline
|
||||||
// In self-hosting mode, these are compiled together from src/
|
// In self-hosting mode, these are compiled together from src/
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Report parser diagnostics attached to a module (Rust-style, with snippets).
|
||||||
|
// Returns the number of diagnostics reported (0 = clean).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func Cli_ReportParseDiags(mod: *Module, sourceName: String) -> int {
|
||||||
|
if mod == null as *Module { return 0; }
|
||||||
|
if mod.diagCount == 0 { return 0; }
|
||||||
|
var i: int = 0;
|
||||||
|
while i < mod.diagCount {
|
||||||
|
let diag: Diagnostic = Diagnostic {
|
||||||
|
message: mod.diags[i].message,
|
||||||
|
line: mod.diags[i].line,
|
||||||
|
column: mod.diags[i].column,
|
||||||
|
severity: 0,
|
||||||
|
};
|
||||||
|
Diagnostic_Print(&diag, sourceName);
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return mod.diagCount;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Compile a single .bux source file
|
// Compile a single .bux source file
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -385,6 +411,7 @@ func Cli_Compile(source: String, sourceName: String, targetTriple: String) -> St
|
|||||||
PrintLine("Parse failed");
|
PrintLine("Parse failed");
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
if Cli_ReportParseDiags(mod, sourceName) > 0 { return ""; }
|
||||||
PrintLine(" Parse done");
|
PrintLine(" Parse done");
|
||||||
|
|
||||||
// Flatten module wrappers: find module decl and hoist its children
|
// Flatten module wrappers: find module decl and hoist its children
|
||||||
@@ -542,11 +569,12 @@ func Cli_Check(srcPath: String) -> int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: Parse
|
// Phase 2: Parse
|
||||||
let mod: *Module = Parser_Parse(lex.tokens, lex.tokenCount);
|
var mod: *Module = Parser_Parse(lex.tokens, lex.tokenCount);
|
||||||
if mod == null as *Module {
|
if mod == null as *Module {
|
||||||
PrintLine("Parse failed");
|
PrintLine("Parse failed");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
if Cli_ReportParseDiags(mod, srcPath) > 0 { return 1; }
|
||||||
PrintLine(" Parse done");
|
PrintLine(" Parse done");
|
||||||
|
|
||||||
// Flatten module wrappers
|
// Flatten module wrappers
|
||||||
@@ -559,6 +587,48 @@ func Cli_Check(srcPath: String) -> int {
|
|||||||
decl2 = decl2.childDecl2;
|
decl2 = decl2.childDecl2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2a: merge stdlib when the file imports Std::* so single-file
|
||||||
|
// checks resolve stdlib symbols the same way project builds do.
|
||||||
|
var mergedStd: bool = false;
|
||||||
|
var hasStdImport: bool = false;
|
||||||
|
var useScan: *Decl = mod.firstItem;
|
||||||
|
while useScan != null as *Decl {
|
||||||
|
if useScan.kind == dkUse && String_StartsWith(useScan.usePath, "Std::") { hasStdImport = true; }
|
||||||
|
useScan = useScan.childDecl2;
|
||||||
|
}
|
||||||
|
if hasStdImport {
|
||||||
|
let stdlibDir: String = Cli_FindStdlibDir("");
|
||||||
|
if !String_Eq(stdlibDir, "") {
|
||||||
|
// Stamp user decls so `check func` output lists only user decls.
|
||||||
|
var stampU: *Decl = mod.firstItem;
|
||||||
|
while stampU != null as *Decl {
|
||||||
|
Cli_StampSourceFile(stampU, srcPath);
|
||||||
|
stampU = stampU.childDecl2;
|
||||||
|
}
|
||||||
|
// User decls shadow stdlib decls with the same name.
|
||||||
|
let maxNames: int = 2048;
|
||||||
|
let userNames: *String = bux_alloc(maxNames * 8) as *String;
|
||||||
|
let userNameCount: int = Cli_CollectNames(mod, userNames, maxNames);
|
||||||
|
let merged: *Module = bux_alloc(sizeof(Module)) as *Module;
|
||||||
|
merged.name = "main";
|
||||||
|
merged.path = "";
|
||||||
|
merged.itemCount = 0;
|
||||||
|
merged.firstItem = null as *Decl;
|
||||||
|
merged.diagCount = 0;
|
||||||
|
merged.diags = null as *ParserDiag;
|
||||||
|
var libCount: int = 0;
|
||||||
|
let libFiles: *String = bux_list_dir(stdlibDir, ".bux", &libCount);
|
||||||
|
var si: int = 0;
|
||||||
|
while si < libCount {
|
||||||
|
discard Cli_MergeFileInto(merged, libFiles[si], userNames, userNameCount);
|
||||||
|
si = si + 1;
|
||||||
|
}
|
||||||
|
Cli_CopyModuleDecls(merged, mod);
|
||||||
|
mod = merged;
|
||||||
|
mergedStd = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 2b: macro expand
|
// Phase 2b: macro expand
|
||||||
let macEx2: *MacroExpander = MacroExpand_ExpandModule(mod);
|
let macEx2: *MacroExpander = MacroExpand_ExpandModule(mod);
|
||||||
if MacroExpand_DiagCount(macEx2) > 0 {
|
if MacroExpand_DiagCount(macEx2) > 0 {
|
||||||
@@ -600,9 +670,12 @@ func Cli_Check(srcPath: String) -> int {
|
|||||||
var dc: *Decl = mod.firstItem;
|
var dc: *Decl = mod.firstItem;
|
||||||
while dc != null as *Decl {
|
while dc != null as *Decl {
|
||||||
if dc.kind == dkFunc {
|
if dc.kind == dkFunc {
|
||||||
|
// With stdlib merged, list only the user file's functions.
|
||||||
|
if !mergedStd || String_Eq(dc.sourceFile, srcPath) {
|
||||||
Print("check func ");
|
Print("check func ");
|
||||||
PrintLine(dc.strValue);
|
PrintLine(dc.strValue);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
dc = dc.childDecl2;
|
dc = dc.childDecl2;
|
||||||
}
|
}
|
||||||
PrintLine("Check passed");
|
PrintLine("Check passed");
|
||||||
@@ -631,6 +704,7 @@ func Cli_CompileSource(source: String, sourceName: String) -> *HirModule {
|
|||||||
PrintLine(sourceName);
|
PrintLine(sourceName);
|
||||||
return null as *HirModule;
|
return null as *HirModule;
|
||||||
}
|
}
|
||||||
|
if Cli_ReportParseDiags(mod, sourceName) > 0 { return null as *HirModule; }
|
||||||
|
|
||||||
// Phase 2b: macro expand
|
// Phase 2b: macro expand
|
||||||
let macEx3: *MacroExpander = MacroExpand_ExpandModule(mod);
|
let macEx3: *MacroExpander = MacroExpand_ExpandModule(mod);
|
||||||
@@ -832,6 +906,10 @@ func Cli_MergeFileInto(target: *Module, path: String, skipNames: *String, skipCo
|
|||||||
if Lexer_DiagCount(lex) > 0 { return 0; }
|
if Lexer_DiagCount(lex) > 0 { return 0; }
|
||||||
let mod: *Module = Parser_Parse(lex.tokens, lex.tokenCount);
|
let mod: *Module = Parser_Parse(lex.tokens, lex.tokenCount);
|
||||||
if mod == null as *Module { return 0; }
|
if mod == null as *Module { return 0; }
|
||||||
|
// Report parse diags but keep merging the recovered decls: this path
|
||||||
|
// handles stdlib/dependency files, and the parser currently emits
|
||||||
|
// recoverable diags for valid multi-line braced imports there.
|
||||||
|
discard Cli_ReportParseDiags(mod, path);
|
||||||
// Tag every decl from this file for #line maps
|
// Tag every decl from this file for #line maps
|
||||||
var stamp: *Decl = mod.firstItem;
|
var stamp: *Decl = mod.firstItem;
|
||||||
while stamp != null as *Decl {
|
while stamp != null as *Decl {
|
||||||
@@ -1924,6 +2002,8 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool,
|
|||||||
userMerged.path = "";
|
userMerged.path = "";
|
||||||
userMerged.itemCount = 0;
|
userMerged.itemCount = 0;
|
||||||
userMerged.firstItem = null as *Decl;
|
userMerged.firstItem = null as *Decl;
|
||||||
|
userMerged.diagCount = 0;
|
||||||
|
userMerged.diags = null as *ParserDiag;
|
||||||
|
|
||||||
// Parse each file and merge declarations into userMerged module
|
// Parse each file and merge declarations into userMerged module
|
||||||
var i: int = 0;
|
var i: int = 0;
|
||||||
@@ -1958,6 +2038,7 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool,
|
|||||||
PrintLine(path);
|
PrintLine(path);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
if Cli_ReportParseDiags(mod, path) > 0 { return 1; }
|
||||||
// Tag decls with this source path for multi-file #line
|
// Tag decls with this source path for multi-file #line
|
||||||
var stampUser: *Decl = mod.firstItem;
|
var stampUser: *Decl = mod.firstItem;
|
||||||
while stampUser != null as *Decl {
|
while stampUser != null as *Decl {
|
||||||
@@ -2027,6 +2108,8 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool,
|
|||||||
merged.path = "";
|
merged.path = "";
|
||||||
merged.itemCount = 0;
|
merged.itemCount = 0;
|
||||||
merged.firstItem = null as *Decl;
|
merged.firstItem = null as *Decl;
|
||||||
|
merged.diagCount = 0;
|
||||||
|
merged.diags = null as *ParserDiag;
|
||||||
|
|
||||||
// Find and merge ALL stdlib declarations
|
// Find and merge ALL stdlib declarations
|
||||||
let stdlibDir: String = Cli_FindStdlibDir(projectDir);
|
let stdlibDir: String = Cli_FindStdlibDir(projectDir);
|
||||||
@@ -2254,7 +2337,7 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if argCount < 2 {
|
if argCount < 2 {
|
||||||
PrintLine("Bux Self-Hosting Compiler v1.0.0");
|
PrintLine("Bux Self-Hosting Compiler v1.0.2");
|
||||||
PrintLine("Usage: buxc <command> [args]");
|
PrintLine("Usage: buxc <command> [args]");
|
||||||
PrintLine("Commands: build, check, new, init, add, remove, fetch, install, search, fmt, doc, test, run, project, help, version");
|
PrintLine("Commands: build, check, new, init, add, remove, fetch, install, search, fmt, doc, test, run, project, help, version");
|
||||||
PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
|
PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
|
||||||
@@ -2272,12 +2355,12 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
|||||||
|
|
||||||
let cmd: String = args[1];
|
let cmd: String = args[1];
|
||||||
if String_Eq(cmd, "version") || String_Eq(cmd, "--version") || String_Eq(cmd, "-v") {
|
if String_Eq(cmd, "version") || String_Eq(cmd, "--version") || String_Eq(cmd, "-v") {
|
||||||
PrintLine("Bux 1.0.0 (self-hosting)");
|
PrintLine("Bux 1.0.2 (self-hosting)");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if String_Eq(cmd, "help") || String_Eq(cmd, "--help") || String_Eq(cmd, "-h") {
|
if String_Eq(cmd, "help") || String_Eq(cmd, "--help") || String_Eq(cmd, "-h") {
|
||||||
PrintLine("Bux Self-Hosting Compiler v1.0.0");
|
PrintLine("Bux Self-Hosting Compiler v1.0.2");
|
||||||
PrintLine("Usage: buxc <command> [args]");
|
PrintLine("Usage: buxc <command> [args]");
|
||||||
PrintLine("Commands: build, check, new, init, add, remove, fetch, install, search, fmt, doc, test, run, project, help, version");
|
PrintLine("Commands: build, check, new, init, add, remove, fetch, install, search, fmt, doc, test, run, project, help, version");
|
||||||
PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
|
PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
|
||||||
|
|||||||
+357
-41
@@ -281,6 +281,29 @@ module HirLower {
|
|||||||
Lcx_GenerateStructInstance(ctx, genStruct, r.typeArgName0, r.typeArgName1, te.typeArgCount);
|
Lcx_GenerateStructInstance(ctx, genStruct, r.typeArgName0, r.typeArgName1, te.typeArgCount);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
let genEnum: *Decl = Lcx_FindGenericEnum(ctx, te.typeName);
|
||||||
|
if genEnum != null as *Decl {
|
||||||
|
var isParametric: bool = false;
|
||||||
|
if te.typeArgCount > 0 && String_Eq(te.typeArgName0, genEnum.typeParam0) { isParametric = true; }
|
||||||
|
if te.typeArgCount > 1 && String_Eq(te.typeArgName1, genEnum.typeParam1) { isParametric = true; }
|
||||||
|
if isParametric && String_Eq(ctx.substParam0, "") && String_Eq(ctx.substParam1, "") {
|
||||||
|
return te;
|
||||||
|
}
|
||||||
|
let r: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
||||||
|
r.kind = tekNamed;
|
||||||
|
r.line = te.line;
|
||||||
|
r.column = te.column;
|
||||||
|
r.typeArgCount = te.typeArgCount;
|
||||||
|
r.typeArgName0 = te.typeArgName0;
|
||||||
|
r.typeArgName1 = te.typeArgName1;
|
||||||
|
if String_Eq(r.typeArgName0, ctx.substParam0) { r.typeArgName0 = ctx.substArg0; }
|
||||||
|
if String_Eq(r.typeArgName0, ctx.substParam1) { r.typeArgName0 = ctx.substArg1; }
|
||||||
|
if String_Eq(r.typeArgName1, ctx.substParam0) { r.typeArgName1 = ctx.substArg0; }
|
||||||
|
if String_Eq(r.typeArgName1, ctx.substParam1) { r.typeArgName1 = ctx.substArg1; }
|
||||||
|
r.typeName = Lcx_MangleName(te.typeName, r.typeArgName0, r.typeArgName1, te.typeArgCount);
|
||||||
|
Lcx_GenerateEnumInstance(ctx, genEnum, r.typeArgName0, r.typeArgName1, te.typeArgCount);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Named type that is a type parameter (only when in instance mode)
|
// Named type that is a type parameter (only when in instance mode)
|
||||||
@@ -429,6 +452,94 @@ module HirLower {
|
|||||||
return null as *Decl;
|
return null as *Decl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Lcx_FindGenericEnum(ctx: *LowerCtx, name: String) -> *Decl {
|
||||||
|
var i: int = 0;
|
||||||
|
while i < ctx.genStructCount {
|
||||||
|
if ctx.genStructs[i].kind == dkEnum && String_Eq(ctx.genStructs[i].strValue, name) {
|
||||||
|
return &ctx.genStructs[i];
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return null as *Decl;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Lcx_GenerateEnumInstance(ctx: *LowerCtx, genDecl: *Decl, typeArg0: String, typeArg1: String, typeArgCount: int) -> String {
|
||||||
|
if String_Eq(genDecl.strValue, "") { return ""; }
|
||||||
|
let mangled: String = Lcx_MangleName(genDecl.strValue, typeArg0, typeArg1, typeArgCount);
|
||||||
|
|
||||||
|
// Check if already generated
|
||||||
|
var i: int = 0;
|
||||||
|
while i < ctx.hm.enumCount {
|
||||||
|
if String_Eq(ctx.hm.enums[i].name, mangled) {
|
||||||
|
return mangled;
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save old substitution
|
||||||
|
let oldParam0: String = ctx.substParam0;
|
||||||
|
let oldArg0: String = ctx.substArg0;
|
||||||
|
let oldParam1: String = ctx.substParam1;
|
||||||
|
let oldArg1: String = ctx.substArg1;
|
||||||
|
|
||||||
|
ctx.substParam0 = genDecl.typeParam0;
|
||||||
|
ctx.substArg0 = typeArg0;
|
||||||
|
ctx.substParam1 = genDecl.typeParam1;
|
||||||
|
ctx.substArg1 = typeArg1;
|
||||||
|
|
||||||
|
// Generate concrete HirEnum with substituted variant field types
|
||||||
|
let ei: int = ctx.hm.enumCount;
|
||||||
|
ctx.hm.enumCount = ctx.hm.enumCount + 1;
|
||||||
|
ctx.hm.enums[ei].name = mangled;
|
||||||
|
ctx.hm.enums[ei].variantCount = genDecl.variantCount;
|
||||||
|
if genDecl.variantCount > 0 {
|
||||||
|
ctx.hm.enums[ei].variants = bux_alloc(genDecl.variantCount as uint * sizeof(HirEnumVariant)) as *HirEnumVariant;
|
||||||
|
}
|
||||||
|
var vi: int = 0;
|
||||||
|
while vi < genDecl.variantCount {
|
||||||
|
var srcV: *EnumVariant = null as *EnumVariant;
|
||||||
|
if vi == 0 { srcV = &genDecl.variant0; }
|
||||||
|
if vi == 1 { srcV = &genDecl.variant1; }
|
||||||
|
if vi == 2 { srcV = &genDecl.variant2; }
|
||||||
|
if vi == 3 { srcV = &genDecl.variant3; }
|
||||||
|
if vi == 4 { srcV = &genDecl.variant4; }
|
||||||
|
if vi == 5 { srcV = &genDecl.variant5; }
|
||||||
|
if vi == 6 { srcV = &genDecl.variant6; }
|
||||||
|
if vi == 7 { srcV = &genDecl.variant7; }
|
||||||
|
if vi == 8 { srcV = &genDecl.variant8; }
|
||||||
|
if srcV != null as *EnumVariant {
|
||||||
|
ctx.hm.enums[ei].variants[vi].name = srcV.name;
|
||||||
|
ctx.hm.enums[ei].variants[vi].fieldCount = srcV.fieldCount;
|
||||||
|
if srcV.fieldCount > 0 {
|
||||||
|
ctx.hm.enums[ei].variants[vi].fieldName0 = String_Concat(srcV.name, "_0");
|
||||||
|
ctx.hm.enums[ei].variants[vi].fieldType0 = Lcx_ResolveTypeKindFromName(srcV.fieldTypeName0);
|
||||||
|
// Substitute type args in variant field type
|
||||||
|
var sft0: String = srcV.fieldTypeName0;
|
||||||
|
if String_Eq(sft0, genDecl.typeParam0) { sft0 = typeArg0; }
|
||||||
|
if String_Eq(sft0, genDecl.typeParam1) { sft0 = typeArg1; }
|
||||||
|
ctx.hm.enums[ei].variants[vi].fieldTypeName0 = sft0;
|
||||||
|
}
|
||||||
|
if srcV.fieldCount > 1 {
|
||||||
|
ctx.hm.enums[ei].variants[vi].fieldName1 = String_Concat(srcV.name, "_1");
|
||||||
|
ctx.hm.enums[ei].variants[vi].fieldType1 = Lcx_ResolveTypeKindFromName(srcV.fieldTypeName1);
|
||||||
|
var sft1: String = srcV.fieldTypeName1;
|
||||||
|
if String_Eq(sft1, genDecl.typeParam0) { sft1 = typeArg0; }
|
||||||
|
if String_Eq(sft1, genDecl.typeParam1) { sft1 = typeArg1; }
|
||||||
|
ctx.hm.enums[ei].variants[vi].fieldTypeName1 = sft1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vi = vi + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore old substitution
|
||||||
|
ctx.substParam0 = oldParam0;
|
||||||
|
ctx.substArg0 = oldArg0;
|
||||||
|
ctx.substParam1 = oldParam1;
|
||||||
|
ctx.substArg1 = oldArg1;
|
||||||
|
|
||||||
|
return mangled;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract element type from mangled collection name: Array_int → int, Iter_String → String
|
// Extract element type from mangled collection name: Array_int → int, Iter_String → String
|
||||||
func Lcx_ExtractElemFromName(typeName: String) -> String {
|
func Lcx_ExtractElemFromName(typeName: String) -> String {
|
||||||
if String_Eq(typeName, "") { return ""; }
|
if String_Eq(typeName, "") { return ""; }
|
||||||
@@ -546,6 +657,10 @@ module HirLower {
|
|||||||
// Lower the generic function with substitution active
|
// Lower the generic function with substitution active
|
||||||
let f: *HirFunc = Lcx_LowerFunc(ctx, genDecl);
|
let f: *HirFunc = Lcx_LowerFunc(ctx, genDecl);
|
||||||
f.name = mangled;
|
f.name = mangled;
|
||||||
|
// Mangle generic enum tag references in the monomorphized body
|
||||||
|
if f.body != null as *HirNode {
|
||||||
|
Lcx_MangleEnumTagsNode(ctx, f.body);
|
||||||
|
}
|
||||||
// Definition-site hygiene: mono body always maps to the generic's source
|
// Definition-site hygiene: mono body always maps to the generic's source
|
||||||
// file (not the call-site module), even if synthetic nodes lacked a path.
|
// file (not the call-site module), even if synthetic nodes lacked a path.
|
||||||
if f.body != null as *HirNode && !String_Eq(genDecl.sourceFile, "") {
|
if f.body != null as *HirNode && !String_Eq(genDecl.sourceFile, "") {
|
||||||
@@ -566,6 +681,64 @@ module HirLower {
|
|||||||
return mangled;
|
return mangled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Lcx_SubstEnumName(ctx: *LowerCtx, name: String) -> String {
|
||||||
|
// HIR nodes may carry null names (e.g. hStore without typeName);
|
||||||
|
// Nim tolerated nil strings, bux_strlen(NULL) segfaults.
|
||||||
|
if name == null as String { return name; }
|
||||||
|
// If name is a bare generic enum name, mangle to concrete name
|
||||||
|
var ge: *Decl = Lcx_FindGenericEnum(ctx, name);
|
||||||
|
if ge != null as *Decl {
|
||||||
|
return Lcx_MangleName(name, ctx.substArg0, ctx.substArg1, ge.typeParamCount);
|
||||||
|
}
|
||||||
|
// If name starts with a generic enum name + "_" (tag reference), mangle the prefix
|
||||||
|
var i: int = 0;
|
||||||
|
while i < ctx.genStructCount {
|
||||||
|
if ctx.genStructs[i].kind == dkEnum {
|
||||||
|
let prefix: String = String_Concat(ctx.genStructs[i].strValue, "_");
|
||||||
|
let prefixLen: int = String_Len(prefix) as int;
|
||||||
|
let nameLen: int = String_Len(name) as int;
|
||||||
|
if nameLen > prefixLen {
|
||||||
|
let namePrefix: String = bux_str_slice(name, 0, prefixLen as uint);
|
||||||
|
if String_Eq(namePrefix, prefix) {
|
||||||
|
let mangledPrefix: String = String_Concat(
|
||||||
|
Lcx_MangleName(ctx.genStructs[i].strValue, ctx.substArg0, ctx.substArg1, ctx.genStructs[i].typeParamCount),
|
||||||
|
"_");
|
||||||
|
let rest: String = bux_str_slice(name, prefixLen as uint, (nameLen - prefixLen) as uint);
|
||||||
|
return String_Concat(mangledPrefix, rest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Lcx_MangleEnumTagsNode(ctx: *LowerCtx, node: *HirNode) {
|
||||||
|
if node == null as *HirNode { return; }
|
||||||
|
if node.kind == hVar {
|
||||||
|
node.strValue = Lcx_SubstEnumName(ctx, node.strValue);
|
||||||
|
}
|
||||||
|
if node.kind == hStructInit {
|
||||||
|
node.strValue = Lcx_SubstEnumName(ctx, node.strValue);
|
||||||
|
}
|
||||||
|
if node.kind == hAlloca || node.kind == hStore {
|
||||||
|
node.typeName = Lcx_SubstEnumName(ctx, node.typeName);
|
||||||
|
}
|
||||||
|
Lcx_MangleEnumTagsNode(ctx, node.child1);
|
||||||
|
Lcx_MangleEnumTagsNode(ctx, node.child2);
|
||||||
|
Lcx_MangleEnumTagsNode(ctx, node.child3);
|
||||||
|
if (node.kind == hCall || node.kind == hCallIndirect) && node.extraData != null as *void {
|
||||||
|
var cur: *HirArgList = node.extraData as *HirArgList;
|
||||||
|
while cur != null as *HirArgList {
|
||||||
|
Lcx_MangleEnumTagsNode(ctx, cur.node);
|
||||||
|
cur = cur.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node.kind == hIf && node.extraData != null as *void {
|
||||||
|
Lcx_MangleEnumTagsNode(ctx, node.extraData as *HirNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Strip type-arg suffix from a mangled generic instance name.
|
// Strip type-arg suffix from a mangled generic instance name.
|
||||||
// E.g. ("Box_int", "int", "", 1) -> "Box"; ("Pair_int_String", "int", "String", 2) -> "Pair".
|
// E.g. ("Box_int", "int", "", 1) -> "Box"; ("Pair_int_String", "int", "String", 2) -> "Pair".
|
||||||
func Lcx_StripTypeArgs(typeName: String, typeArg0: String, typeArg1: String, typeArgCount: int) -> String {
|
func Lcx_StripTypeArgs(typeName: String, typeArg0: String, typeArg1: String, typeArgCount: int) -> String {
|
||||||
@@ -633,7 +806,7 @@ module HirLower {
|
|||||||
func Lcx_EnumHasData(ctx: *LowerCtx, enumName: String) -> bool {
|
func Lcx_EnumHasData(ctx: *LowerCtx, enumName: String) -> bool {
|
||||||
if String_Eq(enumName, "") { return false; }
|
if String_Eq(enumName, "") { return false; }
|
||||||
let sym: Symbol = Scope_Lookup(ctx.scope, enumName);
|
let sym: Symbol = Scope_Lookup(ctx.scope, enumName);
|
||||||
if sym.decl == null as *Decl || sym.decl.kind != dkEnum { return false; }
|
if sym.decl != null as *Decl && sym.decl.kind == dkEnum {
|
||||||
if sym.decl.variantCount > 0 && sym.decl.variant0.fieldCount > 0 { return true; }
|
if sym.decl.variantCount > 0 && sym.decl.variant0.fieldCount > 0 { return true; }
|
||||||
if sym.decl.variantCount > 1 && sym.decl.variant1.fieldCount > 0 { return true; }
|
if sym.decl.variantCount > 1 && sym.decl.variant1.fieldCount > 0 { return true; }
|
||||||
if sym.decl.variantCount > 2 && sym.decl.variant2.fieldCount > 0 { return true; }
|
if sym.decl.variantCount > 2 && sym.decl.variant2.fieldCount > 0 { return true; }
|
||||||
@@ -645,6 +818,40 @@ module HirLower {
|
|||||||
if sym.decl.variantCount > 8 && sym.decl.variant8.fieldCount > 0 { return true; }
|
if sym.decl.variantCount > 8 && sym.decl.variant8.fieldCount > 0 { return true; }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Monomorphized instance (Result_int_String) — check HIR enums
|
||||||
|
if ctx.hm != null as *HirModule {
|
||||||
|
var i: int = 0;
|
||||||
|
while i < ctx.hm.enumCount {
|
||||||
|
if String_Eq(ctx.hm.enums[i].name, enumName) {
|
||||||
|
var vi: int = 0;
|
||||||
|
while vi < ctx.hm.enums[i].variantCount {
|
||||||
|
if ctx.hm.enums[i].variants[vi].fieldCount > 0 { return true; }
|
||||||
|
vi = vi + 1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Bare prefix of monomorphized generic enum: Result_int_String → Result
|
||||||
|
var gi: int = 0;
|
||||||
|
while gi < ctx.genStructCount {
|
||||||
|
if ctx.genStructs[gi].kind == dkEnum {
|
||||||
|
let base: String = ctx.genStructs[gi].strValue;
|
||||||
|
let prefix: String = String_Concat(base, "_");
|
||||||
|
let prefLen: int = String_Len(prefix) as int;
|
||||||
|
let nameLen: int = String_Len(enumName) as int;
|
||||||
|
if nameLen > prefLen {
|
||||||
|
let p: String = bux_str_slice(enumName, 0, prefLen as uint);
|
||||||
|
if String_Eq(p, prefix) {
|
||||||
|
return Lcx_EnumHasData(ctx, base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gi = gi + 1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
func Lcx_MakeLitHir(litKind: int, litText: String, line: uint32, col: uint32) -> *HirNode {
|
func Lcx_MakeLitHir(litKind: int, litText: String, line: uint32, col: uint32) -> *HirNode {
|
||||||
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
@@ -1581,6 +1788,15 @@ module HirLower {
|
|||||||
|
|
||||||
// Unary
|
// Unary
|
||||||
if kind == ekUnary {
|
if kind == ekUnary {
|
||||||
|
// `&NamedFunc` — the ident already lowers to the fat-pointer value
|
||||||
|
// (BuxFn_* struct init via __adapt_); taking its address would yield
|
||||||
|
// a pointer-to-struct where the struct value is expected.
|
||||||
|
if expr.intValue == tkAmp && expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
|
||||||
|
let ampSym: Symbol = Scope_Lookup(ctx.scope, expr.child1.strValue);
|
||||||
|
if ampSym.kind == skFunc {
|
||||||
|
return Lcx_LowerExpr(ctx, expr.child1);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Overflow checking: in @[Checked] mode, lower negation on signed integers to checked call
|
// Overflow checking: in @[Checked] mode, lower negation on signed integers to checked call
|
||||||
if ctx.checkedFunc && !ctx.releaseFunc && expr.intValue == tkMinus {
|
if ctx.checkedFunc && !ctx.releaseFunc && expr.intValue == tkMinus {
|
||||||
var isSignedInt: bool = false;
|
var isSignedInt: bool = false;
|
||||||
@@ -2339,6 +2555,83 @@ module HirLower {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is (type test): expr is Variant — desugar to tag / value equality
|
||||||
|
// Simple enums: subject == Enum_Variant
|
||||||
|
// Algebraic enums: subject.tag == Enum_Variant
|
||||||
|
if kind == ekIs {
|
||||||
|
let operand: *HirNode = Lcx_LowerExpr(ctx, expr.child1);
|
||||||
|
var variantName: String = "";
|
||||||
|
if expr.refType != null as *TypeExpr && !String_Eq(expr.refType.typeName, "") {
|
||||||
|
variantName = expr.refType.typeName;
|
||||||
|
}
|
||||||
|
var enumName: String = "";
|
||||||
|
// Resolve operand type from scope (variable / param), not enum decl name
|
||||||
|
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
|
||||||
|
let sym: Symbol = Scope_Lookup(ctx.scope, expr.child1.strValue);
|
||||||
|
if !String_Eq(sym.typeName, "") {
|
||||||
|
enumName = sym.typeName;
|
||||||
|
} else if sym.refType != null as *TypeExpr {
|
||||||
|
let te: *TypeExpr = Lcx_SubstituteType(ctx, sym.refType);
|
||||||
|
if te != null as *TypeExpr && !String_Eq(te.typeName, "") {
|
||||||
|
enumName = te.typeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: type annotation on the is-expression operand
|
||||||
|
if String_Eq(enumName, "") && expr.child1 != null as *Expr &&
|
||||||
|
expr.child1.refType != null as *TypeExpr {
|
||||||
|
let te: *TypeExpr = Lcx_SubstituteType(ctx, expr.child1.refType);
|
||||||
|
if te != null as *TypeExpr && !String_Eq(te.typeName, "") {
|
||||||
|
enumName = te.typeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !String_Eq(enumName, "") && !String_Eq(variantName, "") {
|
||||||
|
let hasData: bool = Lcx_EnumHasData(ctx, enumName);
|
||||||
|
let tagName: String = String_Concat(String_Concat(enumName, "_"), variantName);
|
||||||
|
if hasData {
|
||||||
|
// subject.tag == Enum_Variant
|
||||||
|
let tagPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
|
tagPtr.kind = hFieldPtr;
|
||||||
|
tagPtr.line = expr.line;
|
||||||
|
tagPtr.column = expr.column;
|
||||||
|
tagPtr.strValue = "tag";
|
||||||
|
tagPtr.child1 = operand;
|
||||||
|
let tagLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
|
tagLoad.kind = hLoad;
|
||||||
|
tagLoad.line = expr.line;
|
||||||
|
tagLoad.column = expr.column;
|
||||||
|
tagLoad.child1 = tagPtr;
|
||||||
|
let tagConst: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
|
tagConst.kind = hVar;
|
||||||
|
tagConst.line = expr.line;
|
||||||
|
tagConst.column = expr.column;
|
||||||
|
tagConst.strValue = tagName;
|
||||||
|
let result: *HirNode = Lcx_MakeBinHir(tkEq, tagLoad, tagConst, expr.line, expr.column);
|
||||||
|
result.sourceFile = ctx.currentSourceFile;
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
// Simple enum: subject == Enum_Variant
|
||||||
|
let tagConst: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
|
tagConst.kind = hVar;
|
||||||
|
tagConst.line = expr.line;
|
||||||
|
tagConst.column = expr.column;
|
||||||
|
tagConst.strValue = tagName;
|
||||||
|
let result: *HirNode = Lcx_MakeBinHir(tkEq, operand, tagConst, expr.line, expr.column);
|
||||||
|
result.sourceFile = ctx.currentSourceFile;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Non-enum / unresolved → false
|
||||||
|
let result: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
|
result.kind = hLit;
|
||||||
|
result.line = expr.line;
|
||||||
|
result.column = expr.column;
|
||||||
|
result.typeKind = tyBool;
|
||||||
|
result.typeName = "bool";
|
||||||
|
result.boolValue = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Struct init: TypeName { field: value, ... }
|
// Struct init: TypeName { field: value, ... }
|
||||||
if kind == ekStructInit {
|
if kind == ekStructInit {
|
||||||
// Simple enum init: EnumName { tag: EnumName_Variant } -> EnumName_Variant
|
// Simple enum init: EnumName { tag: EnumName_Variant } -> EnumName_Variant
|
||||||
@@ -2442,19 +2735,44 @@ module HirLower {
|
|||||||
if stmt.child1 != null as *Expr && stmt.child1.kind == ekTry {
|
if stmt.child1 != null as *Expr && stmt.child1.kind == ekTry {
|
||||||
let tryExpr: *Expr = stmt.child1;
|
let tryExpr: *Expr = stmt.child1;
|
||||||
let operandExpr: *Expr = tryExpr.child1;
|
let operandExpr: *Expr = tryExpr.child1;
|
||||||
let operandTypeExpr: *TypeExpr = operandExpr.refType;
|
var operandTypeExpr: *TypeExpr = null as *TypeExpr;
|
||||||
|
if operandExpr != null as *Expr { operandTypeExpr = operandExpr.refType; }
|
||||||
var typeName: String = "Result";
|
var typeName: String = "Result";
|
||||||
var errTag: String = "Result_Err";
|
var errTag: String = "Result_Err";
|
||||||
var okField: String = "Ok_0";
|
var okField: String = "Ok_0";
|
||||||
if operandTypeExpr != null as *TypeExpr && operandTypeExpr.kind == tekNamed {
|
if operandTypeExpr != null as *TypeExpr && operandTypeExpr.kind == tekNamed {
|
||||||
|
// Substitute/mangle generic enum type args (Result<int,String> → Result_int_String)
|
||||||
|
let subTe: *TypeExpr = Lcx_SubstituteType(ctx, operandTypeExpr);
|
||||||
|
if subTe != null as *TypeExpr && !String_Eq(subTe.typeName, "") {
|
||||||
|
typeName = subTe.typeName;
|
||||||
|
} else {
|
||||||
typeName = operandTypeExpr.typeName;
|
typeName = operandTypeExpr.typeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Detect Option vs Result (bare or monomorphized)
|
||||||
|
var isOption: bool = String_Eq(typeName, "Option");
|
||||||
|
if !isOption {
|
||||||
|
let optPrefix: String = "Option_";
|
||||||
|
let tnLen: int = String_Len(typeName) as int;
|
||||||
|
let prefLen: int = String_Len(optPrefix) as int;
|
||||||
|
if tnLen > prefLen {
|
||||||
|
let p: String = bux_str_slice(typeName, 0, prefLen as uint);
|
||||||
|
if String_Eq(p, optPrefix) { isOption = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isOption {
|
||||||
if String_Eq(typeName, "Option") {
|
if String_Eq(typeName, "Option") {
|
||||||
errTag = "Option_None";
|
errTag = "Option_None";
|
||||||
okField = "Some_0";
|
} else {
|
||||||
} else if !String_Eq(typeName, "Result") {
|
errTag = String_Concat(typeName, "_None");
|
||||||
errTag = String_Concat(String_Concat(typeName, "_"), "Err");
|
|
||||||
okField = "Ok_0";
|
|
||||||
}
|
}
|
||||||
|
okField = "Some_0";
|
||||||
|
} else if String_Eq(typeName, "Result") {
|
||||||
|
errTag = "Result_Err";
|
||||||
|
okField = "Ok_0";
|
||||||
|
} else {
|
||||||
|
errTag = String_Concat(typeName, "_Err");
|
||||||
|
okField = "Ok_0";
|
||||||
}
|
}
|
||||||
let tmpName: String = String_Concat("__try_tmp_", String_FromInt(ctx.tryCounter as int64));
|
let tmpName: String = String_Concat("__try_tmp_", String_FromInt(ctx.tryCounter as int64));
|
||||||
ctx.tryCounter = ctx.tryCounter + 1;
|
ctx.tryCounter = ctx.tryCounter + 1;
|
||||||
@@ -3249,46 +3567,29 @@ module HirLower {
|
|||||||
current = Lcx_LowerBlock(ctx, stmt.refStmtElse, -1);
|
current = Lcx_LowerBlock(ctx, stmt.refStmtElse, -1);
|
||||||
}
|
}
|
||||||
// Cases in reverse order (from caseBlock)
|
// Cases in reverse order (from caseBlock)
|
||||||
|
// Walk linked list: count total, then for each i from total-1 down to 0,
|
||||||
|
// walk from start to find the i-th case. O(n²) but handles any count.
|
||||||
if stmt.refStmtBlock != null as *Block {
|
if stmt.refStmtBlock != null as *Block {
|
||||||
let caseBlock: *Block = stmt.refStmtBlock;
|
let caseBlock: *Block = stmt.refStmtBlock;
|
||||||
var caseCount: int = caseBlock.stmtCount;
|
var total: int = 0;
|
||||||
// Collect cases into array for reverse iteration
|
|
||||||
var c0: *Stmt = null as *Stmt;
|
|
||||||
var c1: *Stmt = null as *Stmt;
|
|
||||||
var c2: *Stmt = null as *Stmt;
|
|
||||||
var c3: *Stmt = null as *Stmt;
|
|
||||||
var c4: *Stmt = null as *Stmt;
|
|
||||||
var c5: *Stmt = null as *Stmt;
|
|
||||||
var c6: *Stmt = null as *Stmt;
|
|
||||||
var c7: *Stmt = null as *Stmt;
|
|
||||||
var ci: int = 0;
|
|
||||||
var cs: *Stmt = caseBlock.firstStmt;
|
var cs: *Stmt = caseBlock.firstStmt;
|
||||||
while cs != null as *Stmt && ci < 8 {
|
while cs != null as *Stmt {
|
||||||
if ci == 0 { c0 = cs; }
|
total = total + 1;
|
||||||
if ci == 1 { c1 = cs; }
|
|
||||||
if ci == 2 { c2 = cs; }
|
|
||||||
if ci == 3 { c3 = cs; }
|
|
||||||
if ci == 4 { c4 = cs; }
|
|
||||||
if ci == 5 { c5 = cs; }
|
|
||||||
if ci == 6 { c6 = cs; }
|
|
||||||
if ci == 7 { c7 = cs; }
|
|
||||||
ci = ci + 1;
|
|
||||||
cs = cs.nextStmt;
|
cs = cs.nextStmt;
|
||||||
}
|
}
|
||||||
while caseCount > 0 {
|
var ri: int = total;
|
||||||
caseCount = caseCount - 1;
|
while ri > 0 {
|
||||||
var c: *Stmt = null as *Stmt;
|
ri = ri - 1;
|
||||||
if caseCount == 0 { c = c0; }
|
// Find ri-th case by walking from start
|
||||||
if caseCount == 1 { c = c1; }
|
var i: int = 0;
|
||||||
if caseCount == 2 { c = c2; }
|
cs = caseBlock.firstStmt;
|
||||||
if caseCount == 3 { c = c3; }
|
while cs != null as *Stmt && i < ri {
|
||||||
if caseCount == 4 { c = c4; }
|
cs = cs.nextStmt;
|
||||||
if caseCount == 5 { c = c5; }
|
i = i + 1;
|
||||||
if caseCount == 6 { c = c6; }
|
}
|
||||||
if caseCount == 7 { c = c7; }
|
if cs != null as *Stmt {
|
||||||
if c != null as *Stmt {
|
let caseVal: *HirNode = Lcx_LowerExpr(ctx, cs.child1);
|
||||||
let caseVal: *HirNode = Lcx_LowerExpr(ctx, c.child1);
|
let caseBody: *HirNode = Lcx_LowerBlock(ctx, cs.refStmtBlock, -1);
|
||||||
let caseBody: *HirNode = Lcx_LowerBlock(ctx, c.refStmtBlock, -1);
|
|
||||||
let cond: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
let cond: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
cond.kind = hBinary;
|
cond.kind = hBinary;
|
||||||
cond.intValue = 74; // tkEq
|
cond.intValue = 74; // tkEq
|
||||||
@@ -3899,6 +4200,11 @@ module HirLower {
|
|||||||
return Lcx_EvalConstExprEnv(ctx, expr.child1, env);
|
return Lcx_EvalConstExprEnv(ctx, expr.child1, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is — not evaluable at compile time
|
||||||
|
if expr.kind == ekIs {
|
||||||
|
return CtVal_Make(0);
|
||||||
|
}
|
||||||
|
|
||||||
// Const function call
|
// Const function call
|
||||||
if expr.kind == ekCall {
|
if expr.kind == ekCall {
|
||||||
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
|
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
|
||||||
@@ -4106,6 +4412,11 @@ module HirLower {
|
|||||||
ctx.genStructs[ctx.genStructCount] = *decl;
|
ctx.genStructs[ctx.genStructCount] = *decl;
|
||||||
ctx.genStructCount = ctx.genStructCount + 1;
|
ctx.genStructCount = ctx.genStructCount + 1;
|
||||||
}
|
}
|
||||||
|
if decl.kind == dkEnum && decl.typeParamCount > 0 {
|
||||||
|
// Store in genStructs array for mono lookup (reuse existing infrastructure)
|
||||||
|
ctx.genStructs[ctx.genStructCount] = *decl;
|
||||||
|
ctx.genStructCount = ctx.genStructCount + 1;
|
||||||
|
}
|
||||||
// Generic impl/extend blocks: methods inherit the impl's type params
|
// Generic impl/extend blocks: methods inherit the impl's type params
|
||||||
if decl.kind == dkImpl && decl.typeParamCount > 0 {
|
if decl.kind == dkImpl && decl.typeParamCount > 0 {
|
||||||
let implTypeName: String = decl.strValue;
|
let implTypeName: String = decl.strValue;
|
||||||
@@ -4210,6 +4521,11 @@ module HirLower {
|
|||||||
hm.constCount = hm.constCount + 1;
|
hm.constCount = hm.constCount + 1;
|
||||||
}
|
}
|
||||||
if decl.kind == dkEnum {
|
if decl.kind == dkEnum {
|
||||||
|
// Skip generic enums — instantiated on demand via Lcx_GenerateEnumInstance
|
||||||
|
if decl.typeParamCount > 0 {
|
||||||
|
decl = decl.childDecl2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let ei: int = hm.enumCount;
|
let ei: int = hm.enumCount;
|
||||||
hm.enums[ei].name = decl.strValue;
|
hm.enums[ei].name = decl.strValue;
|
||||||
// Populate variants
|
// Populate variants
|
||||||
|
|||||||
+68
-2
@@ -470,16 +470,30 @@ module MacroExpand {
|
|||||||
(aexp.kind == ekSlice && aexp.callArgCount > 0);
|
(aexp.kind == ekSlice && aexp.callArgCount > 0);
|
||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
// type — session 87: named / pointer type from call-site expr shape
|
// type — session 87+: named / pointer / generic Array<int> from call-site
|
||||||
if String_Eq(kindStr, "type") {
|
if String_Eq(kindStr, "type") {
|
||||||
if aexp.kind == ekMacroType { return aexp; }
|
if aexp.kind == ekMacroType { return aexp; }
|
||||||
var te: *TypeExpr = null as *TypeExpr;
|
var te: *TypeExpr = null as *TypeExpr;
|
||||||
if aexp.kind == ekIdent {
|
if aexp.kind == ekIdent || aexp.kind == ekGenericCall {
|
||||||
te = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
te = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
||||||
te.kind = tekNamed;
|
te.kind = tekNamed;
|
||||||
te.line = aexp.line;
|
te.line = aexp.line;
|
||||||
te.column = aexp.column;
|
te.column = aexp.column;
|
||||||
|
if aexp.kind == ekGenericCall && !String_Eq(aexp.genericCallee, "") {
|
||||||
|
te.typeName = aexp.genericCallee;
|
||||||
|
} else {
|
||||||
te.typeName = aexp.strValue;
|
te.typeName = aexp.strValue;
|
||||||
|
}
|
||||||
|
// Generic type args: Array<int> / Map<K,V> (selfhost: up to 2 string args)
|
||||||
|
var gcount: int = aexp.genericTypeArgCount;
|
||||||
|
if gcount > 0 {
|
||||||
|
te.typeArgName0 = aexp.genericTypeArg0;
|
||||||
|
te.typeArgCount = 1;
|
||||||
|
if gcount > 1 {
|
||||||
|
te.typeArgName1 = aexp.genericTypeArg1;
|
||||||
|
te.typeArgCount = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if aexp.kind == ekUnary && aexp.intValue == tkStar && aexp.child1 != null as *Expr {
|
} else if aexp.kind == ekUnary && aexp.intValue == tkStar && aexp.child1 != null as *Expr {
|
||||||
// *T from unary star
|
// *T from unary star
|
||||||
let inner: *Expr = Macro_CoerceArg("type", aexp.child1);
|
let inner: *Expr = Macro_CoerceArg("type", aexp.child1);
|
||||||
@@ -518,6 +532,35 @@ module MacroExpand {
|
|||||||
return bound.refType;
|
return bound.refType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Array<$t> / Map<$k,$v> — substitute type-arg name slots
|
||||||
|
if te.kind == tekNamed && te.typeArgCount > 0 {
|
||||||
|
if String_StartsWith(te.typeArgName0, "$") {
|
||||||
|
let b0: *Expr = Env_Lookup(env, te.typeArgName0);
|
||||||
|
if b0 != null as *Expr && b0.kind == ekMacroType && b0.refType != null as *TypeExpr {
|
||||||
|
// Flatten simple named type arg (int, String, …)
|
||||||
|
if b0.refType.kind == tekNamed && b0.refType.typeArgCount == 0 {
|
||||||
|
te.typeArgName0 = b0.refType.typeName;
|
||||||
|
} else if b0.refType.kind == tekNamed {
|
||||||
|
// Nested generic: store mangled-ish "Array_int" for mono
|
||||||
|
te.typeArgName0 = b0.refType.typeName;
|
||||||
|
if b0.refType.typeArgCount > 0 {
|
||||||
|
te.typeArgName0 = String_Concat(te.typeArgName0, "_");
|
||||||
|
te.typeArgName0 = String_Concat(te.typeArgName0, b0.refType.typeArgName0);
|
||||||
|
}
|
||||||
|
} else if b0.refType.kind == tekPointer && b0.refType.pointerPointee != null as *TypeExpr {
|
||||||
|
te.typeArgName0 = String_Concat(b0.refType.pointerPointee.typeName, "p");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if te.typeArgCount > 1 && String_StartsWith(te.typeArgName1, "$") {
|
||||||
|
let b1: *Expr = Env_Lookup(env, te.typeArgName1);
|
||||||
|
if b1 != null as *Expr && b1.kind == ekMacroType && b1.refType != null as *TypeExpr {
|
||||||
|
if b1.refType.kind == tekNamed {
|
||||||
|
te.typeArgName1 = b1.refType.typeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if te.pointerPointee != null as *TypeExpr {
|
if te.pointerPointee != null as *TypeExpr {
|
||||||
te.pointerPointee = Macro_SubstType(te.pointerPointee, env);
|
te.pointerPointee = Macro_SubstType(te.pointerPointee, env);
|
||||||
if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
|
if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
|
||||||
@@ -533,6 +576,27 @@ module MacroExpand {
|
|||||||
return te;
|
return te;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Substitute `$t` in Expr generic type-arg slots (Array_New<$t>)
|
||||||
|
func Macro_SubstGenericTypeArgs(e: *Expr, env: *MacroEnv) {
|
||||||
|
if e == null as *Expr { return; }
|
||||||
|
if e.genericTypeArgCount > 0 && String_StartsWith(e.genericTypeArg0, "$") {
|
||||||
|
let b0: *Expr = Env_Lookup(env, e.genericTypeArg0);
|
||||||
|
if b0 != null as *Expr && b0.kind == ekMacroType && b0.refType != null as *TypeExpr {
|
||||||
|
if b0.refType.kind == tekNamed {
|
||||||
|
e.genericTypeArg0 = b0.refType.typeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.genericTypeArgCount > 1 && String_StartsWith(e.genericTypeArg1, "$") {
|
||||||
|
let b1: *Expr = Env_Lookup(env, e.genericTypeArg1);
|
||||||
|
if b1 != null as *Expr && b1.kind == ekMacroType && b1.refType != null as *TypeExpr {
|
||||||
|
if b1.refType.kind == tekNamed {
|
||||||
|
e.genericTypeArg1 = b1.refType.typeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fragment kind check: "ident" | "literal" | "block" | "stmt" | "pat" | "expr" | "tt"
|
// Fragment kind check: "ident" | "literal" | "block" | "stmt" | "pat" | "expr" | "tt"
|
||||||
func Macro_FragMatches(kindStr: String, aexp: *Expr) -> bool {
|
func Macro_FragMatches(kindStr: String, aexp: *Expr) -> bool {
|
||||||
return Macro_CoerceArg(kindStr, aexp) != null as *Expr;
|
return Macro_CoerceArg(kindStr, aexp) != null as *Expr;
|
||||||
@@ -961,6 +1025,8 @@ module MacroExpand {
|
|||||||
c.child1 = Subst_Expr(c.child1, env, file, line, col);
|
c.child1 = Subst_Expr(c.child1, env, file, line, col);
|
||||||
c.child2 = Subst_Expr(c.child2, env, file, line, col);
|
c.child2 = Subst_Expr(c.child2, env, file, line, col);
|
||||||
c.child3 = Subst_Expr(c.child3, env, file, line, col);
|
c.child3 = Subst_Expr(c.child3, env, file, line, col);
|
||||||
|
// Array_New<$t> / Foo<$t,$u>
|
||||||
|
Macro_SubstGenericTypeArgs(c, env);
|
||||||
// sizeof / cast / is type annotations
|
// sizeof / cast / is type annotations
|
||||||
if c.refType != null as *TypeExpr {
|
if c.refType != null as *TypeExpr {
|
||||||
c.refType = Macro_SubstType(c.refType, env);
|
c.refType = Macro_SubstType(c.refType, env);
|
||||||
|
|||||||
+48
-48
@@ -30,13 +30,6 @@ module Parser {
|
|||||||
macroTemplateMode: bool, // allows $(…)* in macro! bodies
|
macroTemplateMode: bool, // allows $(…)* in macro! bodies
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ParserDiag {
|
|
||||||
line: uint32,
|
|
||||||
column: uint32,
|
|
||||||
message: String,
|
|
||||||
severity: int, /* 0=error (fatal), 1=warning (recoverable) */
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Token helpers
|
// Token helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -120,7 +113,7 @@ module Parser {
|
|||||||
return parserAdvance(p);
|
return parserAdvance(p);
|
||||||
}
|
}
|
||||||
let tok: LexToken = parserCurToken(p);
|
let tok: LexToken = parserCurToken(p);
|
||||||
if p.diagCount < 256 {
|
if p.diagCount < 256 && p.diags != null as *ParserDiag {
|
||||||
p.diags[p.diagCount] = ParserDiag {
|
p.diags[p.diagCount] = ParserDiag {
|
||||||
line: tok.line, column: tok.column, message: msg, severity: 1
|
line: tok.line, column: tok.column, message: msg, severity: 1
|
||||||
};
|
};
|
||||||
@@ -130,7 +123,7 @@ module Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parserEmitDiag(p: *Parser, line: uint32, col: uint32, msg: String) {
|
func parserEmitDiag(p: *Parser, line: uint32, col: uint32, msg: String) {
|
||||||
if p.diagCount < 256 {
|
if p.diagCount < 256 && p.diags != null as *ParserDiag {
|
||||||
p.diags[p.diagCount] = ParserDiag {
|
p.diags[p.diagCount] = ParserDiag {
|
||||||
line: line, column: col, message: msg, severity: 1
|
line: line, column: col, message: msg, severity: 1
|
||||||
};
|
};
|
||||||
@@ -149,7 +142,7 @@ module Parser {
|
|||||||
if tok.kind == tkIdent || parserIsKeyword(tok.kind) {
|
if tok.kind == tkIdent || parserIsKeyword(tok.kind) {
|
||||||
return parserAdvance(p);
|
return parserAdvance(p);
|
||||||
}
|
}
|
||||||
if p.diagCount < 256 {
|
if p.diagCount < 256 && p.diags != null as *ParserDiag {
|
||||||
p.diags[p.diagCount] = ParserDiag {
|
p.diags[p.diagCount] = ParserDiag {
|
||||||
line: tok.line, column: tok.column, message: msg, severity: 1
|
line: tok.line, column: tok.column, message: msg, severity: 1
|
||||||
};
|
};
|
||||||
@@ -1099,7 +1092,10 @@ module Parser {
|
|||||||
if parserCheck(p, tkPipe) || parserPeek(p, 0) == tkEndOfFile {
|
if parserCheck(p, tkPipe) || parserPeek(p, 0) == tkEndOfFile {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if params.paramCount >= 9 { break; }
|
if params.paramCount >= 9 {
|
||||||
|
parserEmitDiag(p, line, col, "too many closure parameters (max 8)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
let nameTok: LexToken = parserExpectIdentOrKeyword(p, "expected parameter name in closure");
|
let nameTok: LexToken = parserExpectIdentOrKeyword(p, "expected parameter name in closure");
|
||||||
discard parserExpect(p, tkColon, "expected ':' in closure parameter");
|
discard parserExpect(p, tkColon, "expected ':' in closure parameter");
|
||||||
let ptype: *TypeExpr = parserParseType(p);
|
let ptype: *TypeExpr = parserParseType(p);
|
||||||
@@ -1989,7 +1985,11 @@ module Parser {
|
|||||||
if parserCheck(p, tkRParen) || parserPeek(p, 0) == tkEndOfFile {
|
if parserCheck(p, tkRParen) || parserPeek(p, 0) == tkEndOfFile {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if d.paramCount >= 9 { break; }
|
if d.paramCount >= 9 {
|
||||||
|
let tok: LexToken = parserCurToken(p);
|
||||||
|
parserEmitDiag(p, tok.line, tok.column, "too many function parameters (max 8)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
let nameTok: LexToken = parserExpectIdentOrKeyword(p, "expected parameter name");
|
let nameTok: LexToken = parserExpectIdentOrKeyword(p, "expected parameter name");
|
||||||
discard parserExpect(p, tkColon, "expected ':' in parameter");
|
discard parserExpect(p, tkColon, "expected ':' in parameter");
|
||||||
let ptype: *TypeExpr = parserParseType(p);
|
let ptype: *TypeExpr = parserParseType(p);
|
||||||
@@ -2168,17 +2168,25 @@ module Parser {
|
|||||||
parserParseTypeParams(p, d);
|
parserParseTypeParams(p, d);
|
||||||
|
|
||||||
discard parserExpect(p, tkLBrace, "expected '{'");
|
discard parserExpect(p, tkLBrace, "expected '{'");
|
||||||
|
var lastWasComma: bool = false;
|
||||||
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
|
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
|
||||||
if fieldCount >= 256 { break; }
|
if fieldCount >= 256 {
|
||||||
|
parserEmitDiag(p, line, col, "too many struct fields (max 255)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
if parserCheck(p, tkNewLine) { discard parserAdvance(p); continue; }
|
if parserCheck(p, tkNewLine) { discard parserAdvance(p); continue; }
|
||||||
if parserCheck(p, tkSemicolon) { discard parserAdvance(p); continue; }
|
if parserCheck(p, tkSemicolon) { discard parserAdvance(p); continue; }
|
||||||
|
// Commas are also valid field separators (incl. trailing comma before '}')
|
||||||
|
if parserCheck(p, tkComma) {
|
||||||
|
if fieldCount == 0 || lastWasComma {
|
||||||
|
parserEmitDiag(p, parserCurToken(p).line, parserCurToken(p).column, "expected field name");
|
||||||
|
}
|
||||||
|
lastWasComma = true;
|
||||||
|
discard parserAdvance(p);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let beforePos: int = p.pos;
|
let beforePos: int = p.pos;
|
||||||
let fName: LexToken = parserExpectIdentOrKeyword(p, "expected field name");
|
let fName: LexToken = parserExpectIdentOrKeyword(p, "expected field name");
|
||||||
if fieldCount >= 250 && fieldCount <= 256 {
|
|
||||||
PrintLine(String_Concat("RAW fieldCount=", bux_int_to_str(fieldCount as int64)));
|
|
||||||
PrintLine(String_Concat("RAW fName=", fName.text));
|
|
||||||
PrintLine(String_Concat("RAW pos=", bux_int_to_str(p.pos as int64)));
|
|
||||||
}
|
|
||||||
discard parserExpect(p, tkColon, "expected ':' in struct field");
|
discard parserExpect(p, tkColon, "expected ':' in struct field");
|
||||||
let fType: *TypeExpr = parserParseType(p);
|
let fType: *TypeExpr = parserParseType(p);
|
||||||
parserMatch(p, tkSemicolon);
|
parserMatch(p, tkSemicolon);
|
||||||
@@ -2191,6 +2199,7 @@ module Parser {
|
|||||||
d.fields[fieldCount].name = fName.text;
|
d.fields[fieldCount].name = fName.text;
|
||||||
d.fields[fieldCount].refFieldType = fType;
|
d.fields[fieldCount].refFieldType = fType;
|
||||||
fieldCount = fieldCount + 1;
|
fieldCount = fieldCount + 1;
|
||||||
|
lastWasComma = false;
|
||||||
}
|
}
|
||||||
d.fieldCount = fieldCount;
|
d.fieldCount = fieldCount;
|
||||||
discard parserExpect(p, tkRBrace, "expected '}'");
|
discard parserExpect(p, tkRBrace, "expected '}'");
|
||||||
@@ -2210,9 +2219,15 @@ module Parser {
|
|||||||
d.isPublic = isPublic;
|
d.isPublic = isPublic;
|
||||||
d.strValue = nameTok.text;
|
d.strValue = nameTok.text;
|
||||||
|
|
||||||
|
// Type params <T: Bound, U: Bound2>
|
||||||
|
parserParseTypeParams(p, d);
|
||||||
|
|
||||||
discard parserExpect(p, tkLBrace, "expected '{'");
|
discard parserExpect(p, tkLBrace, "expected '{'");
|
||||||
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
|
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
|
||||||
if d.variantCount >= 9 { break; }
|
if d.variantCount >= 9 {
|
||||||
|
parserEmitDiag(p, line, col, "too many enum variants (max 8)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
if parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) { discard parserAdvance(p); continue; }
|
if parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) { discard parserAdvance(p); continue; }
|
||||||
let vName: LexToken = parserExpect(p, tkIdent, "expected variant name");
|
let vName: LexToken = parserExpect(p, tkIdent, "expected variant name");
|
||||||
|
|
||||||
@@ -2265,9 +2280,10 @@ module Parser {
|
|||||||
d.isPublic = isPublic;
|
d.isPublic = isPublic;
|
||||||
|
|
||||||
// Parse path: Std::Io::PrintLine
|
// Parse path: Std::Io::PrintLine
|
||||||
|
// Stop before `::{` (multi-import) and `::*` (glob import)
|
||||||
var pathStr: String = "";
|
var pathStr: String = "";
|
||||||
var segCount: int = 0;
|
var segCount: int = 0;
|
||||||
while parserCheck(p, tkIdent) || (segCount > 0 && parserCheck(p, tkColonColon) && parserPeek(p, 1) != tkLBrace) {
|
while parserCheck(p, tkIdent) || (segCount > 0 && parserCheck(p, tkColonColon) && parserPeek(p, 1) != tkLBrace && parserPeek(p, 1) != tkStar) {
|
||||||
if segCount > 0 {
|
if segCount > 0 {
|
||||||
discard parserAdvance(p); // ::
|
discard parserAdvance(p); // ::
|
||||||
if String_Len(pathStr) > 0 {
|
if String_Len(pathStr) > 0 {
|
||||||
@@ -2299,6 +2315,10 @@ module Parser {
|
|||||||
names = String_Concat(names, ",");
|
names = String_Concat(names, ",");
|
||||||
if !parserMatch(p, tkComma) { break; }
|
if !parserMatch(p, tkComma) { break; }
|
||||||
}
|
}
|
||||||
|
// Allow newlines before the closing brace (multi-line import lists)
|
||||||
|
while parserCheck(p, tkNewLine) {
|
||||||
|
discard parserAdvance(p);
|
||||||
|
}
|
||||||
discard parserExpect(p, tkRBrace, "expected '}'");
|
discard parserExpect(p, tkRBrace, "expected '}'");
|
||||||
d.useNames = names;
|
d.useNames = names;
|
||||||
d.useKind = 2; // ukMulti
|
d.useKind = 2; // ukMulti
|
||||||
@@ -2357,6 +2377,8 @@ module Parser {
|
|||||||
var lastMethod: *Decl = null as *Decl;
|
var lastMethod: *Decl = null as *Decl;
|
||||||
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
|
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
|
||||||
if parserCheck(p, tkNewLine) { discard parserAdvance(p); continue; }
|
if parserCheck(p, tkNewLine) { discard parserAdvance(p); continue; }
|
||||||
|
// Bodyless method signatures end with ';'
|
||||||
|
if parserCheck(p, tkSemicolon) { discard parserAdvance(p); continue; }
|
||||||
if parserCheck(p, tkFunc) {
|
if parserCheck(p, tkFunc) {
|
||||||
let m: *Decl = parserParseFuncDecl(p, false, false, false);
|
let m: *Decl = parserParseFuncDecl(p, false, false, false);
|
||||||
if methods == null as *Decl {
|
if methods == null as *Decl {
|
||||||
@@ -2676,6 +2698,9 @@ module Parser {
|
|||||||
d.strValue2 = ifaceName.text;
|
d.strValue2 = ifaceName.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type params <T: Bound, U: Bound2>
|
||||||
|
parserParseTypeParams(p, d);
|
||||||
|
|
||||||
discard parserExpect(p, tkLBrace, "expected '{'");
|
discard parserExpect(p, tkLBrace, "expected '{'");
|
||||||
var methods: *Decl = null as *Decl;
|
var methods: *Decl = null as *Decl;
|
||||||
var lastMethod: *Decl = null as *Decl;
|
var lastMethod: *Decl = null as *Decl;
|
||||||
@@ -2808,34 +2833,9 @@ module Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Print fatal parser diagnostics (severity == 0) or if nothing valid was parsed */
|
// Attach parser diagnostics to the module; the CLI reports them
|
||||||
if p.diagCount > 0 && mod.itemCount == 0 {
|
mod.diagCount = p.diagCount;
|
||||||
var di: int = 0;
|
mod.diags = p.diags;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mod;
|
return mod;
|
||||||
}
|
}
|
||||||
|
|||||||
+163
-77
@@ -93,6 +93,15 @@ module Sema {
|
|||||||
sema.hasError = true;
|
sema.hasError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Sema_EmitExprError(sema: *Sema, expr: *Expr, msg: String) -> int {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, msg);
|
||||||
|
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
||||||
|
te.kind = tekNamed;
|
||||||
|
te.typeName = "?";
|
||||||
|
expr.refType = te;
|
||||||
|
return tyUnknown;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Symbol zero-init helper (bootstrap C backend does not zero-init structs)
|
// Symbol zero-init helper (bootstrap C backend does not zero-init structs)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -123,17 +132,8 @@ module Sema {
|
|||||||
var tail: *TypeExprList = null as *TypeExprList;
|
var tail: *TypeExprList = null as *TypeExprList;
|
||||||
var i: int = 0;
|
var i: int = 0;
|
||||||
while i < decl.paramCount && i < 9 {
|
while i < decl.paramCount && i < 9 {
|
||||||
var p: Param;
|
let p: *Param = Sema_DeclParam(decl, i);
|
||||||
if i == 0 { p = decl.param0; }
|
if p != null as *Param && p.refParamType != null as *TypeExpr {
|
||||||
else if i == 1 { p = decl.param1; }
|
|
||||||
else if i == 2 { p = decl.param2; }
|
|
||||||
else if i == 3 { p = decl.param3; }
|
|
||||||
else if i == 4 { p = decl.param4; }
|
|
||||||
else if i == 5 { p = decl.param5; }
|
|
||||||
else if i == 6 { p = decl.param6; }
|
|
||||||
else if i == 7 { p = decl.param7; }
|
|
||||||
else { p = decl.param8; }
|
|
||||||
if p.refParamType != null as *TypeExpr {
|
|
||||||
let node: *TypeExprList = bux_alloc(sizeof(TypeExprList)) as *TypeExprList;
|
let node: *TypeExprList = bux_alloc(sizeof(TypeExprList)) as *TypeExprList;
|
||||||
node.te = p.refParamType;
|
node.te = p.refParamType;
|
||||||
node.next = null as *TypeExprList;
|
node.next = null as *TypeExprList;
|
||||||
@@ -189,6 +189,58 @@ module Sema {
|
|||||||
return kind == tyBool || kind == tyBool8 || kind == tyBool16 || kind == tyBool32;
|
return kind == tyBool || kind == tyBool8 || kind == tyBool16 || kind == tyBool32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strict numeric predicate for assignment checking: unlike Sema_IsNumeric,
|
||||||
|
// this does NOT treat tyNamed/tyTypeParam as numeric.
|
||||||
|
func Sema_IsStrictNumeric(kind: int) -> bool {
|
||||||
|
if kind == tyInt8 || kind == tyInt16 || kind == tyInt32 || kind == tyInt64 || kind == tyInt { return true; }
|
||||||
|
if kind == tyUInt8 || kind == tyUInt16 || kind == tyUInt32 || kind == tyUInt64 || kind == tyUInt { return true; }
|
||||||
|
if kind == tyFloat32 || kind == tyFloat64 { return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display-name helper for assignment diagnostics.
|
||||||
|
func Sema_TypeNameForDiag(te: *TypeExpr, kind: int) -> String {
|
||||||
|
if te != null as *TypeExpr {
|
||||||
|
// tekFunc TypeExprs have no typeName set (parser leaves it unset) —
|
||||||
|
// never touch te.typeName for them.
|
||||||
|
if te.kind == tekFunc { return "func"; }
|
||||||
|
if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
|
||||||
|
return String_Concat(te.pointerPointee.typeName, "*");
|
||||||
|
}
|
||||||
|
if !String_Eq(te.typeName, "") { return te.typeName; }
|
||||||
|
}
|
||||||
|
if kind == tyBool { return "bool"; }
|
||||||
|
if kind == tyStr { return "String"; }
|
||||||
|
if kind == tyInt { return "int"; }
|
||||||
|
if kind == tyInt64 { return "int64"; }
|
||||||
|
if kind == tyUInt { return "uint"; }
|
||||||
|
if kind == tyFloat64 { return "float64"; }
|
||||||
|
if kind == tyPointer { return "*void"; }
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignability for diagnostics: strict numeric widening, bool family,
|
||||||
|
// exact pointer kind, named types by name (best-effort when refType available).
|
||||||
|
func Sema_CanAssignTo(sema: *Sema, fromKind: int, fromTe: *TypeExpr, toTe: *TypeExpr) -> bool {
|
||||||
|
if fromKind == tyUnknown { return true; }
|
||||||
|
if toTe == null as *TypeExpr { return true; }
|
||||||
|
let toKind: int = Sema_ResolveType(sema, toTe);
|
||||||
|
if toKind == tyUnknown { return true; }
|
||||||
|
if fromKind == tyNamed && toKind == tyNamed {
|
||||||
|
if fromTe != null as *TypeExpr && !String_Eq(fromTe.typeName, "") {
|
||||||
|
return String_Eq(fromTe.typeName, toTe.typeName);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Func-typed values: `&Func` reports tyPointer as its kind while its
|
||||||
|
// refType is tekFunc — accept when both sides are func-typed.
|
||||||
|
if toTe.kind == tekFunc && fromTe != null as *TypeExpr && fromTe.kind == tekFunc { return true; }
|
||||||
|
if fromKind == toKind { return true; }
|
||||||
|
if Sema_IsStrictNumeric(fromKind) && Sema_IsStrictNumeric(toKind) { return true; }
|
||||||
|
if Sema_IsBool(fromKind) && Sema_IsBool(toKind) { return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Block checking helper
|
// Block checking helper
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -237,16 +289,7 @@ module Sema {
|
|||||||
|
|
||||||
var i: int = 0;
|
var i: int = 0;
|
||||||
while i < decl.paramCount {
|
while i < decl.paramCount {
|
||||||
var p: *Param = null as *Param;
|
let p: *Param = Sema_DeclParam(decl, i);
|
||||||
if i == 0 { p = &decl.param0; }
|
|
||||||
else if i == 1 { p = &decl.param1; }
|
|
||||||
else if i == 2 { p = &decl.param2; }
|
|
||||||
else if i == 3 { p = &decl.param3; }
|
|
||||||
else if i == 4 { p = &decl.param4; }
|
|
||||||
else if i == 5 { p = &decl.param5; }
|
|
||||||
else if i == 6 { p = &decl.param6; }
|
|
||||||
else if i == 7 { p = &decl.param7; }
|
|
||||||
else if i == 8 { p = &decl.param8; }
|
|
||||||
|
|
||||||
var matched: *Expr = null as *Expr;
|
var matched: *Expr = null as *Expr;
|
||||||
|
|
||||||
@@ -302,6 +345,25 @@ module Sema {
|
|||||||
i = i + 1;
|
i = i + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report named args that match no parameter
|
||||||
|
var checkArg: *ExprList = expr.callArgs;
|
||||||
|
while checkArg != null as *ExprList {
|
||||||
|
if !String_Eq(checkArg.argName, "") {
|
||||||
|
var foundName: bool = false;
|
||||||
|
var j: int = 0;
|
||||||
|
while j < decl.paramCount {
|
||||||
|
let pj: *Param = Sema_DeclParam(decl, j);
|
||||||
|
if String_Eq(checkArg.argName, pj.name) { foundName = true; }
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
if !foundName {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column,
|
||||||
|
String_Concat("unknown argument name '", String_Concat(checkArg.argName, "'")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkArg = checkArg.next;
|
||||||
|
}
|
||||||
|
|
||||||
expr.callArgs = newFirst;
|
expr.callArgs = newFirst;
|
||||||
expr.callArgCount = newCount;
|
expr.callArgCount = newCount;
|
||||||
}
|
}
|
||||||
@@ -849,8 +911,7 @@ module Sema {
|
|||||||
if sym.kind == 0 && !String_Eq(sym.name, expr.strValue) {
|
if sym.kind == 0 && !String_Eq(sym.name, expr.strValue) {
|
||||||
let errMsg: String = String_Concat("undeclared identifier '", expr.strValue);
|
let errMsg: String = String_Concat("undeclared identifier '", expr.strValue);
|
||||||
let errMsg2: String = String_Concat(errMsg, "'");
|
let errMsg2: String = String_Concat(errMsg, "'");
|
||||||
Sema_EmitError(sema, expr.line, expr.column, errMsg2);
|
return Sema_EmitExprError(sema, expr, errMsg2);
|
||||||
return tyUnknown;
|
|
||||||
}
|
}
|
||||||
if sym.refType != null as *TypeExpr {
|
if sym.refType != null as *TypeExpr {
|
||||||
expr.refType = sym.refType;
|
expr.refType = sym.refType;
|
||||||
@@ -877,8 +938,7 @@ module Sema {
|
|||||||
if kind == ekSelf {
|
if kind == ekSelf {
|
||||||
let sym: Symbol = Scope_Lookup(sema.scope, "self");
|
let sym: Symbol = Scope_Lookup(sema.scope, "self");
|
||||||
if sym.kind == 0 && !String_Eq(sym.name, "self") {
|
if sym.kind == 0 && !String_Eq(sym.name, "self") {
|
||||||
Sema_EmitError(sema, expr.line, expr.column, "self outside method");
|
return Sema_EmitExprError(sema, expr, "self outside method");
|
||||||
return tyUnknown;
|
|
||||||
}
|
}
|
||||||
return sym.typeKind;
|
return sym.typeKind;
|
||||||
}
|
}
|
||||||
@@ -959,7 +1019,7 @@ module Sema {
|
|||||||
if op == tkAmpAmp || op == tkPipePipe || op == tkBang { return tyBool; }
|
if op == tkAmpAmp || op == tkPipePipe || op == tkBang { return tyBool; }
|
||||||
// Arithmetic returns wider type
|
// Arithmetic returns wider type
|
||||||
if !Sema_IsNumeric(left) || !Sema_IsNumeric(right) {
|
if !Sema_IsNumeric(left) || !Sema_IsNumeric(right) {
|
||||||
Sema_EmitError(sema, expr.line, expr.column, "arithmetic requires numeric operands");
|
return Sema_EmitExprError(sema, expr, "arithmetic requires numeric operands");
|
||||||
}
|
}
|
||||||
if left == tyFloat64 || right == tyFloat64 { return tyFloat64; }
|
if left == tyFloat64 || right == tyFloat64 { return tyFloat64; }
|
||||||
return tyInt;
|
return tyInt;
|
||||||
@@ -1000,10 +1060,62 @@ module Sema {
|
|||||||
// Call
|
// Call
|
||||||
if kind == ekCall {
|
if kind == ekCall {
|
||||||
let calleeType: int = Sema_CheckExpr(sema, expr.child1);
|
let calleeType: int = Sema_CheckExpr(sema, expr.child1);
|
||||||
|
// Snapshot diag count: if ResolveCallArgs reports an unknown named
|
||||||
|
// arg, the reordered list is short and the arity error below would
|
||||||
|
// be confusing — suppress it in that case.
|
||||||
|
let diagsBeforeCallArgs: int = sema.diagCount;
|
||||||
Sema_ResolveCallArgs(sema, expr);
|
Sema_ResolveCallArgs(sema, expr);
|
||||||
|
// Resolve callee decl for call-argument checking: direct calls to
|
||||||
|
// non-generic named functions only (inference handles generics).
|
||||||
|
var callDecl: *Decl = null as *Decl;
|
||||||
|
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
|
||||||
|
let callSym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue);
|
||||||
|
if callSym.kind == skFunc && callSym.decl != null as *Decl {
|
||||||
|
if callSym.decl.typeParamCount == 0 {
|
||||||
|
callDecl = callSym.decl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Arity check (runs after Sema_ResolveCallArgs so defaults/named args align)
|
||||||
|
if callDecl != null as *Decl && expr.callArgCount != callDecl.paramCount && sema.diagCount == diagsBeforeCallArgs {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column,
|
||||||
|
String_Concat("expected ", String_Concat(bux_int_to_str(callDecl.paramCount as int64),
|
||||||
|
String_Concat(" arguments, got ", bux_int_to_str(expr.callArgCount as int64)))));
|
||||||
|
callDecl = null as *Decl;
|
||||||
|
}
|
||||||
|
var argIdx: int = 0;
|
||||||
var arg: *ExprList = expr.callArgs;
|
var arg: *ExprList = expr.callArgs;
|
||||||
while arg != null as *ExprList {
|
while arg != null as *ExprList {
|
||||||
discard Sema_CheckExpr(sema, arg.expr);
|
let argKind: int = Sema_CheckExpr(sema, arg.expr);
|
||||||
|
// Per-argument type check against the parameter type
|
||||||
|
if callDecl != null as *Decl && argIdx < callDecl.paramCount {
|
||||||
|
let cp: *Param = Sema_DeclParam(callDecl, argIdx);
|
||||||
|
if cp != null as *Param && cp.refParamType != null as *TypeExpr {
|
||||||
|
// Skip conditions matching the bootstrap: unknown/named/typeparam
|
||||||
|
// arg kinds, and *char8 -> String (C string interop).
|
||||||
|
var argSkip: bool = false;
|
||||||
|
if argKind == tyUnknown || argKind == tyNamed || argKind == tyTypeParam { argSkip = true; }
|
||||||
|
// Function-typed params: selfhost cannot reliably type function
|
||||||
|
// values (e.g. '&Double' is tyPointer) — defer like the bootstrap.
|
||||||
|
if Sema_ResolveType(sema, cp.refParamType) == tyFunc { argSkip = true; }
|
||||||
|
if !argSkip && argKind == tyPointer && Sema_ResolveType(sema, cp.refParamType) == tyStr {
|
||||||
|
if arg.expr != null as *Expr && arg.expr.refType != null as *TypeExpr {
|
||||||
|
if arg.expr.refType.kind == tekPointer && arg.expr.refType.pointerPointee != null as *TypeExpr {
|
||||||
|
if String_Eq(arg.expr.refType.pointerPointee.typeName, "char8") { argSkip = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !argSkip && arg.expr != null as *Expr && !Sema_CanAssignTo(sema, argKind, arg.expr.refType, cp.refParamType) {
|
||||||
|
let wantName: String = Sema_TypeNameForDiag(cp.refParamType, Sema_ResolveType(sema, cp.refParamType));
|
||||||
|
let gotName: String = Sema_TypeNameForDiag(arg.expr.refType, argKind);
|
||||||
|
Sema_EmitError(sema, arg.expr.line, arg.expr.column,
|
||||||
|
String_Concat("argument ", String_Concat(bux_int_to_str((argIdx + 1) as int64),
|
||||||
|
String_Concat(": expected ", String_Concat(wantName,
|
||||||
|
String_Concat(", got ", gotName))))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argIdx = argIdx + 1;
|
||||||
arg = arg.next;
|
arg = arg.next;
|
||||||
}
|
}
|
||||||
// Borrow check: reject double mutable borrow in @[Checked] functions
|
// Borrow check: reject double mutable borrow in @[Checked] functions
|
||||||
@@ -1123,6 +1235,10 @@ module Sema {
|
|||||||
|
|
||||||
// Struct init: TypeName { field: value, ... }
|
// Struct init: TypeName { field: value, ... }
|
||||||
if kind == ekStructInit {
|
if kind == ekStructInit {
|
||||||
|
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
||||||
|
te.kind = tekNamed;
|
||||||
|
te.typeName = expr.structName;
|
||||||
|
expr.refType = te;
|
||||||
return tyNamed;
|
return tyNamed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1332,18 +1448,9 @@ module Sema {
|
|||||||
if params != null as *Decl {
|
if params != null as *Decl {
|
||||||
var i: int = 0;
|
var i: int = 0;
|
||||||
while i < params.paramCount {
|
while i < params.paramCount {
|
||||||
var p: Param;
|
let p: *Param = Sema_DeclParam(params, i);
|
||||||
if i == 0 { p = params.param0; }
|
|
||||||
else if i == 1 { p = params.param1; }
|
|
||||||
else if i == 2 { p = params.param2; }
|
|
||||||
else if i == 3 { p = params.param3; }
|
|
||||||
else if i == 4 { p = params.param4; }
|
|
||||||
else if i == 5 { p = params.param5; }
|
|
||||||
else if i == 6 { p = params.param6; }
|
|
||||||
else if i == 7 { p = params.param7; }
|
|
||||||
else if i == 8 { p = params.param8; }
|
|
||||||
|
|
||||||
if !String_Eq(p.name, "") {
|
if p != null as *Param && !String_Eq(p.name, "") {
|
||||||
var sym: Symbol;
|
var sym: Symbol;
|
||||||
sym.kind = skVar;
|
sym.kind = skVar;
|
||||||
sym.name = p.name;
|
sym.name = p.name;
|
||||||
@@ -1385,19 +1492,12 @@ module Sema {
|
|||||||
var tail: *TypeExprList = null as *TypeExprList;
|
var tail: *TypeExprList = null as *TypeExprList;
|
||||||
var i: int = 0;
|
var i: int = 0;
|
||||||
while i < params.paramCount {
|
while i < params.paramCount {
|
||||||
var p: Param;
|
let p: *Param = Sema_DeclParam(params, i);
|
||||||
if i == 0 { p = params.param0; }
|
var pte: *TypeExpr = null as *TypeExpr;
|
||||||
else if i == 1 { p = params.param1; }
|
if p != null as *Param { pte = p.refParamType; }
|
||||||
else if i == 2 { p = params.param2; }
|
|
||||||
else if i == 3 { p = params.param3; }
|
|
||||||
else if i == 4 { p = params.param4; }
|
|
||||||
else if i == 5 { p = params.param5; }
|
|
||||||
else if i == 6 { p = params.param6; }
|
|
||||||
else if i == 7 { p = params.param7; }
|
|
||||||
else if i == 8 { p = params.param8; }
|
|
||||||
|
|
||||||
let node: *TypeExprList = bux_alloc(sizeof(TypeExprList)) as *TypeExprList;
|
let node: *TypeExprList = bux_alloc(sizeof(TypeExprList)) as *TypeExprList;
|
||||||
node.te = p.refParamType;
|
node.te = pte;
|
||||||
node.next = null as *TypeExprList;
|
node.next = null as *TypeExprList;
|
||||||
if head == null as *TypeExprList {
|
if head == null as *TypeExprList {
|
||||||
head = node;
|
head = node;
|
||||||
@@ -1459,6 +1559,16 @@ module Sema {
|
|||||||
sym.typeName = stmt.child1.refType.typeName;
|
sym.typeName = stmt.child1.refType.typeName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Assignment check: annotation vs initializer (skip when either side is unknown)
|
||||||
|
if stmt.refStmtType != null as *TypeExpr && initType != tyUnknown {
|
||||||
|
if !Sema_CanAssignTo(sema, initType, stmt.child1.refType, stmt.refStmtType) {
|
||||||
|
let gotName: String = Sema_TypeNameForDiag(stmt.child1.refType, initType);
|
||||||
|
let wantName: String = Sema_TypeNameForDiag(stmt.refStmtType, Sema_ResolveType(sema, stmt.refStmtType));
|
||||||
|
let msg: String = String_Concat("cannot assign ",
|
||||||
|
String_Concat(gotName, String_Concat(" to ", wantName)));
|
||||||
|
Sema_EmitError(sema, stmt.line, stmt.column, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
sym.isMutable = stmt.boolValue;
|
sym.isMutable = stmt.boolValue;
|
||||||
sym.isPublic = false;
|
sym.isPublic = false;
|
||||||
sym.decl = null as *Decl;
|
sym.decl = null as *Decl;
|
||||||
@@ -2179,15 +2289,8 @@ module Sema {
|
|||||||
if argExpr == null as *Expr { argList = argList.next; pi = pi + 1; continue; }
|
if argExpr == null as *Expr { argList = argList.next; pi = pi + 1; continue; }
|
||||||
|
|
||||||
var paramType: *TypeExpr = null as *TypeExpr;
|
var paramType: *TypeExpr = null as *TypeExpr;
|
||||||
if pi == 0 { paramType = funcDecl.param0.refParamType; }
|
let fp: *Param = Sema_DeclParam(funcDecl, pi);
|
||||||
else if pi == 1 { paramType = funcDecl.param1.refParamType; }
|
if fp != null as *Param { paramType = fp.refParamType; }
|
||||||
else if pi == 2 { paramType = funcDecl.param2.refParamType; }
|
|
||||||
else if pi == 3 { paramType = funcDecl.param3.refParamType; }
|
|
||||||
else if pi == 4 { paramType = funcDecl.param4.refParamType; }
|
|
||||||
else if pi == 5 { paramType = funcDecl.param5.refParamType; }
|
|
||||||
else if pi == 6 { paramType = funcDecl.param6.refParamType; }
|
|
||||||
else if pi == 7 { paramType = funcDecl.param7.refParamType; }
|
|
||||||
else if pi == 8 { paramType = funcDecl.param8.refParamType; }
|
|
||||||
|
|
||||||
var argType: *TypeExpr = argExpr.refType;
|
var argType: *TypeExpr = argExpr.refType;
|
||||||
// &x → use type of x, wrap as pointer if pattern expects pointer
|
// &x → use type of x, wrap as pointer if pattern expects pointer
|
||||||
@@ -2297,16 +2400,7 @@ module Sema {
|
|||||||
// Add parameters to scope
|
// Add parameters to scope
|
||||||
var i: int = 0;
|
var i: int = 0;
|
||||||
while i < decl.paramCount {
|
while i < decl.paramCount {
|
||||||
var p: *Param = null as *Param;
|
let p: *Param = Sema_DeclParam(decl, i);
|
||||||
if i == 0 { p = &decl.param0; }
|
|
||||||
else if i == 1 { p = &decl.param1; }
|
|
||||||
else if i == 2 { p = &decl.param2; }
|
|
||||||
else if i == 3 { p = &decl.param3; }
|
|
||||||
else if i == 4 { p = &decl.param4; }
|
|
||||||
else if i == 5 { p = &decl.param5; }
|
|
||||||
else if i == 6 { p = &decl.param6; }
|
|
||||||
else if i == 7 { p = &decl.param7; }
|
|
||||||
else if i == 8 { p = &decl.param8; }
|
|
||||||
var pSym: Symbol;
|
var pSym: Symbol;
|
||||||
Sema_ZeroInitSymbol(&pSym);
|
Sema_ZeroInitSymbol(&pSym);
|
||||||
pSym.kind = skVar;
|
pSym.kind = skVar;
|
||||||
@@ -2325,15 +2419,7 @@ module Sema {
|
|||||||
pSym.isMutable = false;
|
pSym.isMutable = false;
|
||||||
pSym.isPublic = false;
|
pSym.isPublic = false;
|
||||||
pSym.decl = null as *Decl;
|
pSym.decl = null as *Decl;
|
||||||
if i == 0 { pSym.name = decl.param0.name; }
|
if p != null as *Param { pSym.name = p.name; }
|
||||||
else if i == 1 { pSym.name = decl.param1.name; }
|
|
||||||
else if i == 2 { pSym.name = decl.param2.name; }
|
|
||||||
else if i == 3 { pSym.name = decl.param3.name; }
|
|
||||||
else if i == 4 { pSym.name = decl.param4.name; }
|
|
||||||
else if i == 5 { pSym.name = decl.param5.name; }
|
|
||||||
else if i == 6 { pSym.name = decl.param6.name; }
|
|
||||||
else if i == 7 { pSym.name = decl.param7.name; }
|
|
||||||
else if i == 8 { pSym.name = decl.param8.name; }
|
|
||||||
discard Scope_Define(&funcScope, pSym);
|
discard Scope_Define(&funcScope, pSym);
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -162,9 +162,22 @@ module Types {
|
|||||||
|
|
||||||
func Type_Eq(a: Type, b: Type) -> bool {
|
func Type_Eq(a: Type, b: Type) -> bool {
|
||||||
if a.kind != b.kind { return false; }
|
if a.kind != b.kind { return false; }
|
||||||
if a.kind == tyNamed || a.kind == tyTypeParam {
|
if a.kind == tyNamed || a.kind == tyTypeParam || a.kind == tyFunc {
|
||||||
return String_Eq(a.name, b.name);
|
return String_Eq(a.name, b.name);
|
||||||
}
|
}
|
||||||
|
if a.kind == tyPointer {
|
||||||
|
if a.innerKind1 != b.innerKind1 { return false; }
|
||||||
|
return String_Eq(a.innerName1, b.innerName1);
|
||||||
|
}
|
||||||
|
if a.kind == tySlice || a.kind == tyTuple {
|
||||||
|
if a.innerKind1 != b.innerKind1 { return false; }
|
||||||
|
if !String_Eq(a.innerName1, b.innerName1) { return false; }
|
||||||
|
if a.innerKind2 != b.innerKind2 { return false; }
|
||||||
|
if !String_Eq(a.innerName2, b.innerName2) { return false; }
|
||||||
|
if a.innerKind3 != b.innerKind3 { return false; }
|
||||||
|
if !String_Eq(a.innerName3, b.innerName3) { return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ import Std::Test::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
func Main() -> int {
|
func Main() -> int {
|
||||||
|
// Zero-capacity grow (v1.0.2): must not segfault
|
||||||
|
var z: Array<int> = Array_New<int>(0);
|
||||||
|
Array_Push<int>(&z, 7);
|
||||||
|
Array_Push<int>(&z, 8);
|
||||||
|
Test_AssertEqInt(Array_Len<int>(&z) as int, 2);
|
||||||
|
Test_AssertEqInt(Array_Get<int>(&z, 0), 7);
|
||||||
|
Test_AssertEqInt(Array_Get<int>(&z, 1), 8);
|
||||||
|
Array_Free<int>(&z);
|
||||||
|
|
||||||
var arr: Array<int> = Array_New<int>(2);
|
var arr: Array<int> = Array_New<int>(2);
|
||||||
Array_Reserve<int>(&arr, 8);
|
Array_Reserve<int>(&arr, 8);
|
||||||
Test_AssertTrue(Array_Cap<int>(&arr) >= 8);
|
Test_AssertTrue(Array_Cap<int>(&arr) >= 8);
|
||||||
|
|||||||
@@ -34,6 +34,23 @@ func Main() -> int {
|
|||||||
Test_AssertTrue(Map_IsEmpty<int, int>(&m));
|
Test_AssertTrue(Map_IsEmpty<int, int>(&m));
|
||||||
Map_Free<int, int>(&m);
|
Map_Free<int, int>(&m);
|
||||||
|
|
||||||
|
// Auto-grow past initial cap (v1.0.2): must not hang
|
||||||
|
var tiny: Map<int, int> = Map_New<int, int>(2);
|
||||||
|
Map_Set<int, int>(&tiny, 1, 10);
|
||||||
|
Map_Set<int, int>(&tiny, 2, 20);
|
||||||
|
Map_Set<int, int>(&tiny, 3, 30);
|
||||||
|
Map_Set<int, int>(&tiny, 4, 40);
|
||||||
|
Map_Set<int, int>(&tiny, 5, 50);
|
||||||
|
Test_AssertEqInt(Map_Len<int, int>(&tiny) as int, 5);
|
||||||
|
Test_AssertEqInt(Map_Get<int, int>(&tiny, 5), 50);
|
||||||
|
Map_Free<int, int>(&tiny);
|
||||||
|
|
||||||
|
// Zero-cap request defaults to usable table (v1.0.2)
|
||||||
|
var z: Map<int, int> = Map_New<int, int>(0);
|
||||||
|
Map_Set<int, int>(&z, 9, 99);
|
||||||
|
Test_AssertEqInt(Map_Get<int, int>(&z, 9), 99);
|
||||||
|
Map_Free<int, int>(&z);
|
||||||
|
|
||||||
var s: Set<int> = Set_New<int>(16);
|
var s: Set<int> = Set_New<int>(16);
|
||||||
Set_Add<int>(&s, 10);
|
Set_Add<int>(&s, 10);
|
||||||
Set_Add<int>(&s, 20);
|
Set_Add<int>(&s, 20);
|
||||||
@@ -45,21 +62,31 @@ func Main() -> int {
|
|||||||
Test_AssertFalse(Set_IsEmpty<int>(&s));
|
Test_AssertFalse(Set_IsEmpty<int>(&s));
|
||||||
Set_Free<int>(&s);
|
Set_Free<int>(&s);
|
||||||
|
|
||||||
let ok: Result = Result_NewOk(42);
|
// Set grow (v1.0.2)
|
||||||
let err: Result = Result_NewErr("boom");
|
var s2: Set<int> = Set_New<int>(2);
|
||||||
Test_AssertTrue(Result_IsOk(ok));
|
Set_Add<int>(&s2, 1);
|
||||||
Test_AssertTrue(Result_IsErr(err));
|
Set_Add<int>(&s2, 2);
|
||||||
Test_AssertEqInt(Result_UnwrapOr(err, -1), -1);
|
Set_Add<int>(&s2, 3);
|
||||||
let recovered: Result = Result_Or(err, Result_NewOk(7));
|
Set_Add<int>(&s2, 4);
|
||||||
Test_AssertEqInt(Result_UnwrapOr(recovered, 0), 7);
|
Test_AssertEqInt(Set_Len<int>(&s2) as int, 4);
|
||||||
Test_AssertTrue(String_Eq(Result_UnwrapErr(err), "boom"));
|
Test_AssertTrue(Set_Has<int>(&s2, 4));
|
||||||
|
Set_Free<int>(&s2);
|
||||||
|
|
||||||
let some: Option = Option_NewSome(5);
|
let ok: Result<int, String> = Result_NewOk<int, String>(42);
|
||||||
let none: Option = Option_NewNone();
|
let err: Result<int, String> = Result_NewErr<int, String>("boom");
|
||||||
Test_AssertTrue(Option_IsSome(some));
|
Test_AssertTrue(Result_IsOk<int, String>(ok));
|
||||||
Test_AssertEqInt(Option_UnwrapOr(none, 9), 9);
|
Test_AssertTrue(Result_IsErr<int, String>(err));
|
||||||
let filled: Option = Option_Or(none, Option_NewSome(3));
|
Test_AssertEqInt(Result_UnwrapOr<int, String>(err, -1), -1);
|
||||||
Test_AssertEqInt(Option_UnwrapOr(filled, 0), 3);
|
let recovered: Result<int, String> = Result_Or<int, String>(err, Result_NewOk<int, String>(7));
|
||||||
|
Test_AssertEqInt(Result_UnwrapOr<int, String>(recovered, 0), 7);
|
||||||
|
Test_AssertTrue(String_Eq(Result_UnwrapErr<int, String>(err), "boom"));
|
||||||
|
|
||||||
|
let some: Option<int> = Option_NewSome<int>(5);
|
||||||
|
let none: Option<int> = Option_NewNone<int>();
|
||||||
|
Test_AssertTrue(Option_IsSome<int>(some));
|
||||||
|
Test_AssertEqInt(Option_UnwrapOr<int>(none, 9), 9);
|
||||||
|
let filled: Option<int> = Option_Or<int>(none, Option_NewSome<int>(3));
|
||||||
|
Test_AssertEqInt(Option_UnwrapOr<int>(filled, 0), 3);
|
||||||
|
|
||||||
PrintLine("stdlib_collections: ok");
|
PrintLine("stdlib_collections: ok");
|
||||||
Test_Pass("stdlib_collections");
|
Test_Pass("stdlib_collections");
|
||||||
|
|||||||
+299
-71
@@ -19,9 +19,13 @@
|
|||||||
# v0.15.0: type hierarchy (prepare / supertypes / subtypes via extend for).
|
# v0.15.0: type hierarchy (prepare / supertypes / subtypes via extend for).
|
||||||
# v0.16.0: workspace type-impl index — hierarchy works for closed multi-file
|
# v0.16.0: workspace type-impl index — hierarchy works for closed multi-file
|
||||||
# docs even when `extend T for I` has no methods (no open required).
|
# docs even when `extend T for I` has no methods (no open required).
|
||||||
|
# v0.17.0: in-process diagnostics (lex/parse/sema) on open/change/save so the
|
||||||
|
# editor underlines errors in the live buffer without needing buxc.
|
||||||
|
# v0.18.0: textDocument/formatting + rangeFormatting via bootstrap `formatSource`
|
||||||
|
# (same rules as `bux fmt` — 4-space brace indent).
|
||||||
|
|
||||||
import std/[json, os, strutils, streams, tables, osproc, sequtils, sets]
|
import std/[json, os, strutils, streams, tables, osproc, sequtils, sets]
|
||||||
import lexer, parser, ast, sema, types, scope, source_location
|
import lexer, parser, ast, sema, types, scope, source_location, fmt
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# JSON-RPC Transport
|
# JSON-RPC Transport
|
||||||
@@ -1075,7 +1079,8 @@ proc enrichWithSema(doc: DocumentState) =
|
|||||||
discard # sema failures must not crash the LSP
|
discard # sema failures must not crash the LSP
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Diagnostics — run `buxc check` when available and parse Rust-style errors
|
# Diagnostics — in-process lex/parse/sema (live buffer underlines)
|
||||||
|
# Optional: also merge `buxc check` when the binary is available.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type
|
type
|
||||||
@@ -1085,19 +1090,159 @@ type
|
|||||||
endCol: int ## 0-based exclusive
|
endCol: int ## 0-based exclusive
|
||||||
severity: int ## 1=error, 2=warning
|
severity: int ## 1=error, 2=warning
|
||||||
message: string
|
message: string
|
||||||
|
source: string ## "bux" | "buxc"
|
||||||
|
|
||||||
|
proc diagSpanEnd(content: string; line0, col0: int): int =
|
||||||
|
## Expand underline to cover the token under the diagnostic start column.
|
||||||
|
let lines = content.split("\n")
|
||||||
|
if line0 < 0 or line0 >= lines.len:
|
||||||
|
return col0 + 1
|
||||||
|
let l = lines[line0]
|
||||||
|
if col0 < 0:
|
||||||
|
return 1
|
||||||
|
if col0 >= l.len:
|
||||||
|
return col0 + 1
|
||||||
|
var endC = col0
|
||||||
|
let ch = l[col0]
|
||||||
|
if ch in {'a'..'z', 'A'..'Z', '_', '0'..'9'}:
|
||||||
|
while endC < l.len and l[endC] in {'a'..'z', 'A'..'Z', '0'..'9', '_'}:
|
||||||
|
inc endC
|
||||||
|
elif ch == '"':
|
||||||
|
inc endC
|
||||||
|
while endC < l.len:
|
||||||
|
if l[endC] == '\\' and endC + 1 < l.len:
|
||||||
|
endC += 2
|
||||||
|
continue
|
||||||
|
if l[endC] == '"':
|
||||||
|
inc endC
|
||||||
|
break
|
||||||
|
inc endC
|
||||||
|
elif ch == '`':
|
||||||
|
inc endC
|
||||||
|
while endC < l.len and l[endC] != '`':
|
||||||
|
inc endC
|
||||||
|
if endC < l.len:
|
||||||
|
inc endC
|
||||||
|
elif ch == '\'':
|
||||||
|
inc endC
|
||||||
|
while endC < l.len:
|
||||||
|
if l[endC] == '\\' and endC + 1 < l.len:
|
||||||
|
endC += 2
|
||||||
|
continue
|
||||||
|
if l[endC] == '\'':
|
||||||
|
inc endC
|
||||||
|
break
|
||||||
|
inc endC
|
||||||
|
else:
|
||||||
|
# Operators / punctuation — underline at least one character
|
||||||
|
endC = col0 + 1
|
||||||
|
if endC <= col0:
|
||||||
|
endC = col0 + 1
|
||||||
|
return endC
|
||||||
|
|
||||||
|
proc locMatchesFile(locFile, sourcePath: string): bool =
|
||||||
|
## True if a diagnostic location belongs to the open buffer.
|
||||||
|
if locFile.len == 0 or sourcePath.len == 0:
|
||||||
|
return true
|
||||||
|
if locFile == sourcePath:
|
||||||
|
return true
|
||||||
|
try:
|
||||||
|
if locFile.absolutePath == sourcePath.absolutePath:
|
||||||
|
return true
|
||||||
|
except CatchableError:
|
||||||
|
discard
|
||||||
|
return locFile.extractFilename == sourcePath.extractFilename
|
||||||
|
|
||||||
|
proc makeDiag(content: string; line1, col1: int; severity: int; message, source: string): LspDiag =
|
||||||
|
let line0 = max(0, line1 - 1)
|
||||||
|
let col0 = max(0, col1 - 1)
|
||||||
|
LspDiag(
|
||||||
|
line: line0,
|
||||||
|
col: col0,
|
||||||
|
endCol: diagSpanEnd(content, line0, col0),
|
||||||
|
severity: severity,
|
||||||
|
message: message,
|
||||||
|
source: source
|
||||||
|
)
|
||||||
|
|
||||||
|
proc collectInProcessDiagnostics(doc: DocumentState): seq[LspDiag] =
|
||||||
|
## Lex / parse / type-check the **buffer content** so squiggles match the editor.
|
||||||
|
result = @[]
|
||||||
|
if doc.content.len == 0:
|
||||||
|
return
|
||||||
|
let path = uriToPath(doc.uri)
|
||||||
|
try:
|
||||||
|
let lexRes = tokenize(doc.content, path)
|
||||||
|
for d in lexRes.diagnostics:
|
||||||
|
let sev = if d.severity == ldsError: 1 else: 2
|
||||||
|
let line1 = int(d.loc.line)
|
||||||
|
let col1 = int(d.loc.column)
|
||||||
|
result.add(makeDiag(doc.content, line1, col1, sev, d.message, "bux"))
|
||||||
|
if lexRes.hasErrors:
|
||||||
|
return
|
||||||
|
|
||||||
|
let parseRes = parse(lexRes.tokens, path)
|
||||||
|
for d in parseRes.diagnostics:
|
||||||
|
let sev = if d.severity == pdsError: 1 else: 2
|
||||||
|
result.add(makeDiag(doc.content, int(d.loc.line), int(d.loc.column),
|
||||||
|
sev, d.message, "bux"))
|
||||||
|
# Still run sema if parse only had warnings; hard parse errors → stop
|
||||||
|
var hardParse = false
|
||||||
|
for d in parseRes.diagnostics:
|
||||||
|
if d.severity == pdsError:
|
||||||
|
hardParse = true
|
||||||
|
break
|
||||||
|
if hardParse:
|
||||||
|
return
|
||||||
|
|
||||||
|
ensureStdlibCached()
|
||||||
|
if cachedStdlibDecls.len == 0:
|
||||||
|
let tryRoot =
|
||||||
|
if path.len > 0: path.parentDir.parentDir
|
||||||
|
else: rootPath
|
||||||
|
cachedStdlibDir = findStdlibDirLocal(tryRoot)
|
||||||
|
if cachedStdlibDir.len > 0:
|
||||||
|
cachedStdlibDecls = loadStdlibDecls(cachedStdlibDir)
|
||||||
|
|
||||||
|
var unified = newModule("lsp")
|
||||||
|
for d in cachedStdlibDecls:
|
||||||
|
unified.items.add(d)
|
||||||
|
for d in parseRes.module.items:
|
||||||
|
if d.kind == dkModule:
|
||||||
|
for sub in d.declModuleItems:
|
||||||
|
unified.items.add(sub)
|
||||||
|
else:
|
||||||
|
unified.items.add(d)
|
||||||
|
|
||||||
|
let (semaRes, _) = analyzeFull(unified)
|
||||||
|
for d in semaRes.diagnostics:
|
||||||
|
if not locMatchesFile(d.loc.file, path):
|
||||||
|
continue
|
||||||
|
let sev = if d.severity == sdsError: 1 else: 2
|
||||||
|
result.add(makeDiag(doc.content, int(d.loc.line), int(d.loc.column),
|
||||||
|
sev, d.message, "bux"))
|
||||||
|
except CatchableError:
|
||||||
|
discard
|
||||||
|
|
||||||
proc findBuxc(): string =
|
proc findBuxc(): string =
|
||||||
## Prefer buxc next to the LSP binary, then PATH.
|
## Prefer buxc next to the LSP binary, repo root (tools/..), cwd, then PATH.
|
||||||
let beside = getAppDir() / "buxc"
|
let candidates = @[
|
||||||
if fileExists(beside): return beside
|
getAppDir() / "buxc",
|
||||||
let beside2 = getCurrentDir() / "buxc"
|
getAppDir() / ".." / "buxc",
|
||||||
if fileExists(beside2): return beside2
|
getAppDir() / ".." / "buxc_debug",
|
||||||
|
getCurrentDir() / "buxc",
|
||||||
|
getCurrentDir() / ".." / "buxc",
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if fileExists(c):
|
||||||
|
return c.absolutePath
|
||||||
result = findExe("buxc")
|
result = findExe("buxc")
|
||||||
|
|
||||||
proc parseBuxcDiagnostics(output, sourcePath: string): seq[LspDiag] =
|
proc parseBuxcDiagnostics(output, sourcePath, content: string): seq[LspDiag] =
|
||||||
## Parse lines like:
|
## Parse lines like:
|
||||||
## error: cannot assign String to int
|
## error: cannot assign String to int
|
||||||
## --> /path/Main.bux:4:18
|
## --> /path/Main.bux:4:18
|
||||||
|
## | ^^^^^^
|
||||||
result = @[]
|
result = @[]
|
||||||
let lines = output.splitLines()
|
let lines = output.splitLines()
|
||||||
var i = 0
|
var i = 0
|
||||||
@@ -1123,61 +1268,77 @@ proc parseBuxcDiagnostics(output, sourcePath: string): seq[LspDiag] =
|
|||||||
|
|
||||||
var fileLine = 1
|
var fileLine = 1
|
||||||
var fileCol = 1
|
var fileCol = 1
|
||||||
|
var pathPart = ""
|
||||||
if i + 1 < lines.len and lines[i + 1].strip().startsWith("-->"):
|
if i + 1 < lines.len and lines[i + 1].strip().startsWith("-->"):
|
||||||
let locPart = lines[i + 1].strip()[3..^1].strip()
|
let locPart = lines[i + 1].strip()[3..^1].strip()
|
||||||
# path:line:col
|
# path:line:col
|
||||||
let parts = locPart.rsplit(':', maxsplit = 2)
|
let parts = locPart.rsplit(':', maxsplit = 2)
|
||||||
if parts.len >= 3:
|
if parts.len >= 3:
|
||||||
|
pathPart = parts[0]
|
||||||
try:
|
try:
|
||||||
fileLine = parseInt(parts[^2])
|
fileLine = parseInt(parts[^2])
|
||||||
fileCol = parseInt(parts[^1])
|
fileCol = parseInt(parts[^1])
|
||||||
except: discard
|
except: discard
|
||||||
# Optionally filter to the open document
|
|
||||||
let pathPart = if parts.len >= 3: parts[0] else: ""
|
|
||||||
if sourcePath.len > 0 and pathPart.len > 0:
|
if sourcePath.len > 0 and pathPart.len > 0:
|
||||||
if not pathPart.endsWith(sourcePath.extractFilename) and
|
if not pathPart.endsWith(sourcePath.extractFilename) and
|
||||||
pathPart != sourcePath:
|
pathPart != sourcePath and
|
||||||
i += 1
|
not locMatchesFile(pathPart, sourcePath):
|
||||||
|
inc i
|
||||||
continue
|
continue
|
||||||
# Estimate end column from message quote or single caret width
|
|
||||||
var endCol = fileCol
|
let line0 = max(0, fileLine - 1)
|
||||||
let q = msg.find('\'')
|
let col0 = max(0, fileCol - 1)
|
||||||
if q >= 0:
|
var endCol = diagSpanEnd(content, line0, col0)
|
||||||
let q2 = msg.find('\'', q + 1)
|
|
||||||
if q2 > q + 1:
|
# Prefer caret underline from following lines: " | ^^^^^^"
|
||||||
endCol = fileCol + (q2 - q - 1)
|
var j = i + 2
|
||||||
if endCol <= fileCol:
|
while j < lines.len and j <= i + 6:
|
||||||
endCol = fileCol + 1
|
let cl = lines[j]
|
||||||
|
let caret = cl.find('^')
|
||||||
|
if caret >= 0 and cl.strip().startsWith("|"):
|
||||||
|
# Map caret columns relative to the pipe-aligned source display
|
||||||
|
var last = caret
|
||||||
|
while last < cl.len and cl[last] == '^':
|
||||||
|
inc last
|
||||||
|
# Display is usually " | <source>" — find source start after "| "
|
||||||
|
let pipe = cl.find('|')
|
||||||
|
if pipe >= 0:
|
||||||
|
let srcStart = pipe + 2
|
||||||
|
let c0 = max(0, caret - srcStart)
|
||||||
|
let c1 = max(c0 + 1, last - srcStart)
|
||||||
|
endCol = c1
|
||||||
|
# also fix start if compiler pointed mid-token
|
||||||
|
# keep fileCol from --> as start; only extend end
|
||||||
|
discard c0
|
||||||
|
break
|
||||||
|
if cl.strip().startsWith("= help:") or cl.strip().startsWith("error:") or
|
||||||
|
cl.strip().startsWith("warning:"):
|
||||||
|
break
|
||||||
|
inc j
|
||||||
|
|
||||||
|
if endCol <= col0:
|
||||||
|
endCol = col0 + 1
|
||||||
|
|
||||||
result.add(LspDiag(
|
result.add(LspDiag(
|
||||||
line: max(0, fileLine - 1),
|
line: line0,
|
||||||
col: max(0, fileCol - 1),
|
col: col0,
|
||||||
endCol: max(0, endCol - 1),
|
endCol: endCol,
|
||||||
severity: sev,
|
severity: sev,
|
||||||
message: msg
|
message: msg,
|
||||||
|
source: "buxc"
|
||||||
))
|
))
|
||||||
inc i
|
inc i
|
||||||
|
|
||||||
proc runBuxcDiagnostics(sourcePath, content: string): seq[LspDiag] =
|
proc runBuxcDiagnostics(sourcePath, content: string): seq[LspDiag] =
|
||||||
|
## Optional project check. Always feeds **buffer content** via a temp package
|
||||||
|
## so unsaved edits still produce squiggles.
|
||||||
result = @[]
|
result = @[]
|
||||||
let buxc = findBuxc()
|
let buxc = findBuxc()
|
||||||
if buxc.len == 0:
|
if buxc.len == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Prefer package root if this file lives under src/
|
let tmp = getTempDir() / "bux-lsp-diag-" & $getCurrentProcessId()
|
||||||
var projectDir = sourcePath.parentDir
|
try:
|
||||||
if projectDir.endsWith("src"):
|
|
||||||
projectDir = projectDir.parentDir
|
|
||||||
let toml = projectDir / "bux.toml"
|
|
||||||
|
|
||||||
var cmd: string
|
|
||||||
var workDir: string
|
|
||||||
if fileExists(toml):
|
|
||||||
workDir = projectDir
|
|
||||||
cmd = buxc & " check --color off"
|
|
||||||
else:
|
|
||||||
# Temp package for free-standing buffers
|
|
||||||
let tmp = getTempDir() / "bux-lsp-" & $getCurrentProcessId()
|
|
||||||
createDir(tmp / "src")
|
createDir(tmp / "src")
|
||||||
writeFile(tmp / "bux.toml", """[Package]
|
writeFile(tmp / "bux.toml", """[Package]
|
||||||
Name = "lsp_tmp"
|
Name = "lsp_tmp"
|
||||||
@@ -1186,16 +1347,28 @@ Type = "bin"
|
|||||||
[Build]
|
[Build]
|
||||||
Output = "Bin"
|
Output = "Bin"
|
||||||
""")
|
""")
|
||||||
writeFile(tmp / "src" / "Main.bux", content)
|
let mainPath = tmp / "src" / "Main.bux"
|
||||||
workDir = tmp
|
writeFile(mainPath, content)
|
||||||
cmd = buxc & " check --color off"
|
let (output, _) = execCmdEx(buxc & " check --color off", workingDir = tmp)
|
||||||
|
# Map diagnostics from temp Main.bux back onto the open URI path filter
|
||||||
try:
|
result = parseBuxcDiagnostics(output, mainPath, content)
|
||||||
let (output, _) = execCmdEx(cmd, workingDir = workDir)
|
|
||||||
result = parseBuxcDiagnostics(output, sourcePath)
|
|
||||||
except CatchableError:
|
except CatchableError:
|
||||||
discard
|
discard
|
||||||
|
|
||||||
|
proc diagKey(d: LspDiag): string =
|
||||||
|
$d.line & ":" & $d.col & ":" & $d.severity & ":" & d.message
|
||||||
|
|
||||||
|
proc mergeDiagnostics(primary, extra: seq[LspDiag]): seq[LspDiag] =
|
||||||
|
result = primary
|
||||||
|
var seen = initHashSet[string]()
|
||||||
|
for d in primary:
|
||||||
|
seen.incl(diagKey(d))
|
||||||
|
for d in extra:
|
||||||
|
let k = diagKey(d)
|
||||||
|
if k notin seen:
|
||||||
|
seen.incl(k)
|
||||||
|
result.add(d)
|
||||||
|
|
||||||
proc publishDiagnostics(stream: FileStream, uri: string, diags: seq[LspDiag] = @[]) =
|
proc publishDiagnostics(stream: FileStream, uri: string, diags: seq[LspDiag] = @[]) =
|
||||||
var arr = newJArray()
|
var arr = newJArray()
|
||||||
for d in diags:
|
for d in diags:
|
||||||
@@ -1205,7 +1378,7 @@ proc publishDiagnostics(stream: FileStream, uri: string, diags: seq[LspDiag] = @
|
|||||||
"end": {"line": d.line, "character": d.endCol}
|
"end": {"line": d.line, "character": d.endCol}
|
||||||
},
|
},
|
||||||
"severity": d.severity,
|
"severity": d.severity,
|
||||||
"source": "buxc",
|
"source": d.source,
|
||||||
"message": d.message
|
"message": d.message
|
||||||
})
|
})
|
||||||
sendNotification(stream, "textDocument/publishDiagnostics", %*{
|
sendNotification(stream, "textDocument/publishDiagnostics", %*{
|
||||||
@@ -1213,7 +1386,7 @@ proc publishDiagnostics(stream: FileStream, uri: string, diags: seq[LspDiag] = @
|
|||||||
"diagnostics": arr
|
"diagnostics": arr
|
||||||
})
|
})
|
||||||
|
|
||||||
proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState) =
|
proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState; runBuxc = true) =
|
||||||
let path = uriToPath(doc.uri)
|
let path = uriToPath(doc.uri)
|
||||||
let updated = analyzeFile(path, doc.content)
|
let updated = analyzeFile(path, doc.content)
|
||||||
doc.symbols = updated.symbols
|
doc.symbols = updated.symbols
|
||||||
@@ -1224,7 +1397,10 @@ proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState) =
|
|||||||
doc.importPaths = updated.importPaths
|
doc.importPaths = updated.importPaths
|
||||||
# Keep / refresh real types for hover (does not replace lightweight outline)
|
# Keep / refresh real types for hover (does not replace lightweight outline)
|
||||||
enrichWithSema(doc)
|
enrichWithSema(doc)
|
||||||
let diags = runBuxcDiagnostics(path, doc.content)
|
# Primary: in-process diags from the live buffer (works without buxc on PATH)
|
||||||
|
var diags = collectInProcessDiagnostics(doc)
|
||||||
|
if runBuxc:
|
||||||
|
diags = mergeDiagnostics(diags, runBuxcDiagnostics(path, doc.content))
|
||||||
publishDiagnostics(stream, doc.uri, diags)
|
publishDiagnostics(stream, doc.uri, diags)
|
||||||
|
|
||||||
proc scanWorkspace(dir: string, depth = 0) =
|
proc scanWorkspace(dir: string, depth = 0) =
|
||||||
@@ -2340,6 +2516,64 @@ proc handleDocumentSymbol(stream: FileStream, id: JsonNode, paramsNode: JsonNode
|
|||||||
})
|
})
|
||||||
sendResponse(stream, id, arr)
|
sendResponse(stream, id, arr)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Document formatting (v0.18 — same engine as `bux fmt`)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc lineCountAndLastLen(s: string): tuple[lines: int, lastLen: int] =
|
||||||
|
## 0-based end position after last character (for full-document TextEdit).
|
||||||
|
if s.len == 0:
|
||||||
|
return (0, 0)
|
||||||
|
var lines = 0
|
||||||
|
var lastLen = 0
|
||||||
|
var i = 0
|
||||||
|
while i < s.len:
|
||||||
|
if s[i] == '\n':
|
||||||
|
inc lines
|
||||||
|
lastLen = 0
|
||||||
|
else:
|
||||||
|
inc lastLen
|
||||||
|
inc i
|
||||||
|
# Trailing content without final newline still occupies a line
|
||||||
|
if s[^1] != '\n':
|
||||||
|
# last line is incomplete — end character is lastLen
|
||||||
|
discard
|
||||||
|
else:
|
||||||
|
# ends with newline: end is (lines, 0) in LSP (exclusive end after last line)
|
||||||
|
discard
|
||||||
|
result = (lines, lastLen)
|
||||||
|
|
||||||
|
proc fullDocumentEdit(uri: string, content: string, formatted: string): JsonNode =
|
||||||
|
## Single TextEdit replacing the whole buffer with formatted text.
|
||||||
|
if formatted == content:
|
||||||
|
return newJArray()
|
||||||
|
let (endLine, endChar) = lineCountAndLastLen(content)
|
||||||
|
var arr = newJArray()
|
||||||
|
arr.add(%*{
|
||||||
|
"range": {
|
||||||
|
"start": {"line": 0, "character": 0},
|
||||||
|
"end": {"line": endLine, "character": endChar}
|
||||||
|
},
|
||||||
|
"newText": formatted
|
||||||
|
})
|
||||||
|
discard uri
|
||||||
|
return arr
|
||||||
|
|
||||||
|
proc handleDocumentFormatting(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
|
## textDocument/formatting — re-indent with 4 spaces (idempotent).
|
||||||
|
let uri = paramsNode["textDocument"]["uri"].getStr()
|
||||||
|
let doc = getDoc(uri)
|
||||||
|
let formatted = formatSource(doc.content)
|
||||||
|
sendResponse(stream, id, fullDocumentEdit(uri, doc.content, formatted))
|
||||||
|
|
||||||
|
proc handleDocumentRangeFormatting(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
|
## textDocument/rangeFormatting — whole-file format (indent is brace-global).
|
||||||
|
## Editors that send a selection still get a consistent full reformat.
|
||||||
|
let uri = paramsNode["textDocument"]["uri"].getStr()
|
||||||
|
let doc = getDoc(uri)
|
||||||
|
let formatted = formatSource(doc.content)
|
||||||
|
sendResponse(stream, id, fullDocumentEdit(uri, doc.content, formatted))
|
||||||
|
|
||||||
proc handleWorkspaceSymbol(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
proc handleWorkspaceSymbol(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
## workspace/symbol — fuzzy-ish substring filter over workspace + open docs.
|
## workspace/symbol — fuzzy-ish substring filter over workspace + open docs.
|
||||||
let query = if paramsNode.hasKey("query"): paramsNode["query"].getStr().toLowerAscii() else: ""
|
let query = if paramsNode.hasKey("query"): paramsNode["query"].getStr().toLowerAscii() else: ""
|
||||||
@@ -3196,9 +3430,11 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
"workspaceSymbolProvider": true,
|
"workspaceSymbolProvider": true,
|
||||||
"callHierarchyProvider": true,
|
"callHierarchyProvider": true,
|
||||||
"implementationProvider": true,
|
"implementationProvider": true,
|
||||||
"typeHierarchyProvider": true
|
"typeHierarchyProvider": true,
|
||||||
|
"documentFormattingProvider": true,
|
||||||
|
"documentRangeFormattingProvider": true
|
||||||
},
|
},
|
||||||
"serverInfo": {"name": "bux-lsp", "version": "0.16.0"}
|
"serverInfo": {"name": "bux-lsp", "version": "0.18.0"}
|
||||||
})
|
})
|
||||||
if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull:
|
if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull:
|
||||||
rootPath = paramsNode["rootPath"].getStr()
|
rootPath = paramsNode["rootPath"].getStr()
|
||||||
@@ -3229,8 +3465,8 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
doc.content = content
|
doc.content = content
|
||||||
if td.hasKey("version"):
|
if td.hasKey("version"):
|
||||||
doc.version = td["version"].getInt()
|
doc.version = td["version"].getInt()
|
||||||
# Lightweight scan + sema enrich + buxc diagnostics
|
# Symbols + hover types + live underlines (in-process; optional buxc)
|
||||||
analyzeAndPublishDiagnostics(stream, doc)
|
analyzeAndPublishDiagnostics(stream, doc, runBuxc = true)
|
||||||
|
|
||||||
of "textDocument/didChange":
|
of "textDocument/didChange":
|
||||||
let td = paramsNode["textDocument"]
|
let td = paramsNode["textDocument"]
|
||||||
@@ -3241,29 +3477,15 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
doc.content = changes[changes.len - 1]["text"].getStr()
|
doc.content = changes[changes.len - 1]["text"].getStr()
|
||||||
if td.hasKey("version"):
|
if td.hasKey("version"):
|
||||||
doc.version = td["version"].getInt()
|
doc.version = td["version"].getInt()
|
||||||
# Fast path: lightweight symbols only; keep previous typeIndex until save/hover refresh
|
# Re-analyze + publish squiggles on every edit (buffer content, not disk).
|
||||||
let updated = analyzeFile(uriToPath(uri), doc.content)
|
# Skip spawning buxc here — in-process lex/parse/sema is enough while typing.
|
||||||
doc.symbols = updated.symbols
|
analyzeAndPublishDiagnostics(stream, doc, runBuxc = false)
|
||||||
doc.ordered = updated.ordered
|
|
||||||
doc.members = updated.members
|
|
||||||
doc.ifaceMethods = updated.ifaceMethods
|
|
||||||
doc.impls = updated.impls
|
|
||||||
doc.importPaths = updated.importPaths
|
|
||||||
# Re-apply typeIndex details onto matching names (don't drop sema types mid-edit)
|
|
||||||
for name, detail in doc.typeIndex.pairs:
|
|
||||||
if doc.symbols.hasKey(name):
|
|
||||||
var info = doc.symbols[name]
|
|
||||||
info.detail = detail
|
|
||||||
info.fromSema = true
|
|
||||||
if doc.kindIndex.hasKey(name):
|
|
||||||
info.kind = doc.kindIndex[name]
|
|
||||||
doc.symbols[name] = info
|
|
||||||
|
|
||||||
of "textDocument/didSave":
|
of "textDocument/didSave":
|
||||||
let td = paramsNode["textDocument"]
|
let td = paramsNode["textDocument"]
|
||||||
let uri = td["uri"].getStr()
|
let uri = td["uri"].getStr()
|
||||||
discard getDoc(uri)
|
discard getDoc(uri)
|
||||||
analyzeAndPublishDiagnostics(stream, getDoc(uri))
|
analyzeAndPublishDiagnostics(stream, getDoc(uri), runBuxc = true)
|
||||||
|
|
||||||
of "textDocument/completion":
|
of "textDocument/completion":
|
||||||
handleCompletion(stream, id, paramsNode)
|
handleCompletion(stream, id, paramsNode)
|
||||||
@@ -3310,6 +3532,12 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
of "typeHierarchy/subtypes":
|
of "typeHierarchy/subtypes":
|
||||||
handleTypeHierarchySubtypes(stream, id, paramsNode)
|
handleTypeHierarchySubtypes(stream, id, paramsNode)
|
||||||
|
|
||||||
|
of "textDocument/formatting":
|
||||||
|
handleDocumentFormatting(stream, id, paramsNode)
|
||||||
|
|
||||||
|
of "textDocument/rangeFormatting":
|
||||||
|
handleDocumentRangeFormatting(stream, id, paramsNode)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if id != nil:
|
if id != nil:
|
||||||
sendError(stream, id, -32601, "method not found: " & methodName)
|
sendError(stream, id, -32601, "method not found: " & methodName)
|
||||||
|
|||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke: freestanding runtime compiles under -ffreestanding; optional package build.
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
RT="$ROOT/rt/runtime_freestanding.c"
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
if [[ ! -f "$RT" ]]; then
|
||||||
|
echo "FAIL: missing $RT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CC="${BUX_CC:-cc}"
|
||||||
|
|
||||||
|
echo "=== freestanding: -ffreestanding -c runtime ==="
|
||||||
|
"$CC" -ffreestanding -std=c11 -Wall -Wextra -c "$RT" -o "$TMP/rt_fs.o"
|
||||||
|
echo "PASS: runtime_freestanding.o"
|
||||||
|
|
||||||
|
echo "=== freestanding: BUX_RUNTIME=freestanding build hello-ish ==="
|
||||||
|
mkdir -p "$TMP/pkg/src"
|
||||||
|
cat > "$TMP/pkg/bux.toml" <<'EOF'
|
||||||
|
[Package]
|
||||||
|
Name = "fs_smoke"
|
||||||
|
Version = "0.1.0"
|
||||||
|
EOF
|
||||||
|
cat > "$TMP/pkg/src/Main.bux" <<'EOF'
|
||||||
|
func Main() -> int {
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ ! -x "$ROOT/buxc" ]]; then
|
||||||
|
echo "building buxc..."
|
||||||
|
(cd "$ROOT" && make build >/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Hosted link still uses libc for crt0; runtime body is freestanding.
|
||||||
|
BUX_RUNTIME=freestanding "$ROOT/buxc" build "$TMP/pkg" --release >/dev/null
|
||||||
|
OUT="$TMP/pkg/build/fs_smoke"
|
||||||
|
if [[ ! -x "$OUT" ]]; then
|
||||||
|
echo "FAIL: binary not produced"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
CODE=$("$OUT"; echo $?)
|
||||||
|
if [[ "$CODE" != "42" ]]; then
|
||||||
|
echo "FAIL: expected exit 42, got $CODE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "PASS: freestanding runtime package exit 42"
|
||||||
|
|
||||||
|
# Optional: object-level nostdlib link experiment (may need extra crt — soft)
|
||||||
|
echo "=== freestanding: optional -ffreestanding object of Main.c ==="
|
||||||
|
# Generate C then compile Main only with freestanding flags
|
||||||
|
BUX_RUNTIME=freestanding "$ROOT/buxc" build "$TMP/pkg" --release >/dev/null
|
||||||
|
if [[ -f "$TMP/pkg/build/main.c" ]]; then
|
||||||
|
if "$CC" -ffreestanding -std=c11 -c "$TMP/pkg/build/main.c" -o "$TMP/main_fs.o" 2>"$TMP/main_fs.err"; then
|
||||||
|
echo "PASS: main.c compiles under -ffreestanding"
|
||||||
|
else
|
||||||
|
# Hosted headers in generated C may pull stdint — not a hard fail
|
||||||
|
echo "SKIP: main.c -ffreestanding (generated C may need hosted headers)"
|
||||||
|
head -5 "$TMP/main_fs.err" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PASS: freestanding smoke"
|
||||||
Executable
+87
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke: textDocument/publishDiagnostics underlines type/parse errors in the buffer.
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
LSP="$ROOT/tools/bux-lsp"
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
if [[ ! -x "$LSP" ]]; then
|
||||||
|
echo "building bux-lsp..."
|
||||||
|
(cd "$ROOT" && make lsp >/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Intentionally wrong: String assigned to int
|
||||||
|
cat > "$TMP/Main.bux" <<'EOF'
|
||||||
|
func Main() -> int {
|
||||||
|
let x: int = "boom";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
rpc() {
|
||||||
|
local body="$1"
|
||||||
|
local len
|
||||||
|
len=$(printf '%s' "$body" | wc -c)
|
||||||
|
printf 'Content-Length: %s\r\n\r\n%s' "$len" "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
CONTENT_JSON=$(python3 -c 'import json,sys; print(json.dumps(open(sys.argv[1]).read()))' "$TMP/Main.bux")
|
||||||
|
URI="file://$TMP/Main.bux"
|
||||||
|
|
||||||
|
# Also test didChange: fix would clear, re-break would re-publish
|
||||||
|
BROKEN2='func Main() -> int {\n let y: int = true;\n return 0;\n}\n'
|
||||||
|
BROKEN2_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1].encode("utf-8").decode("unicode_escape")))' "$BROKEN2")
|
||||||
|
|
||||||
|
{
|
||||||
|
rpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file://'"$TMP"'"}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"initialized","params":{}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"'"$URI"'","languageId":"bux","version":1,"text":'"$CONTENT_JSON"'}}}'
|
||||||
|
# Give server a moment is not needed — sync stdio
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"'"$URI"'","version":2},"contentChanges":[{"text":'"$BROKEN2_JSON"'}]}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","id":2,"method":"shutdown","params":null}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"exit","params":null}'
|
||||||
|
} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt"
|
||||||
|
|
||||||
|
echo "---- diagnostics excerpt ----"
|
||||||
|
grep -o '"method":"textDocument/publishDiagnostics"[^}]*}[^}]*}[^}]*}' "$TMP/out.txt" | head -5 || true
|
||||||
|
# Broader: any publishDiagnostics payload
|
||||||
|
python3 - <<'PY' "$TMP/out.txt"
|
||||||
|
import json, sys, re
|
||||||
|
raw = open(sys.argv[1]).read()
|
||||||
|
# Split on Content-Length framing remnants — we already stripped \r; bodies are JSON objects
|
||||||
|
parts = []
|
||||||
|
for m in re.finditer(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', raw):
|
||||||
|
s = m.group(0)
|
||||||
|
if "publishDiagnostics" in s or '"diagnostics"' in s:
|
||||||
|
parts.append(s)
|
||||||
|
|
||||||
|
# More robust: scan for diagnostics arrays via string search
|
||||||
|
ok = True
|
||||||
|
if "publishDiagnostics" not in raw and "diagnostics" not in raw:
|
||||||
|
print("FAIL: no publishDiagnostics notification")
|
||||||
|
ok = False
|
||||||
|
else:
|
||||||
|
# Must mention type mismatch somehow
|
||||||
|
low = raw.lower()
|
||||||
|
if "cannot assign" not in low and "type" not in low and "error" not in low:
|
||||||
|
print("FAIL: diagnostics payload has no error-like message")
|
||||||
|
ok = False
|
||||||
|
else:
|
||||||
|
print("found diagnostics notification with error content")
|
||||||
|
# severity 1 = Error
|
||||||
|
if '"severity":1' not in raw and '"severity": 1' not in raw:
|
||||||
|
print("WARN: severity=1 not found as literal (may be ok)")
|
||||||
|
# Expect at least one diagnostic on line 1 (0-based) for `let x: int = "boom"`
|
||||||
|
if '"line":1' not in raw and '"line": 1' not in raw:
|
||||||
|
# might be line 0 depending on layout
|
||||||
|
if '"line":0' not in raw and '"line": 0' not in raw:
|
||||||
|
print("WARN: unexpected line numbers")
|
||||||
|
print("PASS markers: publishDiagnostics present")
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
print("---- full output ----")
|
||||||
|
print(raw[:4000])
|
||||||
|
sys.exit(1)
|
||||||
|
print("PASS: LSP diagnostics smoke (error underlines)")
|
||||||
|
PY
|
||||||
Executable
+86
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke: textDocument/formatting re-indents with 4 spaces (same as bux fmt).
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
LSP="$ROOT/tools/bux-lsp"
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
if [[ ! -x "$LSP" ]]; then
|
||||||
|
echo "building bux-lsp..."
|
||||||
|
(cd "$ROOT" && make lsp >/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Intentionally bad indent (2 spaces)
|
||||||
|
cat > "$TMP/Main.bux" <<'EOF'
|
||||||
|
func Main() -> int {
|
||||||
|
let x: int = 1;
|
||||||
|
if x > 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
rpc() {
|
||||||
|
local body="$1"
|
||||||
|
local len
|
||||||
|
len=$(printf '%s' "$body" | wc -c)
|
||||||
|
printf 'Content-Length: %s\r\n\r\n%s' "$len" "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
CONTENT_JSON=$(python3 -c 'import json,sys; print(json.dumps(open(sys.argv[1]).read()))' "$TMP/Main.bux")
|
||||||
|
URI="file://$TMP/Main.bux"
|
||||||
|
|
||||||
|
{
|
||||||
|
rpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file://'"$TMP"'"}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"initialized","params":{}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"'"$URI"'","languageId":"bux","version":1,"text":'"$CONTENT_JSON"'}}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/formatting","params":{"textDocument":{"uri":"'"$URI"'"},"options":{"tabSize":4,"insertSpaces":true}}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","id":3,"method":"shutdown","params":null}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"exit","params":null}'
|
||||||
|
} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$TMP/out.txt"
|
||||||
|
import json, sys, re
|
||||||
|
raw = open(sys.argv[1]).read()
|
||||||
|
|
||||||
|
# Find response id=2 with result TextEdit array
|
||||||
|
# Prefer structured parse of JSON objects containing "newText"
|
||||||
|
ok = False
|
||||||
|
version_ok = "0.18.0" in raw or '"documentFormattingProvider":true' in raw.replace(" ", "")
|
||||||
|
if "0.18.0" not in raw and "documentFormattingProvider" not in raw:
|
||||||
|
# initialize result may be nested; still require a formatting response
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Extract TextEdit newText via regex / brace walk
|
||||||
|
edits = []
|
||||||
|
for m in re.finditer(r'"newText"\s*:\s*"((?:[^"\\]|\\.)*)"', raw):
|
||||||
|
edits.append(bytes(m.group(1), "utf-8").decode("unicode_escape"))
|
||||||
|
|
||||||
|
if not edits:
|
||||||
|
print("FAIL: no TextEdit newText in formatting response")
|
||||||
|
print(raw[:3000])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
formatted = edits[0]
|
||||||
|
# Expected: 4-space indent after func {
|
||||||
|
if " let x: int = 1;" not in formatted:
|
||||||
|
print("FAIL: expected 4-space indent on let")
|
||||||
|
print(repr(formatted))
|
||||||
|
sys.exit(1)
|
||||||
|
if " if x > 0 {" not in formatted and " if x > 0 {" not in formatted:
|
||||||
|
# after let at indent 1, if should be at indent 1 (same block) = 4 spaces
|
||||||
|
print("FAIL: unexpected if indent")
|
||||||
|
print(repr(formatted))
|
||||||
|
sys.exit(1)
|
||||||
|
# Nested body of if at 8 spaces
|
||||||
|
if " return 0;" not in formatted:
|
||||||
|
print("FAIL: expected 8-space indent on return inside if")
|
||||||
|
print(repr(formatted))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("PASS: LSP formatting smoke (4-space brace indent)")
|
||||||
|
if "0.18.0" in raw:
|
||||||
|
print("PASS: serverInfo version 0.18.0")
|
||||||
|
PY
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
out/
|
||||||
|
*.vsix
|
||||||
|
.vscode-test/
|
||||||
|
*.tsbuildinfo
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.vscode/**
|
||||||
|
.vscode-test/**
|
||||||
|
src/**
|
||||||
|
.gitignore
|
||||||
|
.vscodeignore
|
||||||
|
tsconfig.json
|
||||||
|
**/*.ts
|
||||||
|
**/*.map
|
||||||
|
node_modules/**
|
||||||
|
!node_modules/vscode-languageclient/**
|
||||||
|
!node_modules/vscode-jsonrpc/**
|
||||||
|
!node_modules/vscode-languageserver-protocol/**
|
||||||
|
!node_modules/vscode-languageserver-types/**
|
||||||
|
!node_modules/semver/**
|
||||||
|
*.vsix
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Bux Language Support for VS Code
|
||||||
|
|
||||||
|
Syntax highlighting, snippets, editor defaults, and **Language Server Protocol** integration for the [Bux](https://github.com/katehonz/bux) programming language.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
| Area | What you get |
|
||||||
|
|------|----------------|
|
||||||
|
| **Syntax** | Keywords, types, `f"..."` interpolation, raw `` `...` `` strings, C-strings, macros (`macro!` / `name!()`), attributes (`@[Checked]`), numbers (hex/bin/oct + suffixes), lifetimes |
|
||||||
|
| **Snippets** | `main`, `func`, `struct`, `enum`, `match`, `interface`, `extend`, `macro`, `checked`, … |
|
||||||
|
| **LSP** | **Live error underlines** (red squiggles on edit), **Format Document** (same as `bux fmt`), completion, hover, go-to-definition, references, rename, document/workspace symbols, call hierarchy, type hierarchy, go-to-implementation |
|
||||||
|
| **Editor** | Bracket colorization, smart indent / on-enter, fold regions (`// region`), **format-on-save** default for `[bux]` |
|
||||||
|
| **Build** | `buxc` problem matcher for Tasks |
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. **VS Code** ≥ 1.85
|
||||||
|
2. **`bux-lsp`** binary (from this repo):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# from the Bux repository root
|
||||||
|
make lsp
|
||||||
|
# → tools/bux-lsp
|
||||||
|
```
|
||||||
|
|
||||||
|
The extension auto-discovers the server in this order:
|
||||||
|
|
||||||
|
1. Setting `bux.lsp.path` (absolute, relative, or command name)
|
||||||
|
2. `tools/bux-lsp` under any workspace folder (and parent folders for monorepos)
|
||||||
|
3. `bux-lsp` on your `PATH`
|
||||||
|
|
||||||
|
## Install (development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vscode
|
||||||
|
npm install
|
||||||
|
npm run compile
|
||||||
|
|
||||||
|
# Launch Extension Development Host: F5 in VS Code,
|
||||||
|
# or install the folder as an extension:
|
||||||
|
code --install-extension .
|
||||||
|
# or package:
|
||||||
|
npx @vscode/vsce package
|
||||||
|
code --install-extension bux-lang-0.2.0.vsix
|
||||||
|
```
|
||||||
|
|
||||||
|
Symlink into your extensions dir (Linux):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sfn "$(pwd)/vscode" ~/.vscode/extensions/bux-lang.bux-lang-0.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| **Bux: Restart Language Server** | Stop and start `bux-lsp` |
|
||||||
|
| **Bux: Stop Language Server** | Disconnect the client |
|
||||||
|
| **Bux: Show Output Channel** | Open the Bux log |
|
||||||
|
|
||||||
|
Status bar item **Bux** (left): click to restart. Red = missing binary / start failure.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
| Setting | Default | Description |
|
||||||
|
|---------|---------|-------------|
|
||||||
|
| `bux.lsp.enabled` | `true` | Master switch for the language server |
|
||||||
|
| `bux.lsp.path` | `"bux-lsp"` | Path or command for the server binary |
|
||||||
|
| `bux-lsp.trace.server` | `off` | LSP wire trace (`off` / `messages` / `verbose`) |
|
||||||
|
|
||||||
|
## LSP capabilities (bux-lsp)
|
||||||
|
|
||||||
|
Provided by `tools/lsp_server.nim` (see `make lsp` / `make test-lsp`):
|
||||||
|
|
||||||
|
- `textDocument/completion`, `hover`, `definition`, `references`, `rename`
|
||||||
|
- `documentSymbol`, `workspace/symbol`
|
||||||
|
- Call hierarchy, type hierarchy, `implementation`
|
||||||
|
- Diagnostics on open/save (via analyzer / `buxc`)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
1. Status bar shows **error** → run `make lsp` and ensure `tools/bux-lsp` exists and is executable.
|
||||||
|
2. **Bux: Show Output Channel** for client logs.
|
||||||
|
3. Set `"bux-lsp.trace.server": "verbose"` for JSON-RPC traffic.
|
||||||
|
4. Confirm language mode is **Bux** for `.bux` files (status bar language indicator).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — same as the Bux project.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
|
||||||
|
<rect width="128" height="128" rx="28" fill="#0d1117"/>
|
||||||
|
<rect x="8" y="8" width="112" height="112" rx="22" fill="#161b22" stroke="#30363d" stroke-width="2"/>
|
||||||
|
<path d="M34 32h28c12 0 20 6.5 20 16.5 0 7-3.5 12-9.5 14.5 8 2.5 12.5 8.5 12.5 16.5C85 92 76 99 62 99H34V32zm14 11v16h12c5.5 0 8.5-2.5 8.5-8s-3-8-8.5-8H48zm0 27v18h14c6.5 0 10.5-3 10.5-9.5S68.5 70 62 70H48z" fill="#6ee7b7"/>
|
||||||
|
<circle cx="96" cy="40" r="8" fill="#38bdf8" opacity=".9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 533 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<rect width="32" height="32" rx="6" fill="#1a1f2e"/>
|
||||||
|
<path d="M8 8h6.5c2.8 0 4.5 1.5 4.5 3.8 0 1.6-.8 2.8-2.2 3.4 1.8.6 2.9 2 2.9 3.9 0 2.7-2 4.4-5.2 4.4H8V8zm3.2 2.5v3.8h2.8c1.3 0 2-.6 2-1.9s-.7-1.9-2-1.9h-2.8zm0 6.2v4.3h3.2c1.5 0 2.4-.7 2.4-2.2s-.9-2.1-2.4-2.1h-3.2z" fill="#6ee7b7"/>
|
||||||
|
<path d="M20.5 22.5c1.8-1.2 3-3.2 3-5.5 0-3.7-2.8-6.5-6.5-6.5" stroke="#38bdf8" stroke-width="1.6" stroke-linecap="round" opacity=".85"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 511 B |
@@ -8,29 +8,81 @@
|
|||||||
["[", "]"],
|
["[", "]"],
|
||||||
["(", ")"]
|
["(", ")"]
|
||||||
],
|
],
|
||||||
|
"colorizedBracketPairs": [
|
||||||
|
["{", "}"],
|
||||||
|
["[", "]"],
|
||||||
|
["(", ")"]
|
||||||
|
],
|
||||||
"autoClosingPairs": [
|
"autoClosingPairs": [
|
||||||
{ "open": "{", "close": "}" },
|
{ "open": "{", "close": "}" },
|
||||||
{ "open": "[", "close": "]" },
|
{ "open": "[", "close": "]" },
|
||||||
{ "open": "(", "close": ")" },
|
{ "open": "(", "close": ")" },
|
||||||
{ "open": "\"", "close": "\"" },
|
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] },
|
||||||
{ "open": "`", "close": "`" }
|
{ "open": "`", "close": "`", "notIn": ["string", "comment"] },
|
||||||
|
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
|
||||||
|
{ "open": "/*", "close": " */", "notIn": ["string"] }
|
||||||
],
|
],
|
||||||
"surroundingPairs": [
|
"surroundingPairs": [
|
||||||
{ "open": "{", "close": "}" },
|
["{", "}"],
|
||||||
{ "open": "[", "close": "]" },
|
["[", "]"],
|
||||||
{ "open": "(", "close": ")" },
|
["(", ")"],
|
||||||
{ "open": "\"", "close": "\"" },
|
["\"", "\""],
|
||||||
{ "open": "`", "close": "`" }
|
["`", "`"],
|
||||||
|
["'", "'"]
|
||||||
],
|
],
|
||||||
|
"autoCloseBefore": ";:.,=}])>` \n\t",
|
||||||
"folding": {
|
"folding": {
|
||||||
"markers": {
|
"markers": {
|
||||||
"start": "^\\s*//\\s*region\\b",
|
"start": "^\\s*//\\s*#?region\\b",
|
||||||
"end": "^\\s*//\\s*endregion\\b"
|
"end": "^\\s*//\\s*#?endregion\\b"
|
||||||
|
},
|
||||||
|
"offSide": false
|
||||||
|
},
|
||||||
|
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
|
||||||
|
"indentationRules": {
|
||||||
|
"increaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)\\s*$",
|
||||||
|
"decreaseIndentPattern": "^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]\\)].*$"
|
||||||
|
},
|
||||||
|
"onEnterRules": [
|
||||||
|
{
|
||||||
|
"beforeText": "^\\s*/\\*(?!/).*[^*/]\\s*$",
|
||||||
|
"afterText": "^\\s*\\*/$",
|
||||||
|
"action": {
|
||||||
|
"indent": "indentOutdent",
|
||||||
|
"appendText": " * "
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"wordPattern": "[a-zA-Z_][a-zA-Z0-9_]*",
|
{
|
||||||
"indentationRules": {
|
"beforeText": "^\\s*/\\*(?!/).*[^*/]\\s*$",
|
||||||
"increaseIndentPattern": "\\{[^}]*$",
|
"action": {
|
||||||
"decreaseIndentPattern": "^\\s*\\}"
|
"indent": "none",
|
||||||
|
"appendText": " * "
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"beforeText": "^(\\t|(\\ \\ ))*\\ \\*(\\ ([^*]|\\*(?!/))*)?$",
|
||||||
|
"action": {
|
||||||
|
"indent": "none",
|
||||||
|
"appendText": "* "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"beforeText": "^(\\t|(\\ \\ ))*\\ */.*$",
|
||||||
|
"action": {
|
||||||
|
"indent": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"beforeText": "^\\s*(func|async\\s+func|struct|enum|union|interface|extend|module|macro!|if|else|while|for|do|loop|match|switch|case)\\b.*\\{\\s*$",
|
||||||
|
"action": {
|
||||||
|
"indent": "indent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"beforeText": "^\\s*.*=>\\s*$",
|
||||||
|
"action": {
|
||||||
|
"indent": "indent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+128
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"name": "bux-lang",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "bux-lang",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-languageclient": "^9.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"@types/vscode": "^1.85.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.85.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.43",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||||
|
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/vscode": {
|
||||||
|
"version": "1.125.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz",
|
||||||
|
"integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||||
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "5.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||||
|
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/vscode-jsonrpc": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageclient": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^5.1.0",
|
||||||
|
"semver": "^7.3.7",
|
||||||
|
"vscode-languageserver-protocol": "3.17.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.82.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-protocol": {
|
||||||
|
"version": "3.17.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||||
|
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-jsonrpc": "8.2.0",
|
||||||
|
"vscode-languageserver-types": "3.17.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-types": {
|
||||||
|
"version": "3.17.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||||
|
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+131
-13
@@ -1,37 +1,62 @@
|
|||||||
{
|
{
|
||||||
"name": "bux-lang",
|
"name": "bux-lang",
|
||||||
"displayName": "Bux Language Support",
|
"displayName": "Bux Language Support",
|
||||||
"description": "Syntax highlighting, snippets, and LSP support for the Bux programming language",
|
"description": "Syntax highlighting, snippets, and full LSP support for Bux — live error underlines, Format Document, hover, go-to-def, rename, call/type hierarchy",
|
||||||
"version": "0.1.0",
|
"version": "0.2.2",
|
||||||
"publisher": "bux-lang",
|
"publisher": "bux-lang",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
|
"galleryBanner": {
|
||||||
|
"color": "#0d1117",
|
||||||
|
"theme": "dark"
|
||||||
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/katehonz/bux"
|
"url": "https://github.com/katehonz/bux"
|
||||||
},
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/katehonz/bux/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/katehonz/bux",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.85.0"
|
"vscode": "^1.85.0"
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"Programming Languages",
|
"Programming Languages",
|
||||||
"Snippets",
|
"Snippets",
|
||||||
"Linters"
|
"Linters",
|
||||||
|
"Formatters",
|
||||||
|
"Other"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"bux",
|
||||||
|
"bux-lang",
|
||||||
|
"programming-language",
|
||||||
|
"lsp",
|
||||||
|
"syntax",
|
||||||
|
"formatter"
|
||||||
],
|
],
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onLanguage:bux"
|
"onLanguage:bux",
|
||||||
|
"workspaceContains:**/*.bux",
|
||||||
|
"workspaceContains:bux.toml"
|
||||||
],
|
],
|
||||||
"main": "./out/extension.js",
|
"main": "./out/extension.js",
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"languages": [
|
"languages": [
|
||||||
{
|
{
|
||||||
"id": "bux",
|
"id": "bux",
|
||||||
"aliases": ["Bux", "bux"],
|
"aliases": [
|
||||||
"extensions": [".bux"],
|
"Bux",
|
||||||
|
"bux"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
".bux"
|
||||||
|
],
|
||||||
"configuration": "./language-configuration.json",
|
"configuration": "./language-configuration.json",
|
||||||
"icon": {
|
"icon": {
|
||||||
"dark": "./icon.png",
|
"dark": "./icons/bux-file.svg",
|
||||||
"light": "./icon.png"
|
"light": "./icons/bux-file.svg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -48,30 +73,123 @@
|
|||||||
"path": "./snippets/bux.json"
|
"path": "./snippets/bux.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "bux.restartLsp",
|
||||||
|
"title": "Bux: Restart Language Server",
|
||||||
|
"category": "Bux"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "bux.stopLsp",
|
||||||
|
"title": "Bux: Stop Language Server",
|
||||||
|
"category": "Bux"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "bux.showOutput",
|
||||||
|
"title": "Bux: Show Output Channel",
|
||||||
|
"category": "Bux"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"menus": {
|
||||||
|
"commandPalette": [
|
||||||
|
{
|
||||||
|
"command": "bux.restartLsp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "bux.stopLsp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "bux.showOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"title": "Bux",
|
"title": "Bux",
|
||||||
"properties": {
|
"properties": {
|
||||||
"bux.lsp.enabled": {
|
"bux.lsp.enabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"default": true,
|
||||||
"description": "Enable LSP server for diagnostics and autocomplete"
|
"description": "Enable the Bux language server (diagnostics, formatting, hover, go-to-definition, rename, hierarchy)."
|
||||||
},
|
},
|
||||||
"bux.lsp.path": {
|
"bux.lsp.path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "bux-lsp",
|
"default": "bux-lsp",
|
||||||
"description": "Path to the bux-lsp binary"
|
"description": "Path to the bux-lsp binary. Absolute path, workspace-relative path, or command on PATH. If unset/default, the extension also searches tools/bux-lsp under workspace folders."
|
||||||
|
},
|
||||||
|
"bux-lsp.trace.server": {
|
||||||
|
"type": "string",
|
||||||
|
"scope": "window",
|
||||||
|
"enum": [
|
||||||
|
"off",
|
||||||
|
"messages",
|
||||||
|
"verbose"
|
||||||
|
],
|
||||||
|
"default": "off",
|
||||||
|
"description": "Traces the communication between VS Code and the Bux language server."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configurationDefaults": {
|
||||||
|
"[bux]": {
|
||||||
|
"editor.semanticHighlighting.enabled": true,
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.quickSuggestions": {
|
||||||
|
"other": true,
|
||||||
|
"comments": false,
|
||||||
|
"strings": false
|
||||||
|
},
|
||||||
|
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||||
|
"editor.wordBasedSuggestions": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"problemMatchers": [
|
||||||
|
{
|
||||||
|
"name": "buxc",
|
||||||
|
"owner": "bux",
|
||||||
|
"fileLocation": [
|
||||||
|
"relative",
|
||||||
|
"${workspaceFolder}"
|
||||||
|
],
|
||||||
|
"pattern": {
|
||||||
|
"regexp": "^(.*):(\\d+):(\\d+):\\s+(error|warning|note):\\s+(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"severity": 4,
|
||||||
|
"message": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"taskDefinitions": [
|
||||||
|
{
|
||||||
|
"type": "bux",
|
||||||
|
"required": [
|
||||||
|
"task"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"task": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "bux subcommand (build, run, test, check)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"vscode:prepublish": "npm run compile",
|
"vscode:prepublish": "npm run compile",
|
||||||
"compile": "tsc -p ./",
|
"compile": "tsc -p ./",
|
||||||
"watch": "tsc -watch -p ./"
|
"watch": "tsc -watch -p ./",
|
||||||
|
"package": "vsce package --no-dependencies 2>/dev/null || npx @vscode/vsce package --no-dependencies"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-languageclient": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
"@types/vscode": "^1.85.0",
|
"@types/vscode": "^1.85.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.3.0"
|
||||||
"vscode-languageclient": "^9.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+169
-27
@@ -9,11 +9,23 @@
|
|||||||
],
|
],
|
||||||
"description": "Main entry point function"
|
"description": "Main entry point function"
|
||||||
},
|
},
|
||||||
|
"Hello World": {
|
||||||
|
"prefix": "hello",
|
||||||
|
"body": [
|
||||||
|
"import Std::Io::{PrintLine};",
|
||||||
|
"",
|
||||||
|
"func Main() -> int {",
|
||||||
|
" PrintLine(\"${1:Hello, Bux!}\");",
|
||||||
|
" return 0;",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Minimal Hello World program"
|
||||||
|
},
|
||||||
"Function": {
|
"Function": {
|
||||||
"prefix": "func",
|
"prefix": "func",
|
||||||
"body": [
|
"body": [
|
||||||
"func ${1:Name}(${2:params}) -> ${3:void} {",
|
"func ${1:Name}(${2:params}) -> ${3:void} {",
|
||||||
" $4",
|
" $0",
|
||||||
"}"
|
"}"
|
||||||
],
|
],
|
||||||
"description": "Function declaration"
|
"description": "Function declaration"
|
||||||
@@ -22,11 +34,20 @@
|
|||||||
"prefix": "afunc",
|
"prefix": "afunc",
|
||||||
"body": [
|
"body": [
|
||||||
"async func ${1:Name}(${2:params}) -> ${3:void} {",
|
"async func ${1:Name}(${2:params}) -> ${3:void} {",
|
||||||
" $4",
|
" $0",
|
||||||
"}"
|
"}"
|
||||||
],
|
],
|
||||||
"description": "Async function declaration"
|
"description": "Async function declaration"
|
||||||
},
|
},
|
||||||
|
"Generic function": {
|
||||||
|
"prefix": "gfunc",
|
||||||
|
"body": [
|
||||||
|
"func ${1:Name}<${2:T}>(${3:param}: ${2:T}) -> ${2:T} {",
|
||||||
|
" $0",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Generic function"
|
||||||
|
},
|
||||||
"Variable (let)": {
|
"Variable (let)": {
|
||||||
"prefix": "let",
|
"prefix": "let",
|
||||||
"body": [
|
"body": [
|
||||||
@@ -41,6 +62,13 @@
|
|||||||
],
|
],
|
||||||
"description": "Mutable variable"
|
"description": "Mutable variable"
|
||||||
},
|
},
|
||||||
|
"Const": {
|
||||||
|
"prefix": "const",
|
||||||
|
"body": [
|
||||||
|
"const ${1:NAME}: ${2:Type} = ${3:value};"
|
||||||
|
],
|
||||||
|
"description": "Constant declaration"
|
||||||
|
},
|
||||||
"Struct": {
|
"Struct": {
|
||||||
"prefix": "struct",
|
"prefix": "struct",
|
||||||
"body": [
|
"body": [
|
||||||
@@ -60,11 +88,52 @@
|
|||||||
],
|
],
|
||||||
"description": "Enum declaration"
|
"description": "Enum declaration"
|
||||||
},
|
},
|
||||||
|
"Generic enum": {
|
||||||
|
"prefix": "genum",
|
||||||
|
"body": [
|
||||||
|
"enum ${1:Name}<${2:T}> {",
|
||||||
|
" ${3:Some}(${2:T}),",
|
||||||
|
" ${4:None},",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Generic enum declaration"
|
||||||
|
},
|
||||||
|
"Interface": {
|
||||||
|
"prefix": "interface",
|
||||||
|
"body": [
|
||||||
|
"interface ${1:Name} {",
|
||||||
|
" func ${2:Method}(${3:self}) -> ${4:void};",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Interface declaration"
|
||||||
|
},
|
||||||
|
"Extend methods": {
|
||||||
|
"prefix": "extend",
|
||||||
|
"body": [
|
||||||
|
"extend ${1:Type} {",
|
||||||
|
" func ${2:Method}(self) -> ${3:void} {",
|
||||||
|
" $0",
|
||||||
|
" }",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Extend block for inherent methods"
|
||||||
|
},
|
||||||
|
"Extend for interface": {
|
||||||
|
"prefix": "extendfor",
|
||||||
|
"body": [
|
||||||
|
"extend ${1:Type} for ${2:Interface} {",
|
||||||
|
" func ${3:Method}(self) -> ${4:void} {",
|
||||||
|
" $0",
|
||||||
|
" }",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Implement interface for type"
|
||||||
|
},
|
||||||
"If statement": {
|
"If statement": {
|
||||||
"prefix": "if",
|
"prefix": "if",
|
||||||
"body": [
|
"body": [
|
||||||
"if ${1:condition} {",
|
"if ${1:condition} {",
|
||||||
" ${2:body}",
|
" $0",
|
||||||
"}"
|
"}"
|
||||||
],
|
],
|
||||||
"description": "If statement"
|
"description": "If statement"
|
||||||
@@ -75,7 +144,7 @@
|
|||||||
"if ${1:condition} {",
|
"if ${1:condition} {",
|
||||||
" ${2:body}",
|
" ${2:body}",
|
||||||
"} else {",
|
"} else {",
|
||||||
" ${3:elseBody}",
|
" $0",
|
||||||
"}"
|
"}"
|
||||||
],
|
],
|
||||||
"description": "If-else statement"
|
"description": "If-else statement"
|
||||||
@@ -84,20 +153,29 @@
|
|||||||
"prefix": "while",
|
"prefix": "while",
|
||||||
"body": [
|
"body": [
|
||||||
"while ${1:condition} {",
|
"while ${1:condition} {",
|
||||||
" ${2:body}",
|
" $0",
|
||||||
"}"
|
"}"
|
||||||
],
|
],
|
||||||
"description": "While loop"
|
"description": "While loop"
|
||||||
},
|
},
|
||||||
"For loop": {
|
"For-in loop": {
|
||||||
"prefix": "for",
|
"prefix": "for",
|
||||||
"body": [
|
"body": [
|
||||||
"for ${1:item} in ${2:iterable} {",
|
"for ${1:item} in ${2:iterable} {",
|
||||||
" ${3:body}",
|
" $0",
|
||||||
"}"
|
"}"
|
||||||
],
|
],
|
||||||
"description": "For-in loop"
|
"description": "For-in loop"
|
||||||
},
|
},
|
||||||
|
"Loop": {
|
||||||
|
"prefix": "loop",
|
||||||
|
"body": [
|
||||||
|
"loop {",
|
||||||
|
" $0",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Infinite loop"
|
||||||
|
},
|
||||||
"Match": {
|
"Match": {
|
||||||
"prefix": "match",
|
"prefix": "match",
|
||||||
"body": [
|
"body": [
|
||||||
@@ -108,40 +186,61 @@
|
|||||||
],
|
],
|
||||||
"description": "Pattern match expression"
|
"description": "Pattern match expression"
|
||||||
},
|
},
|
||||||
|
"Switch": {
|
||||||
|
"prefix": "switch",
|
||||||
|
"body": [
|
||||||
|
"switch ${1:expr} {",
|
||||||
|
" case ${2:value}:",
|
||||||
|
" $0",
|
||||||
|
" break;",
|
||||||
|
" default:",
|
||||||
|
" break;",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Switch statement"
|
||||||
|
},
|
||||||
"Import": {
|
"Import": {
|
||||||
"prefix": "import",
|
"prefix": "import",
|
||||||
"body": [
|
"body": [
|
||||||
"import Std::${1:Module}::{${2:items}};"
|
"import Std::${1:Io}::{${2:PrintLine}};"
|
||||||
],
|
],
|
||||||
"description": "Import statement"
|
"description": "Import from Std"
|
||||||
},
|
},
|
||||||
"Module": {
|
"Module": {
|
||||||
"prefix": "module",
|
"prefix": "module",
|
||||||
"body": [
|
"body": [
|
||||||
"module ${1:Name} {",
|
"module ${1:Name} {",
|
||||||
"${2:body}",
|
" $0",
|
||||||
"}"
|
"}"
|
||||||
],
|
],
|
||||||
"description": "Module declaration"
|
"description": "Module declaration"
|
||||||
},
|
},
|
||||||
"Extend": {
|
"@Checked function": {
|
||||||
"prefix": "extend",
|
|
||||||
"body": [
|
|
||||||
"extend ${1:Type} {",
|
|
||||||
" ${2:methods}",
|
|
||||||
"}"
|
|
||||||
],
|
|
||||||
"description": "Extend block for methods"
|
|
||||||
},
|
|
||||||
"@Checked": {
|
|
||||||
"prefix": "checked",
|
"prefix": "checked",
|
||||||
"body": [
|
"body": [
|
||||||
"@[Checked]",
|
"@[Checked]",
|
||||||
"func ${1:Name}(${2:params}) -> ${3:void} {",
|
"func ${1:Name}(${2:params}) -> ${3:void} {",
|
||||||
" ${4:body}",
|
" $0",
|
||||||
"}"
|
"}"
|
||||||
],
|
],
|
||||||
"description": "Checked function with borrow checker enabled"
|
"description": "Checked function with borrow checker"
|
||||||
|
},
|
||||||
|
"@Release function": {
|
||||||
|
"prefix": "release",
|
||||||
|
"body": [
|
||||||
|
"@[Release]",
|
||||||
|
"func ${1:Name}(${2:params}) -> ${3:void} {",
|
||||||
|
" $0",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Release (unchecked) function"
|
||||||
|
},
|
||||||
|
"Defer": {
|
||||||
|
"prefix": "defer",
|
||||||
|
"body": [
|
||||||
|
"defer ${1:cleanup};"
|
||||||
|
],
|
||||||
|
"description": "Defer cleanup"
|
||||||
},
|
},
|
||||||
"PrintLine": {
|
"PrintLine": {
|
||||||
"prefix": "pl",
|
"prefix": "pl",
|
||||||
@@ -150,13 +249,56 @@
|
|||||||
],
|
],
|
||||||
"description": "Print with newline"
|
"description": "Print with newline"
|
||||||
},
|
},
|
||||||
"Generic function": {
|
"Interpolated string": {
|
||||||
"prefix": "gfunc",
|
"prefix": "fstr",
|
||||||
"body": [
|
"body": [
|
||||||
"func ${1:Name}<${2:T}>(${3:param}: ${2:T}) -> ${2:T} {",
|
"f\"${1:Hello, }{${2:name}}\""
|
||||||
" ${4:body}",
|
],
|
||||||
|
"description": "Interpolated f-string"
|
||||||
|
},
|
||||||
|
"Macro definition": {
|
||||||
|
"prefix": "macro",
|
||||||
|
"body": [
|
||||||
|
"macro! ${1:name} {",
|
||||||
|
" (\\$${2:x}:expr) => {",
|
||||||
|
" $0",
|
||||||
|
" }",
|
||||||
"}"
|
"}"
|
||||||
],
|
],
|
||||||
"description": "Generic function"
|
"description": "Declarative macro! definition"
|
||||||
|
},
|
||||||
|
"Test assert": {
|
||||||
|
"prefix": "tassert",
|
||||||
|
"body": [
|
||||||
|
"Test_AssertEq${1:Int}(${2:got}, ${3:want});"
|
||||||
|
],
|
||||||
|
"description": "Test equality assertion"
|
||||||
|
},
|
||||||
|
"Spawn task": {
|
||||||
|
"prefix": "spawn",
|
||||||
|
"body": [
|
||||||
|
"let ${1:handle}: *void = spawn ${2:Worker}(${3:args});"
|
||||||
|
],
|
||||||
|
"description": "Spawn green-thread task"
|
||||||
|
},
|
||||||
|
"Result Ok/Err match": {
|
||||||
|
"prefix": "resultmatch",
|
||||||
|
"body": [
|
||||||
|
"match ${1:result} {",
|
||||||
|
" Ok(${2:value}) => ${3:value},",
|
||||||
|
" Err(${4:err}) => ${5:/* handle */},",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Match on Result"
|
||||||
|
},
|
||||||
|
"Option Some/None match": {
|
||||||
|
"prefix": "optmatch",
|
||||||
|
"body": [
|
||||||
|
"match ${1:opt} {",
|
||||||
|
" Some(${2:value}) => ${3:value},",
|
||||||
|
" None => ${4:/* handle */},",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Match on Option"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+233
-35
@@ -1,54 +1,252 @@
|
|||||||
/* Bux Language Server Protocol client for VS Code
|
/* Bux Language Server Protocol client for VS Code
|
||||||
* Launches bux-lsp binary and connects via stdin/stdout.
|
* Discovers and launches the bux-lsp binary, connects via stdio.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import {
|
import {
|
||||||
LanguageClient,
|
LanguageClient,
|
||||||
LanguageClientOptions,
|
LanguageClientOptions,
|
||||||
ServerOptions,
|
ServerOptions,
|
||||||
TransportKind
|
TransportKind,
|
||||||
|
RevealOutputChannelOn,
|
||||||
} from 'vscode-languageclient/node';
|
} from 'vscode-languageclient/node';
|
||||||
|
|
||||||
let client: LanguageClient;
|
let client: LanguageClient | undefined;
|
||||||
|
let outputChannel: vscode.OutputChannel | undefined;
|
||||||
|
let statusBar: vscode.StatusBarItem | undefined;
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
const SETTING_SECTION = 'bux';
|
||||||
const config = vscode.workspace.getConfiguration('bux.lsp');
|
|
||||||
const enabled = config.get<boolean>('enabled', true);
|
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
||||||
if (!enabled) {
|
outputChannel = vscode.window.createOutputChannel('Bux');
|
||||||
vscode.window.showInformationMessage('Bux LSP is disabled in settings');
|
context.subscriptions.push(outputChannel);
|
||||||
return;
|
|
||||||
|
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50);
|
||||||
|
statusBar.command = 'bux.restartLsp';
|
||||||
|
statusBar.tooltip = 'Bux Language Server — click to restart';
|
||||||
|
context.subscriptions.push(statusBar);
|
||||||
|
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('bux.restartLsp', async () => {
|
||||||
|
await restartClient(true);
|
||||||
|
}),
|
||||||
|
vscode.commands.registerCommand('bux.stopLsp', async () => {
|
||||||
|
await stopClient();
|
||||||
|
setStatus('off', 'Bux LSP stopped');
|
||||||
|
outputChannel?.appendLine('[bux] LSP stopped by user');
|
||||||
|
}),
|
||||||
|
vscode.commands.registerCommand('bux.showOutput', () => {
|
||||||
|
outputChannel?.show(true);
|
||||||
|
}),
|
||||||
|
vscode.workspace.onDidChangeConfiguration(async (e) => {
|
||||||
|
if (
|
||||||
|
e.affectsConfiguration('bux.lsp.enabled') ||
|
||||||
|
e.affectsConfiguration('bux.lsp.path')
|
||||||
|
) {
|
||||||
|
outputChannel?.appendLine('[bux] Configuration changed — restarting LSP');
|
||||||
|
await restartClient(false);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
const lspPath = config.get<string>('path', 'bux-lsp');
|
|
||||||
|
|
||||||
const serverOptions: ServerOptions = {
|
|
||||||
command: lspPath,
|
|
||||||
transport: TransportKind.stdio
|
|
||||||
};
|
|
||||||
|
|
||||||
const clientOptions: LanguageClientOptions = {
|
|
||||||
documentSelector: [{ scheme: 'file', language: 'bux' }],
|
|
||||||
synchronize: {
|
|
||||||
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.bux')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
client = new LanguageClient(
|
|
||||||
'bux-lsp',
|
|
||||||
'Bux Language Server',
|
|
||||||
serverOptions,
|
|
||||||
clientOptions
|
|
||||||
);
|
);
|
||||||
|
|
||||||
client.start();
|
await startClient();
|
||||||
vscode.window.showInformationMessage('Bux LSP started');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate(): Thenable<void> | undefined {
|
export function deactivate(): Thenable<void> | undefined {
|
||||||
if (!client) {
|
return stopClient();
|
||||||
return undefined;
|
}
|
||||||
}
|
|
||||||
return client.stop();
|
async function restartClient(userInitiated: boolean): Promise<void> {
|
||||||
|
await stopClient();
|
||||||
|
await startClient();
|
||||||
|
if (userInitiated) {
|
||||||
|
vscode.window.setStatusBarMessage('Bux LSP restarted', 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopClient(): Promise<void> {
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const c = client;
|
||||||
|
client = undefined;
|
||||||
|
try {
|
||||||
|
await c.stop();
|
||||||
|
} catch (err) {
|
||||||
|
outputChannel?.appendLine(`[bux] Error stopping client: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startClient(): Promise<void> {
|
||||||
|
const config = vscode.workspace.getConfiguration(SETTING_SECTION);
|
||||||
|
const enabled = config.get<boolean>('lsp.enabled', true);
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
setStatus('off', 'Bux LSP disabled');
|
||||||
|
outputChannel?.appendLine('[bux] LSP disabled in settings (bux.lsp.enabled)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredPath = config.get<string>('lsp.path', 'bux-lsp') || 'bux-lsp';
|
||||||
|
const resolved = resolveLspPath(configuredPath);
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
setStatus('error', 'bux-lsp not found');
|
||||||
|
const msg =
|
||||||
|
'Bux LSP binary not found. Build with `make lsp` (produces tools/bux-lsp) ' +
|
||||||
|
'or set bux.lsp.path to the full path of the binary.';
|
||||||
|
outputChannel?.appendLine(`[bux] ${msg}`);
|
||||||
|
outputChannel?.appendLine(`[bux] Configured path: ${configuredPath}`);
|
||||||
|
const choice = await vscode.window.showWarningMessage(
|
||||||
|
'Bux: language server (bux-lsp) not found',
|
||||||
|
'Open Output',
|
||||||
|
'Open Settings'
|
||||||
|
);
|
||||||
|
if (choice === 'Open Output') {
|
||||||
|
outputChannel?.show(true);
|
||||||
|
} else if (choice === 'Open Settings') {
|
||||||
|
await vscode.commands.executeCommand('workbench.action.openSettings', 'bux.lsp');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputChannel?.appendLine(`[bux] Using LSP binary: ${resolved}`);
|
||||||
|
setStatus('starting', 'Starting Bux LSP…');
|
||||||
|
|
||||||
|
const serverOptions: ServerOptions = {
|
||||||
|
command: resolved,
|
||||||
|
transport: TransportKind.stdio,
|
||||||
|
options: { env: process.env },
|
||||||
|
};
|
||||||
|
|
||||||
|
const clientOptions: LanguageClientOptions = {
|
||||||
|
documentSelector: [
|
||||||
|
{ scheme: 'file', language: 'bux' },
|
||||||
|
{ scheme: 'untitled', language: 'bux' },
|
||||||
|
],
|
||||||
|
synchronize: {
|
||||||
|
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.{bux,toml}'),
|
||||||
|
},
|
||||||
|
outputChannel,
|
||||||
|
revealOutputChannelOn: RevealOutputChannelOn.Error,
|
||||||
|
traceOutputChannel: outputChannel,
|
||||||
|
};
|
||||||
|
|
||||||
|
client = new LanguageClient('bux-lsp', 'Bux Language Server', serverOptions, clientOptions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.start();
|
||||||
|
setStatus('ready', `Bux LSP ready (${path.basename(resolved)})`);
|
||||||
|
outputChannel?.appendLine('[bux] Language server started');
|
||||||
|
} catch (err) {
|
||||||
|
setStatus('error', 'Bux LSP failed to start');
|
||||||
|
outputChannel?.appendLine(`[bux] Failed to start language server: ${err}`);
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
`Bux LSP failed to start: ${err instanceof Error ? err.message : String(err)}`
|
||||||
|
);
|
||||||
|
client = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve bux-lsp:
|
||||||
|
* 1. Configured absolute / relative path
|
||||||
|
* 2. Workspace tools/bux-lsp (and nearby monorepo parents)
|
||||||
|
* 3. Command on PATH
|
||||||
|
*/
|
||||||
|
function resolveLspPath(configured: string): string | undefined {
|
||||||
|
const candidates: string[] = [];
|
||||||
|
|
||||||
|
if (path.isAbsolute(configured)) {
|
||||||
|
candidates.push(configured);
|
||||||
|
} else if (configured.includes('/') || configured.includes('\\')) {
|
||||||
|
candidates.push(path.resolve(configured));
|
||||||
|
for (const folder of vscode.workspace.workspaceFolders ?? []) {
|
||||||
|
candidates.push(path.join(folder.uri.fsPath, configured));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const folder of vscode.workspace.workspaceFolders ?? []) {
|
||||||
|
const root = folder.uri.fsPath;
|
||||||
|
candidates.push(
|
||||||
|
path.join(root, 'tools', 'bux-lsp'),
|
||||||
|
path.join(root, 'tools', 'bux-lsp.exe'),
|
||||||
|
path.join(root, 'bin', 'bux-lsp'),
|
||||||
|
path.join(root, 'bux-lsp'),
|
||||||
|
path.resolve(root, '..', 'tools', 'bux-lsp'),
|
||||||
|
path.resolve(root, '..', '..', 'tools', 'bux-lsp')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of candidates) {
|
||||||
|
if (isExecutable(c)) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bare name: search PATH
|
||||||
|
const bare = configured.includes('/') || configured.includes('\\') ? 'bux-lsp' : configured;
|
||||||
|
return findInPath(bare);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findInPath(cmd: string): string | undefined {
|
||||||
|
const pathEnv = process.env.PATH ?? process.env.Path ?? '';
|
||||||
|
const sep = process.platform === 'win32' ? ';' : ':';
|
||||||
|
const exts =
|
||||||
|
process.platform === 'win32'
|
||||||
|
? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';')
|
||||||
|
: [''];
|
||||||
|
|
||||||
|
for (const dir of pathEnv.split(sep)) {
|
||||||
|
if (!dir) continue;
|
||||||
|
for (const ext of exts) {
|
||||||
|
const candidate = path.join(dir, cmd + ext);
|
||||||
|
if (isExecutable(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExecutable(filePath: string): boolean {
|
||||||
|
try {
|
||||||
|
const st = fs.statSync(filePath);
|
||||||
|
if (!st.isFile()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fs.accessSync(filePath, fs.constants.X_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(state: 'ready' | 'starting' | 'error' | 'off', text: string): void {
|
||||||
|
if (!statusBar) return;
|
||||||
|
switch (state) {
|
||||||
|
case 'ready':
|
||||||
|
statusBar.text = '$(check) Bux';
|
||||||
|
statusBar.backgroundColor = undefined;
|
||||||
|
break;
|
||||||
|
case 'starting':
|
||||||
|
statusBar.text = '$(sync~spin) Bux';
|
||||||
|
statusBar.backgroundColor = undefined;
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
statusBar.text = '$(error) Bux';
|
||||||
|
statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
|
||||||
|
break;
|
||||||
|
case 'off':
|
||||||
|
statusBar.text = '$(circle-slash) Bux';
|
||||||
|
statusBar.backgroundColor = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
statusBar.tooltip = text;
|
||||||
|
statusBar.show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,33 +3,19 @@
|
|||||||
"name": "Bux",
|
"name": "Bux",
|
||||||
"scopeName": "source.bux",
|
"scopeName": "source.bux",
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{ "include": "#comments" },
|
||||||
"include": "#comments"
|
{ "include": "#attributes" },
|
||||||
},
|
{ "include": "#macros" },
|
||||||
{
|
{ "include": "#strings" },
|
||||||
"include": "#strings"
|
{ "include": "#chars" },
|
||||||
},
|
{ "include": "#keywords" },
|
||||||
{
|
{ "include": "#types" },
|
||||||
"include": "#keywords"
|
{ "include": "#constants" },
|
||||||
},
|
{ "include": "#lifetimes" },
|
||||||
{
|
{ "include": "#functions" },
|
||||||
"include": "#types"
|
{ "include": "#paths" },
|
||||||
},
|
{ "include": "#numbers" },
|
||||||
{
|
{ "include": "#operators" }
|
||||||
"include": "#functions"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#constants"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#operators"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#numbers"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#attributes"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"comments": {
|
"comments": {
|
||||||
@@ -37,20 +23,129 @@
|
|||||||
{
|
{
|
||||||
"name": "comment.block.bux",
|
"name": "comment.block.bux",
|
||||||
"begin": "/\\*",
|
"begin": "/\\*",
|
||||||
"end": "\\*/"
|
"end": "\\*/",
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#comments" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "comment.line.bux",
|
"name": "comment.line.double-slash.bux",
|
||||||
"match": "//.*$"
|
"match": "//.*$"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"attributes": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "meta.attribute.bux",
|
||||||
|
"begin": "@\\[",
|
||||||
|
"end": "\\]",
|
||||||
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.attribute.bux" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.attribute.bux" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "storage.modifier.attribute.bux",
|
||||||
|
"match": "\\b(Checked|Release|Shared|Import|Drop)\\b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "storage.modifier.attribute.bux",
|
||||||
|
"match": "@(Checked|Release|Shared|Import|Drop)\\b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"macros": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "meta.macro.definition.bux",
|
||||||
|
"begin": "\\b(macro!)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": { "name": "keyword.declaration.macro.bux" },
|
||||||
|
"2": { "name": "entity.name.function.macro.bux" }
|
||||||
|
},
|
||||||
|
"end": "(?<=\\})",
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "$self" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta.macro.invocation.bux",
|
||||||
|
"begin": "\\b([A-Za-z_][A-Za-z0-9_]*!)\\s*(\\()",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": { "name": "entity.name.function.macro.bux" },
|
||||||
|
"2": { "name": "punctuation.section.parens.begin.bux" }
|
||||||
|
},
|
||||||
|
"end": "\\)",
|
||||||
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.section.parens.end.bux" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "$self" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "variable.other.macro.fragment.bux",
|
||||||
|
"match": "\\$([A-Za-z_][A-Za-z0-9_]*)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.macro.repeat.bux",
|
||||||
|
"match": "\\$\\(|\\)\\s*[*+?]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"strings": {
|
"strings": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "string.quoted.double.bux",
|
"name": "string.interpolated.bux",
|
||||||
"begin": "\"",
|
"begin": "f\"",
|
||||||
"end": "\"",
|
"end": "\"",
|
||||||
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.begin.bux" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.end.bux" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "constant.character.escape.bux",
|
||||||
|
"match": "\\\\[ntr\\\\\"'{}]|\\\\u\\{[0-9a-fA-F]+\\}|\\\\x[0-9a-fA-F]{2}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta.interpolation.bux",
|
||||||
|
"begin": "\\{",
|
||||||
|
"end": "\\}",
|
||||||
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.section.embedded.begin.bux" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.section.embedded.end.bux" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#keywords" },
|
||||||
|
{ "include": "#types" },
|
||||||
|
{ "include": "#constants" },
|
||||||
|
{ "include": "#numbers" },
|
||||||
|
{ "include": "#functions" },
|
||||||
|
{ "include": "#operators" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "string.quoted.double.cstr.bux",
|
||||||
|
"begin": "c(8|16|32)\"",
|
||||||
|
"end": "\"",
|
||||||
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.begin.bux" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.end.bux" }
|
||||||
|
},
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "constant.character.escape.bux",
|
"name": "constant.character.escape.bux",
|
||||||
@@ -59,9 +154,40 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "string.quoted.backtick.bux",
|
"name": "string.quoted.double.bux",
|
||||||
|
"begin": "\"",
|
||||||
|
"end": "\"",
|
||||||
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.begin.bux" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.end.bux" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "constant.character.escape.bux",
|
||||||
|
"match": "\\\\[ntr\\\\\"']|\\\\u\\{[0-9a-fA-F]+\\}|\\\\x[0-9a-fA-F]{2}|\\\\."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "string.quoted.raw.bux",
|
||||||
"begin": "`",
|
"begin": "`",
|
||||||
"end": "`"
|
"end": "`",
|
||||||
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.begin.bux" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.end.bux" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"chars": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "string.quoted.single.bux",
|
||||||
|
"match": "c?(8|16|32)?'(\\\\.|[^'\\\\])'"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -69,55 +195,87 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "keyword.control.bux",
|
"name": "keyword.control.bux",
|
||||||
"match": "\\b(if|else|while|for|do|loop|in|break|continue|return|match|case|default|spawn|async|await|try|catch|throw|discard)\\b"
|
"match": "\\b(if|else|while|for|do|loop|in|break|continue|return|match|switch|case|default|spawn|async|await|try|catch|throw|discard|defer)\\b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "keyword.declaration.bux",
|
"name": "keyword.declaration.bux",
|
||||||
"match": "\\b(func|var|let|const|type|struct|enum|union|interface|extend|module|import|extern|pub)\\b"
|
"match": "\\b(func|var|let|const|type|struct|enum|union|interface|extend|module|import|extern|pub|macro)\\b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "keyword.operator.bux",
|
"name": "keyword.operator.word.bux",
|
||||||
"match": "\\b(as|is|sizeof|comptime|static_assert|dyn)\\b"
|
"match": "\\b(as|is|sizeof|comptime|static_assert|dyn)\\b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "keyword.ownership.bux",
|
"name": "keyword.ownership.bux",
|
||||||
"match": "\\b(own|mut|borrow)\\b"
|
"match": "\\b(own|mut|borrow|checked)\\b"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "storage.modifier.bux",
|
|
||||||
"match": "@\\[(Checked|Shared|Import)\\]"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"types": {
|
"types": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "support.type.bux",
|
"name": "support.type.primitive.bux",
|
||||||
"match": "\\b(int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|float32|float64|bool|char8|char32|String|void)\\b"
|
"match": "\\b(int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|float32|float64|bool|char8|char16|char32|String|void)\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "support.type.stdlib.bux",
|
||||||
|
"match": "\\b(Result|Option|Array|Map|Set|Slice|Channel|TaskHandle)\\b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "entity.name.type.bux",
|
"name": "entity.name.type.bux",
|
||||||
"match": "\\b[A-Z][a-zA-Z0-9_]*\\b"
|
"match": "\\b[A-Z][A-Za-z0-9_]*\\b"
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"functions": {
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"name": "entity.name.function.bux",
|
|
||||||
"match": "\\b([a-z_][a-zA-Z0-9_]*)(?=\\s*\\()"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"constants": {
|
"constants": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "constant.language.bux",
|
"name": "constant.language.boolean.bux",
|
||||||
"match": "\\b(true|false|null)\\b"
|
"match": "\\b(true|false)\\b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "constant.numeric.bux",
|
"name": "constant.language.null.bux",
|
||||||
"match": "\\b(true|false|null|self|super)\\b"
|
"match": "\\bnull\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "variable.language.bux",
|
||||||
|
"match": "\\b(self|super)\\b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lifetimes": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "storage.modifier.lifetime.bux",
|
||||||
|
"match": "'[A-Za-z_][A-Za-z0-9_]*\\b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"functions": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "meta.function.declaration.bux",
|
||||||
|
"match": "\\b(func|async\\s+func)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "keyword.declaration.bux" },
|
||||||
|
"2": { "name": "entity.name.function.bux" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entity.name.function.bux",
|
||||||
|
"match": "\\b([A-Za-z_][A-Za-z0-9_]*)(?=\\s*[<(])"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "meta.path.bux",
|
||||||
|
"match": "\\b([A-Za-z_][A-Za-z0-9_]*)\\s*(::)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "entity.name.namespace.bux" },
|
||||||
|
"2": { "name": "punctuation.accessor.bux" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -125,31 +283,67 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "constant.numeric.float.bux",
|
"name": "constant.numeric.float.bux",
|
||||||
"match": "\\b[0-9]+\\.[0-9]+(?:[eE][+-]?[0-9]+)?\\b"
|
"match": "\\b[0-9]+\\.[0-9]+(?:[eE][+-]?[0-9]+)?(?:f32|f64)?\\b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "constant.numeric.hex.bux",
|
"name": "constant.numeric.hex.bux",
|
||||||
"match": "\\b0[xX][0-9a-fA-F]+\\b"
|
"match": "\\b0[xX][0-9a-fA-F_]+(?:[iu](?:8|16|32|64)?)?\\b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "constant.numeric.bux",
|
"name": "constant.numeric.octal.bux",
|
||||||
"match": "\\b[0-9]+\\b"
|
"match": "\\b0[oO][0-7_]+(?:[iu](?:8|16|32|64)?)?\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "constant.numeric.binary.bux",
|
||||||
|
"match": "\\b0[bB][01_]+(?:[iu](?:8|16|32|64)?)?\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "constant.numeric.integer.bux",
|
||||||
|
"match": "\\b[0-9][0-9_]*(?:[iu](?:8|16|32|64)?|f32|f64)?\\b"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"operators": {
|
"operators": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "keyword.operator.bux",
|
"name": "keyword.operator.arrow.bux",
|
||||||
"match": "->|=>|==|!=|<=|>=|&&|\\|\\||[+\\-*/%<>=!&|^~@]"
|
"match": "->|=>"
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"attributes": {
|
|
||||||
"patterns": [
|
|
||||||
{
|
{
|
||||||
"name": "storage.modifier.bux",
|
"name": "keyword.operator.range.bux",
|
||||||
"match": "@(Checked|Shared|Import)"
|
"match": "\\.\\.=|\\.\\.\\.|\\.\\."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.comparison.bux",
|
||||||
|
"match": "==|!=|<=|>=|<|>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.logical.bux",
|
||||||
|
"match": "&&|\\|\\||!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.assignment.bux",
|
||||||
|
"match": "\\+=|-=|\\*=|/=|%=|=|:=|\\*\\*="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.arithmetic.bux",
|
||||||
|
"match": "\\*\\*|[+\\-*/%]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.bitwise.bux",
|
||||||
|
"match": "&|\\||\\^|~|<<|>>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.try.bux",
|
||||||
|
"match": "\\?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.borrow.bux",
|
||||||
|
"match": "&mut\\b|&"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "punctuation.accessor.bux",
|
||||||
|
"match": "::|\\."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020"],
|
||||||
"outDir": "out",
|
"outDir": "out",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"types": ["node", "vscode"]
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", ".vscode-test"]
|
"exclude": ["node_modules", ".vscode-test", "out"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user