feat: lifetime elision, tooling CI, registry, and LSP locals
Ship the QUALITY_PLAN stretch from ownership through ecosystem: C.1 lifetime elision (bootstrap + selfhost), bux fmt/test/doc CI hooks, stdlib goldens, package registry (bux search/add), and LSP 0.4 position-sensitive locals with inferred let types. Full-tree format pass plus Map/Set remove double-free fix.
This commit is contained in:
@@ -3,9 +3,9 @@ SRC := bootstrap/main.nim
|
|||||||
OUT := buxc
|
OUT := buxc
|
||||||
BUILD_DIR := build
|
BUILD_DIR := build
|
||||||
|
|
||||||
EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked drop_early_return ctfe 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
|
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 drop_early_return lifetime_elision ctfe 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
|
||||||
|
|
||||||
.PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors selfhost-loop lsp
|
.PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ dev:
|
|||||||
debug: dev
|
debug: dev
|
||||||
@echo "Debug binary: buxc_debug"
|
@echo "Debug binary: buxc_debug"
|
||||||
|
|
||||||
test: build test-examples test-errors
|
test: build fmt-check test-examples test-errors test-stdlib
|
||||||
@echo "Running lexer tests..."
|
@echo "Running lexer tests..."
|
||||||
$(NIM) c -r tests/lexer_test.nim
|
$(NIM) c -r tests/lexer_test.nim
|
||||||
@echo "Running parser tests..."
|
@echo "Running parser tests..."
|
||||||
@@ -105,6 +105,43 @@ test-errors: build
|
|||||||
@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)
|
||||||
|
|
||||||
|
test-stdlib: build
|
||||||
|
@echo "=== Stdlib golden tests ==="
|
||||||
|
@chmod +x tests/stdlib_golden/run.sh
|
||||||
|
@tests/stdlib_golden/run.sh ./$(OUT)
|
||||||
|
|
||||||
|
# Generate stdlib API docs from /// comments → docs/api/stdlib.md
|
||||||
|
docs: build
|
||||||
|
@mkdir -p docs/api
|
||||||
|
@./$(OUT) doc --out docs/api/stdlib.md lib/
|
||||||
|
@echo "docs/api/stdlib.md updated"
|
||||||
|
|
||||||
|
# CI: full-tree format check (lib / examples / src / tests / apps) + dirty-path smoke.
|
||||||
|
fmt-check: build
|
||||||
|
@echo "=== fmt --check (full tree) ==="
|
||||||
|
@./$(OUT) fmt --check lib/
|
||||||
|
@./$(OUT) fmt --check examples/
|
||||||
|
@./$(OUT) fmt --check src/
|
||||||
|
@./$(OUT) fmt --check tests/
|
||||||
|
@./$(OUT) fmt --check apps/
|
||||||
|
@echo "=== fmt --check dirty-path smoke ==="
|
||||||
|
@mkdir -p /tmp/bux_fmt_smoke
|
||||||
|
@printf 'func Main() -> int {\nreturn 0;\n}\n' > /tmp/bux_fmt_smoke/bad.bux
|
||||||
|
@if ./$(OUT) fmt --check /tmp/bux_fmt_smoke/bad.bux >/dev/null 2>&1; then \
|
||||||
|
echo "error: expected --check to fail on dirty file"; exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "fmt --check passed (tree clean + dirty exits 1)"
|
||||||
|
|
||||||
|
# One-shot reformat of the same trees (run before committing style-only fixes)
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt: build
|
||||||
|
@./$(OUT) fmt lib/
|
||||||
|
@./$(OUT) fmt examples/
|
||||||
|
@./$(OUT) fmt src/
|
||||||
|
@./$(OUT) fmt tests/
|
||||||
|
@./$(OUT) fmt apps/
|
||||||
|
@echo "Formatted lib/ examples/ src/ tests/ apps/"
|
||||||
|
|
||||||
selfhost-loop: build
|
selfhost-loop: build
|
||||||
@echo "=== Selfhost loop: bootstrap determinism check ==="
|
@echo "=== Selfhost loop: bootstrap determinism check ==="
|
||||||
@echo "Build A..."
|
@echo "Build A..."
|
||||||
@@ -145,3 +182,17 @@ 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
|
||||||
|
|
||||||
|
.PHONY: test-lsp
|
||||||
|
test-lsp: lsp
|
||||||
|
@echo "=== LSP unit (locals / inference) ==="
|
||||||
|
$(NIM) r --path:bootstrap tools/test_lsp_locals.nim
|
||||||
|
@echo "=== LSP hover smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_hover.sh
|
||||||
|
@tools/smoke_lsp_hover.sh
|
||||||
|
|
||||||
|
.PHONY: test-registry
|
||||||
|
test-registry: build
|
||||||
|
@echo "=== Registry smoke (E.1) ==="
|
||||||
|
@chmod +x tools/smoke_registry.sh
|
||||||
|
@tools/smoke_registry.sh
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ func ReadFile(path: String) -> Result<String, IoError> {
|
|||||||
| `8.2.1` `own` keyword | ✅ | `own T` parsed and resolves to `T`; ready for borrow checker integration |
|
| `8.2.1` `own` keyword | ✅ | `own T` parsed and resolves to `T`; ready for borrow checker integration |
|
||||||
| `8.2.2` `borrow` / `&` | ✅ | `&T` shared reference type checked and enforced |
|
| `8.2.2` `borrow` / `&` | ✅ | `&T` shared reference type checked and enforced |
|
||||||
| `8.2.3` `mut` references | ✅ | `&mut T` mutable reference type checked and enforced |
|
| `8.2.3` `mut` references | ✅ | `&mut T` mutable reference type checked and enforced |
|
||||||
| `8.2.4` Lifetime elision | ⏳ | Simple rules for common cases; explicit `'a` for complex |
|
| `8.2.4` Lifetime elision | ✅ | Single-input elision + dangling return; explicit `'a` for multi-input |
|
||||||
| `8.2.5` Opt-in checker | ✅ | `@[Checked]` attribute enables borrow checking: writes through `&T` are rejected |
|
| `8.2.5` Opt-in checker | ✅ | `@[Checked]` attribute enables borrow checking: writes through `&T` are rejected |
|
||||||
|
|
||||||
```bux
|
```bux
|
||||||
@@ -681,8 +681,8 @@ buxc2 == buxc3 ✅ (binary-identical)
|
|||||||
| `10.2.3` `&mut T` exclusive mutable check | ✅ | No aliasing of mutable refs |
|
| `10.2.3` `&mut T` exclusive mutable check | ✅ | No aliasing of mutable refs |
|
||||||
| `10.2.4` Bounds checking on slices | ✅ | `Slice_Get` / `Array_Get` with `bux_bounds_check` |
|
| `10.2.4` Bounds checking on slices | ✅ | `Slice_Get` / `Array_Get` with `bux_bounds_check` |
|
||||||
| `10.2.5` `@[Release]` zero-cost mode | ✅ | Disables borrow + bounds checks, passes `-O3 -flto` |
|
| `10.2.5` `@[Release]` zero-cost mode | ✅ | Disables borrow + bounds checks, passes `-O3 -flto` |
|
||||||
| `10.2.6` Lifetime elision (simple rules) | ⏳ | 80% of cases without annotations |
|
| `10.2.6` Lifetime elision (simple rules) | ✅ | Single-input elision; multi-input requires `'a` |
|
||||||
| `10.2.7` Explicit lifetimes `'a` | ⏳ | Only for complex cases |
|
| `10.2.7` Explicit lifetimes `'a` | ✅ | Parsed + checked; multi-input + mismatch |
|
||||||
|
|
||||||
### 10.3 — Compiler Architecture Upgrade (v0.6.0 target)
|
### 10.3 — Compiler Architecture Upgrade (v0.6.0 target)
|
||||||
|
|
||||||
|
|||||||
@@ -246,7 +246,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`, LSP (`tools/lsp_server.nim` + `buxc check`) |
|
| **Tooling** | `bux new/build/run/test/check/fmt/doc`, LSP 0.4.0 (locals + inferred lets) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -299,6 +299,8 @@ bux/
|
|||||||
| [`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, and tooling |
|
||||||
| [`docs/QUALITY_PLAN.md`](docs/QUALITY_PLAN.md) | Roadmap toward a “good” v1.0 |
|
| [`docs/QUALITY_PLAN.md`](docs/QUALITY_PLAN.md) | Roadmap toward a “good” v1.0 |
|
||||||
|
| [`docs/Packages.md`](docs/Packages.md) | Package manager + registry |
|
||||||
|
| [`docs/SEMVER.md`](docs/SEMVER.md) | Versioning policy |
|
||||||
| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Feature status (constructs) |
|
| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Feature status (constructs) |
|
||||||
| [`PLAN.md`](PLAN.md) | Long-form phase plan |
|
| [`PLAN.md`](PLAN.md) | Long-form phase plan |
|
||||||
|
|
||||||
@@ -319,14 +321,33 @@ make test-errors
|
|||||||
# Full unit + example suite
|
# Full unit + example suite
|
||||||
make test
|
make test
|
||||||
|
|
||||||
|
# Full-tree format check (lib/ examples/ src/ tests/ apps/)
|
||||||
|
make fmt-check
|
||||||
|
# Reformat those trees
|
||||||
|
make fmt
|
||||||
|
|
||||||
|
# Stdlib behavioral goldens (Array / String / collections)
|
||||||
|
make test-stdlib
|
||||||
|
|
||||||
|
# Generate stdlib API docs from /// comments
|
||||||
|
make docs
|
||||||
|
|
||||||
# Build self-hosted compiler (Bux → C → native)
|
# Build self-hosted compiler (Bux → C → native)
|
||||||
make selfhost
|
make selfhost
|
||||||
|
|
||||||
# Run all tests
|
# Package tests (filter + summary table)
|
||||||
make test
|
./buxc test --filter first _test_runner
|
||||||
|
|
||||||
# Run example programs
|
# Format / CI format check
|
||||||
make test-examples
|
./buxc fmt path/to/file.bux
|
||||||
|
./buxc fmt --check path/
|
||||||
|
|
||||||
|
# API docs
|
||||||
|
./buxc doc --out docs/api/stdlib.md lib/
|
||||||
|
|
||||||
|
# Package registry
|
||||||
|
./buxc search greet
|
||||||
|
make test-registry # add greet → install → build temp app
|
||||||
|
|
||||||
# Verify selfhost binary parity (buxc2 → buxc3, identical)
|
# Verify selfhost binary parity (buxc2 → buxc3, identical)
|
||||||
make selfhost-loop
|
make selfhost-loop
|
||||||
|
|||||||
+323
-15
@@ -1,6 +1,9 @@
|
|||||||
import std/[os, strutils, terminal, strformat, osproc, sets]
|
import std/[os, strutils, terminal, strformat, osproc, sets, algorithm, tables]
|
||||||
import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend
|
import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend
|
||||||
import source_location
|
import source_location
|
||||||
|
import fmt
|
||||||
|
import docgen
|
||||||
|
import registry
|
||||||
|
|
||||||
type
|
type
|
||||||
ColorMode* = enum
|
ColorMode* = enum
|
||||||
@@ -21,16 +24,25 @@ Usage: bux [options] <command> [command-options]
|
|||||||
Commands:
|
Commands:
|
||||||
new <name> Create a new Bux package
|
new <name> Create a new Bux package
|
||||||
init Initialize a Bux package in the current directory
|
init Initialize a Bux package in the current directory
|
||||||
add <name> [ver] Add a dependency (--path, --git)
|
add <name> [ver] Add a dependency (--path, --git, or registry)
|
||||||
install Resolve and install dependencies
|
install Resolve and install dependencies
|
||||||
|
search [query] Search the package registry
|
||||||
build Build the current package
|
build Build the current package
|
||||||
run Build and run the current package
|
run Build and run the current package
|
||||||
test Run tests in tests/ directory
|
test Run tests in tests/ directory
|
||||||
check Type-check the current package
|
check Type-check the current package
|
||||||
|
fmt [path] Format .bux sources (default: .)
|
||||||
|
doc [path] Generate Markdown API docs from /// comments
|
||||||
clean Remove build artifacts
|
clean Remove build artifacts
|
||||||
help Show this help message
|
help Show this help message
|
||||||
version Show version
|
version Show version
|
||||||
|
|
||||||
|
Command options:
|
||||||
|
test --filter <s> Only run tests whose name contains <s>
|
||||||
|
fmt --check Exit 1 if any file would be reformatted (CI)
|
||||||
|
doc --out <file> Write docs to file (default: stdout)
|
||||||
|
add --path / --git Explicit source; else resolve via registry
|
||||||
|
|
||||||
Global options:
|
Global options:
|
||||||
--color <auto|on|off> Control colored output (default: auto)
|
--color <auto|on|off> Control colored output (default: auto)
|
||||||
-q, --quiet Suppress non-error output
|
-q, --quiet Suppress non-error output
|
||||||
@@ -192,6 +204,14 @@ proc hintForMessage(msg: string): string =
|
|||||||
return "provide the missing argument (positional or named)"
|
return "provide the missing argument (positional or named)"
|
||||||
if "use of moved value" in m:
|
if "use of moved value" in m:
|
||||||
return "the value was moved; clone it or restructure ownership"
|
return "the value was moved; clone it or restructure ownership"
|
||||||
|
if "cannot return reference to local" in m:
|
||||||
|
return "return a value, or return a reference borrowed from a function parameter"
|
||||||
|
if "lifetime elision failed" in m:
|
||||||
|
return "add an explicit lifetime, e.g. func F<'a>(x: &'a T, y: &'a U) -> &'a T"
|
||||||
|
if "lifetime mismatch" in m:
|
||||||
|
return "returned reference must share a lifetime with the return type (annotate with 'a)"
|
||||||
|
if "no input reference to borrow from" in m:
|
||||||
|
return "add a '&T' parameter to borrow from, or return an owned value"
|
||||||
if "shared reference" in m or "checked function" in m:
|
if "shared reference" in m or "checked function" in m:
|
||||||
return "use '&mut T' for mutation, or drop @[Checked] for unchecked code"
|
return "use '&mut T' for mutation, or drop @[Checked] for unchecked code"
|
||||||
if "double mutable borrow" in m or "already mutably borrowed" in m:
|
if "double mutable borrow" in m or "already mutably borrowed" in m:
|
||||||
@@ -378,7 +398,11 @@ proc cmdAdd*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
printError("--git requires a value", useColor)
|
printError("--git requires a value", useColor)
|
||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
|
if not args[i].startsWith("-"):
|
||||||
version = args[i]
|
version = args[i]
|
||||||
|
else:
|
||||||
|
printError(&"unknown add option '{args[i]}'", useColor)
|
||||||
|
return 1
|
||||||
inc i
|
inc i
|
||||||
# Append to bux.toml
|
# Append to bux.toml
|
||||||
var depLine = ""
|
var depLine = ""
|
||||||
@@ -387,7 +411,19 @@ proc cmdAdd*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
elif gitUrl.len > 0:
|
elif gitUrl.len > 0:
|
||||||
depLine = &"{depName} = {{ Version = \"{version}\", Source = \"{gitUrl}\" }}"
|
depLine = &"{depName} = {{ Version = \"{version}\", Source = \"{gitUrl}\" }}"
|
||||||
else:
|
else:
|
||||||
depLine = &"{depName} = \"{version}\""
|
# Registry resolve (E.1)
|
||||||
|
let reg = loadRegistry()
|
||||||
|
if reg.path.len == 0:
|
||||||
|
printError("no package registry found (set BUX_REGISTRY or install config/registry.toml)", useColor)
|
||||||
|
return 1
|
||||||
|
let pkg = registryLookup(reg, depName, version)
|
||||||
|
if pkg.name.len == 0:
|
||||||
|
printError(&"package '{depName}' not found in registry ({reg.path})", useColor)
|
||||||
|
printError("hint: bux search | bux add name --git <url> | bux add name --path <dir>", useColor)
|
||||||
|
return 1
|
||||||
|
depLine = formatRegistryDepLine(depName, pkg)
|
||||||
|
if not opts.quiet:
|
||||||
|
printInfo(&"Resolved '{depName}' {pkg.version} from registry {reg.path}", useColor)
|
||||||
var content = readFile(manifestPath)
|
var content = readFile(manifestPath)
|
||||||
# Ensure [Dependencies] section exists
|
# Ensure [Dependencies] section exists
|
||||||
if content.find("[Dependencies]") < 0:
|
if content.find("[Dependencies]") < 0:
|
||||||
@@ -399,6 +435,34 @@ proc cmdAdd*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
printInfo(&"Added dependency '{depName}' to bux.toml", useColor)
|
printInfo(&"Added dependency '{depName}' to bux.toml", useColor)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
proc cmdSearch*(args: seq[string], opts: GlobalOptions): int =
|
||||||
|
let useColor = shouldUseColor(opts)
|
||||||
|
let query = if args.len > 0: args[0] else: ""
|
||||||
|
let reg = loadRegistry()
|
||||||
|
if reg.path.len == 0:
|
||||||
|
printError("no package registry found (set BUX_REGISTRY)", useColor)
|
||||||
|
return 1
|
||||||
|
if not opts.quiet:
|
||||||
|
echo &"Registry: {reg.path}"
|
||||||
|
let hits = registrySearch(reg, query)
|
||||||
|
if hits.len == 0:
|
||||||
|
if not opts.quiet:
|
||||||
|
echo "No packages matched."
|
||||||
|
return 1
|
||||||
|
# Dedupe by name showing latest version
|
||||||
|
var seen = initTable[string, RegistryPackage]()
|
||||||
|
for p in hits:
|
||||||
|
seen[p.name.toLowerAscii()] = p
|
||||||
|
var names: seq[string] = @[]
|
||||||
|
for k in seen.keys:
|
||||||
|
names.add(k)
|
||||||
|
names.sort(system.cmp)
|
||||||
|
for k in names:
|
||||||
|
let p = seen[k]
|
||||||
|
let desc = if p.description.len > 0: p.description else: p.source
|
||||||
|
echo &" {p.name} {p.version} — {desc}"
|
||||||
|
return 0
|
||||||
|
|
||||||
proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
||||||
let useColor = shouldUseColor(opts)
|
let useColor = shouldUseColor(opts)
|
||||||
let root = getCurrentDir()
|
let root = getCurrentDir()
|
||||||
@@ -411,6 +475,7 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
let cacheDir = getHomeDir() / ".bux" / "packages"
|
let cacheDir = getHomeDir() / ".bux" / "packages"
|
||||||
if not dirExists(cacheDir):
|
if not dirExists(cacheDir):
|
||||||
createDir(cacheDir)
|
createDir(cacheDir)
|
||||||
|
let reg = loadRegistry()
|
||||||
# Resolve each dependency
|
# Resolve each dependency
|
||||||
for dep in man.dependencies:
|
for dep in man.dependencies:
|
||||||
case dep.kind
|
case dep.kind
|
||||||
@@ -433,20 +498,43 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
if not dirExists(depDir):
|
if not dirExists(depDir):
|
||||||
if not opts.quiet:
|
if not opts.quiet:
|
||||||
printInfo(&"Cloning '{dep.name}' from {dep.gitUrl}...", useColor)
|
printInfo(&"Cloning '{dep.name}' from {dep.gitUrl}...", useColor)
|
||||||
let (outp, code) = execCmdEx(&"git clone {dep.gitUrl} {depDir} 2>&1")
|
let (outp, code) = execCmdEx(&"git clone --quiet {quoteShell(dep.gitUrl)} {quoteShell(depDir)} 2>&1")
|
||||||
if code != 0:
|
if code != 0:
|
||||||
printError(&"failed to clone {dep.gitUrl}: {outp}", useColor)
|
printError(&"failed to clone {dep.gitUrl}: {outp}", useColor)
|
||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
if not opts.quiet:
|
if not opts.quiet:
|
||||||
printInfo(&"Using cached '{dep.name}' from {depDir}", useColor)
|
printInfo(&"Using cached '{dep.name}' from {depDir}", useColor)
|
||||||
|
# Lock stores git URL; build loads from cache by name
|
||||||
lock.entries.add(LockEntry(name: dep.name, version: dep.gitVersion, source: dep.gitUrl))
|
lock.entries.add(LockEntry(name: dep.name, version: dep.gitVersion, source: dep.gitUrl))
|
||||||
of dkVersion:
|
of dkVersion:
|
||||||
# For version-based deps without a registry, we just record them
|
# Registry lookup (E.1)
|
||||||
# TODO: lookup in registry
|
if reg.path.len == 0:
|
||||||
lock.entries.add(LockEntry(name: dep.name, version: dep.versionReq, source: "registry"))
|
printError(&"cannot resolve '{dep.name}': no package registry (set BUX_REGISTRY)", useColor)
|
||||||
|
return 1
|
||||||
|
let pkg = registryLookup(reg, dep.name, dep.versionReq)
|
||||||
|
if pkg.name.len == 0:
|
||||||
|
printError(&"package '{dep.name}' not found in registry", useColor)
|
||||||
|
return 1
|
||||||
|
if pkg.resolvedPath.len > 0 and dirExists(pkg.resolvedPath):
|
||||||
|
lock.entries.add(LockEntry(name: dep.name, version: pkg.version, source: pkg.resolvedPath))
|
||||||
if not opts.quiet:
|
if not opts.quiet:
|
||||||
printInfo(&"Recorded dependency '{dep.name}' = {dep.versionReq}", useColor)
|
printInfo(&"Resolved '{dep.name}' {pkg.version} → {pkg.resolvedPath}", useColor)
|
||||||
|
elif isGitSource(pkg.source):
|
||||||
|
let depDir = cacheDir / dep.name
|
||||||
|
if not dirExists(depDir):
|
||||||
|
if not opts.quiet:
|
||||||
|
printInfo(&"Cloning '{dep.name}' from {pkg.source}...", useColor)
|
||||||
|
let (outp, code) = execCmdEx(&"git clone --quiet {quoteShell(pkg.source)} {quoteShell(depDir)} 2>&1")
|
||||||
|
if code != 0:
|
||||||
|
printError(&"failed to clone {pkg.source}: {outp}", useColor)
|
||||||
|
return 1
|
||||||
|
lock.entries.add(LockEntry(name: dep.name, version: pkg.version, source: pkg.source))
|
||||||
|
if not opts.quiet:
|
||||||
|
printInfo(&"Resolved '{dep.name}' {pkg.version} → git {pkg.source}", useColor)
|
||||||
|
else:
|
||||||
|
printError(&"registry entry '{dep.name}' has unusable source '{pkg.source}'", useColor)
|
||||||
|
return 1
|
||||||
# Save lockfile
|
# Save lockfile
|
||||||
let lockPath = root / "bux.lock"
|
let lockPath = root / "bux.lock"
|
||||||
saveLockfile(lockPath, lock)
|
saveLockfile(lockPath, lock)
|
||||||
@@ -722,18 +810,93 @@ proc cmdClean*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
printInfo("clean: build directory removed", useColor)
|
printInfo("clean: build directory removed", useColor)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
proc parseTestArgs(args: seq[string]): tuple[filter: string, paths: seq[string], ok: bool] =
|
||||||
|
## Parse `test` args: optional `--filter <s>` / `--filter=<s>`, rest are ignored paths.
|
||||||
|
result.filter = ""
|
||||||
|
result.paths = @[]
|
||||||
|
result.ok = true
|
||||||
|
var i = 0
|
||||||
|
while i < args.len:
|
||||||
|
let a = args[i]
|
||||||
|
if a == "--filter":
|
||||||
|
if i + 1 >= args.len:
|
||||||
|
stderr.writeLine("error: --filter requires an argument")
|
||||||
|
result.ok = false
|
||||||
|
return
|
||||||
|
inc i
|
||||||
|
result.filter = args[i]
|
||||||
|
elif a.startsWith("--filter="):
|
||||||
|
result.filter = a["--filter=".len .. ^1]
|
||||||
|
elif a == "--help" or a == "-h":
|
||||||
|
echo "Usage: bux test [--filter <name>] [project-dir]"
|
||||||
|
echo " --filter <name> Only run tests whose filename contains <name>"
|
||||||
|
result.ok = false # treat as early exit without error in caller? use special
|
||||||
|
# Signal help via empty filter and a sentinel path
|
||||||
|
result.paths = @["__help__"]
|
||||||
|
return
|
||||||
|
elif a.startsWith("-"):
|
||||||
|
stderr.writeLine(&"error: unknown test option '{a}'")
|
||||||
|
result.ok = false
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
result.paths.add(a)
|
||||||
|
inc i
|
||||||
|
|
||||||
|
proc parseFmtArgs(args: seq[string]): tuple[checkOnly: bool, paths: seq[string], ok: bool, help: bool] =
|
||||||
|
result.checkOnly = false
|
||||||
|
result.paths = @[]
|
||||||
|
result.ok = true
|
||||||
|
result.help = false
|
||||||
|
var i = 0
|
||||||
|
while i < args.len:
|
||||||
|
let a = args[i]
|
||||||
|
if a == "--check":
|
||||||
|
result.checkOnly = true
|
||||||
|
elif a == "--help" or a == "-h":
|
||||||
|
result.help = true
|
||||||
|
return
|
||||||
|
elif a.startsWith("-"):
|
||||||
|
stderr.writeLine(&"error: unknown fmt option '{a}'")
|
||||||
|
result.ok = false
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
result.paths.add(a)
|
||||||
|
inc i
|
||||||
|
|
||||||
proc cmdTest*(args: seq[string], opts: GlobalOptions): int =
|
proc cmdTest*(args: seq[string], opts: GlobalOptions): int =
|
||||||
let useColor = shouldUseColor(opts)
|
let useColor = shouldUseColor(opts)
|
||||||
let root = getCurrentDir()
|
let (filter, paths, ok) = parseTestArgs(args)
|
||||||
|
if not ok:
|
||||||
|
if paths.len == 1 and paths[0] == "__help__":
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
let root = if paths.len > 0: absolutePath(paths[0]) else: getCurrentDir()
|
||||||
let testsDir = root / "tests"
|
let testsDir = root / "tests"
|
||||||
var testFiles: seq[string] = @[]
|
var testFiles: seq[string] = @[]
|
||||||
if dirExists(testsDir):
|
if dirExists(testsDir):
|
||||||
for kind, path in walkDir(testsDir):
|
for kind, path in walkDir(testsDir):
|
||||||
if kind == pcFile and path.endsWith(".bux"):
|
if kind == pcFile and path.endsWith(".bux"):
|
||||||
|
let testName = splitFile(path).name
|
||||||
|
if filter.len > 0 and filter notin testName:
|
||||||
|
continue
|
||||||
testFiles.add(path)
|
testFiles.add(path)
|
||||||
|
testFiles.sort(system.cmp)
|
||||||
if testFiles.len == 0:
|
if testFiles.len == 0:
|
||||||
|
if filter.len > 0:
|
||||||
|
printError(&"no tests matching filter '{filter}' in tests/", useColor)
|
||||||
|
else:
|
||||||
printError("no tests found in tests/ directory", useColor)
|
printError("no tests found in tests/ directory", useColor)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
if not opts.quiet:
|
||||||
|
if filter.len > 0:
|
||||||
|
echo &"Running tests (filter: {filter}) in {testsDir}"
|
||||||
|
else:
|
||||||
|
echo &"Running tests in {testsDir}"
|
||||||
|
echo "┌──────────────────────────────┬────────┐"
|
||||||
|
echo "│ Test │ Status │"
|
||||||
|
echo "├──────────────────────────────┼────────┤"
|
||||||
|
|
||||||
var passed = 0
|
var passed = 0
|
||||||
var failed = 0
|
var failed = 0
|
||||||
for testFile in testFiles:
|
for testFile in testFiles:
|
||||||
@@ -742,26 +905,168 @@ proc cmdTest*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
removeDir(tmpDir)
|
removeDir(tmpDir)
|
||||||
createDir(tmpDir / "src")
|
createDir(tmpDir / "src")
|
||||||
copyFile(testFile, tmpDir / "src" / "Main.bux")
|
copyFile(testFile, tmpDir / "src" / "Main.bux")
|
||||||
writeFile(tmpDir / "bux.toml", "[package]\nname = \"" & testName & "\"\nversion = \"0.1.0\"\n")
|
writeFile(tmpDir / "bux.toml",
|
||||||
|
"[Package]\nName = \"" & testName & "\"\nVersion = \"0.1.0\"\nType = \"bin\"\n\n[Build]\nOutput = \"Bin\"\n")
|
||||||
let buildRes = cmdBuild(@[tmpDir], opts)
|
let buildRes = cmdBuild(@[tmpDir], opts)
|
||||||
|
var status: string
|
||||||
|
var statusOk = false
|
||||||
if buildRes != 0:
|
if buildRes != 0:
|
||||||
printError(&" FAIL {testName} (build)", useColor)
|
status = "FAIL"
|
||||||
failed += 1
|
failed += 1
|
||||||
continue
|
else:
|
||||||
var execFile = tmpDir / "build" / testName
|
var execFile = tmpDir / "build" / testName
|
||||||
if not fileExists(execFile):
|
if not fileExists(execFile):
|
||||||
execFile = tmpDir / "build" / "bux_out"
|
execFile = tmpDir / "build" / "bux_out"
|
||||||
let exitCode = execCmd(execFile)
|
let exitCode = execCmd(execFile)
|
||||||
if exitCode == 0:
|
if exitCode == 0:
|
||||||
printInfo(&" PASS {testName}", useColor)
|
status = "PASS"
|
||||||
|
statusOk = true
|
||||||
passed += 1
|
passed += 1
|
||||||
else:
|
else:
|
||||||
printError(&" FAIL {testName} (exit {exitCode})", useColor)
|
status = &"FAIL:{exitCode}"
|
||||||
failed += 1
|
failed += 1
|
||||||
removeDir(tmpDir)
|
removeDir(tmpDir)
|
||||||
echo &"\nResults: {passed} passed, {failed} failed"
|
|
||||||
|
if not opts.quiet:
|
||||||
|
# Pad name to 28 chars for the table column
|
||||||
|
var nameCol = testName
|
||||||
|
if nameCol.len > 28:
|
||||||
|
nameCol = nameCol[0 .. 24] & "..."
|
||||||
|
else:
|
||||||
|
nameCol = nameCol & repeat(' ', 28 - nameCol.len)
|
||||||
|
var stCol = status
|
||||||
|
if stCol.len < 6:
|
||||||
|
stCol = stCol & repeat(' ', 6 - stCol.len)
|
||||||
|
if useColor:
|
||||||
|
if statusOk:
|
||||||
|
stdout.setForegroundColor(fgGreen)
|
||||||
|
else:
|
||||||
|
stdout.setForegroundColor(fgRed)
|
||||||
|
stdout.writeLine(&"│ {nameCol} │ {stCol} │")
|
||||||
|
stdout.resetAttributes()
|
||||||
|
else:
|
||||||
|
echo &"│ {nameCol} │ {stCol} │"
|
||||||
|
|
||||||
|
if not opts.quiet:
|
||||||
|
echo "└──────────────────────────────┴────────┘"
|
||||||
|
echo &"\nResults: {passed} passed, {failed} failed, {testFiles.len} total"
|
||||||
|
# CI-friendly exit codes: 0 = all pass, 1 = some failed
|
||||||
return if failed > 0: 1 else: 0
|
return if failed > 0: 1 else: 0
|
||||||
|
|
||||||
|
proc cmdFmt*(args: seq[string], opts: GlobalOptions): int =
|
||||||
|
let useColor = shouldUseColor(opts)
|
||||||
|
let (checkOnly, paths, ok, help) = parseFmtArgs(args)
|
||||||
|
if not ok:
|
||||||
|
return 1
|
||||||
|
if help:
|
||||||
|
echo "Usage: bux fmt [--check] [path...]"
|
||||||
|
echo " --check Do not write; exit 1 if any file would be reformatted"
|
||||||
|
echo " path File or directory (default: .)"
|
||||||
|
return 0
|
||||||
|
|
||||||
|
let targets = if paths.len > 0: paths else: @["."]
|
||||||
|
var files: seq[string] = @[]
|
||||||
|
for t in targets:
|
||||||
|
let collected = collectBuxFiles(t)
|
||||||
|
for f in collected:
|
||||||
|
if f notin files:
|
||||||
|
files.add(f)
|
||||||
|
files.sort(system.cmp)
|
||||||
|
|
||||||
|
if files.len == 0:
|
||||||
|
printError("no .bux files found", useColor)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
var changed = 0
|
||||||
|
var failed = 0
|
||||||
|
var unchanged = 0
|
||||||
|
for path in files:
|
||||||
|
let (okf, didChange, msg) = formatFile(path, checkOnly)
|
||||||
|
if not okf:
|
||||||
|
printError(&"{path}: {msg}", useColor)
|
||||||
|
failed += 1
|
||||||
|
continue
|
||||||
|
if didChange:
|
||||||
|
changed += 1
|
||||||
|
if not opts.quiet:
|
||||||
|
if checkOnly:
|
||||||
|
printError(&" would reformat {path}", useColor)
|
||||||
|
else:
|
||||||
|
printInfo(&" formatted {path}", useColor)
|
||||||
|
else:
|
||||||
|
unchanged += 1
|
||||||
|
if opts.verbose and not opts.quiet:
|
||||||
|
echo &" ok {path}"
|
||||||
|
|
||||||
|
if not opts.quiet:
|
||||||
|
if checkOnly:
|
||||||
|
echo &"\nfmt --check: {changed} would reformat, {unchanged} ok, {failed} errors"
|
||||||
|
else:
|
||||||
|
echo &"\nFormatted {changed}/{files.len} files ({unchanged} already clean)"
|
||||||
|
|
||||||
|
if failed > 0:
|
||||||
|
return 1
|
||||||
|
if checkOnly and changed > 0:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
proc cmdDoc*(args: seq[string], opts: GlobalOptions): int =
|
||||||
|
## Generate Markdown docs from `///` / adjacent `/* */` comments.
|
||||||
|
var outPath = ""
|
||||||
|
var paths: seq[string] = @[]
|
||||||
|
var i = 0
|
||||||
|
while i < args.len:
|
||||||
|
let a = args[i]
|
||||||
|
if a == "--out" or a == "-o":
|
||||||
|
if i + 1 >= args.len:
|
||||||
|
stderr.writeLine("error: --out requires a path")
|
||||||
|
return 1
|
||||||
|
inc i
|
||||||
|
outPath = args[i]
|
||||||
|
elif a.startsWith("--out="):
|
||||||
|
outPath = a["--out=".len .. ^1]
|
||||||
|
elif a == "--help" or a == "-h":
|
||||||
|
echo "Usage: bux doc [--out file.md] [path...]"
|
||||||
|
echo " Scans .bux files for /// and /* */ docs preceding declarations."
|
||||||
|
echo " Default path: lib/ (stdlib) when omitted."
|
||||||
|
return 0
|
||||||
|
elif a.startsWith("-"):
|
||||||
|
stderr.writeLine(&"error: unknown doc option '{a}'")
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
paths.add(a)
|
||||||
|
inc i
|
||||||
|
|
||||||
|
if paths.len == 0:
|
||||||
|
# Prefer stdlib if present
|
||||||
|
if dirExists("lib"):
|
||||||
|
paths = @["lib"]
|
||||||
|
else:
|
||||||
|
paths = @["."]
|
||||||
|
|
||||||
|
let items = generateDocs(paths)
|
||||||
|
let title =
|
||||||
|
if paths.len == 1 and paths[0] == "lib": "Bux Standard Library"
|
||||||
|
else: "API Reference"
|
||||||
|
let md = renderMarkdown(items, title)
|
||||||
|
|
||||||
|
if outPath.len > 0:
|
||||||
|
try:
|
||||||
|
let parent = parentDir(outPath)
|
||||||
|
if parent.len > 0 and not dirExists(parent):
|
||||||
|
createDir(parent)
|
||||||
|
writeFile(outPath, md)
|
||||||
|
if not opts.quiet:
|
||||||
|
echo &"Wrote {items.len} documented items → {outPath}"
|
||||||
|
except CatchableError as e:
|
||||||
|
stderr.writeLine("error: " & e.msg)
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
stdout.write(md)
|
||||||
|
if items.len == 0 and not opts.quiet:
|
||||||
|
stderr.writeLine("warning: no /// or /* */ documented declarations found")
|
||||||
|
return 0
|
||||||
|
|
||||||
proc cmdVersion*(args: seq[string], opts: GlobalOptions): int =
|
proc cmdVersion*(args: seq[string], opts: GlobalOptions): int =
|
||||||
echo "bux 0.1.0 (bootstrap)"
|
echo "bux 0.1.0 (bootstrap)"
|
||||||
return 0
|
return 0
|
||||||
@@ -782,10 +1087,13 @@ proc runCli*(args: seq[string]): int =
|
|||||||
of "init": return cmdInit(cmdArgs, opts)
|
of "init": return cmdInit(cmdArgs, opts)
|
||||||
of "add": return cmdAdd(cmdArgs, opts)
|
of "add": return cmdAdd(cmdArgs, opts)
|
||||||
of "install": return cmdInstall(cmdArgs, opts)
|
of "install": return cmdInstall(cmdArgs, opts)
|
||||||
|
of "search": return cmdSearch(cmdArgs, opts)
|
||||||
of "build": return cmdBuild(cmdArgs, opts)
|
of "build": return cmdBuild(cmdArgs, opts)
|
||||||
of "run": return cmdRun(cmdArgs, opts)
|
of "run": return cmdRun(cmdArgs, opts)
|
||||||
of "check": return cmdCheck(cmdArgs, opts)
|
of "check": return cmdCheck(cmdArgs, opts)
|
||||||
of "test": return cmdTest(cmdArgs, opts)
|
of "test": return cmdTest(cmdArgs, opts)
|
||||||
|
of "fmt": return cmdFmt(cmdArgs, opts)
|
||||||
|
of "doc": return cmdDoc(cmdArgs, opts)
|
||||||
of "clean": return cmdClean(cmdArgs, opts)
|
of "clean": return cmdClean(cmdArgs, opts)
|
||||||
of "version", "--version", "-v": return cmdVersion(cmdArgs, opts)
|
of "version", "--version", "-v": return cmdVersion(cmdArgs, opts)
|
||||||
of "help", "--help", "-h":
|
of "help", "--help", "-h":
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
## docgen.nim — Extract `///` (and adjacent `/* */`) docs into Markdown.
|
||||||
|
## Used by `bux doc [path...]`.
|
||||||
|
|
||||||
|
import std/[os, strutils, strformat, algorithm]
|
||||||
|
|
||||||
|
type
|
||||||
|
DocItem* = object
|
||||||
|
kind*: string ## module | func | struct | enum | interface | extern
|
||||||
|
name*: string
|
||||||
|
signature*: string ## first declaration line (trimmed)
|
||||||
|
docs*: string
|
||||||
|
file*: string
|
||||||
|
line*: int
|
||||||
|
|
||||||
|
proc isDeclStart(line: string): bool =
|
||||||
|
let s = line.strip()
|
||||||
|
if s.len == 0: return false
|
||||||
|
# Leading attributes @[Checked] etc. — not a decl by themselves
|
||||||
|
if s.startsWith("@["): return false
|
||||||
|
if s.startsWith("func ") or s.startsWith("pub func ") or
|
||||||
|
s.startsWith("extern func ") or s.startsWith("const func ") or
|
||||||
|
s.startsWith("async func "):
|
||||||
|
return true
|
||||||
|
if s.startsWith("struct ") or s.startsWith("pub struct ") or
|
||||||
|
s.startsWith("enum ") or s.startsWith("pub enum ") or
|
||||||
|
s.startsWith("union ") or s.startsWith("interface ") or
|
||||||
|
s.startsWith("module ") or s.startsWith("type "):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
proc declKindAndName(line: string): tuple[kind, name: string] =
|
||||||
|
var s = line.strip()
|
||||||
|
# Strip leading pub/extern/const/async
|
||||||
|
for prefix in ["pub ", "extern ", "const ", "async "]:
|
||||||
|
if s.startsWith(prefix):
|
||||||
|
s = s[prefix.len .. ^1].strip()
|
||||||
|
var kind = "item"
|
||||||
|
if s.startsWith("func "):
|
||||||
|
kind = "func"
|
||||||
|
s = s["func ".len .. ^1]
|
||||||
|
elif s.startsWith("struct "):
|
||||||
|
kind = "struct"
|
||||||
|
s = s["struct ".len .. ^1]
|
||||||
|
elif s.startsWith("enum "):
|
||||||
|
kind = "enum"
|
||||||
|
s = s["enum ".len .. ^1]
|
||||||
|
elif s.startsWith("union "):
|
||||||
|
kind = "union"
|
||||||
|
s = s["union ".len .. ^1]
|
||||||
|
elif s.startsWith("interface "):
|
||||||
|
kind = "interface"
|
||||||
|
s = s["interface ".len .. ^1]
|
||||||
|
elif s.startsWith("module "):
|
||||||
|
kind = "module"
|
||||||
|
s = s["module ".len .. ^1]
|
||||||
|
elif s.startsWith("type "):
|
||||||
|
kind = "type"
|
||||||
|
s = s["type ".len .. ^1]
|
||||||
|
# Name: until `<` `(` `{` `:` space
|
||||||
|
var name = ""
|
||||||
|
for ch in s:
|
||||||
|
if ch in {' ', '<', '(', '{', ':', ';'}:
|
||||||
|
break
|
||||||
|
name.add(ch)
|
||||||
|
if name.len == 0:
|
||||||
|
name = s
|
||||||
|
# extern funcs already stripped "extern "
|
||||||
|
if kind == "func" and line.strip().startsWith("extern"):
|
||||||
|
kind = "extern"
|
||||||
|
return (kind, name)
|
||||||
|
|
||||||
|
proc extractDocsFromSource*(source, path: string): seq[DocItem] =
|
||||||
|
result = @[]
|
||||||
|
var pending: seq[string] = @[]
|
||||||
|
var inBlockComment = false
|
||||||
|
var blockDoc: seq[string] = @[]
|
||||||
|
var lineNo = 0
|
||||||
|
for rawLine in source.splitLines():
|
||||||
|
inc lineNo
|
||||||
|
var line = rawLine
|
||||||
|
let stripped = line.strip()
|
||||||
|
|
||||||
|
# Block comment handling (/* ... */ used in stdlib today)
|
||||||
|
if inBlockComment:
|
||||||
|
let endIdx = stripped.find("*/")
|
||||||
|
if endIdx >= 0:
|
||||||
|
let before = stripped[0 ..< endIdx].strip()
|
||||||
|
if before.len > 0:
|
||||||
|
blockDoc.add(before)
|
||||||
|
inBlockComment = false
|
||||||
|
# Treat completed block as pending doc if non-empty
|
||||||
|
if blockDoc.len > 0:
|
||||||
|
pending = blockDoc
|
||||||
|
blockDoc = @[]
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
blockDoc.add(stripped)
|
||||||
|
continue
|
||||||
|
if stripped.startsWith("/*") and not stripped.startsWith("/***"):
|
||||||
|
let rest = stripped["/*".len .. ^1]
|
||||||
|
let endIdx = rest.find("*/")
|
||||||
|
if endIdx >= 0:
|
||||||
|
let body = rest[0 ..< endIdx].strip()
|
||||||
|
if body.len > 0:
|
||||||
|
pending = @[body]
|
||||||
|
else:
|
||||||
|
inBlockComment = true
|
||||||
|
blockDoc = @[]
|
||||||
|
let body = rest.strip()
|
||||||
|
if body.len > 0:
|
||||||
|
blockDoc.add(body)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Triple-slash doc comments
|
||||||
|
if stripped.startsWith("///"):
|
||||||
|
var body = stripped["///".len .. ^1]
|
||||||
|
if body.startsWith(" "):
|
||||||
|
body = body[1 .. ^1]
|
||||||
|
pending.add(body)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Empty line: keep pending docs (allow blank lines inside doc blocks)
|
||||||
|
if stripped.len == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Attributes immediately before decl: keep pending
|
||||||
|
if stripped.startsWith("@["):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isDeclStart(line):
|
||||||
|
if pending.len > 0:
|
||||||
|
let (kind, name) = declKindAndName(line)
|
||||||
|
result.add(DocItem(
|
||||||
|
kind: kind,
|
||||||
|
name: name,
|
||||||
|
signature: stripped,
|
||||||
|
docs: pending.join("\n"),
|
||||||
|
file: path,
|
||||||
|
line: lineNo
|
||||||
|
))
|
||||||
|
pending = @[]
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Other code clears pending (except plain // comments)
|
||||||
|
if stripped.startsWith("//"):
|
||||||
|
continue
|
||||||
|
pending = @[]
|
||||||
|
|
||||||
|
proc collectBuxFilesForDoc*(root: string): seq[string] =
|
||||||
|
result = @[]
|
||||||
|
if fileExists(root) and root.endsWith(".bux"):
|
||||||
|
result.add(root)
|
||||||
|
return
|
||||||
|
if not dirExists(root):
|
||||||
|
return
|
||||||
|
for path in walkDirRec(root):
|
||||||
|
if path.endsWith(".bux"):
|
||||||
|
result.add(path)
|
||||||
|
result.sort(system.cmp)
|
||||||
|
|
||||||
|
proc renderMarkdown*(items: seq[DocItem], title: string = "API Reference"): string =
|
||||||
|
var sb: string
|
||||||
|
sb.add(&"# {title}\n\n")
|
||||||
|
sb.add("Generated by `bux doc` from `///` and `/* */` documentation comments.\n\n")
|
||||||
|
if items.len == 0:
|
||||||
|
sb.add("_No documented items found._\n")
|
||||||
|
return sb
|
||||||
|
|
||||||
|
# Group by file
|
||||||
|
var byFile: seq[string] = @[]
|
||||||
|
for it in items:
|
||||||
|
if it.file notin byFile:
|
||||||
|
byFile.add(it.file)
|
||||||
|
byFile.sort(system.cmp)
|
||||||
|
|
||||||
|
for f in byFile:
|
||||||
|
let base = splitFile(f).name
|
||||||
|
sb.add(&"## `{base}`\n\n")
|
||||||
|
sb.add(&"_Source: `{f}`_\n\n")
|
||||||
|
for it in items:
|
||||||
|
if it.file != f:
|
||||||
|
continue
|
||||||
|
sb.add(&"### `{it.name}` _{it.kind}_\n\n")
|
||||||
|
sb.add("```bux\n")
|
||||||
|
sb.add(it.signature)
|
||||||
|
sb.add("\n```\n\n")
|
||||||
|
if it.docs.len > 0:
|
||||||
|
sb.add(it.docs)
|
||||||
|
sb.add("\n\n")
|
||||||
|
return sb
|
||||||
|
|
||||||
|
proc generateDocs*(paths: seq[string]): seq[DocItem] =
|
||||||
|
result = @[]
|
||||||
|
var files: seq[string] = @[]
|
||||||
|
for p in paths:
|
||||||
|
for f in collectBuxFilesForDoc(p):
|
||||||
|
if f notin files:
|
||||||
|
files.add(f)
|
||||||
|
files.sort(system.cmp)
|
||||||
|
for f in files:
|
||||||
|
let src = readFile(f)
|
||||||
|
result.add(extractDocsFromSource(src, f))
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
## fmt.nim — Indentation-based Bux source formatter (bootstrap).
|
||||||
|
## Mirrors selfhost `src/fmt.bux`: re-indent by brace depth, preserve content.
|
||||||
|
|
||||||
|
import std/[strutils, os, algorithm]
|
||||||
|
|
||||||
|
proc isInStringOrComment(line: string, pos: int): bool =
|
||||||
|
## Simplified: track `//`, `"..."`, and `'...'` up to `pos`.
|
||||||
|
var inString = false
|
||||||
|
var inChar = false
|
||||||
|
var inComment = false
|
||||||
|
var i = 0
|
||||||
|
while i < pos and i < line.len:
|
||||||
|
let c = line[i]
|
||||||
|
let n = if i + 1 < line.len: line[i + 1] else: '\0'
|
||||||
|
if inComment:
|
||||||
|
inc i
|
||||||
|
continue
|
||||||
|
if c == '/' and n == '/':
|
||||||
|
inComment = true
|
||||||
|
inc i
|
||||||
|
continue
|
||||||
|
if c == '"' and not inChar:
|
||||||
|
inString = not inString
|
||||||
|
if c == '\'' and not inString:
|
||||||
|
inChar = not inChar
|
||||||
|
inc i
|
||||||
|
return inString or inChar or inComment
|
||||||
|
|
||||||
|
proc countBraceDelta(line: string): int =
|
||||||
|
var delta = 0
|
||||||
|
for i in 0 ..< line.len:
|
||||||
|
if isInStringOrComment(line, i):
|
||||||
|
continue
|
||||||
|
let c = line[i]
|
||||||
|
if c == '{':
|
||||||
|
inc delta
|
||||||
|
elif c == '}':
|
||||||
|
dec delta
|
||||||
|
return delta
|
||||||
|
|
||||||
|
proc formatSource*(source: string): string =
|
||||||
|
## Re-indent each non-empty line to 4 spaces × brace depth.
|
||||||
|
## Idempotent: formatting a clean file is a no-op.
|
||||||
|
var sb: string
|
||||||
|
var indent = 0
|
||||||
|
# Nim's splitLines leaves a trailing "" when the source ends with '\n'.
|
||||||
|
# Drop that artifact so we don't accumulate blank lines on re-format.
|
||||||
|
var lines = source.splitLines(keepEol = false)
|
||||||
|
if source.len > 0 and source.endsWith('\n') and lines.len > 0 and lines[^1].len == 0:
|
||||||
|
lines.setLen(lines.len - 1)
|
||||||
|
for line in lines:
|
||||||
|
let trimmed = line.strip(leading = true, trailing = false)
|
||||||
|
if trimmed.len == 0:
|
||||||
|
sb.add('\n')
|
||||||
|
continue
|
||||||
|
|
||||||
|
let delta = countBraceDelta(trimmed)
|
||||||
|
let firstChar = trimmed[0]
|
||||||
|
if firstChar == '}':
|
||||||
|
dec indent
|
||||||
|
if indent < 0:
|
||||||
|
indent = 0
|
||||||
|
|
||||||
|
for _ in 0 ..< indent:
|
||||||
|
sb.add(" ")
|
||||||
|
sb.add(trimmed)
|
||||||
|
sb.add('\n')
|
||||||
|
|
||||||
|
if firstChar != '}':
|
||||||
|
indent = indent + delta
|
||||||
|
else:
|
||||||
|
# Net delta after the initial decrease for a leading `}`
|
||||||
|
indent = indent + delta + 1
|
||||||
|
if indent < 0:
|
||||||
|
indent = 0
|
||||||
|
|
||||||
|
return sb
|
||||||
|
|
||||||
|
proc formatFile*(path: string, checkOnly: bool): tuple[ok: bool, changed: bool, msg: string] =
|
||||||
|
## Format `path` in place, or only check if reformatting would change it.
|
||||||
|
if not fileExists(path):
|
||||||
|
return (false, false, "file not found: " & path)
|
||||||
|
let source = readFile(path)
|
||||||
|
let formatted = formatSource(source)
|
||||||
|
if formatted == source:
|
||||||
|
return (true, false, "")
|
||||||
|
if checkOnly:
|
||||||
|
return (true, true, "would reformat")
|
||||||
|
try:
|
||||||
|
writeFile(path, formatted)
|
||||||
|
return (true, true, "formatted")
|
||||||
|
except CatchableError as e:
|
||||||
|
return (false, false, e.msg)
|
||||||
|
|
||||||
|
proc collectBuxFiles*(root: string): seq[string] =
|
||||||
|
## Collect `.bux` files: single file, or recursive directory walk.
|
||||||
|
result = @[]
|
||||||
|
if fileExists(root) and root.endsWith(".bux"):
|
||||||
|
result.add(root)
|
||||||
|
return
|
||||||
|
if not dirExists(root):
|
||||||
|
return
|
||||||
|
for path in walkDirRec(root):
|
||||||
|
if path.endsWith(".bux"):
|
||||||
|
result.add(path)
|
||||||
|
result.sort(system.cmp)
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
## registry.nim — Bux package registry index (E.1)
|
||||||
|
##
|
||||||
|
## Index format (TOML-ish, one package per [[package]] table):
|
||||||
|
##
|
||||||
|
## [[package]]
|
||||||
|
## name = "greet"
|
||||||
|
## version = "0.1.0"
|
||||||
|
## source = "file:packages/greet" # relative to the registry file
|
||||||
|
## description = "Hello helpers"
|
||||||
|
##
|
||||||
|
## [[package]]
|
||||||
|
## name = "net"
|
||||||
|
## version = "1.2.0"
|
||||||
|
## source = "https://github.com/bux-lang/net.git"
|
||||||
|
##
|
||||||
|
## Lookup order for the index file:
|
||||||
|
## 1. $BUX_REGISTRY (file path)
|
||||||
|
## 2. ~/.bux/registry.toml
|
||||||
|
## 3. <repo>/config/registry.toml next to the compiler / cwd
|
||||||
|
|
||||||
|
import std/[os, strutils, strformat, algorithm]
|
||||||
|
|
||||||
|
type
|
||||||
|
RegistryPackage* = object
|
||||||
|
name*: string
|
||||||
|
version*: string
|
||||||
|
source*: string ## raw source as written in the index
|
||||||
|
description*: string
|
||||||
|
resolvedPath*: string ## absolute path for file: sources (filled on load)
|
||||||
|
|
||||||
|
Registry* = object
|
||||||
|
path*: string ## index file path
|
||||||
|
packages*: seq[RegistryPackage]
|
||||||
|
|
||||||
|
proc resolvePackageSource(pkg: var RegistryPackage, indexDir: string) =
|
||||||
|
if pkg.source.startsWith("file:"):
|
||||||
|
var p = pkg.source["file:".len .. ^1]
|
||||||
|
if p.startsWith("//"):
|
||||||
|
p = p[2 .. ^1]
|
||||||
|
if not p.isAbsolute:
|
||||||
|
p = indexDir / p
|
||||||
|
pkg.resolvedPath = p.absolutePath
|
||||||
|
elif pkg.source.startsWith("path:"):
|
||||||
|
var p = pkg.source["path:".len .. ^1]
|
||||||
|
if not p.isAbsolute:
|
||||||
|
p = indexDir / p
|
||||||
|
pkg.resolvedPath = p.absolutePath
|
||||||
|
pkg.source = "file:" & pkg.resolvedPath
|
||||||
|
|
||||||
|
proc parseRegistryToml(content, indexPath: string): seq[RegistryPackage] =
|
||||||
|
## Minimal parser for repeated [[package]] blocks with string keys.
|
||||||
|
result = @[]
|
||||||
|
var cur: RegistryPackage
|
||||||
|
var inPkg = false
|
||||||
|
let indexDir = indexPath.parentDir
|
||||||
|
|
||||||
|
for raw in content.splitLines():
|
||||||
|
let line = raw.strip()
|
||||||
|
if line.len == 0 or line.startsWith("#"):
|
||||||
|
continue
|
||||||
|
if line == "[[package]]" or line == "[[Package]]":
|
||||||
|
if inPkg and cur.name.len > 0:
|
||||||
|
resolvePackageSource(cur, indexDir)
|
||||||
|
result.add(cur)
|
||||||
|
cur = RegistryPackage()
|
||||||
|
inPkg = true
|
||||||
|
continue
|
||||||
|
if not inPkg:
|
||||||
|
continue
|
||||||
|
let eq = line.find('=')
|
||||||
|
if eq < 0: continue
|
||||||
|
let key = line[0 ..< eq].strip().toLowerAscii()
|
||||||
|
var val = line[eq + 1 .. ^1].strip()
|
||||||
|
if val.len >= 2 and val[0] == '"' and val[^1] == '"':
|
||||||
|
val = val[1 ..< ^1]
|
||||||
|
case key
|
||||||
|
of "name": cur.name = val
|
||||||
|
of "version": cur.version = val
|
||||||
|
of "source": cur.source = val
|
||||||
|
of "description": cur.description = val
|
||||||
|
else: discard
|
||||||
|
if inPkg and cur.name.len > 0:
|
||||||
|
resolvePackageSource(cur, indexDir)
|
||||||
|
result.add(cur)
|
||||||
|
|
||||||
|
proc findRegistryIndex*(): string =
|
||||||
|
## Locate the registry index file.
|
||||||
|
let env = getEnv("BUX_REGISTRY")
|
||||||
|
if env.len > 0 and fileExists(env):
|
||||||
|
return env.absolutePath
|
||||||
|
let homeIdx = getHomeDir() / ".bux" / "registry.toml"
|
||||||
|
if fileExists(homeIdx):
|
||||||
|
return homeIdx
|
||||||
|
let candidates = @[
|
||||||
|
getAppDir() / ".." / "config" / "registry.toml",
|
||||||
|
getAppDir() / "config" / "registry.toml",
|
||||||
|
getCurrentDir() / "config" / "registry.toml",
|
||||||
|
getCurrentDir() / ".." / "config" / "registry.toml",
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if fileExists(c):
|
||||||
|
return c.absolutePath
|
||||||
|
return ""
|
||||||
|
|
||||||
|
proc loadRegistry*(path: string = ""): Registry =
|
||||||
|
result.path = if path.len > 0: path else: findRegistryIndex()
|
||||||
|
result.packages = @[]
|
||||||
|
if result.path.len == 0 or not fileExists(result.path):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
let content = readFile(result.path)
|
||||||
|
result.packages = parseRegistryToml(content, result.path)
|
||||||
|
except CatchableError:
|
||||||
|
result.packages = @[]
|
||||||
|
|
||||||
|
proc registryLookup*(reg: Registry, name: string, versionReq: string = "*"): RegistryPackage =
|
||||||
|
## Find a package by name. versionReq `*` picks the last matching entry
|
||||||
|
## (index order; put newest last). Exact version matches preferred.
|
||||||
|
result = RegistryPackage()
|
||||||
|
var candidates: seq[RegistryPackage] = @[]
|
||||||
|
for p in reg.packages:
|
||||||
|
if p.name.toLowerAscii() == name.toLowerAscii():
|
||||||
|
candidates.add(p)
|
||||||
|
if candidates.len == 0:
|
||||||
|
return
|
||||||
|
if versionReq.len == 0 or versionReq == "*":
|
||||||
|
return candidates[^1]
|
||||||
|
for p in candidates:
|
||||||
|
if p.version == versionReq:
|
||||||
|
return p
|
||||||
|
# Semver prefix match: "1" matches "1.0.0"
|
||||||
|
for p in candidates:
|
||||||
|
if p.version.startsWith(versionReq):
|
||||||
|
return p
|
||||||
|
return candidates[^1]
|
||||||
|
|
||||||
|
proc registrySearch*(reg: Registry, query: string): seq[RegistryPackage] =
|
||||||
|
result = @[]
|
||||||
|
let q = query.toLowerAscii()
|
||||||
|
for p in reg.packages:
|
||||||
|
if q.len == 0 or
|
||||||
|
q in p.name.toLowerAscii() or
|
||||||
|
q in p.description.toLowerAscii():
|
||||||
|
result.add(p)
|
||||||
|
result.sort(proc (a, b: RegistryPackage): int =
|
||||||
|
cmp(a.name.toLowerAscii(), b.name.toLowerAscii()))
|
||||||
|
|
||||||
|
proc isGitSource*(source: string): bool =
|
||||||
|
source.startsWith("http://") or source.startsWith("https://") or
|
||||||
|
source.startsWith("git@") or source.startsWith("git://") or
|
||||||
|
source.startsWith("ssh://")
|
||||||
|
|
||||||
|
proc isFileSource*(source: string): bool =
|
||||||
|
source.startsWith("file:") or source.startsWith("path:")
|
||||||
|
|
||||||
|
proc formatRegistryDepLine*(name: string, pkg: RegistryPackage): string =
|
||||||
|
## Produce a bux.toml Dependencies line for a resolved registry package.
|
||||||
|
if pkg.resolvedPath.len > 0 and dirExists(pkg.resolvedPath):
|
||||||
|
return &"{name} = {{ Path = \"{pkg.resolvedPath}\" }}"
|
||||||
|
if isGitSource(pkg.source):
|
||||||
|
let ver = if pkg.version.len > 0: pkg.version else: "*"
|
||||||
|
return &"{name} = {{ Version = \"{ver}\", Source = \"{pkg.source}\" }}"
|
||||||
|
if isFileSource(pkg.source) and pkg.resolvedPath.len > 0:
|
||||||
|
return &"{name} = {{ Path = \"{pkg.resolvedPath}\" }}"
|
||||||
|
# Fallback: version-only (install will re-resolve)
|
||||||
|
let ver = if pkg.version.len > 0: pkg.version else: "*"
|
||||||
|
return &"{name} = \"{ver}\""
|
||||||
+178
-5
@@ -51,6 +51,11 @@ type
|
|||||||
## When true, ekIdent skips use-while-borrowed (we're forming `&x` itself)
|
## When true, ekIdent skips use-while-borrowed (we're forming `&x` itself)
|
||||||
suppressUseWhileBorrow*: bool
|
suppressUseWhileBorrow*: bool
|
||||||
currentRetType*: Type ## return type of the function being checked
|
currentRetType*: Type ## return type of the function being checked
|
||||||
|
## Lifetime elision / ref-origin tracking (@[Checked] only)
|
||||||
|
## Binding name → lifetime id ("'a", "#elided0", "#local", …)
|
||||||
|
varRefLifetime*: Table[string, string]
|
||||||
|
## Expected lifetime of the function's returned reference ("" if ret is not a ref)
|
||||||
|
returnLifetime*: string
|
||||||
closureDepth*: int ## nesting depth inside closures
|
closureDepth*: int ## nesting depth inside closures
|
||||||
currentClosureExpr*: Expr ## current closure being analyzed
|
currentClosureExpr*: Expr ## current closure being analyzed
|
||||||
closureScope*: Scope ## scope at which the current closure was entered
|
closureScope*: Scope ## scope at which the current closure was entered
|
||||||
@@ -164,6 +169,138 @@ proc checkTempMutBorrow(sema: var Sema, varName: string, loc: SourceLocation) =
|
|||||||
elif sema.activeSharedBorrows.getOrDefault(varName, 0) > 0:
|
elif sema.activeSharedBorrows.getOrDefault(varName, 0) > 0:
|
||||||
sema.emitError(loc, &"cannot mutably borrow '{varName}' while it is shared-borrowed")
|
sema.emitError(loc, &"cannot mutably borrow '{varName}' while it is shared-borrowed")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lifetime elision (C.1) — Rust-style simple rules for @[Checked]
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Rules (common cases, no annotations required):
|
||||||
|
# 1. Each elided input reference (&T / &mut T param) gets a distinct lifetime.
|
||||||
|
# 2. If there is exactly one input lifetime, it is assigned to all elided outputs.
|
||||||
|
# 3. If the first param is `self` / `Self`, its lifetime is preferred for outputs.
|
||||||
|
# 4. Multiple input refs + elided return → error (need explicit `'a`).
|
||||||
|
# 5. Returning a reference derived from a local (or by-value param) is rejected.
|
||||||
|
#
|
||||||
|
|
||||||
|
const
|
||||||
|
LifetimeLocal* = "#local" ## ref derived from a local / by-value place
|
||||||
|
LifetimeOutNone* = "#out" ## return ref with no input to borrow from
|
||||||
|
LifetimeAmbiguous* = "#ambiguous"
|
||||||
|
|
||||||
|
proc isRefTypeExpr(te: TypeExpr): bool =
|
||||||
|
te != nil and te.kind in {tekRef, tekMutRef}
|
||||||
|
|
||||||
|
proc applyLifetimeElision*(sema: var Sema, decl: Decl) =
|
||||||
|
## Assign elided lifetimes for ref params/return of `decl`. Populates
|
||||||
|
## `varRefLifetime` (params) and `returnLifetime`.
|
||||||
|
sema.varRefLifetime = initTable[string, string]()
|
||||||
|
sema.returnLifetime = ""
|
||||||
|
if not sema.checkedFunc:
|
||||||
|
return
|
||||||
|
|
||||||
|
var inputLts: seq[string] = @[]
|
||||||
|
var anon = 0
|
||||||
|
for p in decl.declFuncParams:
|
||||||
|
if not isRefTypeExpr(p.ptype):
|
||||||
|
continue
|
||||||
|
var lt = p.ptype.refLifetime
|
||||||
|
if lt.len == 0:
|
||||||
|
lt = "#elided" & $anon
|
||||||
|
inc anon
|
||||||
|
inputLts.add(lt)
|
||||||
|
sema.varRefLifetime[p.name] = lt
|
||||||
|
|
||||||
|
let ret = decl.declFuncReturnType
|
||||||
|
if not isRefTypeExpr(ret):
|
||||||
|
return
|
||||||
|
|
||||||
|
var rlt = ret.refLifetime
|
||||||
|
if rlt.len == 0:
|
||||||
|
if inputLts.len == 1:
|
||||||
|
rlt = inputLts[0]
|
||||||
|
elif inputLts.len == 0:
|
||||||
|
rlt = LifetimeOutNone
|
||||||
|
elif decl.declFuncParams.len > 0 and
|
||||||
|
decl.declFuncParams[0].name in ["self", "Self"]:
|
||||||
|
rlt = inputLts[0]
|
||||||
|
else:
|
||||||
|
sema.emitError(decl.loc,
|
||||||
|
"lifetime elision failed: return type needs an explicit lifetime " &
|
||||||
|
"(multiple input references); e.g. func F<'a>(a: &'a T, b: &'a U) -> &'a T")
|
||||||
|
rlt = LifetimeAmbiguous
|
||||||
|
sema.returnLifetime = rlt
|
||||||
|
|
||||||
|
proc exprRefLifetime*(sema: Sema, expr: Expr, scope: Scope): string =
|
||||||
|
## Best-effort lifetime of a reference-producing expression.
|
||||||
|
if expr == nil:
|
||||||
|
return ""
|
||||||
|
case expr.kind
|
||||||
|
of ekIdent:
|
||||||
|
if sema.varRefLifetime.hasKey(expr.exprIdent):
|
||||||
|
return sema.varRefLifetime[expr.exprIdent]
|
||||||
|
return ""
|
||||||
|
of ekUnary:
|
||||||
|
if expr.exprUnaryOp == tkAmp:
|
||||||
|
let name = extractBorrowedIdent(expr)
|
||||||
|
if name.len == 0:
|
||||||
|
return LifetimeLocal
|
||||||
|
# Reborrow of an existing ref binding keeps its lifetime
|
||||||
|
if sema.varRefLifetime.hasKey(name):
|
||||||
|
return sema.varRefLifetime[name]
|
||||||
|
# Address-of a by-value local or by-value parameter → local (dangling if returned)
|
||||||
|
return LifetimeLocal
|
||||||
|
# Dereference: *r still carries r's lifetime for field/ref purposes
|
||||||
|
if expr.exprUnaryOp == tkStar:
|
||||||
|
return sema.exprRefLifetime(expr.exprUnaryOperand, scope)
|
||||||
|
return ""
|
||||||
|
of ekBorrow:
|
||||||
|
# `borrow &x` / `borrow &mut x` — same origin rules as unary &
|
||||||
|
if expr.exprBorrowOperand != nil:
|
||||||
|
return sema.exprRefLifetime(expr.exprBorrowOperand, scope)
|
||||||
|
return LifetimeLocal
|
||||||
|
of ekField:
|
||||||
|
# Field projection through a ref keeps the base lifetime: (*p).x or p.x
|
||||||
|
if expr.exprFieldObj != nil:
|
||||||
|
let baseLt = sema.exprRefLifetime(expr.exprFieldObj, scope)
|
||||||
|
if baseLt.len > 0:
|
||||||
|
return baseLt
|
||||||
|
# Base is an ident of a struct local — field address would be local
|
||||||
|
if expr.exprFieldObj.kind == ekIdent:
|
||||||
|
if sema.varRefLifetime.hasKey(expr.exprFieldObj.exprIdent):
|
||||||
|
return sema.varRefLifetime[expr.exprFieldObj.exprIdent]
|
||||||
|
return LifetimeLocal
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
proc checkReturnLifetime*(sema: var Sema, retExpr: Expr, scope: Scope, loc: SourceLocation) =
|
||||||
|
## Reject dangling returns and explicit lifetime mismatches in @[Checked].
|
||||||
|
if not sema.checkedFunc or sema.returnLifetime.len == 0 or retExpr == nil:
|
||||||
|
return
|
||||||
|
let got = sema.exprRefLifetime(retExpr, scope)
|
||||||
|
if sema.returnLifetime == LifetimeOutNone:
|
||||||
|
sema.emitError(loc,
|
||||||
|
"cannot return a reference: function has no input reference to borrow from")
|
||||||
|
return
|
||||||
|
if got == LifetimeLocal:
|
||||||
|
sema.emitError(loc, "cannot return reference to local variable")
|
||||||
|
return
|
||||||
|
if got.len == 0:
|
||||||
|
# Non-trivial expression (call, etc.) — leave for later analysis
|
||||||
|
return
|
||||||
|
if got == LifetimeAmbiguous or sema.returnLifetime == LifetimeAmbiguous:
|
||||||
|
return
|
||||||
|
# Explicit lifetime mismatch (both sides named with ')
|
||||||
|
if got.startsWith("'") and sema.returnLifetime.startsWith("'") and got != sema.returnLifetime:
|
||||||
|
sema.emitError(loc,
|
||||||
|
&"lifetime mismatch: returning '{got}' but function returns '{sema.returnLifetime}'")
|
||||||
|
return
|
||||||
|
# Distinct elided inputs returned into another elided input's return slot
|
||||||
|
if got.startsWith("#elided") and sema.returnLifetime.startsWith("#elided") and
|
||||||
|
got != sema.returnLifetime:
|
||||||
|
sema.emitError(loc,
|
||||||
|
"lifetime mismatch: returned reference does not outlive the return type " &
|
||||||
|
"(multiple input references; annotate with an explicit lifetime)")
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Generic type inference helpers
|
# Generic type inference helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -472,7 +609,7 @@ proc inferTypeArgs(sema: var Sema, funcDecl: Decl, argTypes: seq[Type],
|
|||||||
# Type resolution from AST TypeExpr
|
# Type resolution from AST TypeExpr
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc resolveType(sema: var Sema, te: TypeExpr): Type =
|
proc resolveType*(sema: var Sema, te: TypeExpr): Type =
|
||||||
if te == nil:
|
if te == nil:
|
||||||
return makeUnknown()
|
return makeUnknown()
|
||||||
case te.kind
|
case te.kind
|
||||||
@@ -918,7 +1055,7 @@ proc collectGlobals*(sema: var Sema) =
|
|||||||
# Expression type checking
|
# Expression type checking
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
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 typeImplements(sema: Sema, t: Type, interfaceName: string): bool =
|
proc typeImplements(sema: Sema, t: Type, interfaceName: string): bool =
|
||||||
@@ -1099,7 +1236,7 @@ proc resolveCallArgs(sema: var Sema, expr: Expr, calleeDecl: Decl, scope: Scope)
|
|||||||
expr.exprCallArgs = newArgs
|
expr.exprCallArgs = newArgs
|
||||||
expr.exprCallArgNames = newNames
|
expr.exprCallArgNames = newNames
|
||||||
|
|
||||||
proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||||
if expr == nil:
|
if expr == nil:
|
||||||
return makeUnknown()
|
return makeUnknown()
|
||||||
case expr.kind
|
case expr.kind
|
||||||
@@ -1835,6 +1972,11 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
|
|||||||
# Untyped let + `&x` is typed as &mut by unary lowering
|
# Untyped let + `&x` is typed as &mut by unary lowering
|
||||||
isMut = initType.isMutRef
|
isMut = initType.isMutRef
|
||||||
sema.checkCreateBorrow(bname, isMut, stmt.stmtLetInit.loc)
|
sema.checkCreateBorrow(bname, isMut, stmt.stmtLetInit.loc)
|
||||||
|
# Propagate ref lifetime to the new binding (for return-site checks)
|
||||||
|
if declaredType.isRef or declaredType.isMutRef or initType.isRef or initType.isMutRef:
|
||||||
|
let lt = sema.exprRefLifetime(stmt.stmtLetInit, scope)
|
||||||
|
if lt.len > 0:
|
||||||
|
sema.varRefLifetime[stmt.stmtLetName] = lt
|
||||||
return makeVoid()
|
return makeVoid()
|
||||||
of skIf:
|
of skIf:
|
||||||
let condType = sema.checkExpr(stmt.stmtIfCond, scope)
|
let condType = sema.checkExpr(stmt.stmtIfCond, scope)
|
||||||
@@ -1897,6 +2039,8 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
|
|||||||
let retSym = scope.lookup(stmt.stmtReturnValue.exprIdent)
|
let retSym = scope.lookup(stmt.stmtReturnValue.exprIdent)
|
||||||
if retSym != nil and retSym.isOwn:
|
if retSym != nil and retSym.isOwn:
|
||||||
sema.movedVars.add(stmt.stmtReturnValue.exprIdent)
|
sema.movedVars.add(stmt.stmtReturnValue.exprIdent)
|
||||||
|
# Lifetime: reject dangling returns / explicit mismatches
|
||||||
|
sema.checkReturnLifetime(stmt.stmtReturnValue, scope, stmt.loc)
|
||||||
return makeVoid()
|
return makeVoid()
|
||||||
of skBreak, skContinue:
|
of skBreak, skContinue:
|
||||||
return makeVoid()
|
return makeVoid()
|
||||||
@@ -1951,9 +2095,15 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
|
|||||||
proc checkFunc(sema: var Sema, decl: Decl) =
|
proc checkFunc(sema: var Sema, decl: Decl) =
|
||||||
if decl.declFuncBody == nil:
|
if decl.declFuncBody == nil:
|
||||||
return
|
return
|
||||||
# Skip body type-checking for generic functions — their bodies contain
|
# Skip body type-checking for type-generic functions — their bodies contain
|
||||||
# type parameters that cannot be fully resolved until monomorphization.
|
# type parameters that cannot be fully resolved until monomorphization.
|
||||||
if decl.declFuncTypeParams.len > 0:
|
# Lifetime-only params (`'a`) are fine: we still check the body for elision.
|
||||||
|
var hasTypeGeneric = false
|
||||||
|
for tp in decl.declFuncTypeParams:
|
||||||
|
if not tp.isLifetime:
|
||||||
|
hasTypeGeneric = true
|
||||||
|
break
|
||||||
|
if hasTypeGeneric:
|
||||||
return
|
return
|
||||||
let wasChecked = sema.checkedFunc
|
let wasChecked = sema.checkedFunc
|
||||||
let wasAsync = sema.currentFuncIsAsync
|
let wasAsync = sema.currentFuncIsAsync
|
||||||
@@ -1963,10 +2113,18 @@ proc checkFunc(sema: var Sema, decl: Decl) =
|
|||||||
sema.movedVars = @[]
|
sema.movedVars = @[]
|
||||||
sema.activeMutBorrows = initTable[string, SourceLocation]()
|
sema.activeMutBorrows = initTable[string, SourceLocation]()
|
||||||
sema.activeSharedBorrows = initTable[string, int]()
|
sema.activeSharedBorrows = initTable[string, int]()
|
||||||
|
# C.1: elide lifetimes on params / return before walking the body
|
||||||
|
sema.applyLifetimeElision(decl)
|
||||||
|
else:
|
||||||
|
sema.varRefLifetime = initTable[string, string]()
|
||||||
|
sema.returnLifetime = ""
|
||||||
var funcScope = newScope(sema.globalScope)
|
var funcScope = newScope(sema.globalScope)
|
||||||
# Add type parameters to type table for resolution
|
# Add type parameters to type table for resolution
|
||||||
var addedTypeParams: seq[string] = @[]
|
var addedTypeParams: seq[string] = @[]
|
||||||
for tp in decl.declFuncTypeParams:
|
for tp in decl.declFuncTypeParams:
|
||||||
|
if tp.isLifetime:
|
||||||
|
# Lifetime params are not types; skip typeTable
|
||||||
|
continue
|
||||||
sema.typeTable[tp.name] = makeTypeParam(tp.name)
|
sema.typeTable[tp.name] = makeTypeParam(tp.name)
|
||||||
addedTypeParams.add(tp.name)
|
addedTypeParams.add(tp.name)
|
||||||
# Add parameters
|
# Add parameters
|
||||||
@@ -1982,6 +2140,8 @@ proc checkFunc(sema: var Sema, decl: Decl) =
|
|||||||
sema.typeTable.del(tp)
|
sema.typeTable.del(tp)
|
||||||
sema.checkedFunc = wasChecked
|
sema.checkedFunc = wasChecked
|
||||||
sema.currentFuncIsAsync = wasAsync
|
sema.currentFuncIsAsync = wasAsync
|
||||||
|
sema.varRefLifetime = initTable[string, string]()
|
||||||
|
sema.returnLifetime = ""
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Second pass: check all function bodies
|
# Second pass: check all function bodies
|
||||||
@@ -2026,3 +2186,16 @@ proc analyzeFull*(modu: Module): tuple[result: SemaResult, sema: Sema] =
|
|||||||
sema.collectGlobals()
|
sema.collectGlobals()
|
||||||
sema.checkBodies()
|
sema.checkBodies()
|
||||||
result = (SemaResult(diagnostics: sema.diagnostics), sema)
|
result = (SemaResult(diagnostics: sema.diagnostics), sema)
|
||||||
|
|
||||||
|
proc checkExprForLsp*(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||||
|
## Type-check an expression for IDE use (no borrow/move side effects).
|
||||||
|
let wasChecked = sema.checkedFunc
|
||||||
|
let savedMoved = sema.movedVars
|
||||||
|
let savedMut = sema.activeMutBorrows
|
||||||
|
let savedShared = sema.activeSharedBorrows
|
||||||
|
sema.checkedFunc = false
|
||||||
|
result = sema.checkExpr(expr, scope)
|
||||||
|
sema.checkedFunc = wasChecked
|
||||||
|
sema.movedVars = savedMoved
|
||||||
|
sema.activeMutBorrows = savedMut
|
||||||
|
sema.activeSharedBorrows = savedShared
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Bux package registry index (E.1)
|
||||||
|
# Used by `bux add <name>` and `bux install` when no --path/--git is given.
|
||||||
|
#
|
||||||
|
# Override with: export BUX_REGISTRY=/path/to/registry.toml
|
||||||
|
# Or copy to: ~/.bux/registry.toml
|
||||||
|
#
|
||||||
|
# source forms:
|
||||||
|
# file:relative/or/absolute — local package (relative to this file)
|
||||||
|
# path:relative/or/absolute — same as file:
|
||||||
|
# https://...git — git clone into ~/.bux/packages/<name>
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "greet"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "file:../registry/packages/greet"
|
||||||
|
description = "Tiny Hello helper library (demo registry package)"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "greet"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "file:../registry/packages/greet"
|
||||||
|
description = "Tiny Hello helper library (demo registry package, patch)"
|
||||||
+49
-4
@@ -152,15 +152,60 @@ This runs:
|
|||||||
|
|
||||||
### Project Tests (`bux test`)
|
### Project Tests (`bux test`)
|
||||||
```bash
|
```bash
|
||||||
./buxc test
|
./buxc test # run all tests/*.bux in the current package
|
||||||
|
./buxc test --filter first # only tests whose name contains "first"
|
||||||
|
./buxc test --filter=first _test_runner
|
||||||
```
|
```
|
||||||
|
|
||||||
Builds the project and runs the resulting binary. Reports:
|
Discovers `tests/*.bux`, builds each as a temp package, and runs it. Prints a
|
||||||
- `Tests passed` on exit code 0
|
summary table and exits:
|
||||||
- `Tests failed (exit code N)` on non-zero exit
|
- `0` — all selected tests passed
|
||||||
|
- `1` — at least one failure, or no tests matched the filter
|
||||||
|
|
||||||
Use `Std::Test` module for assertions inside test code.
|
Use `Std::Test` module for assertions inside test code.
|
||||||
|
|
||||||
|
### Format (`bux fmt`)
|
||||||
|
```bash
|
||||||
|
./buxc fmt examples/hello.bux # reformat one file
|
||||||
|
./buxc fmt lib/ # reformat a directory tree
|
||||||
|
make fmt # reformat lib/ examples/ src/ tests/ apps/
|
||||||
|
./buxc fmt --check path/ # exit 1 if any file would change
|
||||||
|
make fmt-check # CI: full-tree clean + dirty smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
Indentation is 4 spaces by brace depth. The formatter is idempotent (safe to re-run).
|
||||||
|
`make fmt-check` enforces a clean tree under `lib/`, `examples/`, `src/`, `tests/`, and `apps/`.
|
||||||
|
|
||||||
|
### Stdlib golden tests
|
||||||
|
```bash
|
||||||
|
make test-stdlib
|
||||||
|
# or: tests/stdlib_golden/run.sh ./buxc
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavioral packages under `tests/stdlib_golden/` (`array`, `string`, `collections`)
|
||||||
|
assert core Array/String/Map/Set/Result/Option APIs and match expected `PASS` lines.
|
||||||
|
|
||||||
|
### API docs (`bux doc`)
|
||||||
|
```bash
|
||||||
|
./buxc doc lib/ # Markdown to stdout
|
||||||
|
./buxc doc --out docs/api/stdlib.md lib/
|
||||||
|
make docs # writes docs/api/stdlib.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Scans `///` line comments (and bootstrap also accepts adjacent `/* */`) immediately
|
||||||
|
before `func` / `struct` / `enum` / `interface` / `module` declarations.
|
||||||
|
|
||||||
|
### Language Server (`bux-lsp` 0.4.0)
|
||||||
|
```bash
|
||||||
|
make lsp # → tools/bux-lsp
|
||||||
|
nim r --path:bootstrap tools/test_lsp_locals.nim
|
||||||
|
./tools/smoke_lsp_hover.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Features: diagnostics (`buxc check`), hover, go-to-def, outline, completion.
|
||||||
|
**Locals are position-sensitive** (nested scopes / shadowing). **Inferred `let` types**
|
||||||
|
appear on hover (`let x: int · inferred`).
|
||||||
|
|
||||||
### Example Programs
|
### Example Programs
|
||||||
```bash
|
```bash
|
||||||
make test-examples
|
make test-examples
|
||||||
|
|||||||
@@ -604,6 +604,44 @@ Moves happen in three contexts:
|
|||||||
msg = "reassigned"; // OK: reinitialization
|
msg = "reassigned"; // OK: reinitialization
|
||||||
PrintLine(msg);
|
PrintLine(msg);
|
||||||
```
|
```
|
||||||
|
- **No dangling returns**: cannot return a reference to a local (or by-value parameter)
|
||||||
|
```bux
|
||||||
|
@[Checked]
|
||||||
|
func Bad(p: &int) -> &int {
|
||||||
|
var x: int = 1;
|
||||||
|
return &x; // ERROR: cannot return reference to local variable
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lifetime elision (C.1)
|
||||||
|
|
||||||
|
In `@[Checked]` functions, most reference signatures need **no** lifetime annotations.
|
||||||
|
Elision applies the usual single-input rules:
|
||||||
|
|
||||||
|
1. Each elided input `&T` / `&mut T` parameter gets a distinct lifetime.
|
||||||
|
2. If there is **exactly one** input lifetime, it is assigned to all elided outputs.
|
||||||
|
3. If the first parameter is named `self` / `Self`, that input lifetime is preferred for outputs.
|
||||||
|
4. Multiple input references + elided return → **error** (write an explicit lifetime).
|
||||||
|
|
||||||
|
```bux
|
||||||
|
// Elided — one input ref, return shares its lifetime
|
||||||
|
@[Checked]
|
||||||
|
func Identity(p: &int) -> &int {
|
||||||
|
return p; // OK
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit — required when several inputs could be returned
|
||||||
|
@[Checked]
|
||||||
|
func Pick<'a>(a: &'a int, b: &'a int) -> &'a int {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Syntax: &'a T and &mut / &'a mut T (lifetime before `mut`)
|
||||||
|
// Type parameters: func F<'a, T>(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Unchecked functions ignore lifetime rules (C-like). Explicit `'a` is optional
|
||||||
|
documentation when a single input would already elide correctly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+75
-27
@@ -1,6 +1,8 @@
|
|||||||
# Bux Package Manager
|
# Bux Package Manager
|
||||||
|
|
||||||
> **Status:** Implemented (Phase 9.1)
|
> **Status:** Path + git + **local/file registry** (E.1). HTTP registry index URL optional later.
|
||||||
|
|
||||||
|
See also: [SEMVER.md](SEMVER.md) for version policy.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -20,52 +22,97 @@ License = "MIT"
|
|||||||
Output = "Bin"
|
Output = "Bin"
|
||||||
|
|
||||||
[Dependencies]
|
[Dependencies]
|
||||||
Std = "1.0"
|
greet = { Path = "/abs/path/to/greet" }
|
||||||
Json = { Version = "2.1", Source = "https://github.com/bux-lang/json" }
|
Json = { Version = "2.1", Source = "https://github.com/bux-lang/json" }
|
||||||
Utils = { Path = "../Utils" }
|
Utils = { Path = "../Utils" }
|
||||||
|
# Registry name-only (resolved by `bux add` / `bux install`):
|
||||||
|
# greet = "0.1.1"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dependency Forms
|
### Dependency Forms
|
||||||
|
|
||||||
| Form | Example | Description |
|
| Form | Example | Description |
|
||||||
|------|---------|-------------|
|
|------|---------|-------------|
|
||||||
| Version string | `Std = "1.0"` | Registry dependency |
|
| Version string | `greet = "0.1.1"` | Registry dependency |
|
||||||
| Wildcard | `Std = "*"` | Latest version |
|
| Wildcard | `greet = "*"` | Latest registry version |
|
||||||
| Inline table (git) | `{ Version = "1.4", Source = "https://..." }` | Git URL + version |
|
| Inline table (git) | `{ Version = "1.4", Source = "https://..." }` | Git URL + version |
|
||||||
| Inline table (path) | `{ Path = "../Lib" }` | Local path dependency |
|
| Inline table (path) | `{ Path = "../Lib" }` | Local path dependency |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Package registry (E.1)
|
||||||
|
|
||||||
|
### Index file
|
||||||
|
|
||||||
|
Default locations (first hit wins):
|
||||||
|
|
||||||
|
1. `$BUX_REGISTRY` — path to a `registry.toml`
|
||||||
|
2. `~/.bux/registry.toml`
|
||||||
|
3. `config/registry.toml` next to the Bux repo / compiler
|
||||||
|
|
||||||
|
Format:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[package]]
|
||||||
|
name = "greet"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "file:../registry/packages/greet" # relative to the index file
|
||||||
|
description = "Hello helpers"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "net"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "https://github.com/example/bux-net.git"
|
||||||
|
description = "TCP helpers"
|
||||||
|
```
|
||||||
|
|
||||||
|
`file:` / `path:` sources are resolved relative to the registry file.
|
||||||
|
Git URLs are cloned into `~/.bux/packages/<name>/` on install.
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Search the index
|
||||||
|
bux search
|
||||||
|
bux search greet
|
||||||
|
|
||||||
|
# Add by registry name (writes Path or git Source into bux.toml)
|
||||||
|
bux add greet
|
||||||
|
bux add greet 0.1.1
|
||||||
|
|
||||||
|
# Explicit sources still work
|
||||||
|
bux add utils --path "../utils"
|
||||||
|
bux add network --git "https://github.com/bux-lang/network"
|
||||||
|
|
||||||
|
# Resolve + write bux.lock
|
||||||
|
bux install
|
||||||
|
```
|
||||||
|
|
||||||
|
Demo package in this monorepo: `registry/packages/greet` (registered in
|
||||||
|
`config/registry.toml`). Smoke test: `tools/smoke_registry.sh`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
### `bux add <name> [version]`
|
### `bux add <name> [version]`
|
||||||
|
|
||||||
Add a dependency to `bux.toml`.
|
Add a dependency to `bux.toml` (registry / `--path` / `--git`).
|
||||||
|
|
||||||
```bash
|
### `bux search [query]`
|
||||||
# Add registry dependency
|
|
||||||
bux add json "2.1"
|
|
||||||
|
|
||||||
# Add path-based dependency
|
List packages in the active registry (filter by name/description).
|
||||||
bux add utils --path "../utils"
|
|
||||||
|
|
||||||
# Add git dependency
|
|
||||||
bux add network --git "https://github.com/bux-lang/network"
|
|
||||||
```
|
|
||||||
|
|
||||||
### `bux install`
|
### `bux install`
|
||||||
|
|
||||||
Resolve dependencies and generate `bux.lock`.
|
Resolve dependencies and generate `bux.lock`.
|
||||||
|
|
||||||
```bash
|
|
||||||
bux install
|
|
||||||
```
|
|
||||||
|
|
||||||
What it does:
|
What it does:
|
||||||
1. Reads `[Dependencies]` from `bux.toml`
|
1. Reads `[Dependencies]` from `bux.toml`
|
||||||
2. Resolves path-based deps (verifies directory exists)
|
2. Resolves path-based deps (verifies directory exists)
|
||||||
3. Clones/pulls git-based deps to `~/.bux/packages/<name>/`
|
3. Clones git-based deps to `~/.bux/packages/<name>/`
|
||||||
4. Generates `bux.lock` with exact versions and sources
|
4. Resolves bare version names via the registry index
|
||||||
|
5. Generates `bux.lock` with exact versions and sources
|
||||||
|
|
||||||
### `bux build` / `bux run`
|
### `bux build` / `bux run`
|
||||||
|
|
||||||
@@ -84,10 +131,9 @@ Auto-generated. **Do not edit manually.**
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[Package]]
|
[[Package]]
|
||||||
Name = "json"
|
Name = "greet"
|
||||||
Version = "2.1.3"
|
Version = "0.1.1"
|
||||||
Source = "https://github.com/bux-lang/json"
|
Source = "/home/user/z-git/bux/bux/registry/packages/greet"
|
||||||
Checksum = "8dcb2a7f..."
|
|
||||||
|
|
||||||
[[Package]]
|
[[Package]]
|
||||||
Name = "utils"
|
Name = "utils"
|
||||||
@@ -103,7 +149,7 @@ The lockfile ensures **reproducible builds** — every developer gets the exact
|
|||||||
|
|
||||||
1. **Path-based** deps are resolved relative to the manifest directory
|
1. **Path-based** deps are resolved relative to the manifest directory
|
||||||
2. **Git-based** deps are cloned to `~/.bux/packages/<name>/`
|
2. **Git-based** deps are cloned to `~/.bux/packages/<name>/`
|
||||||
3. **Version-based** deps (without Source) require a registry (future feature)
|
3. **Version-based** deps look up `config/registry.toml` (or `$BUX_REGISTRY`)
|
||||||
4. Dependencies are loaded from `<dep>/src/*.bux` at build time
|
4. Dependencies are loaded from `<dep>/src/*.bux` at build time
|
||||||
5. Later declarations shadow earlier ones (project > deps > stdlib)
|
5. Later declarations shadow earlier ones (project > deps > stdlib)
|
||||||
|
|
||||||
@@ -114,8 +160,10 @@ The lockfile ensures **reproducible builds** — every developer gets the exact
|
|||||||
```bash
|
```bash
|
||||||
bux new mylib
|
bux new mylib
|
||||||
cd mylib
|
cd mylib
|
||||||
# Edit src/Main.bux → module MyLib { pub func Add(...) }
|
# Edit src/*.bux → module MyLib { func Add(...) }
|
||||||
bux build # Builds as library (Type = "lib")
|
# Set Type = "lib" in bux.toml
|
||||||
|
# Register in your registry.toml with source = "file:..."
|
||||||
|
bux build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example: Using a Library
|
## Example: Using a Library
|
||||||
|
|||||||
+131
-23
@@ -1,7 +1,7 @@
|
|||||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||||
|
|
||||||
> **Дата:** 2026-07-18
|
> **Дата:** 2026-07-18
|
||||||
> **Текущо:** v0.5.x — selfhost loop, gradual ownership, green threads, **43+ examples**, match + guards + **generic HOF inference** + pattern bindings + **`f"..."` interp** bootstrap+selfhost ✅
|
> **Текущо:** v0.5.x — selfhost, C.1, tooling, LSP 0.4, full-tree fmt, **package registry (E.1)** ✅
|
||||||
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
| Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ |
|
| Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ |
|
||||||
| HIR → C | Tuples + fat `func` ABI в bootstrap **и** selfhost | ★★★★☆ |
|
| HIR → C | Tuples + fat `func` ABI в bootstrap **и** selfhost | ★★★★☆ |
|
||||||
| Selfhost (`src/`) | ~12k LOC, binary-identical loop, closures+tuples | ★★★★★ |
|
| Selfhost (`src/`) | ~12k LOC, binary-identical loop, closures+tuples | ★★★★★ |
|
||||||
| Gradual ownership | `@[Checked]`, `&`/`&mut`, move, Drop | ★★★☆☆ (basic) |
|
| Gradual ownership | `@[Checked]`, `&`/`&mut`, move, Drop, **lifetime elision** | ★★★★☆ |
|
||||||
| Concurrency | M:N tasks + channels + async | ★★★★☆ |
|
| Concurrency | M:N tasks + channels + async | ★★★★☆ |
|
||||||
| Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ |
|
| Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ |
|
||||||
| Tooling | `test-errors`, LSP diagnostics + hover/def/outline | ★★★★☆ |
|
| Tooling | `test-errors`, LSP diagnostics + hover/def/outline | ★★★★☆ |
|
||||||
| Ecosystem / registry | path+git deps; няма централен registry | ★☆☆☆☆ |
|
| Ecosystem / registry | path+git + **file registry index** (`bux search/add`) | ★★★☆☆ |
|
||||||
| Документация | README + QUALITY_PLAN синхронизирани (2026-07-15) | ★★★★☆ |
|
| Документация | README + QUALITY_PLAN синхронизирани (2026-07-15) | ★★★★☆ |
|
||||||
|
|
||||||
**Силна ниша:** gradual ownership (C-скорост на писане + opt-in Rust-safety).
|
**Силна ниша:** gradual ownership (C-скорост на писане + opt-in Rust-safety).
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
| # | Задача | Защо | Статус |
|
| # | Задача | Защо | Статус |
|
||||||
|---|--------|------|--------|
|
|---|--------|------|--------|
|
||||||
| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата | ⏳ |
|
| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата | ✅ bootstrap + selfhost |
|
||||||
| C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives | ✅ let-bound + use-while + call conflict |
|
| C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives | ✅ let-bound + use-while + call conflict |
|
||||||
| C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден | ✅ bootstrap + selfhost |
|
| C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден | ✅ bootstrap + selfhost |
|
||||||
| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path | ✅ partial (unchecked path + goldens) |
|
| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path | ✅ partial (unchecked path + goldens) |
|
||||||
@@ -78,21 +78,21 @@
|
|||||||
|
|
||||||
| # | Задача | Защо | Статус |
|
| # | Задача | Защо | Статус |
|
||||||
|---|--------|------|--------|
|
|---|--------|------|--------|
|
||||||
| D.1 | LSP: hover, go-to-def, diagnostics | IDE = adoption | ✅ hover/def/outline + **sema types on hover** (v0.3.0) + `buxc` diags |
|
| D.1 | LSP: hover, go-to-def, diagnostics | IDE = adoption | ✅ v0.4.0: **position-sensitive locals** + **inferred `let`** + sema hover |
|
||||||
| D.2 | `bux fmt` стабилен + CI check | Единен style | ⏳ |
|
| D.2 | `bux fmt` стабилен + CI check | Единен style | ✅ full-tree format + `make fmt-check` enforce |
|
||||||
| D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | ⏳ partial (`bux test` exists) |
|
| D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | ✅ `--filter` / summary / exit 0\|1 |
|
||||||
| D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib | ⏳ |
|
| D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib | ✅ bootstrap+selfhost + `make docs` |
|
||||||
| D.5 | Golden tests за stdlib modules | Регресии без изненади | ⏳ |
|
| D.5 | Golden tests за stdlib modules | Регресии без изненади | ✅ `tests/stdlib_golden/` + `make test-stdlib` |
|
||||||
|
|
||||||
### E — Ecosystem & v1.0 (P2)
|
### E — Ecosystem & v1.0 (P2)
|
||||||
|
|
||||||
| # | Задача | Защо |
|
| # | Задача | Защо | Статус |
|
||||||
|---|--------|------|
|
|---|--------|------|--------|
|
||||||
| E.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks |
|
| E.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks | ✅ local index + file/git sources + `search` |
|
||||||
| E.2 | 3–5 production-quality apps в `apps/` | Showcase |
|
| E.2 | 3–5 production-quality apps в `apps/` | Showcase | ⏳ partial (`nexus`, `boko`, `simpledb`, `jwt-pitbul`) |
|
||||||
| E.3 | Language freeze + semver policy | Trust |
|
| E.3 | Language freeze + semver policy | Trust | ✅ draft `docs/SEMVER.md` |
|
||||||
| E.4 | Debugger/DWARF basics | Systems audience |
|
| E.4 | Debugger/DWARF basics | Systems audience | ⏳ |
|
||||||
| E.5 | Benchmarks vs C/Zig/Nim (micro + nexus) | Marketing + regression |
|
| E.5 | Benchmarks vs C/Zig/Nim (micro + nexus) | Marketing + regression | ⏳ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -114,10 +114,10 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
|
|||||||
|
|
||||||
- [ ] Всички examples + selfhost-loop + 3 apps минават на CI
|
- [ ] Всички examples + selfhost-loop + 3 apps минават на CI
|
||||||
- [ ] Array/Map/String/Test API покрива 90% от ежедневните нужди
|
- [ ] Array/Map/String/Test API покрива 90% от ежедневните нужди
|
||||||
- [ ] `@[Checked]` хваща use-after-move + double `&mut` в documented subset
|
- [x] `@[Checked]` хваща use-after-move + double `&mut` + dangling return / elision fail
|
||||||
- [ ] `bux test` + `bux fmt` + `bux check` са default developer loop
|
- [x] `bux test` + `bux fmt` + `bux check` са default developer loop (`--filter` / `--check` shipped)
|
||||||
- [ ] LanguageRef синхронизиран с компилатора
|
- [x] LanguageRef синхронизиран с компилатора (incl. C.1 elision)
|
||||||
- [ ] Поне един външен проект (не в monorepo) build-ва с git dep
|
- [x] Поне един външен/temp проект build-ва с registry dep (`tools/smoke_registry.sh`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -388,8 +388,116 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Сесия 24 (tooling — D.2 fmt --check + D.3 test --filter)
|
||||||
|
|
||||||
|
1. **Bootstrap `bux fmt`** (`bootstrap/fmt.nim`):
|
||||||
|
- Indent-by-brace-depth formatter (parity with `src/fmt.bux`)
|
||||||
|
- `bux fmt [path...]` writes; `bux fmt --check` exits 1 if any file would change
|
||||||
|
- Collects single file or recursive `.bux` under directories
|
||||||
|
2. **Bootstrap `bux test --filter`**:
|
||||||
|
- `--filter <s>` / `--filter=<s>` — only run `tests/*.bux` whose name contains `s`
|
||||||
|
- Summary table (`PASS` / `FAIL[:code]`) + `Results: N passed, M failed, T total`
|
||||||
|
- Exit `0` all pass, `1` failures or no match
|
||||||
|
3. **Selfhost parity** (`src/cli.bux`, `src/fmt.bux`):
|
||||||
|
- `Fmt_WouldChange` / `Fmt_CheckFile`; `Cli_Fmt(dir, checkOnly)`
|
||||||
|
- `Cli_Test(dir, filter)` with summary + skip count; filter skips Main package run
|
||||||
|
4. **CI hooks:** `make fmt-check` smoke (clean→0, dirty→1); full-tree enforce deferred
|
||||||
|
until a one-shot format pass on `lib/`/`examples/`
|
||||||
|
5. **Idempotence fix:** drop trailing split-empty so re-format is a no-op
|
||||||
|
6. Verified: unit suite + `./buxc test --filter first _test_runner` + selfhost `buxc2`
|
||||||
|
fmt/test parity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 25 (Ownership 2.0 — C.1 lifetime elision)
|
||||||
|
|
||||||
|
1. **Elision rules** in `@[Checked]` (`bootstrap/sema.nim`):
|
||||||
|
- Each elided input `&`/`&mut` → distinct `#elidedN`
|
||||||
|
- One input lifetime → assigned to elided return
|
||||||
|
- First param `self`/`Self` preferred when multiple inputs
|
||||||
|
- Multiple inputs + elided return → `lifetime elision failed` (need `'a`)
|
||||||
|
2. **Return checks:**
|
||||||
|
- `cannot return reference to local variable` (`return &local` / let-bound local ref)
|
||||||
|
- `no input reference to borrow from` (return ref with zero input refs)
|
||||||
|
- Explicit `'a` mismatch between return and value
|
||||||
|
3. **Body check for lifetime-only generics** (`func F<'a>(...)`) — no longer skipped
|
||||||
|
4. **Diagnostics hints** for elision / dangling / mismatch
|
||||||
|
5. **Tests:** 8 new borrow_test cases; goldens `return_local_ref`, `elision_multi_input`
|
||||||
|
6. **Example:** `examples/lifetime_elision.bux` (Identity / explicit / ViaLet / self)
|
||||||
|
7. LanguageRef + QUALITY_PLAN updated
|
||||||
|
8. Verified: borrow_test 24/24, 9 error goldens, example runs
|
||||||
|
|
||||||
|
## Сесия 26 (C.1 selfhost parity)
|
||||||
|
|
||||||
|
1. **Lexer** (`src/lexer.bux` + `tkLifetime=111`): `'a` vs char `'x'` (same heuristic as bootstrap)
|
||||||
|
2. **Parser:**
|
||||||
|
- `&'a T` / `&'a mut T` → `TypeExpr.refLifetime`
|
||||||
|
- `func F<'a, T>(…)` — lifetime params accepted and **skipped** for mono slots
|
||||||
|
3. **Sema** lifetime elision (fixed 8-slot maps, same rules as bootstrap):
|
||||||
|
- single-input elision, `self` preference, multi-input fail
|
||||||
|
- return-local / no-input-ref / explicit mismatch
|
||||||
|
- let-bound ref lifetime propagation
|
||||||
|
4. Fixed `checkFunc` else-branch that wiped `checkedFunc` when retType was void
|
||||||
|
5. Verified: `buxc2 run lifetime_elision` PASS; goldens on buxc2 show same errors;
|
||||||
|
bootstrap still green; **selfhost-loop** expected IDENTICAL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 27 (tooling — D.4 bux doc + D.5 stdlib goldens)
|
||||||
|
|
||||||
|
1. **D.5 Stdlib goldens** (`tests/stdlib_golden/`):
|
||||||
|
- Packages: `array`, `string`, `collections` (Map/Set/Result/Option)
|
||||||
|
- `run.sh` builds via `buxc run` and matches expected PASS lines
|
||||||
|
- `make test-stdlib` wired into `make test`
|
||||||
|
2. **D.4 `bux doc`**:
|
||||||
|
- Bootstrap: `bootstrap/docgen.nim` — `///` + adjacent `/* */`
|
||||||
|
- Selfhost: `Cli_Doc` line scanner for `///`
|
||||||
|
- `bux doc [--out file] [path]` (default path `lib/`)
|
||||||
|
- `make docs` → `docs/api/stdlib.md`
|
||||||
|
3. **Stdlib docs:** `///` on Array / String / Test public helpers
|
||||||
|
4. Verified: `make test-stdlib`, `./buxc doc lib/Array.bux | head`, selfhost build
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 28 (LSP v0.4.0 — position-sensitive locals + inferred lets)
|
||||||
|
|
||||||
|
1. **`LocalBinding`** with scope range (`scopeStartLine`…`scopeEndLine`) per let/param
|
||||||
|
2. **Sema-backed inference** (`checkExprForLsp` / `resolveType`):
|
||||||
|
- `let x = 42` → hover `let x: int` · inferred
|
||||||
|
- `let s: String = "…"` → annotated, not inferred
|
||||||
|
- params: `param a: int` visible for whole function
|
||||||
|
3. **Position-sensitive** hover / go-to-def / completion (innermost scope wins on shadowing)
|
||||||
|
4. Nested scopes: if/while/for/match/block arms
|
||||||
|
5. Version **bux-lsp 0.4.0**; tests: `tools/test_lsp_locals.nim`, `tools/smoke_lsp_hover.sh`
|
||||||
|
6. Verified: hover shows `let sum: int · inferred`, `param a: int`, `let n: int · inferred`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 29 (full-tree `bux fmt` + CI enforce)
|
||||||
|
|
||||||
|
1. **One-shot format** of `lib/` (33), `examples/` (23), `src/` (15), `tests/` (8), `apps/` (12)
|
||||||
|
2. **Idempotent:** second `--check` → 0 would reformat on all trees
|
||||||
|
3. **CI:** `make fmt-check` enforces full tree + dirty-path smoke (exit 1)
|
||||||
|
4. **`make fmt`** helper to reformat the same roots
|
||||||
|
5. Verified: `test-stdlib`, key examples, **selfhost + selfhost-loop IDENTICAL ✓**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 30 (E.1 package registry + E.3 semver draft)
|
||||||
|
|
||||||
|
1. **Registry index** (`config/registry.toml`, `$BUX_REGISTRY`, `~/.bux/registry.toml`)
|
||||||
|
- `[[package]]` with `name` / `version` / `source` / `description`
|
||||||
|
- `file:` / `path:` (relative to index) or git URL
|
||||||
|
2. **CLI:** `bux search [q]`, `bux add <name>` resolves registry, `bux install` locks path/git
|
||||||
|
3. **Demo package:** `registry/packages/greet` (`Greet_Hello`, `Greet_Version`)
|
||||||
|
4. **Smoke:** `tools/smoke_registry.sh` / `make test-registry` — temp app outside tree
|
||||||
|
5. **Semver policy:** `docs/SEMVER.md` (0.x vs 1.0, registry version match)
|
||||||
|
6. Packages.md updated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Следващи стъпки
|
## Следващи стъпки
|
||||||
|
|
||||||
1. C.1 Lifetime elision
|
1. E.2 polish apps / E.5 benchmarks
|
||||||
2. Phase D tooling: `bux fmt` CI, `bux test --filter`, golden stdlib tests
|
2. HTTP-fetchable registry index URL (beyond local file)
|
||||||
3. LSP: position-sensitive locals; inferred `let` types
|
3. LSP: workspace rename / references (optional)
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Bux Semantic Versioning Policy
|
||||||
|
|
||||||
|
> Status: Draft for v0.x → v1.0 freeze (E.3)
|
||||||
|
|
||||||
|
Bux follows [Semantic Versioning 2.0.0](https://semver.org/) with the
|
||||||
|
clarifications below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version numbers
|
||||||
|
|
||||||
|
```
|
||||||
|
MAJOR.MINOR.PATCH[-prerelease]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Component | When it increases |
|
||||||
|
|-----------|-------------------|
|
||||||
|
| **MAJOR** | Breaking language / stdlib / CLI changes |
|
||||||
|
| **MINOR** | Backward-compatible features |
|
||||||
|
| **PATCH** | Backward-compatible bug fixes |
|
||||||
|
|
||||||
|
During **0.x** (pre-1.0):
|
||||||
|
|
||||||
|
- `0.MINOR.PATCH` — MINOR may still introduce breaking changes (documented in
|
||||||
|
the release notes and `MIGRATION_*.sh` when needed).
|
||||||
|
- Prefer deprecation warnings for at least one MINOR before removal when
|
||||||
|
practical.
|
||||||
|
|
||||||
|
After **1.0.0** (language freeze):
|
||||||
|
|
||||||
|
- Breaking changes require a MAJOR bump and a migration guide.
|
||||||
|
- The Language Reference is the normative spec; compiler bugs that contradict
|
||||||
|
the ref are fixed without a MAJOR bump.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What counts as “breaking”
|
||||||
|
|
||||||
|
- Removing or renaming a public stdlib symbol
|
||||||
|
- Changing the type or semantics of a public API
|
||||||
|
- Changing CLI flags that scripts rely on (`build`, `test`, `fmt --check`, …)
|
||||||
|
- Changing `bux.toml` / `bux.lock` fields in an incompatible way
|
||||||
|
- Changing the fat `func` / tuple C ABI in a way that breaks linked code
|
||||||
|
|
||||||
|
**Not breaking:**
|
||||||
|
|
||||||
|
- New keywords that were previously valid identifiers only if reserved carefully
|
||||||
|
(prefer contextual keywords)
|
||||||
|
- New diagnostics / stricter `@[Checked]` (document; may be gated)
|
||||||
|
- Formatter whitespace-only changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Package versions (registry)
|
||||||
|
|
||||||
|
Registry packages use the same MAJOR.MINOR.PATCH scheme.
|
||||||
|
|
||||||
|
`bux add foo` / `bux add foo 0.1` resolution:
|
||||||
|
|
||||||
|
| Request | Matches |
|
||||||
|
|---------|---------|
|
||||||
|
| `*` / omitted | Latest entry for `foo` in the index |
|
||||||
|
| `0.1.1` | Exact version |
|
||||||
|
| `0.1` | First version with that prefix (e.g. `0.1.1`) |
|
||||||
|
|
||||||
|
Lockfiles pin the **resolved** version and source path/URL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release checklist (maintainers)
|
||||||
|
|
||||||
|
1. Update `docs/LanguageRef.md` if behaviour changed
|
||||||
|
2. Update `docs/QUALITY_PLAN.md` / changelog notes
|
||||||
|
3. Run `make test` (includes `fmt-check`, examples, goldens)
|
||||||
|
4. Run `make selfhost-loop`
|
||||||
|
5. Tag `vMAJOR.MINOR.PATCH`
|
||||||
@@ -0,0 +1,749 @@
|
|||||||
|
# API Reference
|
||||||
|
|
||||||
|
Generated by `bux doc` from `///` and `/* */` documentation comments.
|
||||||
|
|
||||||
|
## `Array`
|
||||||
|
|
||||||
|
_Source: `lib/Array.bux`_
|
||||||
|
|
||||||
|
### `Array` _struct_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
struct Array<T> {
|
||||||
|
```
|
||||||
|
|
||||||
|
Growable contiguous buffer of `T` (len + capacity).
|
||||||
|
|
||||||
|
### `Array_New` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_New<T>(cap: uint) -> Array<T> {
|
||||||
|
```
|
||||||
|
|
||||||
|
Create an empty array with the given initial capacity.
|
||||||
|
|
||||||
|
### `Array_Push` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Push<T>(self: *Array<T>, value: T) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Append `value`, growing capacity if needed.
|
||||||
|
|
||||||
|
### `Array_Get` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Get<T>(self: *Array<T>, index: uint) -> T {
|
||||||
|
```
|
||||||
|
|
||||||
|
Element at `index` (bounds-checked unless `@[Release]`).
|
||||||
|
|
||||||
|
### `Array_Set` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Set<T>(self: *Array<T>, index: uint, value: T) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Write `value` at `index` (bounds-checked unless `@[Release]`).
|
||||||
|
|
||||||
|
### `Array_Len` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Len<T>(self: *Array<T>) -> uint {
|
||||||
|
```
|
||||||
|
|
||||||
|
Number of live elements.
|
||||||
|
|
||||||
|
### `Array_Free` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Free<T>(self: *Array<T>) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Free the backing buffer and reset length/capacity to zero.
|
||||||
|
|
||||||
|
### `Array_Drop` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Drop<T>(self: *Array<T>) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Drop trait entry — same as `Array_Free`.
|
||||||
|
|
||||||
|
### `Array_IsEmpty` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_IsEmpty<T>(self: *Array<T>) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if the array has no elements.
|
||||||
|
|
||||||
|
### `Array_Cap` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Cap<T>(self: *Array<T>) -> uint {
|
||||||
|
```
|
||||||
|
|
||||||
|
Current capacity (not length).
|
||||||
|
|
||||||
|
### `Array_Clear` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Clear<T>(self: *Array<T>) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Drop length to zero; keeps allocated capacity.
|
||||||
|
|
||||||
|
### `Array_Reserve` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Reserve<T>(self: *Array<T>, minCap: uint) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure capacity is at least `minCap` (does not shrink).
|
||||||
|
|
||||||
|
### `Array_First` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_First<T>(self: *Array<T>) -> T {
|
||||||
|
```
|
||||||
|
|
||||||
|
First element (bounds-checked if empty).
|
||||||
|
|
||||||
|
### `Array_Last` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Last<T>(self: *Array<T>) -> T {
|
||||||
|
```
|
||||||
|
|
||||||
|
Last element (bounds-checked if empty).
|
||||||
|
|
||||||
|
### `Array_Pop` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Pop<T>(self: *Array<T>) -> T {
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove and return the last element (bounds-checked if empty).
|
||||||
|
|
||||||
|
### `Array_Contains` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Contains<T>(self: *Array<T>, value: T) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
Linear search: true if `value` is present (uses `==`).
|
||||||
|
|
||||||
|
### `Array_IndexOf` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_IndexOf<T>(self: *Array<T>, value: T) -> int {
|
||||||
|
```
|
||||||
|
|
||||||
|
Index of first equal element, or `-1` if not found.
|
||||||
|
|
||||||
|
### `Array_Extend` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Extend<T>(self: *Array<T>, other: *Array<T>) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Append all elements of `other` onto `self`.
|
||||||
|
|
||||||
|
## `Channel`
|
||||||
|
|
||||||
|
_Source: `lib/Channel.bux`_
|
||||||
|
|
||||||
|
### `Channel_SendInt` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Channel_SendInt(ch: *Channel<int>, value: int) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Convenience wrappers for common types
|
||||||
|
|
||||||
|
## `Iter`
|
||||||
|
|
||||||
|
_Source: `lib/Iter.bux`_
|
||||||
|
|
||||||
|
### `Array_Iter` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Array_Iter<T>(arr: *Array<T>) -> Iter<T> {
|
||||||
|
```
|
||||||
|
|
||||||
|
Create an iterator from an Array
|
||||||
|
|
||||||
|
### `Iter_HasNext` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_HasNext<T>(it: *Iter<T>) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
Check if there are more elements
|
||||||
|
|
||||||
|
### `Iter_Next` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Next<T>(it: *Iter<T>) -> T {
|
||||||
|
```
|
||||||
|
|
||||||
|
Get the next element and advance (undefined if HasNext is false)
|
||||||
|
|
||||||
|
### `Iter_Peek` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Peek<T>(it: *Iter<T>) -> T {
|
||||||
|
```
|
||||||
|
|
||||||
|
Peek current element without advancing (undefined if HasNext is false)
|
||||||
|
|
||||||
|
### `Iter_Reset` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Reset<T>(it: *Iter<T>) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Reset iterator to the beginning
|
||||||
|
|
||||||
|
### `Iter_Pos` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Pos<T>(it: *Iter<T>) -> uint {
|
||||||
|
```
|
||||||
|
|
||||||
|
Current position
|
||||||
|
|
||||||
|
### `Iter_Len` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Len<T>(it: *Iter<T>) -> uint {
|
||||||
|
```
|
||||||
|
|
||||||
|
Remaining length
|
||||||
|
|
||||||
|
### `Iter_Count` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Count<T>(it: *Iter<T>) -> uint {
|
||||||
|
```
|
||||||
|
|
||||||
|
Count remaining elements
|
||||||
|
|
||||||
|
### `Iter_Skip` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Skip<T>(it: *Iter<T>, n: uint) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip N elements
|
||||||
|
|
||||||
|
### `Iter_Take` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Take<T>(it: *Iter<T>, n: uint) -> Iter<T> {
|
||||||
|
```
|
||||||
|
|
||||||
|
Take first N elements (by limiting len)
|
||||||
|
|
||||||
|
### `Iter_AnyEq` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_AnyEq<T>(it: *Iter<T>, value: T) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if any remaining element equals value
|
||||||
|
|
||||||
|
### `Iter_AllEq` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_AllEq<T>(it: *Iter<T>, value: T) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if every remaining element equals value (true if empty)
|
||||||
|
|
||||||
|
### `Iter_Collect` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Collect<T>(it: *Iter<T>) -> Array<T> {
|
||||||
|
```
|
||||||
|
|
||||||
|
Collect remaining elements into a new Array
|
||||||
|
|
||||||
|
### `Iter_Map` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Map<T, U>(it: *Iter<T>, f: func(T) -> U) -> Array<U> {
|
||||||
|
```
|
||||||
|
|
||||||
|
Map each remaining element through f: T → U, collect into Array<U>
|
||||||
|
|
||||||
|
### `Iter_Filter` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Filter<T>(it: *Iter<T>, pred: func(T) -> bool) -> Array<T> {
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep remaining elements for which pred returns true
|
||||||
|
|
||||||
|
### `Iter_Fold` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Fold<T, Acc>(it: *Iter<T>, init: Acc, f: func(Acc, T) -> Acc) -> Acc {
|
||||||
|
```
|
||||||
|
|
||||||
|
Left-fold: f(f(...f(init, x0), x1), ...)
|
||||||
|
|
||||||
|
### `Iter_ForEach` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_ForEach<T>(it: *Iter<T>, f: func(T) -> int) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Call f for each remaining element (return value of f is ignored)
|
||||||
|
|
||||||
|
### `Iter_Any` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_Any<T>(it: *Iter<T>, pred: func(T) -> bool) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if any remaining element satisfies pred
|
||||||
|
|
||||||
|
### `Iter_All` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_All<T>(it: *Iter<T>, pred: func(T) -> bool) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if all remaining elements satisfy pred (true if empty)
|
||||||
|
|
||||||
|
### `Iter_SumInt` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Iter_SumInt(it: *Iter<int>) -> int {
|
||||||
|
```
|
||||||
|
|
||||||
|
Sum remaining ints (specialized fold)
|
||||||
|
|
||||||
|
## `Json`
|
||||||
|
|
||||||
|
_Source: `lib/Json.bux`_
|
||||||
|
|
||||||
|
### `JsonValue` _struct_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
struct JsonValue {
|
||||||
|
```
|
||||||
|
|
||||||
|
=== Core type ===
|
||||||
|
|
||||||
|
### `Json_Null` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Json_Null() -> JsonValue {
|
||||||
|
```
|
||||||
|
|
||||||
|
=== Constructors ===
|
||||||
|
|
||||||
|
### `Json_ArrayLen` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Json_ArrayLen(v: JsonValue) -> uint {
|
||||||
|
```
|
||||||
|
|
||||||
|
=== Array helpers ===
|
||||||
|
|
||||||
|
### `Json_ObjectLen` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Json_ObjectLen(v: JsonValue) -> uint {
|
||||||
|
```
|
||||||
|
|
||||||
|
=== Object helpers ===
|
||||||
|
|
||||||
|
### `Json_IsNull` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Json_IsNull(v: JsonValue) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
=== Accessors ===
|
||||||
|
|
||||||
|
### `JsonParser` _struct_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
struct JsonParser {
|
||||||
|
```
|
||||||
|
|
||||||
|
=== Parser ===
|
||||||
|
|
||||||
|
### `Json_Parse` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Json_Parse(s: String) -> JsonValue {
|
||||||
|
```
|
||||||
|
|
||||||
|
=== Public parser ===
|
||||||
|
|
||||||
|
### `Json_StringifyImpl` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Json_StringifyImpl(sb: *StringBuilder, v: JsonValue) {
|
||||||
|
```
|
||||||
|
|
||||||
|
=== Serializer ===
|
||||||
|
|
||||||
|
## `Map`
|
||||||
|
|
||||||
|
_Source: `lib/Map.bux`_
|
||||||
|
|
||||||
|
### `Map_Remove` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Map_Remove<K, V>(m: *Map<K, V>, key: K) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove key if present. Rebuilds the table to keep open-addressing correct.
|
||||||
|
|
||||||
|
## `Net`
|
||||||
|
|
||||||
|
_Source: `lib/Net.bux`_
|
||||||
|
|
||||||
|
### `Net_Create` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Net_Create() -> int {
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a TCP socket. Returns -1 on error.
|
||||||
|
|
||||||
|
### `Net_SetReuse` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Net_SetReuse(fd: int) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable SO_REUSEADDR on a socket.
|
||||||
|
|
||||||
|
### `Net_Bind` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Net_Bind(fd: int, addr: String, port: int) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
Bind a socket to an address and port.
|
||||||
|
|
||||||
|
### `Net_Listen` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Net_Listen(fd: int, backlog: int) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
Start listening for connections.
|
||||||
|
|
||||||
|
### `Net_Accept` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Net_Accept(fd: int) -> int {
|
||||||
|
```
|
||||||
|
|
||||||
|
Accept a connection. Returns new fd or -1 on error.
|
||||||
|
|
||||||
|
### `Net_Connect` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Net_Connect(fd: int, addr: String, port: int) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
Connect to a remote address and port.
|
||||||
|
|
||||||
|
### `Net_Send` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Net_Send(fd: int, data: String) -> int {
|
||||||
|
```
|
||||||
|
|
||||||
|
Send data. Returns bytes sent or -1 on error.
|
||||||
|
|
||||||
|
### `Net_Recv` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Net_Recv(fd: int, maxLen: int) -> String {
|
||||||
|
```
|
||||||
|
|
||||||
|
Receive up to maxLen bytes. Returns empty string on error/EOF.
|
||||||
|
|
||||||
|
### `Net_Close` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Net_Close(fd: int) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
Close a socket.
|
||||||
|
|
||||||
|
### `Net_LastError` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Net_LastError() -> String {
|
||||||
|
```
|
||||||
|
|
||||||
|
Get last socket error as a string.
|
||||||
|
|
||||||
|
## `Option`
|
||||||
|
|
||||||
|
_Source: `lib/Option.bux`_
|
||||||
|
|
||||||
|
### `Option_Expect` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Option_Expect(o: Option, msg: String) -> int {
|
||||||
|
```
|
||||||
|
|
||||||
|
Unwrap Some or panic with a custom message
|
||||||
|
|
||||||
|
### `Option_Or` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Option_Or(o: Option, other: Option) -> Option {
|
||||||
|
```
|
||||||
|
|
||||||
|
If o is Some return it, otherwise return other
|
||||||
|
|
||||||
|
## `Os`
|
||||||
|
|
||||||
|
_Source: `lib/Os.bux`_
|
||||||
|
|
||||||
|
### `Os_Exit` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Os_Exit(code: int) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminate the process with the given exit code
|
||||||
|
|
||||||
|
## `Result`
|
||||||
|
|
||||||
|
_Source: `lib/Result.bux`_
|
||||||
|
|
||||||
|
### `Result_Expect` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Result_Expect(r: Result, msg: String) -> int {
|
||||||
|
```
|
||||||
|
|
||||||
|
Unwrap Ok or panic with a custom message
|
||||||
|
|
||||||
|
### `Result_UnwrapErr` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Result_UnwrapErr(r: Result) -> String {
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract Err payload (panics if Ok)
|
||||||
|
|
||||||
|
### `Result_Or` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Result_Or(r: Result, other: Result) -> Result {
|
||||||
|
```
|
||||||
|
|
||||||
|
If r is Ok return it, otherwise return other
|
||||||
|
|
||||||
|
## `Set`
|
||||||
|
|
||||||
|
_Source: `lib/Set.bux`_
|
||||||
|
|
||||||
|
### `Set_Remove` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Set_Remove<T>(s: *Set<T>, value: T) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove value if present. Rebuilds the table to keep open-addressing correct.
|
||||||
|
|
||||||
|
## `String`
|
||||||
|
|
||||||
|
_Source: `lib/String.bux`_
|
||||||
|
|
||||||
|
### `String_Len` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_Len(s: String) -> uint {
|
||||||
|
```
|
||||||
|
|
||||||
|
Byte length of a C string (`strlen`).
|
||||||
|
|
||||||
|
### `String_IsEmpty` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_IsEmpty(s: String) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if the string has zero length.
|
||||||
|
|
||||||
|
### `String_IsNull` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_IsNull(s: String) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if the pointer is null.
|
||||||
|
|
||||||
|
### `String_Eq` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_Eq(a: String, b: String) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
Lexicographic equality.
|
||||||
|
|
||||||
|
### `String_Concat` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_Concat(a: String, b: String) -> String {
|
||||||
|
```
|
||||||
|
|
||||||
|
Allocate and return `a` concatenated with `b`.
|
||||||
|
|
||||||
|
### `String_Copy` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_Copy(s: String) -> String {
|
||||||
|
```
|
||||||
|
|
||||||
|
Heap-copy of `s`.
|
||||||
|
|
||||||
|
### `String_StartsWith` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_StartsWith(s: String, prefix: String) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if `s` begins with `prefix`.
|
||||||
|
|
||||||
|
### `String_EndsWith` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_EndsWith(s: String, suffix: String) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if `s` ends with `suffix`.
|
||||||
|
|
||||||
|
### `String_Contains` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_Contains(s: String, substr: String) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if `substr` occurs anywhere in `s`.
|
||||||
|
|
||||||
|
### `String_IsBlank` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_IsBlank(s: String) -> bool {
|
||||||
|
```
|
||||||
|
|
||||||
|
True if empty or only whitespace (space, tab, CR, LF).
|
||||||
|
|
||||||
|
### `String_Repeat` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_Repeat(s: String, count: uint) -> String {
|
||||||
|
```
|
||||||
|
|
||||||
|
Repeat `s`, `count` times (`count == 0` → empty string).
|
||||||
|
|
||||||
|
### `String_ReplaceAll` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func String_ReplaceAll(s: String, old: String, new: String) -> String {
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace every non-overlapping occurrence of `old` with `new`.
|
||||||
|
Empty `old` is a no-op (returns `s` unchanged). Safe if `new` contains `old`.
|
||||||
|
|
||||||
|
## `Test`
|
||||||
|
|
||||||
|
_Source: `lib/Test.bux`_
|
||||||
|
|
||||||
|
### `Test_Exit` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Test_Exit(code: int) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit the process with `code` (for test runners).
|
||||||
|
|
||||||
|
### `Test_Assert` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Test_Assert(cond: bool) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert `cond` is true; abort on failure.
|
||||||
|
|
||||||
|
### `Test_AssertEqInt` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Test_AssertEqInt(a: int, b: int) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert two ints are equal; print both values and exit 1 on mismatch.
|
||||||
|
|
||||||
|
### `Test_AssertNeqInt` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Test_AssertNeqInt(a: int, b: int) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert two ints differ.
|
||||||
|
|
||||||
|
### `Test_AssertEqString` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Test_AssertEqString(a: String, b: String) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert two strings are equal (`String_Eq`).
|
||||||
|
|
||||||
|
### `Test_AssertEqBool` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Test_AssertEqBool(a: bool, b: bool) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert two bools are equal.
|
||||||
|
|
||||||
|
### `Test_AssertTrue` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Test_AssertTrue(cond: bool) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert `cond` is true.
|
||||||
|
|
||||||
|
### `Test_AssertFalse` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Test_AssertFalse(cond: bool) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert `cond` is false.
|
||||||
|
|
||||||
|
### `Test_Fail` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Test_Fail(msg: String) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Fail the test with a message and exit 1.
|
||||||
|
|
||||||
|
### `Test_Pass` _func_
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Test_Pass(msg: String) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Print a PASS line (for human-readable runners / goldens).
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// lifetime_elision.bux — C.1: elided lifetimes for common &[Checked] APIs
|
||||||
|
// No 'a annotations needed when there is a single input reference.
|
||||||
|
import Std::Io::{PrintLine, PrintInt};
|
||||||
|
import Std::Test::{Test_AssertEqInt, Test_Pass};
|
||||||
|
|
||||||
|
// Elided: param and return share one lifetime automatically
|
||||||
|
@[Checked]
|
||||||
|
func Identity(p: &int) -> &int {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit lifetime for documentation / multi-ref (same lifetime both sides)
|
||||||
|
@[Checked]
|
||||||
|
func IdentityNamed<'a>(p: &'a int) -> &'a int {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Via intermediate let binding — lifetime is propagated
|
||||||
|
@[Checked]
|
||||||
|
func ViaLet(p: &int) -> &int {
|
||||||
|
let r: &int = p;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// self-style first param: elision prefers the first input for the return
|
||||||
|
@[Checked]
|
||||||
|
func FirstOf(self: &int, _other: int) -> &int {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
@[Checked]
|
||||||
|
func Main() -> int {
|
||||||
|
var x: int = 10;
|
||||||
|
var y: int = 20;
|
||||||
|
|
||||||
|
let a: &int = Identity(&x);
|
||||||
|
Test_AssertEqInt(*a, 10);
|
||||||
|
|
||||||
|
let b: &int = IdentityNamed(&y);
|
||||||
|
Test_AssertEqInt(*b, 20);
|
||||||
|
|
||||||
|
let c: &int = ViaLet(&x);
|
||||||
|
Test_AssertEqInt(*c, 10);
|
||||||
|
|
||||||
|
let d: &int = FirstOf(&y, 0);
|
||||||
|
Test_AssertEqInt(*d, 20);
|
||||||
|
|
||||||
|
Test_Pass("lifetime_elision");
|
||||||
|
PrintLine("lifetime_elision: ok");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+18
-10
@@ -5,17 +5,20 @@ extern func bux_realloc(ptr: *void, size: uint) -> *void;
|
|||||||
extern func bux_free(ptr: *void);
|
extern func bux_free(ptr: *void);
|
||||||
extern func bux_bounds_check(index: uint, len: uint);
|
extern func bux_bounds_check(index: uint, len: uint);
|
||||||
|
|
||||||
|
/// Growable contiguous buffer of `T` (len + capacity).
|
||||||
struct Array<T> {
|
struct Array<T> {
|
||||||
data: *T,
|
data: *T,
|
||||||
len: uint,
|
len: uint,
|
||||||
cap: uint,
|
cap: uint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create an empty array with the given initial capacity.
|
||||||
func Array_New<T>(cap: uint) -> Array<T> {
|
func Array_New<T>(cap: uint) -> Array<T> {
|
||||||
let data = bux_alloc(cap * sizeof(T)) as *T;
|
let data = bux_alloc(cap * sizeof(T)) as *T;
|
||||||
return Array<T> { data: data, len: 0, cap: cap };
|
return Array<T> { data: data, len: 0, cap: cap };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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;
|
self.cap = self.cap * 2;
|
||||||
@@ -25,20 +28,24 @@ func Array_Push<T>(self: *Array<T>, value: T) {
|
|||||||
self.len = self.len + 1;
|
self.len = self.len + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Element at `index` (bounds-checked unless `@[Release]`).
|
||||||
func Array_Get<T>(self: *Array<T>, index: uint) -> T {
|
func Array_Get<T>(self: *Array<T>, index: uint) -> T {
|
||||||
bux_bounds_check(index, self.len);
|
bux_bounds_check(index, self.len);
|
||||||
return self.data[index];
|
return self.data[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write `value` at `index` (bounds-checked unless `@[Release]`).
|
||||||
func Array_Set<T>(self: *Array<T>, index: uint, value: T) {
|
func Array_Set<T>(self: *Array<T>, index: uint, value: T) {
|
||||||
bux_bounds_check(index, self.len);
|
bux_bounds_check(index, self.len);
|
||||||
self.data[index] = value;
|
self.data[index] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Number of live elements.
|
||||||
func Array_Len<T>(self: *Array<T>) -> uint {
|
func Array_Len<T>(self: *Array<T>) -> uint {
|
||||||
return self.len;
|
return self.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Free the backing buffer and reset length/capacity to zero.
|
||||||
func Array_Free<T>(self: *Array<T>) {
|
func Array_Free<T>(self: *Array<T>) {
|
||||||
bux_free(self.data as *void);
|
bux_free(self.data as *void);
|
||||||
self.data = null as *T;
|
self.data = null as *T;
|
||||||
@@ -46,6 +53,7 @@ func Array_Free<T>(self: *Array<T>) {
|
|||||||
self.cap = 0;
|
self.cap = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drop trait entry — same as `Array_Free`.
|
||||||
func Array_Drop<T>(self: *Array<T>) {
|
func Array_Drop<T>(self: *Array<T>) {
|
||||||
Array_Free<T>(self);
|
Array_Free<T>(self);
|
||||||
}
|
}
|
||||||
@@ -58,22 +66,22 @@ func Array_operator_index_set<T>(self: *Array<T>, idx: uint, value: T) {
|
|||||||
Array_Set<T>(self, idx, value);
|
Array_Set<T>(self, idx, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* True if the array has no elements */
|
/// True if the array has no elements.
|
||||||
func Array_IsEmpty<T>(self: *Array<T>) -> bool {
|
func Array_IsEmpty<T>(self: *Array<T>) -> bool {
|
||||||
return self.len == 0;
|
return self.len == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Current capacity (not length) */
|
/// Current capacity (not length).
|
||||||
func Array_Cap<T>(self: *Array<T>) -> uint {
|
func Array_Cap<T>(self: *Array<T>) -> uint {
|
||||||
return self.cap;
|
return self.cap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Drop length to zero; keeps allocated capacity */
|
/// Drop length to zero; keeps allocated capacity.
|
||||||
func Array_Clear<T>(self: *Array<T>) {
|
func Array_Clear<T>(self: *Array<T>) {
|
||||||
self.len = 0;
|
self.len = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure capacity is at least minCap (does not shrink) */
|
/// Ensure capacity is at least `minCap` (does not shrink).
|
||||||
func Array_Reserve<T>(self: *Array<T>, minCap: uint) {
|
func Array_Reserve<T>(self: *Array<T>, minCap: uint) {
|
||||||
if minCap <= self.cap {
|
if minCap <= self.cap {
|
||||||
return;
|
return;
|
||||||
@@ -82,24 +90,24 @@ func Array_Reserve<T>(self: *Array<T>, minCap: uint) {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* First element (panics if empty via bounds check) */
|
/// First element (bounds-checked if empty).
|
||||||
func Array_First<T>(self: *Array<T>) -> T {
|
func Array_First<T>(self: *Array<T>) -> T {
|
||||||
return Array_Get<T>(self, 0);
|
return Array_Get<T>(self, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Last element (panics if empty via bounds check) */
|
/// Last element (bounds-checked if empty).
|
||||||
func Array_Last<T>(self: *Array<T>) -> T {
|
func Array_Last<T>(self: *Array<T>) -> T {
|
||||||
return Array_Get<T>(self, self.len - 1);
|
return Array_Get<T>(self, self.len - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove and return the last element (panics if empty) */
|
/// Remove and return the last element (bounds-checked if empty).
|
||||||
func Array_Pop<T>(self: *Array<T>) -> T {
|
func Array_Pop<T>(self: *Array<T>) -> T {
|
||||||
bux_bounds_check(0, self.len);
|
bux_bounds_check(0, self.len);
|
||||||
self.len = self.len - 1;
|
self.len = self.len - 1;
|
||||||
return self.data[self.len];
|
return self.data[self.len];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Linear search: true if value is present (uses ==) */
|
/// Linear search: true if `value` is present (uses `==`).
|
||||||
func Array_Contains<T>(self: *Array<T>, value: T) -> bool {
|
func Array_Contains<T>(self: *Array<T>, value: T) -> bool {
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
while i < self.len {
|
while i < self.len {
|
||||||
@@ -111,7 +119,7 @@ func Array_Contains<T>(self: *Array<T>, value: T) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Index of first equal element, or -1 if not found */
|
/// Index of first equal element, or `-1` if not found.
|
||||||
func Array_IndexOf<T>(self: *Array<T>, value: T) -> int {
|
func Array_IndexOf<T>(self: *Array<T>, value: T) -> int {
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
while i < self.len {
|
while i < self.len {
|
||||||
@@ -123,7 +131,7 @@ func Array_IndexOf<T>(self: *Array<T>, value: T) -> int {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Append all elements of other onto self */
|
/// Append all elements of `other` onto `self`.
|
||||||
func Array_Extend<T>(self: *Array<T>, other: *Array<T>) {
|
func Array_Extend<T>(self: *Array<T>, other: *Array<T>) {
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
while i < other.len {
|
while i < other.len {
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ func Map_Remove<K, V>(m: *Map<K, V>, key: K) -> bool {
|
|||||||
m.entries = fresh.entries;
|
m.entries = fresh.entries;
|
||||||
m.cap = fresh.cap;
|
m.cap = fresh.cap;
|
||||||
m.len = fresh.len;
|
m.len = fresh.len;
|
||||||
|
// Ownership transferred to `m` — clear `fresh` so auto-Drop does not free twice
|
||||||
|
fresh.entries = null as *MapEntry<K, V>;
|
||||||
|
fresh.cap = 0;
|
||||||
|
fresh.len = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +224,10 @@ func StringMap_Remove<V>(m: *StringMap<V>, key: String) -> bool {
|
|||||||
m.entries = fresh.entries;
|
m.entries = fresh.entries;
|
||||||
m.cap = fresh.cap;
|
m.cap = fresh.cap;
|
||||||
m.len = fresh.len;
|
m.len = fresh.len;
|
||||||
|
// Ownership transferred to `m` — clear `fresh` so auto-Drop does not free twice
|
||||||
|
fresh.entries = null as *StringMapEntry<V>;
|
||||||
|
fresh.cap = 0;
|
||||||
|
fresh.len = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ func Set_Remove<T>(s: *Set<T>, value: T) -> bool {
|
|||||||
s.entries = fresh.entries;
|
s.entries = fresh.entries;
|
||||||
s.cap = fresh.cap;
|
s.cap = fresh.cap;
|
||||||
s.len = fresh.len;
|
s.len = fresh.len;
|
||||||
|
// Ownership transferred to `s` — clear `fresh` so auto-Drop does not free twice
|
||||||
|
fresh.entries = null as *SetEntry<T>;
|
||||||
|
fresh.cap = 0;
|
||||||
|
fresh.len = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-4
@@ -30,22 +30,27 @@ extern func bux_float_to_string(f: float64) -> String;
|
|||||||
extern func bux_str_format(pattern: String, a0: String, a1: String, a2: String, a3: String, a4: String, a5: String, a6: String, a7: String) -> String;
|
extern func bux_str_format(pattern: String, a0: String, a1: String, a2: String, a3: String, a4: String, a5: String, a6: String, a7: String) -> String;
|
||||||
|
|
||||||
|
|
||||||
|
/// Byte length of a C string (`strlen`).
|
||||||
func String_Len(s: String) -> uint {
|
func String_Len(s: String) -> uint {
|
||||||
return bux_strlen(s);
|
return bux_strlen(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if the string has zero length.
|
||||||
func String_IsEmpty(s: String) -> bool {
|
func String_IsEmpty(s: String) -> bool {
|
||||||
return bux_strlen(s) == 0;
|
return bux_strlen(s) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if the pointer is null.
|
||||||
func String_IsNull(s: String) -> bool {
|
func String_IsNull(s: String) -> bool {
|
||||||
return bux_str_is_null(s) != 0;
|
return bux_str_is_null(s) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lexicographic equality.
|
||||||
func String_Eq(a: String, b: String) -> bool {
|
func String_Eq(a: String, b: String) -> bool {
|
||||||
return bux_strcmp(a, b) == 0;
|
return bux_strcmp(a, b) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allocate and return `a` concatenated with `b`.
|
||||||
func String_Concat(a: String, b: String) -> String {
|
func String_Concat(a: String, b: String) -> String {
|
||||||
let len_a: uint = bux_strlen(a);
|
let len_a: uint = bux_strlen(a);
|
||||||
let len_b: uint = bux_strlen(b);
|
let len_b: uint = bux_strlen(b);
|
||||||
@@ -56,6 +61,7 @@ func String_Concat(a: String, b: String) -> String {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Heap-copy of `s`.
|
||||||
func String_Copy(s: String) -> String {
|
func String_Copy(s: String) -> String {
|
||||||
let len: uint = bux_strlen(s);
|
let len: uint = bux_strlen(s);
|
||||||
let buf: *char8 = bux_alloc(len + 1) as *char8;
|
let buf: *char8 = bux_alloc(len + 1) as *char8;
|
||||||
@@ -63,6 +69,7 @@ func String_Copy(s: String) -> String {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if `s` begins with `prefix`.
|
||||||
func String_StartsWith(s: String, prefix: String) -> bool {
|
func String_StartsWith(s: String, prefix: String) -> bool {
|
||||||
let s_len: uint = bux_strlen(s);
|
let s_len: uint = bux_strlen(s);
|
||||||
let p_len: uint = bux_strlen(prefix);
|
let p_len: uint = bux_strlen(prefix);
|
||||||
@@ -73,6 +80,7 @@ func String_StartsWith(s: String, prefix: String) -> bool {
|
|||||||
return r == 0;
|
return r == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if `s` ends with `suffix`.
|
||||||
func String_EndsWith(s: String, suffix: String) -> bool {
|
func String_EndsWith(s: String, suffix: String) -> bool {
|
||||||
let s_len: uint = bux_strlen(s);
|
let s_len: uint = bux_strlen(s);
|
||||||
let suf_len: uint = bux_strlen(suffix);
|
let suf_len: uint = bux_strlen(suffix);
|
||||||
@@ -85,6 +93,7 @@ func String_EndsWith(s: String, suffix: String) -> bool {
|
|||||||
return eq;
|
return eq;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if `substr` occurs anywhere in `s`.
|
||||||
func String_Contains(s: String, substr: String) -> bool {
|
func String_Contains(s: String, substr: String) -> bool {
|
||||||
let r: int = bux_str_contains(s, substr);
|
let r: int = bux_str_contains(s, substr);
|
||||||
return r != 0;
|
return r != 0;
|
||||||
@@ -151,7 +160,7 @@ func StringBuilder_Free(sb: *StringBuilder) {
|
|||||||
bux_sb_free(sb.handle);
|
bux_sb_free(sb.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* True if empty or only whitespace (space, tab, CR, LF) */
|
/// True if empty or only whitespace (space, tab, CR, LF).
|
||||||
func String_IsBlank(s: String) -> bool {
|
func String_IsBlank(s: String) -> bool {
|
||||||
let n: uint = bux_strlen(s);
|
let n: uint = bux_strlen(s);
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
@@ -165,7 +174,7 @@ func String_IsBlank(s: String) -> bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Repeat s, count times (count==0 → empty string) */
|
/// Repeat `s`, `count` times (`count == 0` → empty string).
|
||||||
func String_Repeat(s: String, count: uint) -> String {
|
func String_Repeat(s: String, count: uint) -> String {
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return "";
|
return "";
|
||||||
@@ -231,8 +240,8 @@ func String_Replace(s: String, old: String, new: String) -> String {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Replace every non-overlapping occurrence of old with new.
|
/// Replace every non-overlapping occurrence of `old` with `new`.
|
||||||
Empty old is a no-op (returns s unchanged). Safe if new contains old. */
|
/// Empty `old` is a no-op (returns `s` unchanged). Safe if `new` contains `old`.
|
||||||
func String_ReplaceAll(s: String, old: String, new: String) -> String {
|
func String_ReplaceAll(s: String, old: String, new: String) -> String {
|
||||||
let oldLen: uint = bux_strlen(old);
|
let oldLen: uint = bux_strlen(old);
|
||||||
if oldLen == 0 {
|
if oldLen == 0 {
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ import Std::String::{String_Eq};
|
|||||||
extern func bux_exit(code: int);
|
extern func bux_exit(code: int);
|
||||||
extern func bux_assert(cond: int, file: String, line: int, expr: String);
|
extern func bux_assert(cond: int, file: String, line: int, expr: String);
|
||||||
|
|
||||||
|
/// Exit the process with `code` (for test runners).
|
||||||
func Test_Exit(code: int) {
|
func Test_Exit(code: int) {
|
||||||
bux_exit(code);
|
bux_exit(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assert `cond` is true; abort on failure.
|
||||||
func Test_Assert(cond: bool) {
|
func Test_Assert(cond: bool) {
|
||||||
bux_assert(cond as int, "", 0, "");
|
bux_assert(cond as int, "", 0, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assert two ints are equal; print both values and exit 1 on mismatch.
|
||||||
func Test_AssertEqInt(a: int, b: int) {
|
func Test_AssertEqInt(a: int, b: int) {
|
||||||
if a != b {
|
if a != b {
|
||||||
PrintLine("ASSERT_EQ_INT FAILED:");
|
PrintLine("ASSERT_EQ_INT FAILED:");
|
||||||
@@ -23,6 +26,7 @@ func Test_AssertEqInt(a: int, b: int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assert two ints differ.
|
||||||
func Test_AssertNeqInt(a: int, b: int) {
|
func Test_AssertNeqInt(a: int, b: int) {
|
||||||
if a == b {
|
if a == b {
|
||||||
PrintLine("ASSERT_NEQ_INT FAILED: both are");
|
PrintLine("ASSERT_NEQ_INT FAILED: both are");
|
||||||
@@ -31,6 +35,7 @@ func Test_AssertNeqInt(a: int, b: int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assert two strings are equal (`String_Eq`).
|
||||||
func Test_AssertEqString(a: String, b: String) {
|
func Test_AssertEqString(a: String, b: String) {
|
||||||
if !String_Eq(a, b) {
|
if !String_Eq(a, b) {
|
||||||
PrintLine("ASSERT_EQ_STRING FAILED:");
|
PrintLine("ASSERT_EQ_STRING FAILED:");
|
||||||
@@ -41,6 +46,7 @@ func Test_AssertEqString(a: String, b: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assert two bools are equal.
|
||||||
func Test_AssertEqBool(a: bool, b: bool) {
|
func Test_AssertEqBool(a: bool, b: bool) {
|
||||||
if a != b {
|
if a != b {
|
||||||
PrintLine("ASSERT_EQ_BOOL FAILED");
|
PrintLine("ASSERT_EQ_BOOL FAILED");
|
||||||
@@ -48,6 +54,7 @@ func Test_AssertEqBool(a: bool, b: bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assert `cond` is true.
|
||||||
func Test_AssertTrue(cond: bool) {
|
func Test_AssertTrue(cond: bool) {
|
||||||
if !cond {
|
if !cond {
|
||||||
PrintLine("ASSERT_TRUE FAILED");
|
PrintLine("ASSERT_TRUE FAILED");
|
||||||
@@ -55,6 +62,7 @@ func Test_AssertTrue(cond: bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assert `cond` is false.
|
||||||
func Test_AssertFalse(cond: bool) {
|
func Test_AssertFalse(cond: bool) {
|
||||||
if cond {
|
if cond {
|
||||||
PrintLine("ASSERT_FALSE FAILED");
|
PrintLine("ASSERT_FALSE FAILED");
|
||||||
@@ -62,12 +70,14 @@ func Test_AssertFalse(cond: bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fail the test with a message and exit 1.
|
||||||
func Test_Fail(msg: String) {
|
func Test_Fail(msg: String) {
|
||||||
PrintLine("FAIL:");
|
PrintLine("FAIL:");
|
||||||
PrintLine(msg);
|
PrintLine(msg);
|
||||||
bux_exit(1);
|
bux_exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Print a PASS line (for human-readable runners / goldens).
|
||||||
func Test_Pass(msg: String) {
|
func Test_Pass(msg: String) {
|
||||||
PrintLine("PASS:");
|
PrintLine("PASS:");
|
||||||
PrintLine(msg);
|
PrintLine(msg);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# greet
|
||||||
|
|
||||||
|
Demo package for the Bux registry (`config/registry.toml`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bux add greet
|
||||||
|
bux install
|
||||||
|
```
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Main() -> int {
|
||||||
|
PrintLine(Greet_Hello("Bux"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "greet"
|
||||||
|
Version = "0.1.1"
|
||||||
|
Type = "lib"
|
||||||
|
Authors = ["Bux Core"]
|
||||||
|
License = "MIT"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Lib"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// greet — demo registry package (E.1)
|
||||||
|
module Greet {
|
||||||
|
|
||||||
|
/// Return a greeting for `name`.
|
||||||
|
func Greet_Hello(name: String) -> String {
|
||||||
|
return String_Concat("Hello, ", String_Concat(name, "!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the package version string.
|
||||||
|
func Greet_Version() -> String {
|
||||||
|
return "0.1.1";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+2
-1
@@ -48,7 +48,8 @@ struct TypeExpr {
|
|||||||
typeArgName1: String,
|
typeArgName1: String,
|
||||||
typeArgCount: int,
|
typeArgCount: int,
|
||||||
sliceElement: *TypeExpr, // for tekSlice
|
sliceElement: *TypeExpr, // for tekSlice
|
||||||
pointerPointee: *TypeExpr, // for tekPointer
|
pointerPointee: *TypeExpr, // for tekPointer / tekRef / tekMutRef
|
||||||
|
refLifetime: String, // for tekRef / tekMutRef: "'a" or "" (elided)
|
||||||
funcParams: *TypeExprList, // for tekFunc
|
funcParams: *TypeExprList, // for tekFunc
|
||||||
funcRet: *TypeExpr, // for tekFunc
|
funcRet: *TypeExpr, // for tekFunc
|
||||||
funcParamCount: int, // for tekFunc
|
funcParamCount: int, // for tekFunc
|
||||||
|
|||||||
+309
-32
@@ -922,12 +922,184 @@ func Cli_Fetch() -> int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Fmt command — format source files
|
// Doc command — Markdown from /// comments (D.4)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func Cli_Fmt(dir: String) -> int {
|
func Cli_DocIsDeclStart(line: String) -> bool {
|
||||||
// If dir is a file, format just that file
|
if String_StartsWith(line, "func ") { return true; }
|
||||||
|
if String_StartsWith(line, "pub func ") { return true; }
|
||||||
|
if String_StartsWith(line, "extern func ") { return true; }
|
||||||
|
if String_StartsWith(line, "const func ") { return true; }
|
||||||
|
if String_StartsWith(line, "async func ") { return true; }
|
||||||
|
if String_StartsWith(line, "struct ") { return true; }
|
||||||
|
if String_StartsWith(line, "pub struct ") { return true; }
|
||||||
|
if String_StartsWith(line, "enum ") { return true; }
|
||||||
|
if String_StartsWith(line, "interface ") { return true; }
|
||||||
|
if String_StartsWith(line, "module ") { return true; }
|
||||||
|
if String_StartsWith(line, "type ") { return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Cli_DocExtractName(line: String) -> String {
|
||||||
|
// Skip leading keywords
|
||||||
|
var s: String = line;
|
||||||
|
if String_StartsWith(s, "pub ") { s = bux_str_slice(s, 4, bux_strlen(s) - 4); }
|
||||||
|
if String_StartsWith(s, "extern ") { s = bux_str_slice(s, 7, bux_strlen(s) - 7); }
|
||||||
|
if String_StartsWith(s, "const ") { s = bux_str_slice(s, 6, bux_strlen(s) - 6); }
|
||||||
|
if String_StartsWith(s, "async ") { s = bux_str_slice(s, 6, bux_strlen(s) - 6); }
|
||||||
|
if String_StartsWith(s, "func ") { s = bux_str_slice(s, 5, bux_strlen(s) - 5); }
|
||||||
|
else if String_StartsWith(s, "struct ") { s = bux_str_slice(s, 7, bux_strlen(s) - 7); }
|
||||||
|
else if String_StartsWith(s, "enum ") { s = bux_str_slice(s, 5, bux_strlen(s) - 5); }
|
||||||
|
else if String_StartsWith(s, "interface ") { s = bux_str_slice(s, 10, bux_strlen(s) - 10); }
|
||||||
|
else if String_StartsWith(s, "module ") { s = bux_str_slice(s, 7, bux_strlen(s) - 7); }
|
||||||
|
else if String_StartsWith(s, "type ") { s = bux_str_slice(s, 5, bux_strlen(s) - 5); }
|
||||||
|
// Take until space, <, (, {, :, ;
|
||||||
|
var i: uint = 0;
|
||||||
|
let n: uint = bux_strlen(s);
|
||||||
|
while i < n {
|
||||||
|
let ch: String = bux_str_slice(s, i, 1);
|
||||||
|
if String_Eq(ch, " ") || String_Eq(ch, "<") || String_Eq(ch, "(") ||
|
||||||
|
String_Eq(ch, "{") || String_Eq(ch, ":") || String_Eq(ch, ";") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
if i == 0 { return s; }
|
||||||
|
return bux_str_slice(s, 0, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Cli_DocProcessFile(path: String, outSb: *StringBuilder) -> int {
|
||||||
|
let source: String = ReadFile(path);
|
||||||
|
if source == null as String || String_Eq(source, "") { return 0; }
|
||||||
|
var itemCount: int = 0;
|
||||||
|
var pending: String = "";
|
||||||
|
var hasPending: bool = false;
|
||||||
|
let lineCount: uint = bux_str_split_count(source, "\n");
|
||||||
|
// Drop trailing empty split artifact
|
||||||
|
var nLines: uint = lineCount;
|
||||||
|
if nLines > 0 {
|
||||||
|
let last: String = bux_str_split_part(source, "\n", nLines - 1);
|
||||||
|
if String_Eq(last, "") { nLines = nLines - 1; }
|
||||||
|
}
|
||||||
|
var li: uint = 0;
|
||||||
|
var wroteHeader: bool = false;
|
||||||
|
while li < nLines {
|
||||||
|
let raw: String = bux_str_split_part(source, "\n", li);
|
||||||
|
let line: String = String_Trim(raw);
|
||||||
|
if String_StartsWith(line, "///") {
|
||||||
|
var body: String = bux_str_slice(line, 3, bux_strlen(line) - 3);
|
||||||
|
if String_StartsWith(body, " ") {
|
||||||
|
body = bux_str_slice(body, 1, bux_strlen(body) - 1);
|
||||||
|
}
|
||||||
|
if hasPending {
|
||||||
|
pending = String_Concat(pending, String_Concat("\n", body));
|
||||||
|
} else {
|
||||||
|
pending = body;
|
||||||
|
hasPending = true;
|
||||||
|
}
|
||||||
|
li = li + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if String_Eq(line, "") || String_StartsWith(line, "@[") {
|
||||||
|
li = li + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if hasPending && Cli_DocIsDeclStart(line) {
|
||||||
|
if !wroteHeader {
|
||||||
|
StringBuilder_Append(outSb, "## `");
|
||||||
|
StringBuilder_Append(outSb, Cli_FileNameFromPath(path));
|
||||||
|
StringBuilder_Append(outSb, "`\n\n");
|
||||||
|
StringBuilder_Append(outSb, "_Source: `");
|
||||||
|
StringBuilder_Append(outSb, path);
|
||||||
|
StringBuilder_Append(outSb, "`_\n\n");
|
||||||
|
wroteHeader = true;
|
||||||
|
}
|
||||||
|
let name: String = Cli_DocExtractName(line);
|
||||||
|
StringBuilder_Append(outSb, "### `");
|
||||||
|
StringBuilder_Append(outSb, name);
|
||||||
|
StringBuilder_Append(outSb, "`\n\n");
|
||||||
|
StringBuilder_Append(outSb, "```bux\n");
|
||||||
|
StringBuilder_Append(outSb, line);
|
||||||
|
StringBuilder_Append(outSb, "\n```\n\n");
|
||||||
|
StringBuilder_Append(outSb, pending);
|
||||||
|
StringBuilder_Append(outSb, "\n\n");
|
||||||
|
itemCount = itemCount + 1;
|
||||||
|
hasPending = false;
|
||||||
|
pending = "";
|
||||||
|
li = li + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if String_StartsWith(line, "//") {
|
||||||
|
li = li + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Other code clears pending
|
||||||
|
hasPending = false;
|
||||||
|
pending = "";
|
||||||
|
li = li + 1;
|
||||||
|
}
|
||||||
|
return itemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Cli_Doc(dir: String, outPath: String) -> int {
|
||||||
|
var sb: StringBuilder = StringBuilder_NewCap(16384);
|
||||||
|
StringBuilder_Append(&sb, "# API Reference\n\n");
|
||||||
|
StringBuilder_Append(&sb, "Generated by `bux doc` from `///` documentation comments.\n\n");
|
||||||
|
var total: int = 0;
|
||||||
if FileExists(dir) {
|
if FileExists(dir) {
|
||||||
|
total = total + Cli_DocProcessFile(dir, &sb);
|
||||||
|
} else if DirExists(dir) {
|
||||||
|
var fileCount: int = 0;
|
||||||
|
let files: *String = bux_list_dir(dir, ".bux", &fileCount);
|
||||||
|
var i: int = 0;
|
||||||
|
while i < fileCount {
|
||||||
|
total = total + Cli_DocProcessFile(files[i], &sb);
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Print("Error: path not found: ");
|
||||||
|
PrintLine(dir);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let md: String = StringBuilder_Build(&sb);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
if String_Eq(outPath, "") {
|
||||||
|
Print(md);
|
||||||
|
} else {
|
||||||
|
if !WriteFile(outPath, md) {
|
||||||
|
Print("Error: cannot write ");
|
||||||
|
PrintLine(outPath);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Print("Wrote ");
|
||||||
|
PrintInt(total as int64);
|
||||||
|
Print(" documented items → ");
|
||||||
|
PrintLine(outPath);
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
PrintLine("warning: no /// documented declarations found");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fmt command — format source files (write or --check for CI)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func Cli_Fmt(dir: String, checkOnly: bool) -> int {
|
||||||
|
// If dir is a file, format/check just that file
|
||||||
|
if FileExists(dir) {
|
||||||
|
if checkOnly {
|
||||||
|
if Fmt_CheckFile(dir) == 0 {
|
||||||
|
Print(" ok ");
|
||||||
|
PrintLine(dir);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Print(" would reformat ");
|
||||||
|
PrintLine(dir);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
Print("Formatting ");
|
Print("Formatting ");
|
||||||
PrintLine(dir);
|
PrintLine(dir);
|
||||||
if Fmt_FormatFile(dir) {
|
if Fmt_FormatFile(dir) {
|
||||||
@@ -936,8 +1108,12 @@ func Cli_Fmt(dir: String) -> int {
|
|||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// Otherwise format all .bux files in directory
|
// Otherwise format/check all .bux files in directory
|
||||||
|
if checkOnly {
|
||||||
|
Print("Checking format in ");
|
||||||
|
} else {
|
||||||
Print("Formatting ");
|
Print("Formatting ");
|
||||||
|
}
|
||||||
PrintLine(dir);
|
PrintLine(dir);
|
||||||
var fileCount: int = 0;
|
var fileCount: int = 0;
|
||||||
let files: *String = bux_list_dir(dir, ".bux", &fileCount);
|
let files: *String = bux_list_dir(dir, ".bux", &fileCount);
|
||||||
@@ -947,14 +1123,34 @@ func Cli_Fmt(dir: String) -> int {
|
|||||||
}
|
}
|
||||||
var i: int = 0;
|
var i: int = 0;
|
||||||
var okCount: int = 0;
|
var okCount: int = 0;
|
||||||
|
var changeCount: int = 0;
|
||||||
while i < fileCount {
|
while i < fileCount {
|
||||||
|
if checkOnly {
|
||||||
|
if Fmt_CheckFile(files[i]) == 0 {
|
||||||
|
okCount = okCount + 1;
|
||||||
|
} else {
|
||||||
|
Print(" would reformat ");
|
||||||
|
PrintLine(files[i]);
|
||||||
|
changeCount = changeCount + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Print(" ");
|
Print(" ");
|
||||||
PrintLine(files[i]);
|
PrintLine(files[i]);
|
||||||
if Fmt_FormatFile(files[i]) {
|
if Fmt_FormatFile(files[i]) {
|
||||||
okCount = okCount + 1;
|
okCount = okCount + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
}
|
}
|
||||||
|
if checkOnly {
|
||||||
|
Print("fmt --check: ");
|
||||||
|
PrintInt(changeCount as int64);
|
||||||
|
Print(" would reformat, ");
|
||||||
|
PrintInt(okCount as int64);
|
||||||
|
PrintLine(" ok");
|
||||||
|
if changeCount > 0 { return 1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Print("Formatted "); PrintInt(okCount as int64); Print("/"); PrintInt(fileCount as int64); PrintLine(" files");
|
Print("Formatted "); PrintInt(okCount as int64); Print("/"); PrintInt(fileCount as int64); PrintLine(" files");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -985,10 +1181,17 @@ func Cli_StripExtension(name: String) -> String {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
func Cli_Test(projectDir: String) -> int {
|
func Cli_Test(projectDir: String, filter: String) -> int {
|
||||||
Print("Testing project: ");
|
Print("Testing project: ");
|
||||||
PrintLine(projectDir);
|
PrintLine(projectDir);
|
||||||
// Build and run the project's own Main first
|
if !String_Eq(filter, "") {
|
||||||
|
Print("Filter: ");
|
||||||
|
PrintLine(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without --filter, build and run the project's own Main first.
|
||||||
|
// With --filter, only run matching tests/*.bux files.
|
||||||
|
if String_Eq(filter, "") {
|
||||||
let mainRc: int = Cli_BuildProject(projectDir, "", false);
|
let mainRc: int = Cli_BuildProject(projectDir, "", false);
|
||||||
if mainRc != 0 {
|
if mainRc != 0 {
|
||||||
PrintLine("Main test build failed");
|
PrintLine("Main test build failed");
|
||||||
@@ -1011,6 +1214,7 @@ func Cli_Test(projectDir: String) -> int {
|
|||||||
return mainResult;
|
return mainResult;
|
||||||
}
|
}
|
||||||
PrintLine("Main tests passed");
|
PrintLine("Main tests passed");
|
||||||
|
}
|
||||||
|
|
||||||
// Propagate the project's stdlib to temp test packages.
|
// Propagate the project's stdlib to temp test packages.
|
||||||
let stdlibDir: String = Cli_FindStdlibDir(projectDir);
|
let stdlibDir: String = Cli_FindStdlibDir(projectDir);
|
||||||
@@ -1031,15 +1235,31 @@ func Cli_Test(projectDir: String) -> int {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PrintLine("┌──────────────────────────────┬────────┐");
|
||||||
|
PrintLine("│ Test │ Status │");
|
||||||
|
PrintLine("├──────────────────────────────┼────────┤");
|
||||||
|
|
||||||
var passed: int = 0;
|
var passed: int = 0;
|
||||||
var failed: int = 0;
|
var failed: int = 0;
|
||||||
|
var skipped: int = 0;
|
||||||
var i: int = 0;
|
var i: int = 0;
|
||||||
while i < testCount {
|
while i < testCount {
|
||||||
let testPath: String = testFiles[i];
|
let testPath: String = testFiles[i];
|
||||||
let fileName: String = Cli_FileNameFromPath(testPath);
|
let fileName: String = Cli_FileNameFromPath(testPath);
|
||||||
let testName: String = Cli_StripExtension(fileName);
|
let testName: String = Cli_StripExtension(fileName);
|
||||||
Print(" Test: ");
|
|
||||||
|
// --filter: only run tests whose name contains the filter substring
|
||||||
|
if !String_Eq(filter, "") {
|
||||||
|
if !String_Contains(testName, filter) {
|
||||||
|
skipped = skipped + 1;
|
||||||
|
i = i + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Print("│ ");
|
||||||
Print(testName);
|
Print(testName);
|
||||||
|
// Pad status column roughly (name may be long)
|
||||||
Print(" ... ");
|
Print(" ... ");
|
||||||
|
|
||||||
// Create temp package for this test file
|
// Create temp package for this test file
|
||||||
@@ -1050,14 +1270,14 @@ func Cli_Test(projectDir: String) -> int {
|
|||||||
|
|
||||||
let source: String = ReadFile(testPath);
|
let source: String = ReadFile(testPath);
|
||||||
if String_Eq(source, "") {
|
if String_Eq(source, "") {
|
||||||
PrintLine("FAIL (cannot read test file)");
|
PrintLine("FAIL │");
|
||||||
failed = failed + 1;
|
failed = failed + 1;
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let tmpMain: String = bux_path_join(tmpSrc, "Main.bux");
|
let tmpMain: String = bux_path_join(tmpSrc, "Main.bux");
|
||||||
if !WriteFile(tmpMain, source) {
|
if !WriteFile(tmpMain, source) {
|
||||||
PrintLine("FAIL (cannot write temp Main.bux)");
|
PrintLine("FAIL │");
|
||||||
failed = failed + 1;
|
failed = failed + 1;
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -1066,7 +1286,7 @@ func Cli_Test(projectDir: String) -> int {
|
|||||||
let tmpToml: String = bux_path_join(tmpDir, "bux.toml");
|
let tmpToml: String = bux_path_join(tmpDir, "bux.toml");
|
||||||
var tomlContent: String = "[Package]\nName = \"_test_tmp\"\nVersion = \"0.1.0\"\nType = \"bin\"\n\n[Build]\nOutput = \"Bin\"\n";
|
var tomlContent: String = "[Package]\nName = \"_test_tmp\"\nVersion = \"0.1.0\"\nType = \"bin\"\n\n[Build]\nOutput = \"Bin\"\n";
|
||||||
if !WriteFile(tmpToml, tomlContent) {
|
if !WriteFile(tmpToml, tomlContent) {
|
||||||
PrintLine("FAIL (cannot write temp bux.toml)");
|
PrintLine("FAIL │");
|
||||||
failed = failed + 1;
|
failed = failed + 1;
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -1091,36 +1311,48 @@ func Cli_Test(projectDir: String) -> int {
|
|||||||
|
|
||||||
let buildRc: int = Cli_BuildProject(tmpDir, "", false);
|
let buildRc: int = Cli_BuildProject(tmpDir, "", false);
|
||||||
if buildRc != 0 {
|
if buildRc != 0 {
|
||||||
PrintLine("FAIL (build error)");
|
PrintLine("FAIL │");
|
||||||
failed = failed + 1;
|
failed = failed + 1;
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let testBin: String = bux_path_join(bux_path_join(tmpDir, "build"), "_test_tmp");
|
let testBin: String = bux_path_join(bux_path_join(tmpDir, "build"), "_test_tmp");
|
||||||
if !FileExists(testBin) {
|
if !FileExists(testBin) {
|
||||||
PrintLine("FAIL (test binary not found)");
|
PrintLine("FAIL │");
|
||||||
failed = failed + 1;
|
failed = failed + 1;
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let runRc: int = bux_system(testBin);
|
let runRc: int = bux_system(testBin);
|
||||||
if runRc == 0 {
|
if runRc == 0 {
|
||||||
PrintLine("PASS");
|
PrintLine("PASS │");
|
||||||
passed = passed + 1;
|
passed = passed + 1;
|
||||||
} else {
|
} else {
|
||||||
Print("FAIL (exit ");
|
Print("FAIL │");
|
||||||
PrintInt(runRc as int64);
|
PrintLine("");
|
||||||
PrintLine(")");
|
|
||||||
failed = failed + 1;
|
failed = failed + 1;
|
||||||
}
|
}
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Print("Tests: ");
|
PrintLine("└──────────────────────────────┴────────┘");
|
||||||
|
Print("Results: ");
|
||||||
PrintInt(passed as int64);
|
PrintInt(passed as int64);
|
||||||
Print(" passed, ");
|
Print(" passed, ");
|
||||||
PrintInt(failed as int64);
|
PrintInt(failed as int64);
|
||||||
PrintLine(" failed");
|
Print(" failed");
|
||||||
|
if skipped > 0 {
|
||||||
|
Print(", ");
|
||||||
|
PrintInt(skipped as int64);
|
||||||
|
Print(" skipped");
|
||||||
|
}
|
||||||
|
PrintLine("");
|
||||||
|
if !String_Eq(filter, "") && passed == 0 && failed == 0 {
|
||||||
|
Print("No tests matching filter '");
|
||||||
|
Print(filter);
|
||||||
|
PrintLine("'");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
if failed > 0 { return 1; }
|
if failed > 0 { return 1; }
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1493,7 +1725,10 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
|||||||
if argCount < 2 {
|
if argCount < 2 {
|
||||||
PrintLine("Bux Self-Hosting Compiler v0.2.0");
|
PrintLine("Bux Self-Hosting Compiler v0.2.0");
|
||||||
PrintLine("Usage: buxc <command> [args]");
|
PrintLine("Usage: buxc <command> [args]");
|
||||||
PrintLine("Commands: build, check, new, init, add, remove, fetch, fmt, test, run, project, help, version");
|
PrintLine("Commands: build, check, new, init, add, remove, fetch, fmt, doc, test, run, project, help, version");
|
||||||
|
PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
|
||||||
|
PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)");
|
||||||
|
PrintLine(" doc --out file.md [path] API docs from /// comments");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1506,14 +1741,16 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
|||||||
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 v0.2.0");
|
PrintLine("Bux Self-Hosting Compiler v0.2.0");
|
||||||
PrintLine("Usage: buxc <command> [args]");
|
PrintLine("Usage: buxc <command> [args]");
|
||||||
PrintLine("Commands: build, check, new, init, add, remove, fetch, fmt, test, run, project, help, version");
|
PrintLine("Commands: build, check, new, init, add, remove, fetch, fmt, doc, test, run, project, help, version");
|
||||||
|
PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
|
||||||
|
PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)");
|
||||||
|
PrintLine(" doc --out file.md [path] API docs from /// comments");
|
||||||
PrintLine("Pipeline modules:");
|
PrintLine("Pipeline modules:");
|
||||||
PrintLine(" Lexer ✅ 695 lines");
|
PrintLine(" Lexer ✅");
|
||||||
PrintLine(" Parser ✅ 1004 lines");
|
PrintLine(" Parser ✅");
|
||||||
PrintLine(" Sema ✅ 393 lines");
|
PrintLine(" Sema ✅");
|
||||||
PrintLine(" HirLower ✅ 307 lines");
|
PrintLine(" HirLower ✅");
|
||||||
PrintLine(" CBackend ✅ 585 lines");
|
PrintLine(" CBackend ✅");
|
||||||
PrintLine(" Total: 3830 lines of Bux");
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1550,9 +1787,36 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if String_Eq(cmd, "fmt") {
|
if String_Eq(cmd, "fmt") {
|
||||||
let dir: String = ".";
|
var dir: String = ".";
|
||||||
if argCount >= 3 { dir = args[2]; }
|
var checkOnly: bool = false;
|
||||||
return Cli_Fmt(dir);
|
var fi: int = 2;
|
||||||
|
while fi < argCount {
|
||||||
|
if String_Eq(args[fi], "--check") {
|
||||||
|
checkOnly = true;
|
||||||
|
} else {
|
||||||
|
dir = args[fi];
|
||||||
|
}
|
||||||
|
fi = fi + 1;
|
||||||
|
}
|
||||||
|
return Cli_Fmt(dir, checkOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Eq(cmd, "doc") {
|
||||||
|
var dir: String = "lib";
|
||||||
|
var outPath: String = "";
|
||||||
|
var di: int = 2;
|
||||||
|
while di < argCount {
|
||||||
|
if String_Eq(args[di], "--out") && di + 1 < argCount {
|
||||||
|
outPath = args[di + 1];
|
||||||
|
di = di + 1;
|
||||||
|
} else if String_StartsWith(args[di], "--out=") {
|
||||||
|
outPath = bux_str_slice(args[di], 6, bux_strlen(args[di]) - 6);
|
||||||
|
} else if !String_StartsWith(args[di], "-") {
|
||||||
|
dir = args[di];
|
||||||
|
}
|
||||||
|
di = di + 1;
|
||||||
|
}
|
||||||
|
return Cli_Doc(dir, outPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if String_Eq(cmd, "check") {
|
if String_Eq(cmd, "check") {
|
||||||
@@ -1578,9 +1842,22 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if String_Eq(cmd, "test") {
|
if String_Eq(cmd, "test") {
|
||||||
let dir: String = ".";
|
var dir: String = ".";
|
||||||
if argCount >= 3 { dir = args[2]; }
|
var filter: String = "";
|
||||||
return Cli_Test(dir);
|
var ti: int = 2;
|
||||||
|
while ti < argCount {
|
||||||
|
if String_Eq(args[ti], "--filter") && ti + 1 < argCount {
|
||||||
|
filter = args[ti + 1];
|
||||||
|
ti = ti + 1;
|
||||||
|
} else if String_StartsWith(args[ti], "--filter=") {
|
||||||
|
// --filter=name form
|
||||||
|
filter = bux_str_slice(args[ti], 9, bux_strlen(args[ti]) - 9);
|
||||||
|
} else if !String_StartsWith(args[ti], "-") {
|
||||||
|
dir = args[ti];
|
||||||
|
}
|
||||||
|
ti = ti + 1;
|
||||||
|
}
|
||||||
|
return Cli_Test(dir, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if String_Eq(cmd, "run") {
|
if String_Eq(cmd, "run") {
|
||||||
|
|||||||
+32
-2
@@ -77,13 +77,22 @@ func Fmt_FormatSource(source: String) -> String {
|
|||||||
let sb: StringBuilder = StringBuilder_NewCap(8192);
|
let sb: StringBuilder = StringBuilder_NewCap(8192);
|
||||||
var indent: int = 0;
|
var indent: int = 0;
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
let lineCount: uint = bux_str_split_count(source, "\n");
|
var lineCount: uint = bux_str_split_count(source, "\n");
|
||||||
|
|
||||||
|
// Trailing "\n" yields a final empty part (split artifact). Drop it so
|
||||||
|
// re-formatting is idempotent and does not accumulate blank lines.
|
||||||
|
if lineCount > 0 {
|
||||||
|
let last: String = bux_str_split_part(source, "\n", lineCount - 1);
|
||||||
|
if String_Eq(last, "") {
|
||||||
|
lineCount = lineCount - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while i < lineCount {
|
while i < lineCount {
|
||||||
let line: String = bux_str_split_part(source, "\n", i);
|
let line: String = bux_str_split_part(source, "\n", i);
|
||||||
let trimmed: String = Fmt_TrimLeft(line);
|
let trimmed: String = Fmt_TrimLeft(line);
|
||||||
|
|
||||||
// Skip empty lines
|
// Empty line (intentional blank) — keep a single newline
|
||||||
if String_Eq(trimmed, "") {
|
if String_Eq(trimmed, "") {
|
||||||
StringBuilder_Append(&sb, "\n");
|
StringBuilder_Append(&sb, "\n");
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
@@ -135,4 +144,25 @@ func Fmt_FormatFile(path: String) -> bool {
|
|||||||
return bux_write_file(path, formatted);
|
return bux_write_file(path, formatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true if formatting would change the file (CI --check).
|
||||||
|
func Fmt_WouldChange(path: String) -> bool {
|
||||||
|
let source: String = bux_read_file(path);
|
||||||
|
if source == null as String { return false; }
|
||||||
|
let formatted: String = Fmt_FormatSource(source);
|
||||||
|
return !String_Eq(formatted, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check a file without writing. Returns 0 if clean, 1 if would reformat / error.
|
||||||
|
func Fmt_CheckFile(path: String) -> int {
|
||||||
|
let source: String = bux_read_file(path);
|
||||||
|
if source == null as String {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let formatted: String = Fmt_FormatSource(source);
|
||||||
|
if String_Eq(formatted, source) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -741,6 +741,19 @@ func lexNextToken(lex: *Lexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c == 39 { // '
|
if c == 39 { // '
|
||||||
|
// Lifetime 'a vs char literal 'x' / '\n'
|
||||||
|
// Lifetime: ' + ident-start, and the char after that is NOT closing '
|
||||||
|
let n1: uint32 = lexPeek(lex, 1);
|
||||||
|
let n2: uint32 = lexPeek(lex, 2);
|
||||||
|
if Lex_IsIdentStart(n1) && n2 != 39 && n2 != 0 {
|
||||||
|
lexMarkStart(lex);
|
||||||
|
discard lexAdvance(lex); // '
|
||||||
|
while !lexIsAtEnd(lex) && Lex_IsIdentChar(lexPeek(lex, 0)) {
|
||||||
|
discard lexAdvance(lex);
|
||||||
|
}
|
||||||
|
lexEmitToken(lex, tkLifetime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
lexScanChar(lex); return;
|
lexScanChar(lex); return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-8
@@ -188,9 +188,16 @@ func parserParseType(p: *Parser) -> *TypeExpr {
|
|||||||
let col: uint32 = parserCurToken(p).column;
|
let col: uint32 = parserCurToken(p).column;
|
||||||
let kindTok: int = parserPeek(p, 0);
|
let kindTok: int = parserPeek(p, 0);
|
||||||
|
|
||||||
// &T (shared reference) and &mut T (mutable reference)
|
// &T / &'a T (shared) and &mut T / &'a mut T (mutable)
|
||||||
if kindTok == tkAmp {
|
if kindTok == tkAmp {
|
||||||
discard parserAdvance(p); // &
|
discard parserAdvance(p); // &
|
||||||
|
var lt: String = "";
|
||||||
|
// Optional lifetime: &'a or &'a mut
|
||||||
|
if parserCheck(p, tkLifetime) {
|
||||||
|
let ltTok: LexToken = parserCurToken(p);
|
||||||
|
lt = ltTok.text;
|
||||||
|
discard parserAdvance(p);
|
||||||
|
}
|
||||||
var isMut: bool = false;
|
var isMut: bool = false;
|
||||||
// Check for "mut" keyword
|
// Check for "mut" keyword
|
||||||
if parserCheck(p, tkIdent) {
|
if parserCheck(p, tkIdent) {
|
||||||
@@ -208,6 +215,7 @@ func parserParseType(p: *Parser) -> *TypeExpr {
|
|||||||
}
|
}
|
||||||
te.line = line;
|
te.line = line;
|
||||||
te.column = col;
|
te.column = col;
|
||||||
|
te.refLifetime = lt;
|
||||||
te.pointerPointee = parserParseType(p);
|
te.pointerPointee = parserParseType(p);
|
||||||
if te.pointerPointee != null as *TypeExpr {
|
if te.pointerPointee != null as *TypeExpr {
|
||||||
te.typeName = String_Concat(te.pointerPointee.typeName, "*");
|
te.typeName = String_Concat(te.pointerPointee.typeName, "*");
|
||||||
@@ -1861,24 +1869,51 @@ func parserParseParamList(p: *Parser) -> *Decl {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func parserParseTypeParams(p: *Parser, d: *Decl) {
|
func parserParseTypeParams(p: *Parser, d: *Decl) {
|
||||||
|
// Lifetime params ('a) are accepted and skipped for monomorphization —
|
||||||
|
// they only annotate &/'a T on parameters/returns (stored in TypeExpr.refLifetime).
|
||||||
if !parserCheck(p, tkLt) { return; }
|
if !parserCheck(p, tkLt) { return; }
|
||||||
discard parserAdvance(p);
|
discard parserAdvance(p);
|
||||||
let tp0: LexToken = parserExpect(p, tkIdent, "expected type param");
|
var typeCount: int = 0;
|
||||||
d.typeParam0 = tp0.text;
|
var first: bool = true;
|
||||||
d.typeParamCount = 1;
|
while !parserCheck(p, tkGt) && parserPeek(p, 0) != tkEndOfFile {
|
||||||
|
if !first {
|
||||||
|
if !parserMatch(p, tkComma) { break; }
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
if parserCheck(p, tkGt) { break; }
|
||||||
|
// Lifetime type param: 'a — parse and ignore for mono slots
|
||||||
|
if parserCheck(p, tkLifetime) {
|
||||||
|
discard parserAdvance(p);
|
||||||
|
// optional trait bound is nonsense for lifetimes; skip : Bound if present
|
||||||
|
if parserMatch(p, tkColon) {
|
||||||
|
discard parserExpect(p, tkIdent, "expected trait bound name");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let tpTok: LexToken = parserExpect(p, tkIdent, "expected type param");
|
||||||
|
if typeCount == 0 {
|
||||||
|
d.typeParam0 = tpTok.text;
|
||||||
|
typeCount = 1;
|
||||||
if parserMatch(p, tkColon) {
|
if parserMatch(p, tkColon) {
|
||||||
let bound0: LexToken = parserExpect(p, tkIdent, "expected trait bound name");
|
let bound0: LexToken = parserExpect(p, tkIdent, "expected trait bound name");
|
||||||
d.typeParam0Bound = bound0.text;
|
d.typeParam0Bound = bound0.text;
|
||||||
}
|
}
|
||||||
if parserMatch(p, tkComma) {
|
} else if typeCount == 1 {
|
||||||
let tp1: LexToken = parserExpect(p, tkIdent, "expected type param");
|
d.typeParam1 = tpTok.text;
|
||||||
d.typeParam1 = tp1.text;
|
typeCount = 2;
|
||||||
d.typeParamCount = 2;
|
|
||||||
if parserMatch(p, tkColon) {
|
if parserMatch(p, tkColon) {
|
||||||
let bound1: LexToken = parserExpect(p, tkIdent, "expected trait bound name");
|
let bound1: LexToken = parserExpect(p, tkIdent, "expected trait bound name");
|
||||||
d.typeParam1Bound = bound1.text;
|
d.typeParam1Bound = bound1.text;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Extra type params beyond 2 — consume and ignore
|
||||||
|
if parserMatch(p, tkColon) {
|
||||||
|
discard parserExpect(p, tkIdent, "expected trait bound name");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.typeParamCount = typeCount;
|
||||||
discard parserExpect(p, tkGt, "expected '>'");
|
discard parserExpect(p, tkGt, "expected '>'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+252
-2
@@ -30,6 +30,26 @@ struct Sema {
|
|||||||
movedName5: String;
|
movedName5: String;
|
||||||
movedName6: String;
|
movedName6: String;
|
||||||
movedName7: String;
|
movedName7: String;
|
||||||
|
// Lifetime elision (C.1) — binding name → lifetime id (up to 8)
|
||||||
|
ltCount: int;
|
||||||
|
ltName0: String;
|
||||||
|
ltName1: String;
|
||||||
|
ltName2: String;
|
||||||
|
ltName3: String;
|
||||||
|
ltName4: String;
|
||||||
|
ltName5: String;
|
||||||
|
ltName6: String;
|
||||||
|
ltName7: String;
|
||||||
|
ltVal0: String;
|
||||||
|
ltVal1: String;
|
||||||
|
ltVal2: String;
|
||||||
|
ltVal3: String;
|
||||||
|
ltVal4: String;
|
||||||
|
ltVal5: String;
|
||||||
|
ltVal6: String;
|
||||||
|
ltVal7: String;
|
||||||
|
returnLifetime: String; // expected return ref lifetime ("" if not a ref return)
|
||||||
|
ltAnon: int; // next #elidedN counter
|
||||||
closureDepth: int; // nesting depth inside closures
|
closureDepth: int; // nesting depth inside closures
|
||||||
currentClosureExpr: *Expr; // current closure being analyzed (for capture tracking)
|
currentClosureExpr: *Expr; // current closure being analyzed (for capture tracking)
|
||||||
closureScope: *Scope; // scope at which the current closure was entered
|
closureScope: *Scope; // scope at which the current closure was entered
|
||||||
@@ -344,6 +364,208 @@ func Sema_RemoveMoved(sema: *Sema, name: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lifetime elision helpers (C.1) — selfhost parity with bootstrap
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func Sema_ClearLifetimes(sema: *Sema) {
|
||||||
|
if sema == null as *Sema { return; }
|
||||||
|
sema.ltCount = 0;
|
||||||
|
sema.returnLifetime = "";
|
||||||
|
sema.ltAnon = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sema_SetVarLifetime(sema: *Sema, name: String, lt: String) {
|
||||||
|
if sema == null as *Sema { return; }
|
||||||
|
if String_Eq(name, "") { return; }
|
||||||
|
// Update existing binding
|
||||||
|
if sema.ltCount > 0 && String_Eq(sema.ltName0, name) { sema.ltVal0 = lt; return; }
|
||||||
|
if sema.ltCount > 1 && String_Eq(sema.ltName1, name) { sema.ltVal1 = lt; return; }
|
||||||
|
if sema.ltCount > 2 && String_Eq(sema.ltName2, name) { sema.ltVal2 = lt; return; }
|
||||||
|
if sema.ltCount > 3 && String_Eq(sema.ltName3, name) { sema.ltVal3 = lt; return; }
|
||||||
|
if sema.ltCount > 4 && String_Eq(sema.ltName4, name) { sema.ltVal4 = lt; return; }
|
||||||
|
if sema.ltCount > 5 && String_Eq(sema.ltName5, name) { sema.ltVal5 = lt; return; }
|
||||||
|
if sema.ltCount > 6 && String_Eq(sema.ltName6, name) { sema.ltVal6 = lt; return; }
|
||||||
|
if sema.ltCount > 7 && String_Eq(sema.ltName7, name) { sema.ltVal7 = lt; return; }
|
||||||
|
if sema.ltCount >= 8 { return; }
|
||||||
|
if sema.ltCount == 0 { sema.ltName0 = name; sema.ltVal0 = lt; }
|
||||||
|
else if sema.ltCount == 1 { sema.ltName1 = name; sema.ltVal1 = lt; }
|
||||||
|
else if sema.ltCount == 2 { sema.ltName2 = name; sema.ltVal2 = lt; }
|
||||||
|
else if sema.ltCount == 3 { sema.ltName3 = name; sema.ltVal3 = lt; }
|
||||||
|
else if sema.ltCount == 4 { sema.ltName4 = name; sema.ltVal4 = lt; }
|
||||||
|
else if sema.ltCount == 5 { sema.ltName5 = name; sema.ltVal5 = lt; }
|
||||||
|
else if sema.ltCount == 6 { sema.ltName6 = name; sema.ltVal6 = lt; }
|
||||||
|
else if sema.ltCount == 7 { sema.ltName7 = name; sema.ltVal7 = lt; }
|
||||||
|
sema.ltCount = sema.ltCount + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sema_GetVarLifetime(sema: *Sema, name: String) -> String {
|
||||||
|
if sema == null as *Sema { return ""; }
|
||||||
|
if sema.ltCount > 0 && String_Eq(sema.ltName0, name) { return sema.ltVal0; }
|
||||||
|
if sema.ltCount > 1 && String_Eq(sema.ltName1, name) { return sema.ltVal1; }
|
||||||
|
if sema.ltCount > 2 && String_Eq(sema.ltName2, name) { return sema.ltVal2; }
|
||||||
|
if sema.ltCount > 3 && String_Eq(sema.ltName3, name) { return sema.ltVal3; }
|
||||||
|
if sema.ltCount > 4 && String_Eq(sema.ltName4, name) { return sema.ltVal4; }
|
||||||
|
if sema.ltCount > 5 && String_Eq(sema.ltName5, name) { return sema.ltVal5; }
|
||||||
|
if sema.ltCount > 6 && String_Eq(sema.ltName6, name) { return sema.ltVal6; }
|
||||||
|
if sema.ltCount > 7 && String_Eq(sema.ltName7, name) { return sema.ltVal7; }
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sema_DeclParam(decl: *Decl, i: int) -> *Param {
|
||||||
|
if decl == null as *Decl { return null as *Param; }
|
||||||
|
if i == 0 { return &decl.param0; }
|
||||||
|
if i == 1 { return &decl.param1; }
|
||||||
|
if i == 2 { return &decl.param2; }
|
||||||
|
if i == 3 { return &decl.param3; }
|
||||||
|
if i == 4 { return &decl.param4; }
|
||||||
|
if i == 5 { return &decl.param5; }
|
||||||
|
if i == 6 { return &decl.param6; }
|
||||||
|
if i == 7 { return &decl.param7; }
|
||||||
|
if i == 8 { return &decl.param8; }
|
||||||
|
return null as *Param;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sema_ApplyLifetimeElision(sema: *Sema, decl: *Decl) {
|
||||||
|
// Rust-style elision for @[Checked] functions (single-input + self).
|
||||||
|
Sema_ClearLifetimes(sema);
|
||||||
|
if sema == null as *Sema || decl == null as *Decl { return; }
|
||||||
|
if !sema.checkedFunc || sema.releaseFunc { return; }
|
||||||
|
|
||||||
|
var inputLt0: String = "";
|
||||||
|
var inputCount: int = 0;
|
||||||
|
var firstParamName: String = "";
|
||||||
|
var i: int = 0;
|
||||||
|
while i < decl.paramCount {
|
||||||
|
let p: *Param = Sema_DeclParam(decl, i);
|
||||||
|
if p != null as *Param && p.refParamType != null as *TypeExpr {
|
||||||
|
let pk: int = p.refParamType.kind;
|
||||||
|
if pk == tekRef || pk == tekMutRef {
|
||||||
|
var lt: String = p.refParamType.refLifetime;
|
||||||
|
if String_Eq(lt, "") {
|
||||||
|
lt = String_Concat("#elided", bux_int_to_str(sema.ltAnon as int64));
|
||||||
|
sema.ltAnon = sema.ltAnon + 1;
|
||||||
|
}
|
||||||
|
Sema_SetVarLifetime(sema, p.name, lt);
|
||||||
|
if inputCount == 0 { inputLt0 = lt; }
|
||||||
|
inputCount = inputCount + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i == 0 && p != null as *Param {
|
||||||
|
firstParamName = p.name;
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if decl.retType == null as *TypeExpr { return; }
|
||||||
|
let rk: int = decl.retType.kind;
|
||||||
|
if rk != tekRef && rk != tekMutRef { return; }
|
||||||
|
|
||||||
|
var rlt: String = decl.retType.refLifetime;
|
||||||
|
if String_Eq(rlt, "") {
|
||||||
|
if inputCount == 1 {
|
||||||
|
rlt = inputLt0;
|
||||||
|
} else if inputCount == 0 {
|
||||||
|
rlt = "#out";
|
||||||
|
} else if String_Eq(firstParamName, "self") || String_Eq(firstParamName, "Self") {
|
||||||
|
rlt = inputLt0;
|
||||||
|
} else {
|
||||||
|
Sema_EmitError(sema, decl.line, decl.column,
|
||||||
|
"lifetime elision failed: return type needs an explicit lifetime (multiple input references); e.g. func F<'a>(a: &'a T, b: &'a U) -> &'a T");
|
||||||
|
rlt = "#ambiguous";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sema.returnLifetime = rlt;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sema_ExtractBorrowedIdent(expr: *Expr) -> String {
|
||||||
|
// Identify source var of &x
|
||||||
|
if expr == null as *Expr { return ""; }
|
||||||
|
if expr.kind == ekUnary && expr.intValue == tkAmp {
|
||||||
|
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
|
||||||
|
return expr.child1.strValue;
|
||||||
|
}
|
||||||
|
if expr.child1 != null as *Expr && expr.child1.kind == ekUnary &&
|
||||||
|
expr.child1.intValue == tkAmp && expr.child1.child1 != null as *Expr &&
|
||||||
|
expr.child1.child1.kind == ekIdent {
|
||||||
|
return expr.child1.child1.strValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sema_ExprRefLifetime(sema: *Sema, expr: *Expr) -> String {
|
||||||
|
if sema == null as *Sema || expr == null as *Expr { return ""; }
|
||||||
|
if expr.kind == ekIdent {
|
||||||
|
return Sema_GetVarLifetime(sema, expr.strValue);
|
||||||
|
}
|
||||||
|
if expr.kind == ekUnary && expr.intValue == tkAmp {
|
||||||
|
let name: String = Sema_ExtractBorrowedIdent(expr);
|
||||||
|
if String_Eq(name, "") { return "#local"; }
|
||||||
|
let existing: String = Sema_GetVarLifetime(sema, name);
|
||||||
|
if !String_Eq(existing, "") {
|
||||||
|
// Reborrow of an existing ref binding keeps its lifetime
|
||||||
|
let sym: Symbol = Scope_Lookup(sema.scope, name);
|
||||||
|
if sym.refType != null as *TypeExpr {
|
||||||
|
if sym.refType.kind == tekRef || sym.refType.kind == tekMutRef {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Named as input lifetime but not a ref type? still use it
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
return "#local";
|
||||||
|
}
|
||||||
|
if expr.kind == ekUnary && expr.intValue == tkStar {
|
||||||
|
return Sema_ExprRefLifetime(sema, expr.child1);
|
||||||
|
}
|
||||||
|
if expr.kind == ekField {
|
||||||
|
let baseLt: String = Sema_ExprRefLifetime(sema, expr.child1);
|
||||||
|
if !String_Eq(baseLt, "") { return baseLt; }
|
||||||
|
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
|
||||||
|
let bl: String = Sema_GetVarLifetime(sema, expr.child1.strValue);
|
||||||
|
if !String_Eq(bl, "") { return bl; }
|
||||||
|
return "#local";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sema_CheckReturnLifetime(sema: *Sema, retExpr: *Expr, line: uint32, col: uint32) {
|
||||||
|
if sema == null as *Sema { return; }
|
||||||
|
if !sema.checkedFunc || sema.releaseFunc { return; }
|
||||||
|
if String_Eq(sema.returnLifetime, "") { return; }
|
||||||
|
if retExpr == null as *Expr { return; }
|
||||||
|
|
||||||
|
let got: String = Sema_ExprRefLifetime(sema, retExpr);
|
||||||
|
if String_Eq(sema.returnLifetime, "#out") {
|
||||||
|
Sema_EmitError(sema, line, col,
|
||||||
|
"cannot return a reference: function has no input reference to borrow from");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if String_Eq(got, "#local") {
|
||||||
|
Sema_EmitError(sema, line, col, "cannot return reference to local variable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if String_Eq(got, "") { return; }
|
||||||
|
if String_Eq(got, "#ambiguous") || String_Eq(sema.returnLifetime, "#ambiguous") { return; }
|
||||||
|
// Explicit lifetime mismatch
|
||||||
|
if String_StartsWith(got, "'") && String_StartsWith(sema.returnLifetime, "'") &&
|
||||||
|
!String_Eq(got, sema.returnLifetime) {
|
||||||
|
Sema_EmitError(sema, line, col,
|
||||||
|
String_Concat("lifetime mismatch: returning '",
|
||||||
|
String_Concat(got, String_Concat("' but function returns '",
|
||||||
|
String_Concat(sema.returnLifetime, "'")))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if String_StartsWith(got, "#elided") && String_StartsWith(sema.returnLifetime, "#elided") &&
|
||||||
|
!String_Eq(got, sema.returnLifetime) {
|
||||||
|
Sema_EmitError(sema, line, col,
|
||||||
|
"lifetime mismatch: returned reference does not outlive the return type (multiple input references; annotate with an explicit lifetime)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Capture tracking for closures
|
// Capture tracking for closures
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1221,6 +1443,30 @@ func Sema_CheckStmt(sema: *Sema, stmt: *Stmt) {
|
|||||||
sym.isPublic = false;
|
sym.isPublic = false;
|
||||||
sym.decl = null as *Decl;
|
sym.decl = null as *Decl;
|
||||||
discard Scope_Define(sema.scope, sym);
|
discard Scope_Define(sema.scope, sym);
|
||||||
|
// Propagate ref lifetime for return-site checks
|
||||||
|
if sema.checkedFunc && !sema.releaseFunc && stmt.child1 != null as *Expr {
|
||||||
|
var isRefBind: bool = false;
|
||||||
|
if stmt.refStmtType != null as *TypeExpr {
|
||||||
|
if stmt.refStmtType.kind == tekRef || stmt.refStmtType.kind == tekMutRef {
|
||||||
|
isRefBind = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isRefBind && stmt.child1.refType != null as *TypeExpr {
|
||||||
|
if stmt.child1.refType.kind == tekRef || stmt.child1.refType.kind == tekMutRef {
|
||||||
|
isRefBind = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also treat address-of as creating a ref binding
|
||||||
|
if !isRefBind && stmt.child1.kind == ekUnary && stmt.child1.intValue == tkAmp {
|
||||||
|
isRefBind = true;
|
||||||
|
}
|
||||||
|
if isRefBind {
|
||||||
|
let lt: String = Sema_ExprRefLifetime(sema, stmt.child1);
|
||||||
|
if !String_Eq(lt, "") {
|
||||||
|
Sema_SetVarLifetime(sema, stmt.strValue, lt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1241,6 +1487,8 @@ func Sema_CheckStmt(sema: *Sema, stmt: *Stmt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// C.1 lifetime: reject dangling returns / elision mismatches
|
||||||
|
Sema_CheckReturnLifetime(sema, stmt.child1, stmt.line, stmt.column);
|
||||||
} else {
|
} else {
|
||||||
if sema.currentRetType != tyVoid && sema.currentRetType != tyUnknown {
|
if sema.currentRetType != tyVoid && sema.currentRetType != tyUnknown {
|
||||||
Sema_EmitError(sema, stmt.line, stmt.column, "missing return value");
|
Sema_EmitError(sema, stmt.line, stmt.column, "missing return value");
|
||||||
@@ -2075,8 +2323,6 @@ func Sema_Analyze(mod: *Module) -> *Sema {
|
|||||||
s.currentRetType = Sema_ResolveType(s, decl.retType);
|
s.currentRetType = Sema_ResolveType(s, decl.retType);
|
||||||
} else {
|
} else {
|
||||||
s.currentRetType = tyVoid;
|
s.currentRetType = tyVoid;
|
||||||
s.checkedFunc = false;
|
|
||||||
s.movedCount = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable borrow checking for @[Checked] functions
|
// Enable borrow checking for @[Checked] functions
|
||||||
@@ -2084,6 +2330,9 @@ func Sema_Analyze(mod: *Module) -> *Sema {
|
|||||||
s.checkedFunc = decl.isChecked != 0;
|
s.checkedFunc = decl.isChecked != 0;
|
||||||
let wasRelease: bool = s.releaseFunc;
|
let wasRelease: bool = s.releaseFunc;
|
||||||
s.releaseFunc = decl.isRelease != 0;
|
s.releaseFunc = decl.isRelease != 0;
|
||||||
|
s.movedCount = 0;
|
||||||
|
// C.1: lifetime elision before walking the body
|
||||||
|
Sema_ApplyLifetimeElision(s, decl);
|
||||||
|
|
||||||
// Check body statements
|
// Check body statements
|
||||||
var stmt: *Stmt = decl.refBody.firstStmt;
|
var stmt: *Stmt = decl.refBody.firstStmt;
|
||||||
@@ -2094,6 +2343,7 @@ func Sema_Analyze(mod: *Module) -> *Sema {
|
|||||||
|
|
||||||
s.checkedFunc = wasChecked;
|
s.checkedFunc = wasChecked;
|
||||||
s.releaseFunc = wasRelease;
|
s.releaseFunc = wasRelease;
|
||||||
|
Sema_ClearLifetimes(s);
|
||||||
s.scope = prevScope;
|
s.scope = prevScope;
|
||||||
}
|
}
|
||||||
decl = decl.childDecl2;
|
decl = decl.childDecl2;
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ const tkCase: int = 108;
|
|||||||
const tkDefault: int = 109;
|
const tkDefault: int = 109;
|
||||||
const tkUnsafe: int = 110;
|
const tkUnsafe: int = 110;
|
||||||
|
|
||||||
|
// Lifetime parameter token: 'a, 'b, ... (not a char literal)
|
||||||
|
const tkLifetime: int = 111;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Token struct
|
// Token struct
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -339,6 +342,7 @@ func Token_KindName(kind: int) -> String {
|
|||||||
if kind == tkHashDate { return "#date"; }
|
if kind == tkHashDate { return "#date"; }
|
||||||
if kind == tkHashTime { return "#time"; }
|
if kind == tkHashTime { return "#time"; }
|
||||||
if kind == tkHashModule { return "#module"; }
|
if kind == tkHashModule { return "#module"; }
|
||||||
|
if kind == tkLifetime { return "lifetime"; }
|
||||||
if kind == tkNewLine { return "newline"; }
|
if kind == tkNewLine { return "newline"; }
|
||||||
if kind == tkEndOfFile { return "end of file"; }
|
if kind == tkEndOfFile { return "end of file"; }
|
||||||
return "unknown token";
|
return "unknown token";
|
||||||
|
|||||||
@@ -248,5 +248,126 @@ func Main() -> int {
|
|||||||
let val: int = (*r).x;
|
let val: int = (*r).x;
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
""")
|
||||||
|
check(not res.hasErrors)
|
||||||
|
|
||||||
|
# --- C.1 Lifetime elision ---
|
||||||
|
|
||||||
|
test "@[Checked] elided lifetime: return param ref is OK":
|
||||||
|
let res = checkSource("""
|
||||||
|
@[Checked]
|
||||||
|
func Identity(p: &int) -> &int {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
@[Checked]
|
||||||
|
func Main() -> int {
|
||||||
|
var x: int = 7;
|
||||||
|
let r: &int = Identity(&x);
|
||||||
|
return *r;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
check(not res.hasErrors)
|
||||||
|
|
||||||
|
test "@[Checked] explicit lifetime 'a works":
|
||||||
|
let res = checkSource("""
|
||||||
|
@[Checked]
|
||||||
|
func Identity<'a>(p: &'a int) -> &'a int {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
@[Checked]
|
||||||
|
func Main() -> int {
|
||||||
|
var x: int = 3;
|
||||||
|
let r: &int = Identity(&x);
|
||||||
|
return *r;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
check(not res.hasErrors)
|
||||||
|
|
||||||
|
test "@[Checked] rejects return of reference to local":
|
||||||
|
let res = checkSource("""
|
||||||
|
@[Checked]
|
||||||
|
func Dangle(p: &int) -> &int {
|
||||||
|
var x: int = 1;
|
||||||
|
return &x;
|
||||||
|
}
|
||||||
|
@[Checked]
|
||||||
|
func Main() -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
check(res.hasErrors)
|
||||||
|
check(res.diagnostics[0].message.contains("local"))
|
||||||
|
|
||||||
|
test "@[Checked] rejects return ref with no input reference":
|
||||||
|
let res = checkSource("""
|
||||||
|
@[Checked]
|
||||||
|
func Bad() -> &int {
|
||||||
|
var x: int = 1;
|
||||||
|
return &x;
|
||||||
|
}
|
||||||
|
@[Checked]
|
||||||
|
func Main() -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
check(res.hasErrors)
|
||||||
|
check(res.diagnostics[0].message.contains("no input reference") or
|
||||||
|
res.diagnostics[0].message.contains("local"))
|
||||||
|
|
||||||
|
test "@[Checked] elision fails with multiple input refs":
|
||||||
|
let res = checkSource("""
|
||||||
|
@[Checked]
|
||||||
|
func Pick(a: &int, b: &int) -> &int {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
@[Checked]
|
||||||
|
func Main() -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
check(res.hasErrors)
|
||||||
|
check(res.diagnostics[0].message.contains("lifetime elision failed") or
|
||||||
|
res.diagnostics[0].message.contains("lifetime mismatch"))
|
||||||
|
|
||||||
|
test "@[Checked] multiple inputs OK with explicit lifetime":
|
||||||
|
let res = checkSource("""
|
||||||
|
@[Checked]
|
||||||
|
func Pick<'a>(a: &'a int, b: &'a int) -> &'a int {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
@[Checked]
|
||||||
|
func Main() -> int {
|
||||||
|
var x: int = 1;
|
||||||
|
var y: int = 2;
|
||||||
|
let r: &int = Pick(&x, &y);
|
||||||
|
return *r;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
check(not res.hasErrors)
|
||||||
|
|
||||||
|
test "@[Checked] let-bound reborrow of param may be returned":
|
||||||
|
let res = checkSource("""
|
||||||
|
@[Checked]
|
||||||
|
func ViaLet(p: &int) -> &int {
|
||||||
|
let r: &int = p;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
@[Checked]
|
||||||
|
func Main() -> int {
|
||||||
|
var x: int = 9;
|
||||||
|
return *ViaLet(&x);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
check(not res.hasErrors)
|
||||||
|
|
||||||
|
test "unchecked may return &local (no lifetime checks)":
|
||||||
|
let res = checkSource("""
|
||||||
|
func Dangle() -> &int {
|
||||||
|
var x: int = 1;
|
||||||
|
return &x;
|
||||||
|
}
|
||||||
|
func Main() -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
""")
|
""")
|
||||||
check(not res.hasErrors)
|
check(not res.hasErrors)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "elision_multi_input"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
error: type errors in project
|
||||||
|
error: lifetime elision failed: return type needs an explicit lifetime (multiple input references); e.g. func F<'a>(a: &'a T, b: &'a U) -> &'a T
|
||||||
|
--> FILE:2:1
|
||||||
|
|
|
||||||
|
2 | func Pick(a: &int, b: &int) -> &int {
|
||||||
|
| ^^^^
|
||||||
|
= help: add an explicit lifetime, e.g. func F<'a>(x: &'a T, y: &'a U) -> &'a T
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@[Checked]
|
||||||
|
func Pick(a: &int, b: &int) -> &int {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@[Checked]
|
||||||
|
func Main() -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "return_local_ref"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
error: type errors in project
|
||||||
|
error: cannot return reference to local variable
|
||||||
|
--> FILE:4:5
|
||||||
|
|
|
||||||
|
4 | return &x;
|
||||||
|
| ^^^^^^
|
||||||
|
= help: return a value, or return a reference borrowed from a function parameter
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@[Checked]
|
||||||
|
func Dangle(p: &int) -> &int {
|
||||||
|
var x: int = 42;
|
||||||
|
return &x;
|
||||||
|
}
|
||||||
|
|
||||||
|
@[Checked]
|
||||||
|
func Main() -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "stdlib_array"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
stdlib_array: ok
|
||||||
|
PASS:
|
||||||
|
stdlib_array
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Stdlib golden: Array helpers + Contains/IndexOf/Extend
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::Array::{
|
||||||
|
Array, Array_New, Array_Push, Array_Pop, Array_Clear, Array_IsEmpty,
|
||||||
|
Array_First, Array_Last, Array_Cap, Array_Reserve, Array_Len, Array_Get,
|
||||||
|
Array_Contains, Array_IndexOf, Array_Extend, Array_Free
|
||||||
|
};
|
||||||
|
import Std::Test::{
|
||||||
|
Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_Pass
|
||||||
|
};
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
var arr: Array<int> = Array_New<int>(2);
|
||||||
|
Array_Reserve<int>(&arr, 8);
|
||||||
|
Test_AssertTrue(Array_Cap<int>(&arr) >= 8);
|
||||||
|
Test_AssertTrue(Array_IsEmpty<int>(&arr));
|
||||||
|
|
||||||
|
Array_Push<int>(&arr, 10);
|
||||||
|
Array_Push<int>(&arr, 20);
|
||||||
|
Array_Push<int>(&arr, 30);
|
||||||
|
|
||||||
|
Test_AssertFalse(Array_IsEmpty<int>(&arr));
|
||||||
|
Test_AssertEqInt(Array_Len<int>(&arr) as int, 3);
|
||||||
|
Test_AssertEqInt(Array_First<int>(&arr), 10);
|
||||||
|
Test_AssertEqInt(Array_Last<int>(&arr), 30);
|
||||||
|
Test_AssertTrue(Array_Contains<int>(&arr, 20));
|
||||||
|
Test_AssertFalse(Array_Contains<int>(&arr, 99));
|
||||||
|
Test_AssertEqInt(Array_IndexOf<int>(&arr, 30), 2);
|
||||||
|
|
||||||
|
let popped: int = Array_Pop<int>(&arr);
|
||||||
|
Test_AssertEqInt(popped, 30);
|
||||||
|
Test_AssertEqInt(Array_Len<int>(&arr) as int, 2);
|
||||||
|
|
||||||
|
var extra: Array<int> = Array_New<int>(2);
|
||||||
|
Array_Push<int>(&extra, 40);
|
||||||
|
Array_Push<int>(&extra, 50);
|
||||||
|
Array_Extend<int>(&arr, &extra);
|
||||||
|
Test_AssertEqInt(Array_Len<int>(&arr) as int, 4);
|
||||||
|
Test_AssertEqInt(Array_Get<int>(&arr, 3), 50);
|
||||||
|
|
||||||
|
Array_Clear<int>(&arr);
|
||||||
|
Test_AssertTrue(Array_IsEmpty<int>(&arr));
|
||||||
|
Test_AssertTrue(Array_Cap<int>(&arr) >= 8);
|
||||||
|
|
||||||
|
Array_Free<int>(&arr);
|
||||||
|
Array_Free<int>(&extra);
|
||||||
|
PrintLine("stdlib_array: ok");
|
||||||
|
Test_Pass("stdlib_array");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "stdlib_collections"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
stdlib_collections: ok
|
||||||
|
PASS:
|
||||||
|
stdlib_collections
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Stdlib golden: Map / Set / Result / Option helpers
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::Map::{
|
||||||
|
Map, Map_New, Map_Set, Map_Get, Map_Has, Map_Remove, Map_Clear,
|
||||||
|
Map_Len, Map_IsEmpty, Map_Free
|
||||||
|
};
|
||||||
|
import Std::Set::{
|
||||||
|
Set, Set_New, Set_Add, Set_Has, Set_Remove, Set_Len, Set_IsEmpty, Set_Free
|
||||||
|
};
|
||||||
|
import Std::Result::{
|
||||||
|
Result, Result_NewOk, Result_NewErr, Result_IsOk, Result_IsErr,
|
||||||
|
Result_UnwrapOr, Result_Or, Result_UnwrapErr
|
||||||
|
};
|
||||||
|
import Std::Option::{
|
||||||
|
Option, Option_NewSome, Option_NewNone, Option_IsSome, Option_Or, Option_UnwrapOr
|
||||||
|
};
|
||||||
|
import Std::String::{String_Eq};
|
||||||
|
import Std::Test::{
|
||||||
|
Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_Pass
|
||||||
|
};
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
var m: Map<int, int> = Map_New<int, int>(16);
|
||||||
|
Map_Set<int, int>(&m, 1, 100);
|
||||||
|
Map_Set<int, int>(&m, 2, 200);
|
||||||
|
Map_Set<int, int>(&m, 3, 300);
|
||||||
|
Test_AssertEqInt(Map_Len<int, int>(&m) as int, 3);
|
||||||
|
Test_AssertTrue(Map_Has<int, int>(&m, 2));
|
||||||
|
Test_AssertTrue(Map_Remove<int, int>(&m, 2));
|
||||||
|
Test_AssertFalse(Map_Has<int, int>(&m, 2));
|
||||||
|
Test_AssertEqInt(Map_Get<int, int>(&m, 1), 100);
|
||||||
|
Test_AssertFalse(Map_Remove<int, int>(&m, 99));
|
||||||
|
Map_Clear<int, int>(&m);
|
||||||
|
Test_AssertTrue(Map_IsEmpty<int, int>(&m));
|
||||||
|
Map_Free<int, int>(&m);
|
||||||
|
|
||||||
|
var s: Set<int> = Set_New<int>(16);
|
||||||
|
Set_Add<int>(&s, 10);
|
||||||
|
Set_Add<int>(&s, 20);
|
||||||
|
Set_Add<int>(&s, 30);
|
||||||
|
Test_AssertTrue(Set_Remove<int>(&s, 20));
|
||||||
|
Test_AssertFalse(Set_Has<int>(&s, 20));
|
||||||
|
Test_AssertTrue(Set_Has<int>(&s, 10));
|
||||||
|
Test_AssertEqInt(Set_Len<int>(&s) as int, 2);
|
||||||
|
Test_AssertFalse(Set_IsEmpty<int>(&s));
|
||||||
|
Set_Free<int>(&s);
|
||||||
|
|
||||||
|
let ok: Result = Result_NewOk(42);
|
||||||
|
let err: Result = Result_NewErr("boom");
|
||||||
|
Test_AssertTrue(Result_IsOk(ok));
|
||||||
|
Test_AssertTrue(Result_IsErr(err));
|
||||||
|
Test_AssertEqInt(Result_UnwrapOr(err, -1), -1);
|
||||||
|
let recovered: Result = Result_Or(err, Result_NewOk(7));
|
||||||
|
Test_AssertEqInt(Result_UnwrapOr(recovered, 0), 7);
|
||||||
|
Test_AssertTrue(String_Eq(Result_UnwrapErr(err), "boom"));
|
||||||
|
|
||||||
|
let some: Option = Option_NewSome(5);
|
||||||
|
let none: Option = Option_NewNone();
|
||||||
|
Test_AssertTrue(Option_IsSome(some));
|
||||||
|
Test_AssertEqInt(Option_UnwrapOr(none, 9), 9);
|
||||||
|
let filled: Option = Option_Or(none, Option_NewSome(3));
|
||||||
|
Test_AssertEqInt(Option_UnwrapOr(filled, 0), 3);
|
||||||
|
|
||||||
|
PrintLine("stdlib_collections: ok");
|
||||||
|
Test_Pass("stdlib_collections");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Executable
+103
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Golden behavioral tests for stdlib modules.
|
||||||
|
# Usage: from repo root: tests/stdlib_golden/run.sh [path/to/buxc]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
BUXC_ARG="${1:-$ROOT/buxc}"
|
||||||
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# Resolve to absolute path so `cd` into test packages still finds the binary.
|
||||||
|
if [[ "$BUXC_ARG" = /* ]]; then
|
||||||
|
BUXC="$BUXC_ARG"
|
||||||
|
else
|
||||||
|
BUXC="$(cd "$(dirname "$BUXC_ARG")" && pwd)/$(basename "$BUXC_ARG")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$BUXC" && ! -f "$BUXC" ]]; then
|
||||||
|
echo "error: buxc not found at $BUXC (run make build first)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
passed=0
|
||||||
|
failed=0
|
||||||
|
skipped=0
|
||||||
|
|
||||||
|
normalize_out() {
|
||||||
|
# Drop absolute paths; trim trailing whitespace/blank lines
|
||||||
|
sed -E \
|
||||||
|
-e "s|$ROOT|ROOT|g" \
|
||||||
|
-e "s|$DIR|DIR|g" \
|
||||||
|
-e 's/[[:space:]]+$//' \
|
||||||
|
| sed -e :a -e '/^\n*$/{$d;N;ba' -e '}'
|
||||||
|
}
|
||||||
|
|
||||||
|
for case_dir in "$DIR"/*/; do
|
||||||
|
name="$(basename "$case_dir")"
|
||||||
|
[[ -f "$case_dir/bux.toml" ]] || continue
|
||||||
|
[[ -f "$case_dir/src/Main.bux" ]] || continue
|
||||||
|
|
||||||
|
if [[ ! -f "$case_dir/expected.out" ]]; then
|
||||||
|
echo " SKIP $name (no expected.out)"
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build + run; capture stdout+stderr
|
||||||
|
out=""
|
||||||
|
if ! out="$(cd "$case_dir" && "$BUXC" run . 2>&1)"; then
|
||||||
|
echo " FAIL $name (build/run non-zero)"
|
||||||
|
printf '%s\n' "$out" | head -40
|
||||||
|
failed=$((failed + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
got="$(printf '%s\n' "$out" | normalize_out)"
|
||||||
|
exp="$(cat "$case_dir/expected.out" | normalize_out)"
|
||||||
|
|
||||||
|
# Match on key status lines (tests may also print build noise)
|
||||||
|
if printf '%s\n' "$got" | grep -Fqx "$(printf '%s' "$exp" | head -1)" 2>/dev/null; then
|
||||||
|
# Prefer full expected lines all present
|
||||||
|
all_ok=1
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
if ! printf '%s\n' "$got" | grep -Fqx "$line"; then
|
||||||
|
all_ok=0
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done <<< "$exp"
|
||||||
|
if [[ $all_ok -eq 1 ]]; then
|
||||||
|
echo " PASS $name"
|
||||||
|
passed=$((passed + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: every non-empty expected line appears as substring
|
||||||
|
all_ok=1
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
if ! printf '%s\n' "$got" | grep -Fq "$line"; then
|
||||||
|
all_ok=0
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done <<< "$exp"
|
||||||
|
|
||||||
|
if [[ $all_ok -eq 1 ]]; then
|
||||||
|
echo " PASS $name"
|
||||||
|
passed=$((passed + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL $name"
|
||||||
|
echo "---- expected lines ----"
|
||||||
|
printf '%s\n' "$exp"
|
||||||
|
echo "---- got (tail) ----"
|
||||||
|
printf '%s\n' "$got" | tail -20
|
||||||
|
echo "--------------"
|
||||||
|
failed=$((failed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Stdlib golden tests: $passed passed, $failed failed, $skipped skipped"
|
||||||
|
if [[ $failed -gt 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "stdlib_string"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
stdlib_string: ok
|
||||||
|
PASS:
|
||||||
|
stdlib_string
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Stdlib golden: String_IsEmpty / IsBlank / Repeat / ReplaceAll
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::String::{
|
||||||
|
String_IsEmpty, String_IsBlank, String_Repeat, String_ReplaceAll,
|
||||||
|
String_Eq, String_Len, String_Contains, String_StartsWith, String_EndsWith
|
||||||
|
};
|
||||||
|
import Std::Test::{
|
||||||
|
Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_AssertEqString, Test_Pass
|
||||||
|
};
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
Test_AssertTrue(String_IsEmpty(""));
|
||||||
|
Test_AssertFalse(String_IsEmpty("x"));
|
||||||
|
Test_AssertTrue(String_IsBlank(""));
|
||||||
|
Test_AssertTrue(String_IsBlank(" \t\n"));
|
||||||
|
Test_AssertFalse(String_IsBlank(" x "));
|
||||||
|
|
||||||
|
Test_AssertEqString(String_Repeat(".", 5), ".....");
|
||||||
|
Test_AssertEqInt(String_Len(String_Repeat("ab", 3)) as int, 6);
|
||||||
|
Test_AssertEqString(String_Repeat("x", 0), "");
|
||||||
|
Test_AssertEqString(String_Repeat("ok", 1), "ok");
|
||||||
|
|
||||||
|
let multi: String = String_ReplaceAll("a-b-a-b-a", "a", "X");
|
||||||
|
Test_AssertEqString(multi, "X-b-X-b-X");
|
||||||
|
let safe: String = String_ReplaceAll("..", ".", "x.");
|
||||||
|
Test_AssertEqString(safe, "x.x.");
|
||||||
|
|
||||||
|
Test_AssertTrue(String_Contains("hello", "ell"));
|
||||||
|
Test_AssertTrue(String_StartsWith("hello", "he"));
|
||||||
|
Test_AssertTrue(String_EndsWith("hello", "lo"));
|
||||||
|
|
||||||
|
PrintLine("stdlib_string: ok");
|
||||||
|
Test_Pass("stdlib_string");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+259
-43
@@ -4,10 +4,10 @@
|
|||||||
# Usage: bux-lsp
|
# Usage: bux-lsp
|
||||||
# The editor spawns this binary and communicates via stdin/stdout.
|
# The editor spawns this binary and communicates via stdin/stdout.
|
||||||
#
|
#
|
||||||
# Hover uses real bootstrap sema types when possible (globals + stdlib);
|
# Hover uses real bootstrap sema types when possible (globals + stdlib).
|
||||||
# completion/outline still use a fast lightweight scan.
|
# Locals are position-sensitive (scoped) and include inferred `let` types (v0.4.0).
|
||||||
|
|
||||||
import std/[json, os, strutils, streams, tables, osproc, sequtils]
|
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
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -88,6 +88,17 @@ type
|
|||||||
detail: string ## signature / type annotation
|
detail: string ## signature / type annotation
|
||||||
container: string ## optional parent (module / type)
|
container: string ## optional parent (module / type)
|
||||||
fromSema: bool ## detail came from real type checker
|
fromSema: bool ## detail came from real type checker
|
||||||
|
## Scoped local binding for position-sensitive hover / go-to-def
|
||||||
|
LocalBinding = object
|
||||||
|
name: string
|
||||||
|
detail: string ## e.g. "let x: int" (inferred or annotated)
|
||||||
|
kind: string ## variable | parameter
|
||||||
|
declLine: int ## 0-based declaration line
|
||||||
|
declCol: int ## 0-based start of name
|
||||||
|
scopeStartLine: int ## first line where name is visible
|
||||||
|
scopeEndLine: int ## last line where name is visible (inclusive)
|
||||||
|
container: string ## enclosing function name
|
||||||
|
inferred: bool ## type came from initializer, not annotation
|
||||||
DocumentState = ref object
|
DocumentState = ref object
|
||||||
uri: string
|
uri: string
|
||||||
content: string
|
content: string
|
||||||
@@ -97,6 +108,8 @@ type
|
|||||||
## Full-project type index for hover (includes stdlib after sema enrich)
|
## Full-project type index for hover (includes stdlib after sema enrich)
|
||||||
typeIndex: Table[string, string] ## name → type / signature string
|
typeIndex: Table[string, string] ## name → type / signature string
|
||||||
kindIndex: Table[string, string] ## name → kind label
|
kindIndex: Table[string, string] ## name → kind label
|
||||||
|
## Position-sensitive locals (filled by enrichWithSema)
|
||||||
|
locals: seq[LocalBinding]
|
||||||
|
|
||||||
var
|
var
|
||||||
documents = initTable[string, DocumentState]()
|
documents = initTable[string, DocumentState]()
|
||||||
@@ -355,8 +368,14 @@ proc typeExprToStr(te: TypeExpr): string =
|
|||||||
of tekOwn:
|
of tekOwn:
|
||||||
result = "own " & typeExprToStr(te.pointerPointee)
|
result = "own " & typeExprToStr(te.pointerPointee)
|
||||||
of tekRef:
|
of tekRef:
|
||||||
|
if te.refLifetime.len > 0:
|
||||||
|
result = "&" & te.refLifetime & " " & typeExprToStr(te.pointerPointee)
|
||||||
|
else:
|
||||||
result = "&" & typeExprToStr(te.pointerPointee)
|
result = "&" & typeExprToStr(te.pointerPointee)
|
||||||
of tekMutRef:
|
of tekMutRef:
|
||||||
|
if te.refLifetime.len > 0:
|
||||||
|
result = "&" & te.refLifetime & " mut " & typeExprToStr(te.pointerPointee)
|
||||||
|
else:
|
||||||
result = "&mut " & typeExprToStr(te.pointerPointee)
|
result = "&mut " & typeExprToStr(te.pointerPointee)
|
||||||
of tekSlice:
|
of tekSlice:
|
||||||
result = typeExprToStr(te.sliceElement) & "[]"
|
result = typeExprToStr(te.sliceElement) & "[]"
|
||||||
@@ -590,56 +609,173 @@ proc enrichWithSema(doc: DocumentState) =
|
|||||||
else:
|
else:
|
||||||
indexDecl(d)
|
indexDecl(d)
|
||||||
|
|
||||||
# Walk this file's AST for local lets with explicit types (function bodies)
|
# --- Position-sensitive locals + inferred let types ---
|
||||||
proc walkBlock(blk: Block, container: string) =
|
doc.locals = @[]
|
||||||
|
|
||||||
|
proc blockEndLine(blk: Block): int =
|
||||||
|
## Last 0-based line covered by statements in `blk` (best-effort).
|
||||||
|
if blk == nil: return 0
|
||||||
|
result = max(0, int(blk.loc.line) - 1)
|
||||||
|
for stmt in blk.stmts:
|
||||||
|
result = max(result, max(0, int(stmt.loc.line) - 1))
|
||||||
|
case stmt.kind
|
||||||
|
of skIf:
|
||||||
|
result = max(result, blockEndLine(stmt.stmtIfThen))
|
||||||
|
result = max(result, blockEndLine(stmt.stmtIfElse))
|
||||||
|
for br in stmt.stmtIfElseIfs:
|
||||||
|
result = max(result, blockEndLine(br.blk))
|
||||||
|
of skWhile:
|
||||||
|
result = max(result, blockEndLine(stmt.stmtWhileBody))
|
||||||
|
of skDoWhile:
|
||||||
|
result = max(result, blockEndLine(stmt.stmtDoWhileBody))
|
||||||
|
of skLoop:
|
||||||
|
result = max(result, blockEndLine(stmt.stmtLoopBody))
|
||||||
|
of skFor:
|
||||||
|
result = max(result, blockEndLine(stmt.stmtForBody))
|
||||||
|
of skMatch:
|
||||||
|
for arm in stmt.stmtMatchArms:
|
||||||
|
if arm.body != nil and arm.body.kind == ekBlock:
|
||||||
|
result = max(result, blockEndLine(arm.body.exprBlock))
|
||||||
|
elif arm.body != nil:
|
||||||
|
result = max(result, max(0, int(arm.body.loc.line) - 1))
|
||||||
|
of skExpr:
|
||||||
|
if stmt.stmtExpr != nil and stmt.stmtExpr.kind == ekBlock:
|
||||||
|
result = max(result, blockEndLine(stmt.stmtExpr.exprBlock))
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc collectLocals(sema: var Sema, blk: Block, sc: Scope, scopeEnd: int,
|
||||||
|
container: string) =
|
||||||
if blk == nil: return
|
if blk == nil: return
|
||||||
|
let endLine = max(scopeEnd, blockEndLine(blk))
|
||||||
for stmt in blk.stmts:
|
for stmt in blk.stmts:
|
||||||
case stmt.kind
|
case stmt.kind
|
||||||
of skLet:
|
of skLet:
|
||||||
let n = stmt.stmtLetName
|
let n = stmt.stmtLetName
|
||||||
if n.len == 0: continue
|
if n.len == 0: continue
|
||||||
var typStr = ""
|
var typ: Type = makeUnknown()
|
||||||
|
var inferred = false
|
||||||
if stmt.stmtLetType != nil:
|
if stmt.stmtLetType != nil:
|
||||||
typStr = typeExprToStr(stmt.stmtLetType)
|
typ = sema.resolveType(stmt.stmtLetType)
|
||||||
|
if (typ == nil or typ.isUnknown) and stmt.stmtLetInit != nil:
|
||||||
|
typ = sema.checkExprForLsp(stmt.stmtLetInit, sc)
|
||||||
|
inferred = true
|
||||||
|
elif stmt.stmtLetType == nil and stmt.stmtLetInit != nil:
|
||||||
|
# Explicit absence of annotation — still type the initializer
|
||||||
|
typ = sema.checkExprForLsp(stmt.stmtLetInit, sc)
|
||||||
|
inferred = true
|
||||||
let kw = if stmt.stmtLetMut: "var" else: "let"
|
let kw = if stmt.stmtLetMut: "var" else: "let"
|
||||||
let detail = if typStr.len > 0: kw & " " & n & ": " & typStr else: kw & " " & n
|
let typStr = if typ != nil and not typ.isUnknown: typ.toString else: ""
|
||||||
let loc = stmt.loc
|
let detail =
|
||||||
let line = max(0, int(loc.line) - 1)
|
if typStr.len > 0: kw & " " & n & ": " & typStr
|
||||||
let col = max(0, int(loc.column) - 1)
|
else: kw & " " & n
|
||||||
# Prefer sema-enriched detail if name already global; else add local
|
let line = max(0, int(stmt.loc.line) - 1)
|
||||||
if not doc.symbols.hasKey(n) or not doc.symbols[n].fromSema:
|
let col = max(0, int(stmt.loc.column) - 1)
|
||||||
|
doc.locals.add(LocalBinding(
|
||||||
|
name: n, detail: detail, kind: "variable",
|
||||||
|
declLine: line, declCol: col,
|
||||||
|
scopeStartLine: line, scopeEndLine: endLine,
|
||||||
|
container: container, inferred: inferred and typStr.len > 0))
|
||||||
|
# Also keep latest flat entry for outline (position lookup prefers locals)
|
||||||
doc.symbols[n] = SymbolInfo(
|
doc.symbols[n] = SymbolInfo(
|
||||||
line: line, col: col, kind: "variable", detail: detail,
|
line: line, col: col, kind: "variable", detail: detail,
|
||||||
container: container, fromSema: typStr.len > 0)
|
container: container, fromSema: typStr.len > 0)
|
||||||
if n notin doc.ordered:
|
if n notin doc.ordered:
|
||||||
doc.ordered.add(n)
|
doc.ordered.add(n)
|
||||||
if typStr.len > 0:
|
# Define in scope for subsequent inference
|
||||||
doc.typeIndex[n] = detail
|
let sym = Symbol(kind: skVar, name: n, typ: typ,
|
||||||
doc.kindIndex[n] = "variable"
|
isMutable: stmt.stmtLetMut, isOwn: false)
|
||||||
|
discard sc.define(sym)
|
||||||
of skExpr:
|
of skExpr:
|
||||||
if stmt.stmtExpr != nil and stmt.stmtExpr.kind == ekBlock:
|
if stmt.stmtExpr != nil and stmt.stmtExpr.kind == ekBlock:
|
||||||
walkBlock(stmt.stmtExpr.exprBlock, container)
|
var child = newScope(sc)
|
||||||
|
collectLocals(sema, stmt.stmtExpr.exprBlock, child,
|
||||||
|
blockEndLine(stmt.stmtExpr.exprBlock), container)
|
||||||
of skIf:
|
of skIf:
|
||||||
walkBlock(stmt.stmtIfThen, container)
|
var thenSc = newScope(sc)
|
||||||
walkBlock(stmt.stmtIfElse, container)
|
collectLocals(sema, stmt.stmtIfThen, thenSc,
|
||||||
|
blockEndLine(stmt.stmtIfThen), container)
|
||||||
for br in stmt.stmtIfElseIfs:
|
for br in stmt.stmtIfElseIfs:
|
||||||
walkBlock(br.blk, container)
|
var elifSc = newScope(sc)
|
||||||
|
collectLocals(sema, br.blk, elifSc, blockEndLine(br.blk), container)
|
||||||
|
if stmt.stmtIfElse != nil:
|
||||||
|
var elseSc = newScope(sc)
|
||||||
|
collectLocals(sema, stmt.stmtIfElse, elseSc,
|
||||||
|
blockEndLine(stmt.stmtIfElse), container)
|
||||||
of skWhile:
|
of skWhile:
|
||||||
walkBlock(stmt.stmtWhileBody, container)
|
var wSc = newScope(sc)
|
||||||
of skFor:
|
collectLocals(sema, stmt.stmtWhileBody, wSc,
|
||||||
walkBlock(stmt.stmtForBody, container)
|
blockEndLine(stmt.stmtWhileBody), container)
|
||||||
|
of skDoWhile:
|
||||||
|
var dSc = newScope(sc)
|
||||||
|
collectLocals(sema, stmt.stmtDoWhileBody, dSc,
|
||||||
|
blockEndLine(stmt.stmtDoWhileBody), container)
|
||||||
of skLoop:
|
of skLoop:
|
||||||
walkBlock(stmt.stmtLoopBody, container)
|
var lSc = newScope(sc)
|
||||||
|
collectLocals(sema, stmt.stmtLoopBody, lSc,
|
||||||
|
blockEndLine(stmt.stmtLoopBody), container)
|
||||||
|
of skFor:
|
||||||
|
var fSc = newScope(sc)
|
||||||
|
if stmt.stmtForVar.len > 0:
|
||||||
|
let fline = max(0, int(stmt.loc.line) - 1)
|
||||||
|
let fcol = max(0, int(stmt.loc.column) - 1)
|
||||||
|
let fend = blockEndLine(stmt.stmtForBody)
|
||||||
|
# Best-effort: element type unknown without iterator typing
|
||||||
|
let detail = "for " & stmt.stmtForVar
|
||||||
|
doc.locals.add(LocalBinding(
|
||||||
|
name: stmt.stmtForVar, detail: detail, kind: "variable",
|
||||||
|
declLine: fline, declCol: fcol,
|
||||||
|
scopeStartLine: fline, scopeEndLine: fend,
|
||||||
|
container: container, inferred: false))
|
||||||
|
discard fSc.define(Symbol(kind: skVar, name: stmt.stmtForVar,
|
||||||
|
typ: makeUnknown(), isMutable: false))
|
||||||
|
collectLocals(sema, stmt.stmtForBody, fSc,
|
||||||
|
blockEndLine(stmt.stmtForBody), container)
|
||||||
|
of skMatch:
|
||||||
|
for arm in stmt.stmtMatchArms:
|
||||||
|
if arm.body != nil and arm.body.kind == ekBlock:
|
||||||
|
var mSc = newScope(sc)
|
||||||
|
collectLocals(sema, arm.body.exprBlock, mSc,
|
||||||
|
blockEndLine(arm.body.exprBlock), container)
|
||||||
else:
|
else:
|
||||||
discard
|
discard
|
||||||
|
|
||||||
|
proc collectFuncLocals(sema: var Sema, d: Decl) =
|
||||||
|
if d == nil or d.kind != dkFunc or d.declFuncBody == nil:
|
||||||
|
return
|
||||||
|
let fname = d.declFuncName
|
||||||
|
let bodyEnd = blockEndLine(d.declFuncBody)
|
||||||
|
var funcScope = newScope(sema.globalScope)
|
||||||
|
# Parameters — visible for entire function body
|
||||||
|
let funcStart = max(0, int(d.loc.line) - 1)
|
||||||
|
for p in d.declFuncParams:
|
||||||
|
if p.name.len == 0: continue
|
||||||
|
var pType = makeUnknown()
|
||||||
|
if p.ptype != nil:
|
||||||
|
pType = sema.resolveType(p.ptype)
|
||||||
|
let typStr = if pType != nil and not pType.isUnknown: pType.toString else: ""
|
||||||
|
let detail =
|
||||||
|
if typStr.len > 0: "param " & p.name & ": " & typStr
|
||||||
|
else: "param " & p.name
|
||||||
|
let pline = max(0, int(p.loc.line) - 1)
|
||||||
|
let pcol = max(0, int(p.loc.column) - 1)
|
||||||
|
doc.locals.add(LocalBinding(
|
||||||
|
name: p.name, detail: detail, kind: "parameter",
|
||||||
|
declLine: pline, declCol: pcol,
|
||||||
|
scopeStartLine: funcStart, scopeEndLine: bodyEnd,
|
||||||
|
container: fname, inferred: false))
|
||||||
|
discard funcScope.define(Symbol(kind: skVar, name: p.name, typ: pType,
|
||||||
|
isMutable: false))
|
||||||
|
collectLocals(sema, d.declFuncBody, funcScope, bodyEnd, fname)
|
||||||
|
|
||||||
|
var semaMut = semaCtx
|
||||||
for d in parseRes.module.items:
|
for d in parseRes.module.items:
|
||||||
if d.kind == dkFunc and d.declFuncBody != nil:
|
if d.kind == dkFunc:
|
||||||
walkBlock(d.declFuncBody, d.declFuncName)
|
collectFuncLocals(semaMut, d)
|
||||||
elif d.kind == dkModule:
|
elif d.kind == dkModule:
|
||||||
for sub in d.declModuleItems:
|
for sub in d.declModuleItems:
|
||||||
if sub.kind == dkFunc and sub.declFuncBody != nil:
|
if sub.kind == dkFunc:
|
||||||
walkBlock(sub.declFuncBody, sub.declFuncName)
|
collectFuncLocals(semaMut, sub)
|
||||||
|
|
||||||
except:
|
except:
|
||||||
discard # sema failures must not crash the LSP
|
discard # sema failures must not crash the LSP
|
||||||
@@ -862,27 +998,55 @@ proc handleCompletion(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
|||||||
return
|
return
|
||||||
|
|
||||||
ensureAnalyzed(doc)
|
ensureAnalyzed(doc)
|
||||||
|
if doc.locals.len == 0 and doc.content.len > 0:
|
||||||
|
enrichWithSema(doc)
|
||||||
let prefix = findWordAt(doc.content, lineNum, col)
|
let prefix = findWordAt(doc.content, lineNum, col)
|
||||||
|
|
||||||
var items = newJArray()
|
var items = newJArray()
|
||||||
|
var offered = initHashSet[string]()
|
||||||
|
|
||||||
|
# Position-sensitive locals / params first (highest priority)
|
||||||
|
for b in doc.locals:
|
||||||
|
if lineNum < b.scopeStartLine or lineNum > b.scopeEndLine: continue
|
||||||
|
if prefix != "" and not b.name.toLowerAscii().startsWith(prefix.toLowerAscii()):
|
||||||
|
continue
|
||||||
|
# Prefer later/narrower binding for same name
|
||||||
|
if offered.contains(b.name):
|
||||||
|
continue
|
||||||
|
offered.incl(b.name)
|
||||||
|
let k = if b.kind == "parameter": 6 else: completionKind("variable")
|
||||||
|
items.add(%*{
|
||||||
|
"label": b.name,
|
||||||
|
"kind": k,
|
||||||
|
"detail": b.detail,
|
||||||
|
"sortText": "0_" & b.name,
|
||||||
|
"documentation": {"kind": "markdown",
|
||||||
|
"value": "```bux\n" & b.detail & "\n```\n\n_" & b.kind &
|
||||||
|
(if b.inferred: " · inferred" else: "") & "_"}
|
||||||
|
})
|
||||||
|
|
||||||
for name, info in doc.symbols.pairs:
|
for name, info in doc.symbols.pairs:
|
||||||
|
if offered.contains(name): continue
|
||||||
if prefix == "" or name.toLowerAscii().startsWith(prefix.toLowerAscii()):
|
if prefix == "" or name.toLowerAscii().startsWith(prefix.toLowerAscii()):
|
||||||
|
offered.incl(name)
|
||||||
items.add(%*{
|
items.add(%*{
|
||||||
"label": name,
|
"label": name,
|
||||||
"kind": completionKind(info.kind),
|
"kind": completionKind(info.kind),
|
||||||
"detail": info.detail,
|
"detail": info.detail,
|
||||||
|
"sortText": "1_" & name,
|
||||||
"documentation": {"kind": "markdown", "value": "```bux\n" & info.detail & "\n```\n\n_" & info.kind & "_"}
|
"documentation": {"kind": "markdown", "value": "```bux\n" & info.detail & "\n```\n\n_" & info.kind & "_"}
|
||||||
})
|
})
|
||||||
|
|
||||||
# Also offer workspace symbols (other open / scanned files)
|
# Also offer workspace symbols (other open / scanned files)
|
||||||
for name, ws in workspaceSymbols.pairs:
|
for name, ws in workspaceSymbols.pairs:
|
||||||
if doc.symbols.hasKey(name):
|
if offered.contains(name): continue
|
||||||
continue
|
|
||||||
if prefix == "" or name.toLowerAscii().startsWith(prefix.toLowerAscii()):
|
if prefix == "" or name.toLowerAscii().startsWith(prefix.toLowerAscii()):
|
||||||
|
offered.incl(name)
|
||||||
items.add(%*{
|
items.add(%*{
|
||||||
"label": name,
|
"label": name,
|
||||||
"kind": completionKind(ws.info.kind),
|
"kind": completionKind(ws.info.kind),
|
||||||
"detail": ws.info.detail & " (workspace)",
|
"detail": ws.info.detail & " (workspace)",
|
||||||
|
"sortText": "2_" & name,
|
||||||
"documentation": {"kind": "markdown", "value": "```bux\n" & ws.info.detail & "\n```"}
|
"documentation": {"kind": "markdown", "value": "```bux\n" & ws.info.detail & "\n```"}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -896,11 +1060,32 @@ proc handleCompletion(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
|||||||
items.add(%*{
|
items.add(%*{
|
||||||
"label": kw,
|
"label": kw,
|
||||||
"kind": 14,
|
"kind": 14,
|
||||||
"detail": "keyword"
|
"detail": "keyword",
|
||||||
|
"sortText": "3_" & kw
|
||||||
})
|
})
|
||||||
|
|
||||||
sendResponse(stream, id, %*{"isIncomplete": false, "items": items})
|
sendResponse(stream, id, %*{"isIncomplete": false, "items": items})
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Position-sensitive local lookup
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc lookupLocalAt*(doc: DocumentState, name: string, line: int): tuple[ok: bool, b: LocalBinding] =
|
||||||
|
## Innermost local/parameter binding for `name` visible at `line` (0-based).
|
||||||
|
result.ok = false
|
||||||
|
var bestSpan = high(int)
|
||||||
|
var bestStart = -1
|
||||||
|
for b in doc.locals:
|
||||||
|
if b.name != name: continue
|
||||||
|
if line < b.scopeStartLine or line > b.scopeEndLine: continue
|
||||||
|
let span = b.scopeEndLine - b.scopeStartLine
|
||||||
|
# Prefer narrower scope; on ties prefer later declaration (shadowing)
|
||||||
|
if span < bestSpan or (span == bestSpan and b.scopeStartLine >= bestStart):
|
||||||
|
bestSpan = span
|
||||||
|
bestStart = b.scopeStartLine
|
||||||
|
result.b = b
|
||||||
|
result.ok = true
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Go-to-definition
|
# Go-to-definition
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -917,13 +1102,26 @@ proc handleDefinition(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
|||||||
return
|
return
|
||||||
|
|
||||||
ensureAnalyzed(doc)
|
ensureAnalyzed(doc)
|
||||||
|
if doc.locals.len == 0 and doc.content.len > 0:
|
||||||
|
enrichWithSema(doc)
|
||||||
|
|
||||||
let word = findWordAt(doc.content, lineNum, col)
|
let word = findWordAt(doc.content, lineNum, col)
|
||||||
if word.len == 0:
|
if word.len == 0:
|
||||||
sendResponse(stream, id, %*[])
|
sendResponse(stream, id, %*[])
|
||||||
return
|
return
|
||||||
|
|
||||||
var locs = newJArray()
|
var locs = newJArray()
|
||||||
if doc.symbols.hasKey(word):
|
# Position-sensitive local first
|
||||||
|
let (lok, lb) = lookupLocalAt(doc, word, lineNum)
|
||||||
|
if lok:
|
||||||
|
locs.add(%*{
|
||||||
|
"uri": uri,
|
||||||
|
"range": {
|
||||||
|
"start": {"line": lb.declLine, "character": lb.declCol},
|
||||||
|
"end": {"line": lb.declLine, "character": lb.declCol + word.len}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
elif doc.symbols.hasKey(word):
|
||||||
let info = doc.symbols[word]
|
let info = doc.symbols[word]
|
||||||
locs.add(%*{
|
locs.add(%*{
|
||||||
"uri": uri,
|
"uri": uri,
|
||||||
@@ -949,6 +1147,7 @@ proc handleDefinition(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
|||||||
|
|
||||||
proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
## Hover with accurate range; prefer real sema types when available.
|
## Hover with accurate range; prefer real sema types when available.
|
||||||
|
## Locals are resolved by position (shadowing / nested scopes).
|
||||||
let uri = paramsNode["textDocument"]["uri"].getStr()
|
let uri = paramsNode["textDocument"]["uri"].getStr()
|
||||||
let position = paramsNode["position"]
|
let position = paramsNode["position"]
|
||||||
let lineNum = position["line"].getInt()
|
let lineNum = position["line"].getInt()
|
||||||
@@ -961,7 +1160,7 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
|||||||
|
|
||||||
ensureAnalyzed(doc)
|
ensureAnalyzed(doc)
|
||||||
# Lazy sema enrich on first hover if not yet run (e.g. only didChange so far)
|
# Lazy sema enrich on first hover if not yet run (e.g. only didChange so far)
|
||||||
if doc.typeIndex.len == 0 and doc.content.len > 0:
|
if (doc.typeIndex.len == 0 or doc.locals.len == 0) and doc.content.len > 0:
|
||||||
enrichWithSema(doc)
|
enrichWithSema(doc)
|
||||||
|
|
||||||
let lines = doc.content.split("\n")
|
let lines = doc.content.split("\n")
|
||||||
@@ -983,23 +1182,39 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
|||||||
var detail = ""
|
var detail = ""
|
||||||
var kind = ""
|
var kind = ""
|
||||||
var found = false
|
var found = false
|
||||||
|
var fromSema = false
|
||||||
|
var inferred = false
|
||||||
|
var scopeNote = ""
|
||||||
|
|
||||||
# Prefer file-local symbol (may be sema-upgraded)
|
# 1) Position-sensitive local / parameter
|
||||||
if doc.symbols.hasKey(word):
|
let (lok, lb) = lookupLocalAt(doc, word, lineNum)
|
||||||
|
if lok:
|
||||||
|
detail = lb.detail
|
||||||
|
kind = lb.kind
|
||||||
|
found = true
|
||||||
|
fromSema = true
|
||||||
|
inferred = lb.inferred
|
||||||
|
if lb.container.len > 0:
|
||||||
|
scopeNote = " in `" & lb.container & "`"
|
||||||
|
|
||||||
|
# 2) File-level / global symbols (functions, types, …)
|
||||||
|
if not found and doc.symbols.hasKey(word):
|
||||||
let info = doc.symbols[word]
|
let info = doc.symbols[word]
|
||||||
detail = info.detail
|
detail = info.detail
|
||||||
kind = info.kind
|
kind = info.kind
|
||||||
found = true
|
found = true
|
||||||
# Prefer pure sema typeIndex when richer
|
fromSema = info.fromSema
|
||||||
if doc.typeIndex.hasKey(word) and doc.typeIndex[word].len >= detail.len:
|
if doc.typeIndex.hasKey(word) and doc.typeIndex[word].len >= detail.len:
|
||||||
detail = doc.typeIndex[word]
|
detail = doc.typeIndex[word]
|
||||||
if doc.kindIndex.hasKey(word):
|
if doc.kindIndex.hasKey(word):
|
||||||
kind = doc.kindIndex[word]
|
kind = doc.kindIndex[word]
|
||||||
elif doc.typeIndex.hasKey(word):
|
fromSema = true
|
||||||
|
elif not found and doc.typeIndex.hasKey(word):
|
||||||
detail = doc.typeIndex[word]
|
detail = doc.typeIndex[word]
|
||||||
kind = if doc.kindIndex.hasKey(word): doc.kindIndex[word] else: "symbol"
|
kind = if doc.kindIndex.hasKey(word): doc.kindIndex[word] else: "symbol"
|
||||||
found = true
|
found = true
|
||||||
elif workspaceSymbols.hasKey(word):
|
fromSema = true
|
||||||
|
elif not found and workspaceSymbols.hasKey(word):
|
||||||
let info = workspaceSymbols[word].info
|
let info = workspaceSymbols[word].info
|
||||||
detail = info.detail
|
detail = info.detail
|
||||||
kind = info.kind
|
kind = info.kind
|
||||||
@@ -1010,10 +1225,12 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
|||||||
return
|
return
|
||||||
|
|
||||||
var md = "```bux\n" & detail & "\n```\n\n_" & kind & "_"
|
var md = "```bux\n" & detail & "\n```\n\n_" & kind & "_"
|
||||||
if doc.symbols.hasKey(word) and doc.symbols[word].fromSema:
|
if scopeNote.len > 0:
|
||||||
md &= " · sema"
|
md &= scopeNote
|
||||||
elif doc.typeIndex.hasKey(word):
|
if fromSema:
|
||||||
md &= " · sema"
|
md &= " · sema"
|
||||||
|
if inferred:
|
||||||
|
md &= " · inferred"
|
||||||
|
|
||||||
sendResponse(stream, id, %*{
|
sendResponse(stream, id, %*{
|
||||||
"contents": {"kind": "markdown", "value": md},
|
"contents": {"kind": "markdown", "value": md},
|
||||||
@@ -1022,7 +1239,6 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
|||||||
"end": {"line": lineNum, "character": endC}
|
"end": {"line": lineNum, "character": endC}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Document symbols (outline)
|
# Document symbols (outline)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1088,7 +1304,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
"hoverProvider": true,
|
"hoverProvider": true,
|
||||||
"documentSymbolProvider": true
|
"documentSymbolProvider": true
|
||||||
},
|
},
|
||||||
"serverInfo": {"name": "bux-lsp", "version": "0.3.0"}
|
"serverInfo": {"name": "bux-lsp", "version": "0.4.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()
|
||||||
|
|||||||
Executable
+78
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke: hover on inferred let + parameter via bux-lsp JSON-RPC.
|
||||||
|
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
|
||||||
|
|
||||||
|
cat > "$TMP/Main.bux" <<'EOF'
|
||||||
|
func Add(a: int, b: int) -> int {
|
||||||
|
let sum = a + b;
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
func Main() -> int {
|
||||||
|
let n = 10;
|
||||||
|
return Add(n, 2);
|
||||||
|
}
|
||||||
|
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"'}}}'
|
||||||
|
# hover on `sum` (line 1)
|
||||||
|
rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/hover","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":1,"character":8}}}'
|
||||||
|
# hover on param a (line 0)
|
||||||
|
rpc '{"jsonrpc":"2.0","id":3,"method":"textDocument/hover","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":0,"character":9}}}'
|
||||||
|
# hover on n in Main (line 5)
|
||||||
|
rpc '{"jsonrpc":"2.0","id":4,"method":"textDocument/hover","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":5,"character":8}}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","id":5,"method":"shutdown","params":null}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"exit","params":null}'
|
||||||
|
} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt"
|
||||||
|
|
||||||
|
echo "---- hover responses (excerpt) ----"
|
||||||
|
grep -o '"value":"[^"]*"' "$TMP/out.txt" | head -20 || true
|
||||||
|
|
||||||
|
# Must have typed sum / n and param a somewhere in output
|
||||||
|
ok=1
|
||||||
|
if ! grep -q 'sum' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: no hover for sum"
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
if ! grep -Eq 'let sum: int|sum: int' "$TMP/out.txt"; then
|
||||||
|
echo "WARN: sum type not clearly int (may still pass if detail present)"
|
||||||
|
# Soft fail only if completely missing inferred path
|
||||||
|
if ! grep -q 'inferred' "$TMP/out.txt" && ! grep -q 'let sum' "$TMP/out.txt"; then
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if ! grep -Eq 'param a|a: int' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: expected param a hover"
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
if ! grep -Eq 'let n: int|n: int' "$TMP/out.txt"; then
|
||||||
|
echo "WARN: n type not clearly int"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $ok -eq 0 ]]; then
|
||||||
|
echo "---- full output ----"
|
||||||
|
cat "$TMP/out.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "PASS: LSP hover smoke (locals + params + inferred lets)"
|
||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke: registry search + add + install + build with greet package (E.1)
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
BUXC="$ROOT/buxc"
|
||||||
|
export BUX_REGISTRY="$ROOT/config/registry.toml"
|
||||||
|
|
||||||
|
if [[ ! -x "$BUXC" ]]; then
|
||||||
|
(cd "$ROOT" && make build >/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
echo "=== bux search greet ==="
|
||||||
|
"$BUXC" search greet | tee "$TMP/search.out"
|
||||||
|
grep -q greet "$TMP/search.out"
|
||||||
|
|
||||||
|
echo "=== create consumer project ==="
|
||||||
|
mkdir -p "$TMP/app/src"
|
||||||
|
cat > "$TMP/app/bux.toml" <<'EOF'
|
||||||
|
[Package]
|
||||||
|
Name = "registry_consumer"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$TMP/app/src/Main.bux" <<'EOF'
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::String::{String_Eq};
|
||||||
|
import Std::Test::{Test_AssertTrue, Test_Pass};
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let msg: String = Greet_Hello("Bux");
|
||||||
|
Test_AssertTrue(String_Eq(msg, "Hello, Bux!"));
|
||||||
|
Test_AssertTrue(String_Eq(Greet_Version(), "0.1.1"));
|
||||||
|
PrintLine(msg);
|
||||||
|
Test_Pass("registry_consumer");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cd "$TMP/app"
|
||||||
|
export BUX_STDLIB="$ROOT/lib"
|
||||||
|
|
||||||
|
echo "=== bux add greet ==="
|
||||||
|
"$BUXC" add greet
|
||||||
|
grep -q greet bux.toml
|
||||||
|
cat bux.toml
|
||||||
|
|
||||||
|
echo "=== bux install ==="
|
||||||
|
"$BUXC" install
|
||||||
|
test -f bux.lock
|
||||||
|
grep -q greet bux.lock
|
||||||
|
cat bux.lock
|
||||||
|
|
||||||
|
echo "=== bux run ==="
|
||||||
|
"$BUXC" run . | tee "$TMP/run.out"
|
||||||
|
grep -q "Hello, Bux!" "$TMP/run.out"
|
||||||
|
|
||||||
|
echo "PASS: registry smoke (search + add + install + build)"
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
## Smoke test for position-sensitive locals + inferred let types.
|
||||||
|
## Run: nim r --path:../bootstrap tools/test_lsp_locals.nim
|
||||||
|
import std/[os, strutils, tables, unittest]
|
||||||
|
import lexer, parser, ast, sema, types, scope
|
||||||
|
|
||||||
|
# Minimal mirror of LSP collect (keeps the test free of JSON-RPC)
|
||||||
|
|
||||||
|
proc typeOfLet(sema: var Sema, stmt: Stmt, sc: Scope): tuple[t: Type, inferred: bool] =
|
||||||
|
result.inferred = false
|
||||||
|
result.t = makeUnknown()
|
||||||
|
if stmt.stmtLetType != nil:
|
||||||
|
result.t = sema.resolveType(stmt.stmtLetType)
|
||||||
|
if (result.t == nil or result.t.isUnknown) and stmt.stmtLetInit != nil:
|
||||||
|
result.t = sema.checkExprForLsp(stmt.stmtLetInit, sc)
|
||||||
|
result.inferred = true
|
||||||
|
elif stmt.stmtLetType == nil and stmt.stmtLetInit != nil:
|
||||||
|
result.t = sema.checkExprForLsp(stmt.stmtLetInit, sc)
|
||||||
|
result.inferred = true
|
||||||
|
|
||||||
|
suite "LSP locals / inference":
|
||||||
|
test "inferred let int from literal":
|
||||||
|
let src = """
|
||||||
|
func Main() -> int {
|
||||||
|
let x = 42;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let lexRes = tokenize(src, "t.bux")
|
||||||
|
check(not lexRes.hasErrors)
|
||||||
|
let parseRes = parse(lexRes.tokens, "t.bux")
|
||||||
|
check(parseRes.diagnostics.len == 0)
|
||||||
|
var (res, semaCtx) = analyzeFull(parseRes.module)
|
||||||
|
discard res
|
||||||
|
var found = false
|
||||||
|
for d in parseRes.module.items:
|
||||||
|
if d.kind != dkFunc: continue
|
||||||
|
var sc = newScope(semaCtx.globalScope)
|
||||||
|
for stmt in d.declFuncBody.stmts:
|
||||||
|
if stmt.kind == skLet and stmt.stmtLetName == "x":
|
||||||
|
let (t, inf) = typeOfLet(semaCtx, stmt, sc)
|
||||||
|
check(inf)
|
||||||
|
check(t.toString == "int" or t.kind == tkInt)
|
||||||
|
found = true
|
||||||
|
check(found)
|
||||||
|
|
||||||
|
test "explicit type not marked inferred":
|
||||||
|
let src = """
|
||||||
|
func Main() -> int {
|
||||||
|
let s: String = "hi";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let lexRes = tokenize(src, "t.bux")
|
||||||
|
let parseRes = parse(lexRes.tokens, "t.bux")
|
||||||
|
var (res, semaCtx) = analyzeFull(parseRes.module)
|
||||||
|
discard res
|
||||||
|
for d in parseRes.module.items:
|
||||||
|
if d.kind != dkFunc: continue
|
||||||
|
var sc = newScope(semaCtx.globalScope)
|
||||||
|
for stmt in d.declFuncBody.stmts:
|
||||||
|
if stmt.kind == skLet and stmt.stmtLetName == "s":
|
||||||
|
let (t, inf) = typeOfLet(semaCtx, stmt, sc)
|
||||||
|
check(not inf)
|
||||||
|
check(t.toString == "String" or t.kind == tkStr)
|
||||||
|
|
||||||
|
test "shadowed local: outer then inner":
|
||||||
|
let src = """
|
||||||
|
func Main() -> int {
|
||||||
|
let x = 1;
|
||||||
|
if true {
|
||||||
|
let x = 2;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let lexRes = tokenize(src, "t.bux")
|
||||||
|
let parseRes = parse(lexRes.tokens, "t.bux")
|
||||||
|
check(parseRes.diagnostics.len == 0)
|
||||||
|
# Both lets parse; inner is nested under if
|
||||||
|
var outer, inner: bool
|
||||||
|
for d in parseRes.module.items:
|
||||||
|
if d.kind != dkFunc: continue
|
||||||
|
for stmt in d.declFuncBody.stmts:
|
||||||
|
if stmt.kind == skLet and stmt.stmtLetName == "x":
|
||||||
|
outer = true
|
||||||
|
if stmt.kind == skIf:
|
||||||
|
for s2 in stmt.stmtIfThen.stmts:
|
||||||
|
if s2.kind == skLet and s2.stmtLetName == "x":
|
||||||
|
inner = true
|
||||||
|
check(outer and inner)
|
||||||
|
|
||||||
|
echo "LSP locals unit checks done"
|
||||||
Reference in New Issue
Block a user