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:
2026-07-19 16:35:08 +03:00
parent 3eb1ad3a82
commit 53b43b0f79
130 changed files with 20494 additions and 16738 deletions
+54 -3
View File
@@ -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
+3 -3
View File
@@ -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)
+26 -5
View File
@@ -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
View File
@@ -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":
+202
View File
@@ -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))
+106
View File
@@ -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)
+167
View File
@@ -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
View File
@@ -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
+22
View File
@@ -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
View File
@@ -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
+38
View File
@@ -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
View File
@@ -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
View File
@@ -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 | 35 production-quality apps в `apps/` | Showcase | | E.2 | 35 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)
+76
View File
@@ -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`
+749
View File
@@ -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).
+51
View File
@@ -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
View File
@@ -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 {
+8
View File
@@ -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;
} }
+4
View File
@@ -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
View File
@@ -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 {
+10
View File
@@ -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);
+15
View File
@@ -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;
}
```
+9
View File
@@ -0,0 +1,9 @@
[Package]
Name = "greet"
Version = "0.1.1"
Type = "lib"
Authors = ["Bux Core"]
License = "MIT"
[Build]
Output = "Lib"
+14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
} }
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+4
View File
@@ -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";
+121
View File
@@ -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;
}
+7
View File
@@ -0,0 +1,7 @@
[Package]
Name = "stdlib_array"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
+3
View File
@@ -0,0 +1,3 @@
stdlib_array: ok
PASS:
stdlib_array
+50
View File
@@ -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;
}
+7
View File
@@ -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;
}
+103
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
[Package]
Name = "stdlib_string"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
+3
View File
@@ -0,0 +1,3 @@
stdlib_string: ok
PASS:
stdlib_string
+35
View File
@@ -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
View File
@@ -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()
+78
View File
@@ -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)"
+64
View File
@@ -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)"
+93
View File
@@ -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"