feat: lifetime elision, tooling CI, registry, and LSP locals
Ship the QUALITY_PLAN stretch from ownership through ecosystem: C.1 lifetime elision (bootstrap + selfhost), bux fmt/test/doc CI hooks, stdlib goldens, package registry (bux search/add), and LSP 0.4 position-sensitive locals with inferred let types. Full-tree format pass plus Map/Set remove double-free fix.
This commit is contained in:
@@ -3,9 +3,9 @@ SRC := bootstrap/main.nim
|
||||
OUT := buxc
|
||||
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
|
||||
|
||||
@@ -19,7 +19,7 @@ dev:
|
||||
debug: dev
|
||||
@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..."
|
||||
$(NIM) c -r tests/lexer_test.nim
|
||||
@echo "Running parser tests..."
|
||||
@@ -105,6 +105,43 @@ test-errors: build
|
||||
@chmod +x tests/error_golden/run.sh
|
||||
@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
|
||||
@echo "=== Selfhost loop: bootstrap determinism check ==="
|
||||
@echo "Build A..."
|
||||
@@ -145,3 +182,17 @@ lsp: tools/bux-lsp
|
||||
|
||||
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
|
||||
|
||||
.PHONY: test-lsp
|
||||
test-lsp: lsp
|
||||
@echo "=== LSP unit (locals / inference) ==="
|
||||
$(NIM) r --path:bootstrap tools/test_lsp_locals.nim
|
||||
@echo "=== LSP hover smoke ==="
|
||||
@chmod +x tools/smoke_lsp_hover.sh
|
||||
@tools/smoke_lsp_hover.sh
|
||||
|
||||
.PHONY: test-registry
|
||||
test-registry: build
|
||||
@echo "=== Registry smoke (E.1) ==="
|
||||
@chmod +x tools/smoke_registry.sh
|
||||
@tools/smoke_registry.sh
|
||||
|
||||
@@ -432,7 +432,7 @@ func ReadFile(path: String) -> Result<String, IoError> {
|
||||
| `8.2.1` `own` keyword | ✅ | `own T` parsed and resolves to `T`; ready for borrow checker integration |
|
||||
| `8.2.2` `borrow` / `&` | ✅ | `&T` shared 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 |
|
||||
|
||||
```bux
|
||||
@@ -681,8 +681,8 @@ buxc2 == buxc3 ✅ (binary-identical)
|
||||
| `10.2.3` `&mut T` exclusive mutable check | ✅ | No aliasing of mutable refs |
|
||||
| `10.2.4` Bounds checking on slices | ✅ | `Slice_Get` / `Array_Get` with `bux_bounds_check` |
|
||||
| `10.2.5` `@[Release]` zero-cost mode | ✅ | Disables borrow + bounds checks, passes `-O3 -flto` |
|
||||
| `10.2.6` Lifetime elision (simple rules) | ⏳ | 80% of cases without annotations |
|
||||
| `10.2.7` Explicit lifetimes `'a` | ⏳ | Only for complex cases |
|
||||
| `10.2.6` Lifetime elision (simple rules) | ✅ | Single-input elision; multi-input requires `'a` |
|
||||
| `10.2.7` Explicit lifetimes `'a` | ✅ | Parsed + checked; multi-input + mismatch |
|
||||
|
||||
### 10.3 — Compiler Architecture Upgrade (v0.6.0 target)
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ func Main() -> int {
|
||||
| **Package Manager** | `bux add`, `bux install`, `bux.lock`, path + git deps |
|
||||
| **Cross-Compilation** | `--target <triple>` via clang (e.g. `aarch64-linux-gnu`) |
|
||||
| **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/BuildAndTest.md`](docs/BuildAndTest.md) | Build, test, and tooling |
|
||||
| [`docs/QUALITY_PLAN.md`](docs/QUALITY_PLAN.md) | Roadmap toward a “good” v1.0 |
|
||||
| [`docs/Packages.md`](docs/Packages.md) | Package manager + registry |
|
||||
| [`docs/SEMVER.md`](docs/SEMVER.md) | Versioning policy |
|
||||
| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Feature status (constructs) |
|
||||
| [`PLAN.md`](PLAN.md) | Long-form phase plan |
|
||||
|
||||
@@ -319,14 +321,33 @@ make test-errors
|
||||
# Full unit + example suite
|
||||
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)
|
||||
make selfhost
|
||||
|
||||
# Run all tests
|
||||
make test
|
||||
# Package tests (filter + summary table)
|
||||
./buxc test --filter first _test_runner
|
||||
|
||||
# Run example programs
|
||||
make test-examples
|
||||
# Format / CI format check
|
||||
./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)
|
||||
make selfhost-loop
|
||||
|
||||
+323
-15
@@ -1,6 +1,9 @@
|
||||
import std/[os, strutils, terminal, strformat, osproc, sets]
|
||||
import std/[os, strutils, terminal, strformat, osproc, sets, algorithm, tables]
|
||||
import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend
|
||||
import source_location
|
||||
import fmt
|
||||
import docgen
|
||||
import registry
|
||||
|
||||
type
|
||||
ColorMode* = enum
|
||||
@@ -21,16 +24,25 @@ Usage: bux [options] <command> [command-options]
|
||||
Commands:
|
||||
new <name> Create a new Bux package
|
||||
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
|
||||
search [query] Search the package registry
|
||||
build Build the current package
|
||||
run Build and run the current package
|
||||
test Run tests in tests/ directory
|
||||
check Type-check the current package
|
||||
fmt [path] Format .bux sources (default: .)
|
||||
doc [path] Generate Markdown API docs from /// comments
|
||||
clean Remove build artifacts
|
||||
help Show this help message
|
||||
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:
|
||||
--color <auto|on|off> Control colored output (default: auto)
|
||||
-q, --quiet Suppress non-error output
|
||||
@@ -192,6 +204,14 @@ proc hintForMessage(msg: string): string =
|
||||
return "provide the missing argument (positional or named)"
|
||||
if "use of moved value" in m:
|
||||
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:
|
||||
return "use '&mut T' for mutation, or drop @[Checked] for unchecked code"
|
||||
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)
|
||||
return 1
|
||||
else:
|
||||
if not args[i].startsWith("-"):
|
||||
version = args[i]
|
||||
else:
|
||||
printError(&"unknown add option '{args[i]}'", useColor)
|
||||
return 1
|
||||
inc i
|
||||
# Append to bux.toml
|
||||
var depLine = ""
|
||||
@@ -387,7 +411,19 @@ proc cmdAdd*(args: seq[string], opts: GlobalOptions): int =
|
||||
elif gitUrl.len > 0:
|
||||
depLine = &"{depName} = {{ Version = \"{version}\", Source = \"{gitUrl}\" }}"
|
||||
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)
|
||||
# Ensure [Dependencies] section exists
|
||||
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)
|
||||
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 =
|
||||
let useColor = shouldUseColor(opts)
|
||||
let root = getCurrentDir()
|
||||
@@ -411,6 +475,7 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
||||
let cacheDir = getHomeDir() / ".bux" / "packages"
|
||||
if not dirExists(cacheDir):
|
||||
createDir(cacheDir)
|
||||
let reg = loadRegistry()
|
||||
# Resolve each dependency
|
||||
for dep in man.dependencies:
|
||||
case dep.kind
|
||||
@@ -433,20 +498,43 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
||||
if not dirExists(depDir):
|
||||
if not opts.quiet:
|
||||
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:
|
||||
printError(&"failed to clone {dep.gitUrl}: {outp}", useColor)
|
||||
return 1
|
||||
else:
|
||||
if not opts.quiet:
|
||||
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))
|
||||
of dkVersion:
|
||||
# For version-based deps without a registry, we just record them
|
||||
# TODO: lookup in registry
|
||||
lock.entries.add(LockEntry(name: dep.name, version: dep.versionReq, source: "registry"))
|
||||
# Registry lookup (E.1)
|
||||
if reg.path.len == 0:
|
||||
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:
|
||||
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
|
||||
let lockPath = root / "bux.lock"
|
||||
saveLockfile(lockPath, lock)
|
||||
@@ -722,18 +810,93 @@ proc cmdClean*(args: seq[string], opts: GlobalOptions): int =
|
||||
printInfo("clean: build directory removed", useColor)
|
||||
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 =
|
||||
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"
|
||||
var testFiles: seq[string] = @[]
|
||||
if dirExists(testsDir):
|
||||
for kind, path in walkDir(testsDir):
|
||||
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.sort(system.cmp)
|
||||
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)
|
||||
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 failed = 0
|
||||
for testFile in testFiles:
|
||||
@@ -742,26 +905,168 @@ proc cmdTest*(args: seq[string], opts: GlobalOptions): int =
|
||||
removeDir(tmpDir)
|
||||
createDir(tmpDir / "src")
|
||||
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)
|
||||
var status: string
|
||||
var statusOk = false
|
||||
if buildRes != 0:
|
||||
printError(&" FAIL {testName} (build)", useColor)
|
||||
status = "FAIL"
|
||||
failed += 1
|
||||
continue
|
||||
else:
|
||||
var execFile = tmpDir / "build" / testName
|
||||
if not fileExists(execFile):
|
||||
execFile = tmpDir / "build" / "bux_out"
|
||||
let exitCode = execCmd(execFile)
|
||||
if exitCode == 0:
|
||||
printInfo(&" PASS {testName}", useColor)
|
||||
status = "PASS"
|
||||
statusOk = true
|
||||
passed += 1
|
||||
else:
|
||||
printError(&" FAIL {testName} (exit {exitCode})", useColor)
|
||||
status = &"FAIL:{exitCode}"
|
||||
failed += 1
|
||||
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
|
||||
|
||||
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 =
|
||||
echo "bux 0.1.0 (bootstrap)"
|
||||
return 0
|
||||
@@ -782,10 +1087,13 @@ proc runCli*(args: seq[string]): int =
|
||||
of "init": return cmdInit(cmdArgs, opts)
|
||||
of "add": return cmdAdd(cmdArgs, opts)
|
||||
of "install": return cmdInstall(cmdArgs, opts)
|
||||
of "search": return cmdSearch(cmdArgs, opts)
|
||||
of "build": return cmdBuild(cmdArgs, opts)
|
||||
of "run": return cmdRun(cmdArgs, opts)
|
||||
of "check": return cmdCheck(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 "version", "--version", "-v": return cmdVersion(cmdArgs, opts)
|
||||
of "help", "--help", "-h":
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
## docgen.nim — Extract `///` (and adjacent `/* */`) docs into Markdown.
|
||||
## Used by `bux doc [path...]`.
|
||||
|
||||
import std/[os, strutils, strformat, algorithm]
|
||||
|
||||
type
|
||||
DocItem* = object
|
||||
kind*: string ## module | func | struct | enum | interface | extern
|
||||
name*: string
|
||||
signature*: string ## first declaration line (trimmed)
|
||||
docs*: string
|
||||
file*: string
|
||||
line*: int
|
||||
|
||||
proc isDeclStart(line: string): bool =
|
||||
let s = line.strip()
|
||||
if s.len == 0: return false
|
||||
# Leading attributes @[Checked] etc. — not a decl by themselves
|
||||
if s.startsWith("@["): return false
|
||||
if s.startsWith("func ") or s.startsWith("pub func ") or
|
||||
s.startsWith("extern func ") or s.startsWith("const func ") or
|
||||
s.startsWith("async func "):
|
||||
return true
|
||||
if s.startsWith("struct ") or s.startsWith("pub struct ") or
|
||||
s.startsWith("enum ") or s.startsWith("pub enum ") or
|
||||
s.startsWith("union ") or s.startsWith("interface ") or
|
||||
s.startsWith("module ") or s.startsWith("type "):
|
||||
return true
|
||||
return false
|
||||
|
||||
proc declKindAndName(line: string): tuple[kind, name: string] =
|
||||
var s = line.strip()
|
||||
# Strip leading pub/extern/const/async
|
||||
for prefix in ["pub ", "extern ", "const ", "async "]:
|
||||
if s.startsWith(prefix):
|
||||
s = s[prefix.len .. ^1].strip()
|
||||
var kind = "item"
|
||||
if s.startsWith("func "):
|
||||
kind = "func"
|
||||
s = s["func ".len .. ^1]
|
||||
elif s.startsWith("struct "):
|
||||
kind = "struct"
|
||||
s = s["struct ".len .. ^1]
|
||||
elif s.startsWith("enum "):
|
||||
kind = "enum"
|
||||
s = s["enum ".len .. ^1]
|
||||
elif s.startsWith("union "):
|
||||
kind = "union"
|
||||
s = s["union ".len .. ^1]
|
||||
elif s.startsWith("interface "):
|
||||
kind = "interface"
|
||||
s = s["interface ".len .. ^1]
|
||||
elif s.startsWith("module "):
|
||||
kind = "module"
|
||||
s = s["module ".len .. ^1]
|
||||
elif s.startsWith("type "):
|
||||
kind = "type"
|
||||
s = s["type ".len .. ^1]
|
||||
# Name: until `<` `(` `{` `:` space
|
||||
var name = ""
|
||||
for ch in s:
|
||||
if ch in {' ', '<', '(', '{', ':', ';'}:
|
||||
break
|
||||
name.add(ch)
|
||||
if name.len == 0:
|
||||
name = s
|
||||
# extern funcs already stripped "extern "
|
||||
if kind == "func" and line.strip().startsWith("extern"):
|
||||
kind = "extern"
|
||||
return (kind, name)
|
||||
|
||||
proc extractDocsFromSource*(source, path: string): seq[DocItem] =
|
||||
result = @[]
|
||||
var pending: seq[string] = @[]
|
||||
var inBlockComment = false
|
||||
var blockDoc: seq[string] = @[]
|
||||
var lineNo = 0
|
||||
for rawLine in source.splitLines():
|
||||
inc lineNo
|
||||
var line = rawLine
|
||||
let stripped = line.strip()
|
||||
|
||||
# Block comment handling (/* ... */ used in stdlib today)
|
||||
if inBlockComment:
|
||||
let endIdx = stripped.find("*/")
|
||||
if endIdx >= 0:
|
||||
let before = stripped[0 ..< endIdx].strip()
|
||||
if before.len > 0:
|
||||
blockDoc.add(before)
|
||||
inBlockComment = false
|
||||
# Treat completed block as pending doc if non-empty
|
||||
if blockDoc.len > 0:
|
||||
pending = blockDoc
|
||||
blockDoc = @[]
|
||||
continue
|
||||
else:
|
||||
blockDoc.add(stripped)
|
||||
continue
|
||||
if stripped.startsWith("/*") and not stripped.startsWith("/***"):
|
||||
let rest = stripped["/*".len .. ^1]
|
||||
let endIdx = rest.find("*/")
|
||||
if endIdx >= 0:
|
||||
let body = rest[0 ..< endIdx].strip()
|
||||
if body.len > 0:
|
||||
pending = @[body]
|
||||
else:
|
||||
inBlockComment = true
|
||||
blockDoc = @[]
|
||||
let body = rest.strip()
|
||||
if body.len > 0:
|
||||
blockDoc.add(body)
|
||||
continue
|
||||
|
||||
# Triple-slash doc comments
|
||||
if stripped.startsWith("///"):
|
||||
var body = stripped["///".len .. ^1]
|
||||
if body.startsWith(" "):
|
||||
body = body[1 .. ^1]
|
||||
pending.add(body)
|
||||
continue
|
||||
|
||||
# Empty line: keep pending docs (allow blank lines inside doc blocks)
|
||||
if stripped.len == 0:
|
||||
continue
|
||||
|
||||
# Attributes immediately before decl: keep pending
|
||||
if stripped.startsWith("@["):
|
||||
continue
|
||||
|
||||
if isDeclStart(line):
|
||||
if pending.len > 0:
|
||||
let (kind, name) = declKindAndName(line)
|
||||
result.add(DocItem(
|
||||
kind: kind,
|
||||
name: name,
|
||||
signature: stripped,
|
||||
docs: pending.join("\n"),
|
||||
file: path,
|
||||
line: lineNo
|
||||
))
|
||||
pending = @[]
|
||||
continue
|
||||
|
||||
# Other code clears pending (except plain // comments)
|
||||
if stripped.startsWith("//"):
|
||||
continue
|
||||
pending = @[]
|
||||
|
||||
proc collectBuxFilesForDoc*(root: string): seq[string] =
|
||||
result = @[]
|
||||
if fileExists(root) and root.endsWith(".bux"):
|
||||
result.add(root)
|
||||
return
|
||||
if not dirExists(root):
|
||||
return
|
||||
for path in walkDirRec(root):
|
||||
if path.endsWith(".bux"):
|
||||
result.add(path)
|
||||
result.sort(system.cmp)
|
||||
|
||||
proc renderMarkdown*(items: seq[DocItem], title: string = "API Reference"): string =
|
||||
var sb: string
|
||||
sb.add(&"# {title}\n\n")
|
||||
sb.add("Generated by `bux doc` from `///` and `/* */` documentation comments.\n\n")
|
||||
if items.len == 0:
|
||||
sb.add("_No documented items found._\n")
|
||||
return sb
|
||||
|
||||
# Group by file
|
||||
var byFile: seq[string] = @[]
|
||||
for it in items:
|
||||
if it.file notin byFile:
|
||||
byFile.add(it.file)
|
||||
byFile.sort(system.cmp)
|
||||
|
||||
for f in byFile:
|
||||
let base = splitFile(f).name
|
||||
sb.add(&"## `{base}`\n\n")
|
||||
sb.add(&"_Source: `{f}`_\n\n")
|
||||
for it in items:
|
||||
if it.file != f:
|
||||
continue
|
||||
sb.add(&"### `{it.name}` _{it.kind}_\n\n")
|
||||
sb.add("```bux\n")
|
||||
sb.add(it.signature)
|
||||
sb.add("\n```\n\n")
|
||||
if it.docs.len > 0:
|
||||
sb.add(it.docs)
|
||||
sb.add("\n\n")
|
||||
return sb
|
||||
|
||||
proc generateDocs*(paths: seq[string]): seq[DocItem] =
|
||||
result = @[]
|
||||
var files: seq[string] = @[]
|
||||
for p in paths:
|
||||
for f in collectBuxFilesForDoc(p):
|
||||
if f notin files:
|
||||
files.add(f)
|
||||
files.sort(system.cmp)
|
||||
for f in files:
|
||||
let src = readFile(f)
|
||||
result.add(extractDocsFromSource(src, f))
|
||||
@@ -0,0 +1,106 @@
|
||||
## fmt.nim — Indentation-based Bux source formatter (bootstrap).
|
||||
## Mirrors selfhost `src/fmt.bux`: re-indent by brace depth, preserve content.
|
||||
|
||||
import std/[strutils, os, algorithm]
|
||||
|
||||
proc isInStringOrComment(line: string, pos: int): bool =
|
||||
## Simplified: track `//`, `"..."`, and `'...'` up to `pos`.
|
||||
var inString = false
|
||||
var inChar = false
|
||||
var inComment = false
|
||||
var i = 0
|
||||
while i < pos and i < line.len:
|
||||
let c = line[i]
|
||||
let n = if i + 1 < line.len: line[i + 1] else: '\0'
|
||||
if inComment:
|
||||
inc i
|
||||
continue
|
||||
if c == '/' and n == '/':
|
||||
inComment = true
|
||||
inc i
|
||||
continue
|
||||
if c == '"' and not inChar:
|
||||
inString = not inString
|
||||
if c == '\'' and not inString:
|
||||
inChar = not inChar
|
||||
inc i
|
||||
return inString or inChar or inComment
|
||||
|
||||
proc countBraceDelta(line: string): int =
|
||||
var delta = 0
|
||||
for i in 0 ..< line.len:
|
||||
if isInStringOrComment(line, i):
|
||||
continue
|
||||
let c = line[i]
|
||||
if c == '{':
|
||||
inc delta
|
||||
elif c == '}':
|
||||
dec delta
|
||||
return delta
|
||||
|
||||
proc formatSource*(source: string): string =
|
||||
## Re-indent each non-empty line to 4 spaces × brace depth.
|
||||
## Idempotent: formatting a clean file is a no-op.
|
||||
var sb: string
|
||||
var indent = 0
|
||||
# Nim's splitLines leaves a trailing "" when the source ends with '\n'.
|
||||
# Drop that artifact so we don't accumulate blank lines on re-format.
|
||||
var lines = source.splitLines(keepEol = false)
|
||||
if source.len > 0 and source.endsWith('\n') and lines.len > 0 and lines[^1].len == 0:
|
||||
lines.setLen(lines.len - 1)
|
||||
for line in lines:
|
||||
let trimmed = line.strip(leading = true, trailing = false)
|
||||
if trimmed.len == 0:
|
||||
sb.add('\n')
|
||||
continue
|
||||
|
||||
let delta = countBraceDelta(trimmed)
|
||||
let firstChar = trimmed[0]
|
||||
if firstChar == '}':
|
||||
dec indent
|
||||
if indent < 0:
|
||||
indent = 0
|
||||
|
||||
for _ in 0 ..< indent:
|
||||
sb.add(" ")
|
||||
sb.add(trimmed)
|
||||
sb.add('\n')
|
||||
|
||||
if firstChar != '}':
|
||||
indent = indent + delta
|
||||
else:
|
||||
# Net delta after the initial decrease for a leading `}`
|
||||
indent = indent + delta + 1
|
||||
if indent < 0:
|
||||
indent = 0
|
||||
|
||||
return sb
|
||||
|
||||
proc formatFile*(path: string, checkOnly: bool): tuple[ok: bool, changed: bool, msg: string] =
|
||||
## Format `path` in place, or only check if reformatting would change it.
|
||||
if not fileExists(path):
|
||||
return (false, false, "file not found: " & path)
|
||||
let source = readFile(path)
|
||||
let formatted = formatSource(source)
|
||||
if formatted == source:
|
||||
return (true, false, "")
|
||||
if checkOnly:
|
||||
return (true, true, "would reformat")
|
||||
try:
|
||||
writeFile(path, formatted)
|
||||
return (true, true, "formatted")
|
||||
except CatchableError as e:
|
||||
return (false, false, e.msg)
|
||||
|
||||
proc collectBuxFiles*(root: string): seq[string] =
|
||||
## Collect `.bux` files: single file, or recursive directory walk.
|
||||
result = @[]
|
||||
if fileExists(root) and root.endsWith(".bux"):
|
||||
result.add(root)
|
||||
return
|
||||
if not dirExists(root):
|
||||
return
|
||||
for path in walkDirRec(root):
|
||||
if path.endsWith(".bux"):
|
||||
result.add(path)
|
||||
result.sort(system.cmp)
|
||||
@@ -0,0 +1,167 @@
|
||||
## registry.nim — Bux package registry index (E.1)
|
||||
##
|
||||
## Index format (TOML-ish, one package per [[package]] table):
|
||||
##
|
||||
## [[package]]
|
||||
## name = "greet"
|
||||
## version = "0.1.0"
|
||||
## source = "file:packages/greet" # relative to the registry file
|
||||
## description = "Hello helpers"
|
||||
##
|
||||
## [[package]]
|
||||
## name = "net"
|
||||
## version = "1.2.0"
|
||||
## source = "https://github.com/bux-lang/net.git"
|
||||
##
|
||||
## Lookup order for the index file:
|
||||
## 1. $BUX_REGISTRY (file path)
|
||||
## 2. ~/.bux/registry.toml
|
||||
## 3. <repo>/config/registry.toml next to the compiler / cwd
|
||||
|
||||
import std/[os, strutils, strformat, algorithm]
|
||||
|
||||
type
|
||||
RegistryPackage* = object
|
||||
name*: string
|
||||
version*: string
|
||||
source*: string ## raw source as written in the index
|
||||
description*: string
|
||||
resolvedPath*: string ## absolute path for file: sources (filled on load)
|
||||
|
||||
Registry* = object
|
||||
path*: string ## index file path
|
||||
packages*: seq[RegistryPackage]
|
||||
|
||||
proc resolvePackageSource(pkg: var RegistryPackage, indexDir: string) =
|
||||
if pkg.source.startsWith("file:"):
|
||||
var p = pkg.source["file:".len .. ^1]
|
||||
if p.startsWith("//"):
|
||||
p = p[2 .. ^1]
|
||||
if not p.isAbsolute:
|
||||
p = indexDir / p
|
||||
pkg.resolvedPath = p.absolutePath
|
||||
elif pkg.source.startsWith("path:"):
|
||||
var p = pkg.source["path:".len .. ^1]
|
||||
if not p.isAbsolute:
|
||||
p = indexDir / p
|
||||
pkg.resolvedPath = p.absolutePath
|
||||
pkg.source = "file:" & pkg.resolvedPath
|
||||
|
||||
proc parseRegistryToml(content, indexPath: string): seq[RegistryPackage] =
|
||||
## Minimal parser for repeated [[package]] blocks with string keys.
|
||||
result = @[]
|
||||
var cur: RegistryPackage
|
||||
var inPkg = false
|
||||
let indexDir = indexPath.parentDir
|
||||
|
||||
for raw in content.splitLines():
|
||||
let line = raw.strip()
|
||||
if line.len == 0 or line.startsWith("#"):
|
||||
continue
|
||||
if line == "[[package]]" or line == "[[Package]]":
|
||||
if inPkg and cur.name.len > 0:
|
||||
resolvePackageSource(cur, indexDir)
|
||||
result.add(cur)
|
||||
cur = RegistryPackage()
|
||||
inPkg = true
|
||||
continue
|
||||
if not inPkg:
|
||||
continue
|
||||
let eq = line.find('=')
|
||||
if eq < 0: continue
|
||||
let key = line[0 ..< eq].strip().toLowerAscii()
|
||||
var val = line[eq + 1 .. ^1].strip()
|
||||
if val.len >= 2 and val[0] == '"' and val[^1] == '"':
|
||||
val = val[1 ..< ^1]
|
||||
case key
|
||||
of "name": cur.name = val
|
||||
of "version": cur.version = val
|
||||
of "source": cur.source = val
|
||||
of "description": cur.description = val
|
||||
else: discard
|
||||
if inPkg and cur.name.len > 0:
|
||||
resolvePackageSource(cur, indexDir)
|
||||
result.add(cur)
|
||||
|
||||
proc findRegistryIndex*(): string =
|
||||
## Locate the registry index file.
|
||||
let env = getEnv("BUX_REGISTRY")
|
||||
if env.len > 0 and fileExists(env):
|
||||
return env.absolutePath
|
||||
let homeIdx = getHomeDir() / ".bux" / "registry.toml"
|
||||
if fileExists(homeIdx):
|
||||
return homeIdx
|
||||
let candidates = @[
|
||||
getAppDir() / ".." / "config" / "registry.toml",
|
||||
getAppDir() / "config" / "registry.toml",
|
||||
getCurrentDir() / "config" / "registry.toml",
|
||||
getCurrentDir() / ".." / "config" / "registry.toml",
|
||||
]
|
||||
for c in candidates:
|
||||
if fileExists(c):
|
||||
return c.absolutePath
|
||||
return ""
|
||||
|
||||
proc loadRegistry*(path: string = ""): Registry =
|
||||
result.path = if path.len > 0: path else: findRegistryIndex()
|
||||
result.packages = @[]
|
||||
if result.path.len == 0 or not fileExists(result.path):
|
||||
return
|
||||
try:
|
||||
let content = readFile(result.path)
|
||||
result.packages = parseRegistryToml(content, result.path)
|
||||
except CatchableError:
|
||||
result.packages = @[]
|
||||
|
||||
proc registryLookup*(reg: Registry, name: string, versionReq: string = "*"): RegistryPackage =
|
||||
## Find a package by name. versionReq `*` picks the last matching entry
|
||||
## (index order; put newest last). Exact version matches preferred.
|
||||
result = RegistryPackage()
|
||||
var candidates: seq[RegistryPackage] = @[]
|
||||
for p in reg.packages:
|
||||
if p.name.toLowerAscii() == name.toLowerAscii():
|
||||
candidates.add(p)
|
||||
if candidates.len == 0:
|
||||
return
|
||||
if versionReq.len == 0 or versionReq == "*":
|
||||
return candidates[^1]
|
||||
for p in candidates:
|
||||
if p.version == versionReq:
|
||||
return p
|
||||
# Semver prefix match: "1" matches "1.0.0"
|
||||
for p in candidates:
|
||||
if p.version.startsWith(versionReq):
|
||||
return p
|
||||
return candidates[^1]
|
||||
|
||||
proc registrySearch*(reg: Registry, query: string): seq[RegistryPackage] =
|
||||
result = @[]
|
||||
let q = query.toLowerAscii()
|
||||
for p in reg.packages:
|
||||
if q.len == 0 or
|
||||
q in p.name.toLowerAscii() or
|
||||
q in p.description.toLowerAscii():
|
||||
result.add(p)
|
||||
result.sort(proc (a, b: RegistryPackage): int =
|
||||
cmp(a.name.toLowerAscii(), b.name.toLowerAscii()))
|
||||
|
||||
proc isGitSource*(source: string): bool =
|
||||
source.startsWith("http://") or source.startsWith("https://") or
|
||||
source.startsWith("git@") or source.startsWith("git://") or
|
||||
source.startsWith("ssh://")
|
||||
|
||||
proc isFileSource*(source: string): bool =
|
||||
source.startsWith("file:") or source.startsWith("path:")
|
||||
|
||||
proc formatRegistryDepLine*(name: string, pkg: RegistryPackage): string =
|
||||
## Produce a bux.toml Dependencies line for a resolved registry package.
|
||||
if pkg.resolvedPath.len > 0 and dirExists(pkg.resolvedPath):
|
||||
return &"{name} = {{ Path = \"{pkg.resolvedPath}\" }}"
|
||||
if isGitSource(pkg.source):
|
||||
let ver = if pkg.version.len > 0: pkg.version else: "*"
|
||||
return &"{name} = {{ Version = \"{ver}\", Source = \"{pkg.source}\" }}"
|
||||
if isFileSource(pkg.source) and pkg.resolvedPath.len > 0:
|
||||
return &"{name} = {{ Path = \"{pkg.resolvedPath}\" }}"
|
||||
# Fallback: version-only (install will re-resolve)
|
||||
let ver = if pkg.version.len > 0: pkg.version else: "*"
|
||||
return &"{name} = \"{ver}\""
|
||||
+178
-5
@@ -51,6 +51,11 @@ type
|
||||
## When true, ekIdent skips use-while-borrowed (we're forming `&x` itself)
|
||||
suppressUseWhileBorrow*: bool
|
||||
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
|
||||
currentClosureExpr*: Expr ## current closure being analyzed
|
||||
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:
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -472,7 +609,7 @@ proc inferTypeArgs(sema: var Sema, funcDecl: Decl, argTypes: seq[Type],
|
||||
# Type resolution from AST TypeExpr
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc resolveType(sema: var Sema, te: TypeExpr): Type =
|
||||
proc resolveType*(sema: var Sema, te: TypeExpr): Type =
|
||||
if te == nil:
|
||||
return makeUnknown()
|
||||
case te.kind
|
||||
@@ -918,7 +1055,7 @@ proc collectGlobals*(sema: var Sema) =
|
||||
# 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 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.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:
|
||||
return makeUnknown()
|
||||
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
|
||||
isMut = initType.isMutRef
|
||||
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()
|
||||
of skIf:
|
||||
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)
|
||||
if retSym != nil and retSym.isOwn:
|
||||
sema.movedVars.add(stmt.stmtReturnValue.exprIdent)
|
||||
# Lifetime: reject dangling returns / explicit mismatches
|
||||
sema.checkReturnLifetime(stmt.stmtReturnValue, scope, stmt.loc)
|
||||
return makeVoid()
|
||||
of skBreak, skContinue:
|
||||
return makeVoid()
|
||||
@@ -1951,9 +2095,15 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
|
||||
proc checkFunc(sema: var Sema, decl: Decl) =
|
||||
if decl.declFuncBody == nil:
|
||||
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.
|
||||
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
|
||||
let wasChecked = sema.checkedFunc
|
||||
let wasAsync = sema.currentFuncIsAsync
|
||||
@@ -1963,10 +2113,18 @@ proc checkFunc(sema: var Sema, decl: Decl) =
|
||||
sema.movedVars = @[]
|
||||
sema.activeMutBorrows = initTable[string, SourceLocation]()
|
||||
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)
|
||||
# Add type parameters to type table for resolution
|
||||
var addedTypeParams: seq[string] = @[]
|
||||
for tp in decl.declFuncTypeParams:
|
||||
if tp.isLifetime:
|
||||
# Lifetime params are not types; skip typeTable
|
||||
continue
|
||||
sema.typeTable[tp.name] = makeTypeParam(tp.name)
|
||||
addedTypeParams.add(tp.name)
|
||||
# Add parameters
|
||||
@@ -1982,6 +2140,8 @@ proc checkFunc(sema: var Sema, decl: Decl) =
|
||||
sema.typeTable.del(tp)
|
||||
sema.checkedFunc = wasChecked
|
||||
sema.currentFuncIsAsync = wasAsync
|
||||
sema.varRefLifetime = initTable[string, string]()
|
||||
sema.returnLifetime = ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Second pass: check all function bodies
|
||||
@@ -2026,3 +2186,16 @@ proc analyzeFull*(modu: Module): tuple[result: SemaResult, sema: Sema] =
|
||||
sema.collectGlobals()
|
||||
sema.checkBodies()
|
||||
result = (SemaResult(diagnostics: sema.diagnostics), sema)
|
||||
|
||||
proc checkExprForLsp*(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
## Type-check an expression for IDE use (no borrow/move side effects).
|
||||
let wasChecked = sema.checkedFunc
|
||||
let savedMoved = sema.movedVars
|
||||
let savedMut = sema.activeMutBorrows
|
||||
let savedShared = sema.activeSharedBorrows
|
||||
sema.checkedFunc = false
|
||||
result = sema.checkExpr(expr, scope)
|
||||
sema.checkedFunc = wasChecked
|
||||
sema.movedVars = savedMoved
|
||||
sema.activeMutBorrows = savedMut
|
||||
sema.activeSharedBorrows = savedShared
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Bux package registry index (E.1)
|
||||
# Used by `bux add <name>` and `bux install` when no --path/--git is given.
|
||||
#
|
||||
# Override with: export BUX_REGISTRY=/path/to/registry.toml
|
||||
# Or copy to: ~/.bux/registry.toml
|
||||
#
|
||||
# source forms:
|
||||
# file:relative/or/absolute — local package (relative to this file)
|
||||
# path:relative/or/absolute — same as file:
|
||||
# https://...git — git clone into ~/.bux/packages/<name>
|
||||
|
||||
[[package]]
|
||||
name = "greet"
|
||||
version = "0.1.0"
|
||||
source = "file:../registry/packages/greet"
|
||||
description = "Tiny Hello helper library (demo registry package)"
|
||||
|
||||
[[package]]
|
||||
name = "greet"
|
||||
version = "0.1.1"
|
||||
source = "file:../registry/packages/greet"
|
||||
description = "Tiny Hello helper library (demo registry package, patch)"
|
||||
+49
-4
@@ -152,15 +152,60 @@ This runs:
|
||||
|
||||
### Project Tests (`bux test`)
|
||||
```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:
|
||||
- `Tests passed` on exit code 0
|
||||
- `Tests failed (exit code N)` on non-zero exit
|
||||
Discovers `tests/*.bux`, builds each as a temp package, and runs it. Prints a
|
||||
summary table and exits:
|
||||
- `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.
|
||||
|
||||
### 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
|
||||
```bash
|
||||
make test-examples
|
||||
|
||||
@@ -604,6 +604,44 @@ Moves happen in three contexts:
|
||||
msg = "reassigned"; // OK: reinitialization
|
||||
PrintLine(msg);
|
||||
```
|
||||
- **No dangling returns**: cannot return a reference to a local (or by-value parameter)
|
||||
```bux
|
||||
@[Checked]
|
||||
func Bad(p: &int) -> &int {
|
||||
var x: int = 1;
|
||||
return &x; // ERROR: cannot return reference to local variable
|
||||
}
|
||||
```
|
||||
|
||||
### Lifetime elision (C.1)
|
||||
|
||||
In `@[Checked]` functions, most reference signatures need **no** lifetime annotations.
|
||||
Elision applies the usual single-input rules:
|
||||
|
||||
1. Each elided input `&T` / `&mut T` parameter gets a distinct lifetime.
|
||||
2. If there is **exactly one** input lifetime, it is assigned to all elided outputs.
|
||||
3. If the first parameter is named `self` / `Self`, that input lifetime is preferred for outputs.
|
||||
4. Multiple input references + elided return → **error** (write an explicit lifetime).
|
||||
|
||||
```bux
|
||||
// Elided — one input ref, return shares its lifetime
|
||||
@[Checked]
|
||||
func Identity(p: &int) -> &int {
|
||||
return p; // OK
|
||||
}
|
||||
|
||||
// Explicit — required when several inputs could be returned
|
||||
@[Checked]
|
||||
func Pick<'a>(a: &'a int, b: &'a int) -> &'a int {
|
||||
return a;
|
||||
}
|
||||
|
||||
// Syntax: &'a T and &mut / &'a mut T (lifetime before `mut`)
|
||||
// Type parameters: func F<'a, T>(...)
|
||||
```
|
||||
|
||||
Unchecked functions ignore lifetime rules (C-like). Explicit `'a` is optional
|
||||
documentation when a single input would already elide correctly.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+75
-27
@@ -1,6 +1,8 @@
|
||||
# Bux Package Manager
|
||||
|
||||
> **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"
|
||||
|
||||
[Dependencies]
|
||||
Std = "1.0"
|
||||
greet = { Path = "/abs/path/to/greet" }
|
||||
Json = { Version = "2.1", Source = "https://github.com/bux-lang/json" }
|
||||
Utils = { Path = "../Utils" }
|
||||
# Registry name-only (resolved by `bux add` / `bux install`):
|
||||
# greet = "0.1.1"
|
||||
```
|
||||
|
||||
### Dependency Forms
|
||||
|
||||
| Form | Example | Description |
|
||||
|------|---------|-------------|
|
||||
| Version string | `Std = "1.0"` | Registry dependency |
|
||||
| Wildcard | `Std = "*"` | Latest version |
|
||||
| Version string | `greet = "0.1.1"` | Registry dependency |
|
||||
| Wildcard | `greet = "*"` | Latest registry version |
|
||||
| Inline table (git) | `{ Version = "1.4", Source = "https://..." }` | Git URL + version |
|
||||
| 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
|
||||
|
||||
### `bux add <name> [version]`
|
||||
|
||||
Add a dependency to `bux.toml`.
|
||||
Add a dependency to `bux.toml` (registry / `--path` / `--git`).
|
||||
|
||||
```bash
|
||||
# Add registry dependency
|
||||
bux add json "2.1"
|
||||
### `bux search [query]`
|
||||
|
||||
# Add path-based dependency
|
||||
bux add utils --path "../utils"
|
||||
|
||||
# Add git dependency
|
||||
bux add network --git "https://github.com/bux-lang/network"
|
||||
```
|
||||
List packages in the active registry (filter by name/description).
|
||||
|
||||
### `bux install`
|
||||
|
||||
Resolve dependencies and generate `bux.lock`.
|
||||
|
||||
```bash
|
||||
bux install
|
||||
```
|
||||
|
||||
What it does:
|
||||
1. Reads `[Dependencies]` from `bux.toml`
|
||||
2. Resolves path-based deps (verifies directory exists)
|
||||
3. Clones/pulls git-based deps to `~/.bux/packages/<name>/`
|
||||
4. Generates `bux.lock` with exact versions and sources
|
||||
3. Clones git-based deps to `~/.bux/packages/<name>/`
|
||||
4. Resolves bare version names via the registry index
|
||||
5. Generates `bux.lock` with exact versions and sources
|
||||
|
||||
### `bux build` / `bux run`
|
||||
|
||||
@@ -84,10 +131,9 @@ Auto-generated. **Do not edit manually.**
|
||||
|
||||
```toml
|
||||
[[Package]]
|
||||
Name = "json"
|
||||
Version = "2.1.3"
|
||||
Source = "https://github.com/bux-lang/json"
|
||||
Checksum = "8dcb2a7f..."
|
||||
Name = "greet"
|
||||
Version = "0.1.1"
|
||||
Source = "/home/user/z-git/bux/bux/registry/packages/greet"
|
||||
|
||||
[[Package]]
|
||||
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
|
||||
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
|
||||
5. Later declarations shadow earlier ones (project > deps > stdlib)
|
||||
|
||||
@@ -114,8 +160,10 @@ The lockfile ensures **reproducible builds** — every developer gets the exact
|
||||
```bash
|
||||
bux new mylib
|
||||
cd mylib
|
||||
# Edit src/Main.bux → module MyLib { pub func Add(...) }
|
||||
bux build # Builds as library (Type = "lib")
|
||||
# Edit src/*.bux → module MyLib { func Add(...) }
|
||||
# Set Type = "lib" in bux.toml
|
||||
# Register in your registry.toml with source = "file:..."
|
||||
bux build
|
||||
```
|
||||
|
||||
## Example: Using a Library
|
||||
|
||||
+131
-23
@@ -1,7 +1,7 @@
|
||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||
|
||||
> **Дата:** 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.
|
||||
|
||||
---
|
||||
@@ -14,11 +14,11 @@
|
||||
| Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ |
|
||||
| HIR → C | Tuples + fat `func` ABI в bootstrap **и** selfhost | ★★★★☆ |
|
||||
| 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 | ★★★★☆ |
|
||||
| Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ |
|
||||
| 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) | ★★★★☆ |
|
||||
|
||||
**Силна ниша:** 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.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) |
|
||||
@@ -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.2 | `bux fmt` стабилен + CI check | Единен style | ⏳ |
|
||||
| D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | ⏳ partial (`bux test` exists) |
|
||||
| D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib | ⏳ |
|
||||
| D.5 | Golden tests за stdlib modules | Регресии без изненади | ⏳ |
|
||||
| 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 | ✅ full-tree format + `make fmt-check` enforce |
|
||||
| D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | ✅ `--filter` / summary / exit 0\|1 |
|
||||
| D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib | ✅ bootstrap+selfhost + `make docs` |
|
||||
| D.5 | Golden tests за stdlib modules | Регресии без изненади | ✅ `tests/stdlib_golden/` + `make test-stdlib` |
|
||||
|
||||
### E — Ecosystem & v1.0 (P2)
|
||||
|
||||
| # | Задача | Защо |
|
||||
|---|--------|------|
|
||||
| E.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks |
|
||||
| E.2 | 3–5 production-quality apps в `apps/` | Showcase |
|
||||
| E.3 | Language freeze + semver policy | Trust |
|
||||
| E.4 | Debugger/DWARF basics | Systems audience |
|
||||
| E.5 | Benchmarks vs C/Zig/Nim (micro + nexus) | Marketing + regression |
|
||||
| # | Задача | Защо | Статус |
|
||||
|---|--------|------|--------|
|
||||
| E.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks | ✅ local index + file/git sources + `search` |
|
||||
| E.2 | 3–5 production-quality apps в `apps/` | Showcase | ⏳ partial (`nexus`, `boko`, `simpledb`, `jwt-pitbul`) |
|
||||
| E.3 | Language freeze + semver policy | Trust | ✅ draft `docs/SEMVER.md` |
|
||||
| E.4 | Debugger/DWARF basics | Systems audience | ⏳ |
|
||||
| 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
|
||||
- [ ] Array/Map/String/Test API покрива 90% от ежедневните нужди
|
||||
- [ ] `@[Checked]` хваща use-after-move + double `&mut` в documented subset
|
||||
- [ ] `bux test` + `bux fmt` + `bux check` са default developer loop
|
||||
- [ ] LanguageRef синхронизиран с компилатора
|
||||
- [ ] Поне един външен проект (не в monorepo) build-ва с git dep
|
||||
- [x] `@[Checked]` хваща use-after-move + double `&mut` + dangling return / elision fail
|
||||
- [x] `bux test` + `bux fmt` + `bux check` са default developer loop (`--filter` / `--check` shipped)
|
||||
- [x] LanguageRef синхронизиран с компилатора (incl. C.1 elision)
|
||||
- [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
|
||||
2. Phase D tooling: `bux fmt` CI, `bux test --filter`, golden stdlib tests
|
||||
3. LSP: position-sensitive locals; inferred `let` types
|
||||
1. E.2 polish apps / E.5 benchmarks
|
||||
2. HTTP-fetchable registry index URL (beyond local file)
|
||||
3. LSP: workspace rename / references (optional)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# Bux Semantic Versioning Policy
|
||||
|
||||
> Status: Draft for v0.x → v1.0 freeze (E.3)
|
||||
|
||||
Bux follows [Semantic Versioning 2.0.0](https://semver.org/) with the
|
||||
clarifications below.
|
||||
|
||||
---
|
||||
|
||||
## Version numbers
|
||||
|
||||
```
|
||||
MAJOR.MINOR.PATCH[-prerelease]
|
||||
```
|
||||
|
||||
| Component | When it increases |
|
||||
|-----------|-------------------|
|
||||
| **MAJOR** | Breaking language / stdlib / CLI changes |
|
||||
| **MINOR** | Backward-compatible features |
|
||||
| **PATCH** | Backward-compatible bug fixes |
|
||||
|
||||
During **0.x** (pre-1.0):
|
||||
|
||||
- `0.MINOR.PATCH` — MINOR may still introduce breaking changes (documented in
|
||||
the release notes and `MIGRATION_*.sh` when needed).
|
||||
- Prefer deprecation warnings for at least one MINOR before removal when
|
||||
practical.
|
||||
|
||||
After **1.0.0** (language freeze):
|
||||
|
||||
- Breaking changes require a MAJOR bump and a migration guide.
|
||||
- The Language Reference is the normative spec; compiler bugs that contradict
|
||||
the ref are fixed without a MAJOR bump.
|
||||
|
||||
---
|
||||
|
||||
## What counts as “breaking”
|
||||
|
||||
- Removing or renaming a public stdlib symbol
|
||||
- Changing the type or semantics of a public API
|
||||
- Changing CLI flags that scripts rely on (`build`, `test`, `fmt --check`, …)
|
||||
- Changing `bux.toml` / `bux.lock` fields in an incompatible way
|
||||
- Changing the fat `func` / tuple C ABI in a way that breaks linked code
|
||||
|
||||
**Not breaking:**
|
||||
|
||||
- New keywords that were previously valid identifiers only if reserved carefully
|
||||
(prefer contextual keywords)
|
||||
- New diagnostics / stricter `@[Checked]` (document; may be gated)
|
||||
- Formatter whitespace-only changes
|
||||
|
||||
---
|
||||
|
||||
## Package versions (registry)
|
||||
|
||||
Registry packages use the same MAJOR.MINOR.PATCH scheme.
|
||||
|
||||
`bux add foo` / `bux add foo 0.1` resolution:
|
||||
|
||||
| Request | Matches |
|
||||
|---------|---------|
|
||||
| `*` / omitted | Latest entry for `foo` in the index |
|
||||
| `0.1.1` | Exact version |
|
||||
| `0.1` | First version with that prefix (e.g. `0.1.1`) |
|
||||
|
||||
Lockfiles pin the **resolved** version and source path/URL.
|
||||
|
||||
---
|
||||
|
||||
## Release checklist (maintainers)
|
||||
|
||||
1. Update `docs/LanguageRef.md` if behaviour changed
|
||||
2. Update `docs/QUALITY_PLAN.md` / changelog notes
|
||||
3. Run `make test` (includes `fmt-check`, examples, goldens)
|
||||
4. Run `make selfhost-loop`
|
||||
5. Tag `vMAJOR.MINOR.PATCH`
|
||||
@@ -0,0 +1,749 @@
|
||||
# API Reference
|
||||
|
||||
Generated by `bux doc` from `///` and `/* */` documentation comments.
|
||||
|
||||
## `Array`
|
||||
|
||||
_Source: `lib/Array.bux`_
|
||||
|
||||
### `Array` _struct_
|
||||
|
||||
```bux
|
||||
struct Array<T> {
|
||||
```
|
||||
|
||||
Growable contiguous buffer of `T` (len + capacity).
|
||||
|
||||
### `Array_New` _func_
|
||||
|
||||
```bux
|
||||
func Array_New<T>(cap: uint) -> Array<T> {
|
||||
```
|
||||
|
||||
Create an empty array with the given initial capacity.
|
||||
|
||||
### `Array_Push` _func_
|
||||
|
||||
```bux
|
||||
func Array_Push<T>(self: *Array<T>, value: T) {
|
||||
```
|
||||
|
||||
Append `value`, growing capacity if needed.
|
||||
|
||||
### `Array_Get` _func_
|
||||
|
||||
```bux
|
||||
func Array_Get<T>(self: *Array<T>, index: uint) -> T {
|
||||
```
|
||||
|
||||
Element at `index` (bounds-checked unless `@[Release]`).
|
||||
|
||||
### `Array_Set` _func_
|
||||
|
||||
```bux
|
||||
func Array_Set<T>(self: *Array<T>, index: uint, value: T) {
|
||||
```
|
||||
|
||||
Write `value` at `index` (bounds-checked unless `@[Release]`).
|
||||
|
||||
### `Array_Len` _func_
|
||||
|
||||
```bux
|
||||
func Array_Len<T>(self: *Array<T>) -> uint {
|
||||
```
|
||||
|
||||
Number of live elements.
|
||||
|
||||
### `Array_Free` _func_
|
||||
|
||||
```bux
|
||||
func Array_Free<T>(self: *Array<T>) {
|
||||
```
|
||||
|
||||
Free the backing buffer and reset length/capacity to zero.
|
||||
|
||||
### `Array_Drop` _func_
|
||||
|
||||
```bux
|
||||
func Array_Drop<T>(self: *Array<T>) {
|
||||
```
|
||||
|
||||
Drop trait entry — same as `Array_Free`.
|
||||
|
||||
### `Array_IsEmpty` _func_
|
||||
|
||||
```bux
|
||||
func Array_IsEmpty<T>(self: *Array<T>) -> bool {
|
||||
```
|
||||
|
||||
True if the array has no elements.
|
||||
|
||||
### `Array_Cap` _func_
|
||||
|
||||
```bux
|
||||
func Array_Cap<T>(self: *Array<T>) -> uint {
|
||||
```
|
||||
|
||||
Current capacity (not length).
|
||||
|
||||
### `Array_Clear` _func_
|
||||
|
||||
```bux
|
||||
func Array_Clear<T>(self: *Array<T>) {
|
||||
```
|
||||
|
||||
Drop length to zero; keeps allocated capacity.
|
||||
|
||||
### `Array_Reserve` _func_
|
||||
|
||||
```bux
|
||||
func Array_Reserve<T>(self: *Array<T>, minCap: uint) {
|
||||
```
|
||||
|
||||
Ensure capacity is at least `minCap` (does not shrink).
|
||||
|
||||
### `Array_First` _func_
|
||||
|
||||
```bux
|
||||
func Array_First<T>(self: *Array<T>) -> T {
|
||||
```
|
||||
|
||||
First element (bounds-checked if empty).
|
||||
|
||||
### `Array_Last` _func_
|
||||
|
||||
```bux
|
||||
func Array_Last<T>(self: *Array<T>) -> T {
|
||||
```
|
||||
|
||||
Last element (bounds-checked if empty).
|
||||
|
||||
### `Array_Pop` _func_
|
||||
|
||||
```bux
|
||||
func Array_Pop<T>(self: *Array<T>) -> T {
|
||||
```
|
||||
|
||||
Remove and return the last element (bounds-checked if empty).
|
||||
|
||||
### `Array_Contains` _func_
|
||||
|
||||
```bux
|
||||
func Array_Contains<T>(self: *Array<T>, value: T) -> bool {
|
||||
```
|
||||
|
||||
Linear search: true if `value` is present (uses `==`).
|
||||
|
||||
### `Array_IndexOf` _func_
|
||||
|
||||
```bux
|
||||
func Array_IndexOf<T>(self: *Array<T>, value: T) -> int {
|
||||
```
|
||||
|
||||
Index of first equal element, or `-1` if not found.
|
||||
|
||||
### `Array_Extend` _func_
|
||||
|
||||
```bux
|
||||
func Array_Extend<T>(self: *Array<T>, other: *Array<T>) {
|
||||
```
|
||||
|
||||
Append all elements of `other` onto `self`.
|
||||
|
||||
## `Channel`
|
||||
|
||||
_Source: `lib/Channel.bux`_
|
||||
|
||||
### `Channel_SendInt` _func_
|
||||
|
||||
```bux
|
||||
func Channel_SendInt(ch: *Channel<int>, value: int) {
|
||||
```
|
||||
|
||||
Convenience wrappers for common types
|
||||
|
||||
## `Iter`
|
||||
|
||||
_Source: `lib/Iter.bux`_
|
||||
|
||||
### `Array_Iter` _func_
|
||||
|
||||
```bux
|
||||
func Array_Iter<T>(arr: *Array<T>) -> Iter<T> {
|
||||
```
|
||||
|
||||
Create an iterator from an Array
|
||||
|
||||
### `Iter_HasNext` _func_
|
||||
|
||||
```bux
|
||||
func Iter_HasNext<T>(it: *Iter<T>) -> bool {
|
||||
```
|
||||
|
||||
Check if there are more elements
|
||||
|
||||
### `Iter_Next` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Next<T>(it: *Iter<T>) -> T {
|
||||
```
|
||||
|
||||
Get the next element and advance (undefined if HasNext is false)
|
||||
|
||||
### `Iter_Peek` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Peek<T>(it: *Iter<T>) -> T {
|
||||
```
|
||||
|
||||
Peek current element without advancing (undefined if HasNext is false)
|
||||
|
||||
### `Iter_Reset` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Reset<T>(it: *Iter<T>) {
|
||||
```
|
||||
|
||||
Reset iterator to the beginning
|
||||
|
||||
### `Iter_Pos` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Pos<T>(it: *Iter<T>) -> uint {
|
||||
```
|
||||
|
||||
Current position
|
||||
|
||||
### `Iter_Len` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Len<T>(it: *Iter<T>) -> uint {
|
||||
```
|
||||
|
||||
Remaining length
|
||||
|
||||
### `Iter_Count` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Count<T>(it: *Iter<T>) -> uint {
|
||||
```
|
||||
|
||||
Count remaining elements
|
||||
|
||||
### `Iter_Skip` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Skip<T>(it: *Iter<T>, n: uint) {
|
||||
```
|
||||
|
||||
Skip N elements
|
||||
|
||||
### `Iter_Take` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Take<T>(it: *Iter<T>, n: uint) -> Iter<T> {
|
||||
```
|
||||
|
||||
Take first N elements (by limiting len)
|
||||
|
||||
### `Iter_AnyEq` _func_
|
||||
|
||||
```bux
|
||||
func Iter_AnyEq<T>(it: *Iter<T>, value: T) -> bool {
|
||||
```
|
||||
|
||||
True if any remaining element equals value
|
||||
|
||||
### `Iter_AllEq` _func_
|
||||
|
||||
```bux
|
||||
func Iter_AllEq<T>(it: *Iter<T>, value: T) -> bool {
|
||||
```
|
||||
|
||||
True if every remaining element equals value (true if empty)
|
||||
|
||||
### `Iter_Collect` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Collect<T>(it: *Iter<T>) -> Array<T> {
|
||||
```
|
||||
|
||||
Collect remaining elements into a new Array
|
||||
|
||||
### `Iter_Map` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Map<T, U>(it: *Iter<T>, f: func(T) -> U) -> Array<U> {
|
||||
```
|
||||
|
||||
Map each remaining element through f: T → U, collect into Array<U>
|
||||
|
||||
### `Iter_Filter` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Filter<T>(it: *Iter<T>, pred: func(T) -> bool) -> Array<T> {
|
||||
```
|
||||
|
||||
Keep remaining elements for which pred returns true
|
||||
|
||||
### `Iter_Fold` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Fold<T, Acc>(it: *Iter<T>, init: Acc, f: func(Acc, T) -> Acc) -> Acc {
|
||||
```
|
||||
|
||||
Left-fold: f(f(...f(init, x0), x1), ...)
|
||||
|
||||
### `Iter_ForEach` _func_
|
||||
|
||||
```bux
|
||||
func Iter_ForEach<T>(it: *Iter<T>, f: func(T) -> int) {
|
||||
```
|
||||
|
||||
Call f for each remaining element (return value of f is ignored)
|
||||
|
||||
### `Iter_Any` _func_
|
||||
|
||||
```bux
|
||||
func Iter_Any<T>(it: *Iter<T>, pred: func(T) -> bool) -> bool {
|
||||
```
|
||||
|
||||
True if any remaining element satisfies pred
|
||||
|
||||
### `Iter_All` _func_
|
||||
|
||||
```bux
|
||||
func Iter_All<T>(it: *Iter<T>, pred: func(T) -> bool) -> bool {
|
||||
```
|
||||
|
||||
True if all remaining elements satisfy pred (true if empty)
|
||||
|
||||
### `Iter_SumInt` _func_
|
||||
|
||||
```bux
|
||||
func Iter_SumInt(it: *Iter<int>) -> int {
|
||||
```
|
||||
|
||||
Sum remaining ints (specialized fold)
|
||||
|
||||
## `Json`
|
||||
|
||||
_Source: `lib/Json.bux`_
|
||||
|
||||
### `JsonValue` _struct_
|
||||
|
||||
```bux
|
||||
struct JsonValue {
|
||||
```
|
||||
|
||||
=== Core type ===
|
||||
|
||||
### `Json_Null` _func_
|
||||
|
||||
```bux
|
||||
func Json_Null() -> JsonValue {
|
||||
```
|
||||
|
||||
=== Constructors ===
|
||||
|
||||
### `Json_ArrayLen` _func_
|
||||
|
||||
```bux
|
||||
func Json_ArrayLen(v: JsonValue) -> uint {
|
||||
```
|
||||
|
||||
=== Array helpers ===
|
||||
|
||||
### `Json_ObjectLen` _func_
|
||||
|
||||
```bux
|
||||
func Json_ObjectLen(v: JsonValue) -> uint {
|
||||
```
|
||||
|
||||
=== Object helpers ===
|
||||
|
||||
### `Json_IsNull` _func_
|
||||
|
||||
```bux
|
||||
func Json_IsNull(v: JsonValue) -> bool {
|
||||
```
|
||||
|
||||
=== Accessors ===
|
||||
|
||||
### `JsonParser` _struct_
|
||||
|
||||
```bux
|
||||
struct JsonParser {
|
||||
```
|
||||
|
||||
=== Parser ===
|
||||
|
||||
### `Json_Parse` _func_
|
||||
|
||||
```bux
|
||||
func Json_Parse(s: String) -> JsonValue {
|
||||
```
|
||||
|
||||
=== Public parser ===
|
||||
|
||||
### `Json_StringifyImpl` _func_
|
||||
|
||||
```bux
|
||||
func Json_StringifyImpl(sb: *StringBuilder, v: JsonValue) {
|
||||
```
|
||||
|
||||
=== Serializer ===
|
||||
|
||||
## `Map`
|
||||
|
||||
_Source: `lib/Map.bux`_
|
||||
|
||||
### `Map_Remove` _func_
|
||||
|
||||
```bux
|
||||
func Map_Remove<K, V>(m: *Map<K, V>, key: K) -> bool {
|
||||
```
|
||||
|
||||
Remove key if present. Rebuilds the table to keep open-addressing correct.
|
||||
|
||||
## `Net`
|
||||
|
||||
_Source: `lib/Net.bux`_
|
||||
|
||||
### `Net_Create` _func_
|
||||
|
||||
```bux
|
||||
func Net_Create() -> int {
|
||||
```
|
||||
|
||||
Create a TCP socket. Returns -1 on error.
|
||||
|
||||
### `Net_SetReuse` _func_
|
||||
|
||||
```bux
|
||||
func Net_SetReuse(fd: int) -> bool {
|
||||
```
|
||||
|
||||
Enable SO_REUSEADDR on a socket.
|
||||
|
||||
### `Net_Bind` _func_
|
||||
|
||||
```bux
|
||||
func Net_Bind(fd: int, addr: String, port: int) -> bool {
|
||||
```
|
||||
|
||||
Bind a socket to an address and port.
|
||||
|
||||
### `Net_Listen` _func_
|
||||
|
||||
```bux
|
||||
func Net_Listen(fd: int, backlog: int) -> bool {
|
||||
```
|
||||
|
||||
Start listening for connections.
|
||||
|
||||
### `Net_Accept` _func_
|
||||
|
||||
```bux
|
||||
func Net_Accept(fd: int) -> int {
|
||||
```
|
||||
|
||||
Accept a connection. Returns new fd or -1 on error.
|
||||
|
||||
### `Net_Connect` _func_
|
||||
|
||||
```bux
|
||||
func Net_Connect(fd: int, addr: String, port: int) -> bool {
|
||||
```
|
||||
|
||||
Connect to a remote address and port.
|
||||
|
||||
### `Net_Send` _func_
|
||||
|
||||
```bux
|
||||
func Net_Send(fd: int, data: String) -> int {
|
||||
```
|
||||
|
||||
Send data. Returns bytes sent or -1 on error.
|
||||
|
||||
### `Net_Recv` _func_
|
||||
|
||||
```bux
|
||||
func Net_Recv(fd: int, maxLen: int) -> String {
|
||||
```
|
||||
|
||||
Receive up to maxLen bytes. Returns empty string on error/EOF.
|
||||
|
||||
### `Net_Close` _func_
|
||||
|
||||
```bux
|
||||
func Net_Close(fd: int) -> bool {
|
||||
```
|
||||
|
||||
Close a socket.
|
||||
|
||||
### `Net_LastError` _func_
|
||||
|
||||
```bux
|
||||
func Net_LastError() -> String {
|
||||
```
|
||||
|
||||
Get last socket error as a string.
|
||||
|
||||
## `Option`
|
||||
|
||||
_Source: `lib/Option.bux`_
|
||||
|
||||
### `Option_Expect` _func_
|
||||
|
||||
```bux
|
||||
func Option_Expect(o: Option, msg: String) -> int {
|
||||
```
|
||||
|
||||
Unwrap Some or panic with a custom message
|
||||
|
||||
### `Option_Or` _func_
|
||||
|
||||
```bux
|
||||
func Option_Or(o: Option, other: Option) -> Option {
|
||||
```
|
||||
|
||||
If o is Some return it, otherwise return other
|
||||
|
||||
## `Os`
|
||||
|
||||
_Source: `lib/Os.bux`_
|
||||
|
||||
### `Os_Exit` _func_
|
||||
|
||||
```bux
|
||||
func Os_Exit(code: int) {
|
||||
```
|
||||
|
||||
Terminate the process with the given exit code
|
||||
|
||||
## `Result`
|
||||
|
||||
_Source: `lib/Result.bux`_
|
||||
|
||||
### `Result_Expect` _func_
|
||||
|
||||
```bux
|
||||
func Result_Expect(r: Result, msg: String) -> int {
|
||||
```
|
||||
|
||||
Unwrap Ok or panic with a custom message
|
||||
|
||||
### `Result_UnwrapErr` _func_
|
||||
|
||||
```bux
|
||||
func Result_UnwrapErr(r: Result) -> String {
|
||||
```
|
||||
|
||||
Extract Err payload (panics if Ok)
|
||||
|
||||
### `Result_Or` _func_
|
||||
|
||||
```bux
|
||||
func Result_Or(r: Result, other: Result) -> Result {
|
||||
```
|
||||
|
||||
If r is Ok return it, otherwise return other
|
||||
|
||||
## `Set`
|
||||
|
||||
_Source: `lib/Set.bux`_
|
||||
|
||||
### `Set_Remove` _func_
|
||||
|
||||
```bux
|
||||
func Set_Remove<T>(s: *Set<T>, value: T) -> bool {
|
||||
```
|
||||
|
||||
Remove value if present. Rebuilds the table to keep open-addressing correct.
|
||||
|
||||
## `String`
|
||||
|
||||
_Source: `lib/String.bux`_
|
||||
|
||||
### `String_Len` _func_
|
||||
|
||||
```bux
|
||||
func String_Len(s: String) -> uint {
|
||||
```
|
||||
|
||||
Byte length of a C string (`strlen`).
|
||||
|
||||
### `String_IsEmpty` _func_
|
||||
|
||||
```bux
|
||||
func String_IsEmpty(s: String) -> bool {
|
||||
```
|
||||
|
||||
True if the string has zero length.
|
||||
|
||||
### `String_IsNull` _func_
|
||||
|
||||
```bux
|
||||
func String_IsNull(s: String) -> bool {
|
||||
```
|
||||
|
||||
True if the pointer is null.
|
||||
|
||||
### `String_Eq` _func_
|
||||
|
||||
```bux
|
||||
func String_Eq(a: String, b: String) -> bool {
|
||||
```
|
||||
|
||||
Lexicographic equality.
|
||||
|
||||
### `String_Concat` _func_
|
||||
|
||||
```bux
|
||||
func String_Concat(a: String, b: String) -> String {
|
||||
```
|
||||
|
||||
Allocate and return `a` concatenated with `b`.
|
||||
|
||||
### `String_Copy` _func_
|
||||
|
||||
```bux
|
||||
func String_Copy(s: String) -> String {
|
||||
```
|
||||
|
||||
Heap-copy of `s`.
|
||||
|
||||
### `String_StartsWith` _func_
|
||||
|
||||
```bux
|
||||
func String_StartsWith(s: String, prefix: String) -> bool {
|
||||
```
|
||||
|
||||
True if `s` begins with `prefix`.
|
||||
|
||||
### `String_EndsWith` _func_
|
||||
|
||||
```bux
|
||||
func String_EndsWith(s: String, suffix: String) -> bool {
|
||||
```
|
||||
|
||||
True if `s` ends with `suffix`.
|
||||
|
||||
### `String_Contains` _func_
|
||||
|
||||
```bux
|
||||
func String_Contains(s: String, substr: String) -> bool {
|
||||
```
|
||||
|
||||
True if `substr` occurs anywhere in `s`.
|
||||
|
||||
### `String_IsBlank` _func_
|
||||
|
||||
```bux
|
||||
func String_IsBlank(s: String) -> bool {
|
||||
```
|
||||
|
||||
True if empty or only whitespace (space, tab, CR, LF).
|
||||
|
||||
### `String_Repeat` _func_
|
||||
|
||||
```bux
|
||||
func String_Repeat(s: String, count: uint) -> String {
|
||||
```
|
||||
|
||||
Repeat `s`, `count` times (`count == 0` → empty string).
|
||||
|
||||
### `String_ReplaceAll` _func_
|
||||
|
||||
```bux
|
||||
func String_ReplaceAll(s: String, old: String, new: String) -> String {
|
||||
```
|
||||
|
||||
Replace every non-overlapping occurrence of `old` with `new`.
|
||||
Empty `old` is a no-op (returns `s` unchanged). Safe if `new` contains `old`.
|
||||
|
||||
## `Test`
|
||||
|
||||
_Source: `lib/Test.bux`_
|
||||
|
||||
### `Test_Exit` _func_
|
||||
|
||||
```bux
|
||||
func Test_Exit(code: int) {
|
||||
```
|
||||
|
||||
Exit the process with `code` (for test runners).
|
||||
|
||||
### `Test_Assert` _func_
|
||||
|
||||
```bux
|
||||
func Test_Assert(cond: bool) {
|
||||
```
|
||||
|
||||
Assert `cond` is true; abort on failure.
|
||||
|
||||
### `Test_AssertEqInt` _func_
|
||||
|
||||
```bux
|
||||
func Test_AssertEqInt(a: int, b: int) {
|
||||
```
|
||||
|
||||
Assert two ints are equal; print both values and exit 1 on mismatch.
|
||||
|
||||
### `Test_AssertNeqInt` _func_
|
||||
|
||||
```bux
|
||||
func Test_AssertNeqInt(a: int, b: int) {
|
||||
```
|
||||
|
||||
Assert two ints differ.
|
||||
|
||||
### `Test_AssertEqString` _func_
|
||||
|
||||
```bux
|
||||
func Test_AssertEqString(a: String, b: String) {
|
||||
```
|
||||
|
||||
Assert two strings are equal (`String_Eq`).
|
||||
|
||||
### `Test_AssertEqBool` _func_
|
||||
|
||||
```bux
|
||||
func Test_AssertEqBool(a: bool, b: bool) {
|
||||
```
|
||||
|
||||
Assert two bools are equal.
|
||||
|
||||
### `Test_AssertTrue` _func_
|
||||
|
||||
```bux
|
||||
func Test_AssertTrue(cond: bool) {
|
||||
```
|
||||
|
||||
Assert `cond` is true.
|
||||
|
||||
### `Test_AssertFalse` _func_
|
||||
|
||||
```bux
|
||||
func Test_AssertFalse(cond: bool) {
|
||||
```
|
||||
|
||||
Assert `cond` is false.
|
||||
|
||||
### `Test_Fail` _func_
|
||||
|
||||
```bux
|
||||
func Test_Fail(msg: String) {
|
||||
```
|
||||
|
||||
Fail the test with a message and exit 1.
|
||||
|
||||
### `Test_Pass` _func_
|
||||
|
||||
```bux
|
||||
func Test_Pass(msg: String) {
|
||||
```
|
||||
|
||||
Print a PASS line (for human-readable runners / goldens).
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// lifetime_elision.bux — C.1: elided lifetimes for common &[Checked] APIs
|
||||
// No 'a annotations needed when there is a single input reference.
|
||||
import Std::Io::{PrintLine, PrintInt};
|
||||
import Std::Test::{Test_AssertEqInt, Test_Pass};
|
||||
|
||||
// Elided: param and return share one lifetime automatically
|
||||
@[Checked]
|
||||
func Identity(p: &int) -> &int {
|
||||
return p;
|
||||
}
|
||||
|
||||
// Explicit lifetime for documentation / multi-ref (same lifetime both sides)
|
||||
@[Checked]
|
||||
func IdentityNamed<'a>(p: &'a int) -> &'a int {
|
||||
return p;
|
||||
}
|
||||
|
||||
// Via intermediate let binding — lifetime is propagated
|
||||
@[Checked]
|
||||
func ViaLet(p: &int) -> &int {
|
||||
let r: &int = p;
|
||||
return r;
|
||||
}
|
||||
|
||||
// self-style first param: elision prefers the first input for the return
|
||||
@[Checked]
|
||||
func FirstOf(self: &int, _other: int) -> &int {
|
||||
return self;
|
||||
}
|
||||
|
||||
@[Checked]
|
||||
func Main() -> int {
|
||||
var x: int = 10;
|
||||
var y: int = 20;
|
||||
|
||||
let a: &int = Identity(&x);
|
||||
Test_AssertEqInt(*a, 10);
|
||||
|
||||
let b: &int = IdentityNamed(&y);
|
||||
Test_AssertEqInt(*b, 20);
|
||||
|
||||
let c: &int = ViaLet(&x);
|
||||
Test_AssertEqInt(*c, 10);
|
||||
|
||||
let d: &int = FirstOf(&y, 0);
|
||||
Test_AssertEqInt(*d, 20);
|
||||
|
||||
Test_Pass("lifetime_elision");
|
||||
PrintLine("lifetime_elision: ok");
|
||||
return 0;
|
||||
}
|
||||
+18
-10
@@ -5,17 +5,20 @@ extern func bux_realloc(ptr: *void, size: uint) -> *void;
|
||||
extern func bux_free(ptr: *void);
|
||||
extern func bux_bounds_check(index: uint, len: uint);
|
||||
|
||||
/// Growable contiguous buffer of `T` (len + capacity).
|
||||
struct Array<T> {
|
||||
data: *T,
|
||||
len: uint,
|
||||
cap: uint,
|
||||
}
|
||||
|
||||
/// Create an empty array with the given initial capacity.
|
||||
func Array_New<T>(cap: uint) -> Array<T> {
|
||||
let data = bux_alloc(cap * sizeof(T)) as *T;
|
||||
return Array<T> { data: data, len: 0, cap: cap };
|
||||
}
|
||||
|
||||
/// Append `value`, growing capacity if needed.
|
||||
func Array_Push<T>(self: *Array<T>, value: T) {
|
||||
if self.len >= self.cap {
|
||||
self.cap = self.cap * 2;
|
||||
@@ -25,20 +28,24 @@ func Array_Push<T>(self: *Array<T>, value: T) {
|
||||
self.len = self.len + 1;
|
||||
}
|
||||
|
||||
/// Element at `index` (bounds-checked unless `@[Release]`).
|
||||
func Array_Get<T>(self: *Array<T>, index: uint) -> T {
|
||||
bux_bounds_check(index, self.len);
|
||||
return self.data[index];
|
||||
}
|
||||
|
||||
/// Write `value` at `index` (bounds-checked unless `@[Release]`).
|
||||
func Array_Set<T>(self: *Array<T>, index: uint, value: T) {
|
||||
bux_bounds_check(index, self.len);
|
||||
self.data[index] = value;
|
||||
}
|
||||
|
||||
/// Number of live elements.
|
||||
func Array_Len<T>(self: *Array<T>) -> uint {
|
||||
return self.len;
|
||||
}
|
||||
|
||||
/// Free the backing buffer and reset length/capacity to zero.
|
||||
func Array_Free<T>(self: *Array<T>) {
|
||||
bux_free(self.data as *void);
|
||||
self.data = null as *T;
|
||||
@@ -46,6 +53,7 @@ func Array_Free<T>(self: *Array<T>) {
|
||||
self.cap = 0;
|
||||
}
|
||||
|
||||
/// Drop trait entry — same as `Array_Free`.
|
||||
func Array_Drop<T>(self: *Array<T>) {
|
||||
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);
|
||||
}
|
||||
|
||||
/* True if the array has no elements */
|
||||
/// True if the array has no elements.
|
||||
func Array_IsEmpty<T>(self: *Array<T>) -> bool {
|
||||
return self.len == 0;
|
||||
}
|
||||
|
||||
/* Current capacity (not length) */
|
||||
/// Current capacity (not length).
|
||||
func Array_Cap<T>(self: *Array<T>) -> uint {
|
||||
return self.cap;
|
||||
}
|
||||
|
||||
/* Drop length to zero; keeps allocated capacity */
|
||||
/// Drop length to zero; keeps allocated capacity.
|
||||
func Array_Clear<T>(self: *Array<T>) {
|
||||
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) {
|
||||
if minCap <= self.cap {
|
||||
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;
|
||||
}
|
||||
|
||||
/* First element (panics if empty via bounds check) */
|
||||
/// First element (bounds-checked if empty).
|
||||
func Array_First<T>(self: *Array<T>) -> T {
|
||||
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 {
|
||||
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 {
|
||||
bux_bounds_check(0, self.len);
|
||||
self.len = self.len - 1;
|
||||
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 {
|
||||
var i: uint = 0;
|
||||
while i < self.len {
|
||||
@@ -111,7 +119,7 @@ func Array_Contains<T>(self: *Array<T>, value: T) -> bool {
|
||||
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 {
|
||||
var i: uint = 0;
|
||||
while i < self.len {
|
||||
@@ -123,7 +131,7 @@ func Array_IndexOf<T>(self: *Array<T>, value: T) -> int {
|
||||
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>) {
|
||||
var i: uint = 0;
|
||||
while i < other.len {
|
||||
|
||||
@@ -103,6 +103,10 @@ func Map_Remove<K, V>(m: *Map<K, V>, key: K) -> bool {
|
||||
m.entries = fresh.entries;
|
||||
m.cap = fresh.cap;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -220,6 +224,10 @@ func StringMap_Remove<V>(m: *StringMap<V>, key: String) -> bool {
|
||||
m.entries = fresh.entries;
|
||||
m.cap = fresh.cap;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,10 @@ func Set_Remove<T>(s: *Set<T>, value: T) -> bool {
|
||||
s.entries = fresh.entries;
|
||||
s.cap = fresh.cap;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+13
-4
@@ -30,22 +30,27 @@ extern func bux_float_to_string(f: float64) -> String;
|
||||
extern func bux_str_format(pattern: String, a0: String, a1: String, a2: String, a3: String, a4: String, a5: String, a6: String, a7: String) -> String;
|
||||
|
||||
|
||||
/// Byte length of a C string (`strlen`).
|
||||
func String_Len(s: String) -> uint {
|
||||
return bux_strlen(s);
|
||||
}
|
||||
|
||||
/// True if the string has zero length.
|
||||
func String_IsEmpty(s: String) -> bool {
|
||||
return bux_strlen(s) == 0;
|
||||
}
|
||||
|
||||
/// True if the pointer is null.
|
||||
func String_IsNull(s: String) -> bool {
|
||||
return bux_str_is_null(s) != 0;
|
||||
}
|
||||
|
||||
/// Lexicographic equality.
|
||||
func String_Eq(a: String, b: String) -> bool {
|
||||
return bux_strcmp(a, b) == 0;
|
||||
}
|
||||
|
||||
/// Allocate and return `a` concatenated with `b`.
|
||||
func String_Concat(a: String, b: String) -> String {
|
||||
let len_a: uint = bux_strlen(a);
|
||||
let len_b: uint = bux_strlen(b);
|
||||
@@ -56,6 +61,7 @@ func String_Concat(a: String, b: String) -> String {
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// Heap-copy of `s`.
|
||||
func String_Copy(s: String) -> String {
|
||||
let len: uint = bux_strlen(s);
|
||||
let buf: *char8 = bux_alloc(len + 1) as *char8;
|
||||
@@ -63,6 +69,7 @@ func String_Copy(s: String) -> String {
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// True if `s` begins with `prefix`.
|
||||
func String_StartsWith(s: String, prefix: String) -> bool {
|
||||
let s_len: uint = bux_strlen(s);
|
||||
let p_len: uint = bux_strlen(prefix);
|
||||
@@ -73,6 +80,7 @@ func String_StartsWith(s: String, prefix: String) -> bool {
|
||||
return r == 0;
|
||||
}
|
||||
|
||||
/// True if `s` ends with `suffix`.
|
||||
func String_EndsWith(s: String, suffix: String) -> bool {
|
||||
let s_len: uint = bux_strlen(s);
|
||||
let suf_len: uint = bux_strlen(suffix);
|
||||
@@ -85,6 +93,7 @@ func String_EndsWith(s: String, suffix: String) -> bool {
|
||||
return eq;
|
||||
}
|
||||
|
||||
/// True if `substr` occurs anywhere in `s`.
|
||||
func String_Contains(s: String, substr: String) -> bool {
|
||||
let r: int = bux_str_contains(s, substr);
|
||||
return r != 0;
|
||||
@@ -151,7 +160,7 @@ func StringBuilder_Free(sb: *StringBuilder) {
|
||||
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 {
|
||||
let n: uint = bux_strlen(s);
|
||||
var i: uint = 0;
|
||||
@@ -165,7 +174,7 @@ func String_IsBlank(s: String) -> bool {
|
||||
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 {
|
||||
if count == 0 {
|
||||
return "";
|
||||
@@ -231,8 +240,8 @@ func String_Replace(s: String, old: String, new: String) -> String {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Replace every non-overlapping occurrence of old with new.
|
||||
Empty old is a no-op (returns s unchanged). Safe if new contains old. */
|
||||
/// Replace every non-overlapping occurrence of `old` with `new`.
|
||||
/// Empty `old` is a no-op (returns `s` unchanged). Safe if `new` contains `old`.
|
||||
func String_ReplaceAll(s: String, old: String, new: String) -> String {
|
||||
let oldLen: uint = bux_strlen(old);
|
||||
if oldLen == 0 {
|
||||
|
||||
@@ -5,14 +5,17 @@ import Std::String::{String_Eq};
|
||||
extern func bux_exit(code: int);
|
||||
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) {
|
||||
bux_exit(code);
|
||||
}
|
||||
|
||||
/// Assert `cond` is true; abort on failure.
|
||||
func Test_Assert(cond: bool) {
|
||||
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) {
|
||||
if a != b {
|
||||
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) {
|
||||
if a == b {
|
||||
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) {
|
||||
if !String_Eq(a, b) {
|
||||
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) {
|
||||
if a != b {
|
||||
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) {
|
||||
if !cond {
|
||||
PrintLine("ASSERT_TRUE FAILED");
|
||||
@@ -55,6 +62,7 @@ func Test_AssertTrue(cond: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert `cond` is false.
|
||||
func Test_AssertFalse(cond: bool) {
|
||||
if cond {
|
||||
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) {
|
||||
PrintLine("FAIL:");
|
||||
PrintLine(msg);
|
||||
bux_exit(1);
|
||||
}
|
||||
|
||||
/// Print a PASS line (for human-readable runners / goldens).
|
||||
func Test_Pass(msg: String) {
|
||||
PrintLine("PASS:");
|
||||
PrintLine(msg);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# greet
|
||||
|
||||
Demo package for the Bux registry (`config/registry.toml`).
|
||||
|
||||
```bash
|
||||
bux add greet
|
||||
bux install
|
||||
```
|
||||
|
||||
```bux
|
||||
func Main() -> int {
|
||||
PrintLine(Greet_Hello("Bux"));
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
[Package]
|
||||
Name = "greet"
|
||||
Version = "0.1.1"
|
||||
Type = "lib"
|
||||
Authors = ["Bux Core"]
|
||||
License = "MIT"
|
||||
|
||||
[Build]
|
||||
Output = "Lib"
|
||||
@@ -0,0 +1,14 @@
|
||||
// greet — demo registry package (E.1)
|
||||
module Greet {
|
||||
|
||||
/// Return a greeting for `name`.
|
||||
func Greet_Hello(name: String) -> String {
|
||||
return String_Concat("Hello, ", String_Concat(name, "!"));
|
||||
}
|
||||
|
||||
/// Return the package version string.
|
||||
func Greet_Version() -> String {
|
||||
return "0.1.1";
|
||||
}
|
||||
|
||||
}
|
||||
+2
-1
@@ -48,7 +48,8 @@ struct TypeExpr {
|
||||
typeArgName1: String,
|
||||
typeArgCount: int,
|
||||
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
|
||||
funcRet: *TypeExpr, // for tekFunc
|
||||
funcParamCount: int, // for tekFunc
|
||||
|
||||
+309
-32
@@ -922,12 +922,184 @@ func Cli_Fetch() -> int {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fmt command — format source files
|
||||
// Doc command — Markdown from /// comments (D.4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func Cli_Fmt(dir: String) -> int {
|
||||
// If dir is a file, format just that file
|
||||
func Cli_DocIsDeclStart(line: String) -> bool {
|
||||
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) {
|
||||
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 ");
|
||||
PrintLine(dir);
|
||||
if Fmt_FormatFile(dir) {
|
||||
@@ -936,8 +1108,12 @@ func Cli_Fmt(dir: String) -> int {
|
||||
}
|
||||
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 ");
|
||||
}
|
||||
PrintLine(dir);
|
||||
var fileCount: int = 0;
|
||||
let files: *String = bux_list_dir(dir, ".bux", &fileCount);
|
||||
@@ -947,14 +1123,34 @@ func Cli_Fmt(dir: String) -> int {
|
||||
}
|
||||
var i: int = 0;
|
||||
var okCount: int = 0;
|
||||
var changeCount: int = 0;
|
||||
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(" ");
|
||||
PrintLine(files[i]);
|
||||
if Fmt_FormatFile(files[i]) {
|
||||
okCount = okCount + 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");
|
||||
return 0;
|
||||
}
|
||||
@@ -985,10 +1181,17 @@ func Cli_StripExtension(name: String) -> String {
|
||||
return name;
|
||||
}
|
||||
|
||||
func Cli_Test(projectDir: String) -> int {
|
||||
func Cli_Test(projectDir: String, filter: String) -> int {
|
||||
Print("Testing project: ");
|
||||
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);
|
||||
if mainRc != 0 {
|
||||
PrintLine("Main test build failed");
|
||||
@@ -1011,6 +1214,7 @@ func Cli_Test(projectDir: String) -> int {
|
||||
return mainResult;
|
||||
}
|
||||
PrintLine("Main tests passed");
|
||||
}
|
||||
|
||||
// Propagate the project's stdlib to temp test packages.
|
||||
let stdlibDir: String = Cli_FindStdlibDir(projectDir);
|
||||
@@ -1031,15 +1235,31 @@ func Cli_Test(projectDir: String) -> int {
|
||||
return 0;
|
||||
}
|
||||
|
||||
PrintLine("┌──────────────────────────────┬────────┐");
|
||||
PrintLine("│ Test │ Status │");
|
||||
PrintLine("├──────────────────────────────┼────────┤");
|
||||
|
||||
var passed: int = 0;
|
||||
var failed: int = 0;
|
||||
var skipped: int = 0;
|
||||
var i: int = 0;
|
||||
while i < testCount {
|
||||
let testPath: String = testFiles[i];
|
||||
let fileName: String = Cli_FileNameFromPath(testPath);
|
||||
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);
|
||||
// Pad status column roughly (name may be long)
|
||||
Print(" ... ");
|
||||
|
||||
// Create temp package for this test file
|
||||
@@ -1050,14 +1270,14 @@ func Cli_Test(projectDir: String) -> int {
|
||||
|
||||
let source: String = ReadFile(testPath);
|
||||
if String_Eq(source, "") {
|
||||
PrintLine("FAIL (cannot read test file)");
|
||||
PrintLine("FAIL │");
|
||||
failed = failed + 1;
|
||||
i = i + 1;
|
||||
continue;
|
||||
}
|
||||
let tmpMain: String = bux_path_join(tmpSrc, "Main.bux");
|
||||
if !WriteFile(tmpMain, source) {
|
||||
PrintLine("FAIL (cannot write temp Main.bux)");
|
||||
PrintLine("FAIL │");
|
||||
failed = failed + 1;
|
||||
i = i + 1;
|
||||
continue;
|
||||
@@ -1066,7 +1286,7 @@ func Cli_Test(projectDir: String) -> int {
|
||||
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";
|
||||
if !WriteFile(tmpToml, tomlContent) {
|
||||
PrintLine("FAIL (cannot write temp bux.toml)");
|
||||
PrintLine("FAIL │");
|
||||
failed = failed + 1;
|
||||
i = i + 1;
|
||||
continue;
|
||||
@@ -1091,36 +1311,48 @@ func Cli_Test(projectDir: String) -> int {
|
||||
|
||||
let buildRc: int = Cli_BuildProject(tmpDir, "", false);
|
||||
if buildRc != 0 {
|
||||
PrintLine("FAIL (build error)");
|
||||
PrintLine("FAIL │");
|
||||
failed = failed + 1;
|
||||
i = i + 1;
|
||||
continue;
|
||||
}
|
||||
let testBin: String = bux_path_join(bux_path_join(tmpDir, "build"), "_test_tmp");
|
||||
if !FileExists(testBin) {
|
||||
PrintLine("FAIL (test binary not found)");
|
||||
PrintLine("FAIL │");
|
||||
failed = failed + 1;
|
||||
i = i + 1;
|
||||
continue;
|
||||
}
|
||||
let runRc: int = bux_system(testBin);
|
||||
if runRc == 0 {
|
||||
PrintLine("PASS");
|
||||
PrintLine("PASS │");
|
||||
passed = passed + 1;
|
||||
} else {
|
||||
Print("FAIL (exit ");
|
||||
PrintInt(runRc as int64);
|
||||
PrintLine(")");
|
||||
Print("FAIL │");
|
||||
PrintLine("");
|
||||
failed = failed + 1;
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
Print("Tests: ");
|
||||
PrintLine("└──────────────────────────────┴────────┘");
|
||||
Print("Results: ");
|
||||
PrintInt(passed as int64);
|
||||
Print(" passed, ");
|
||||
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; }
|
||||
return 0;
|
||||
}
|
||||
@@ -1493,7 +1725,10 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
||||
if argCount < 2 {
|
||||
PrintLine("Bux Self-Hosting Compiler v0.2.0");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
PrintLine("Bux Self-Hosting Compiler v0.2.0");
|
||||
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(" Lexer ✅ 695 lines");
|
||||
PrintLine(" Parser ✅ 1004 lines");
|
||||
PrintLine(" Sema ✅ 393 lines");
|
||||
PrintLine(" HirLower ✅ 307 lines");
|
||||
PrintLine(" CBackend ✅ 585 lines");
|
||||
PrintLine(" Total: 3830 lines of Bux");
|
||||
PrintLine(" Lexer ✅");
|
||||
PrintLine(" Parser ✅");
|
||||
PrintLine(" Sema ✅");
|
||||
PrintLine(" HirLower ✅");
|
||||
PrintLine(" CBackend ✅");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1550,9 +1787,36 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "fmt") {
|
||||
let dir: String = ".";
|
||||
if argCount >= 3 { dir = args[2]; }
|
||||
return Cli_Fmt(dir);
|
||||
var dir: String = ".";
|
||||
var checkOnly: bool = false;
|
||||
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") {
|
||||
@@ -1578,9 +1842,22 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "test") {
|
||||
let dir: String = ".";
|
||||
if argCount >= 3 { dir = args[2]; }
|
||||
return Cli_Test(dir);
|
||||
var dir: String = ".";
|
||||
var filter: String = "";
|
||||
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") {
|
||||
|
||||
+32
-2
@@ -77,13 +77,22 @@ func Fmt_FormatSource(source: String) -> String {
|
||||
let sb: StringBuilder = StringBuilder_NewCap(8192);
|
||||
var indent: int = 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 {
|
||||
let line: String = bux_str_split_part(source, "\n", i);
|
||||
let trimmed: String = Fmt_TrimLeft(line);
|
||||
|
||||
// Skip empty lines
|
||||
// Empty line (intentional blank) — keep a single newline
|
||||
if String_Eq(trimmed, "") {
|
||||
StringBuilder_Append(&sb, "\n");
|
||||
i = i + 1;
|
||||
@@ -135,4 +144,25 @@ func Fmt_FormatFile(path: String) -> bool {
|
||||
return bux_write_file(path, formatted);
|
||||
}
|
||||
|
||||
// Returns true if formatting would change the file (CI --check).
|
||||
func Fmt_WouldChange(path: String) -> bool {
|
||||
let source: String = bux_read_file(path);
|
||||
if source == null as String { return false; }
|
||||
let formatted: String = Fmt_FormatSource(source);
|
||||
return !String_Eq(formatted, source);
|
||||
}
|
||||
|
||||
// Check a file without writing. Returns 0 if clean, 1 if would reformat / error.
|
||||
func Fmt_CheckFile(path: String) -> int {
|
||||
let source: String = bux_read_file(path);
|
||||
if source == null as String {
|
||||
return 1;
|
||||
}
|
||||
let formatted: String = Fmt_FormatSource(source);
|
||||
if String_Eq(formatted, source) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -741,6 +741,19 @@ func lexNextToken(lex: *Lexer) {
|
||||
}
|
||||
|
||||
if c == 39 { // '
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
+43
-8
@@ -188,9 +188,16 @@ func parserParseType(p: *Parser) -> *TypeExpr {
|
||||
let col: uint32 = parserCurToken(p).column;
|
||||
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 {
|
||||
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;
|
||||
// Check for "mut" keyword
|
||||
if parserCheck(p, tkIdent) {
|
||||
@@ -208,6 +215,7 @@ func parserParseType(p: *Parser) -> *TypeExpr {
|
||||
}
|
||||
te.line = line;
|
||||
te.column = col;
|
||||
te.refLifetime = lt;
|
||||
te.pointerPointee = parserParseType(p);
|
||||
if te.pointerPointee != null as *TypeExpr {
|
||||
te.typeName = String_Concat(te.pointerPointee.typeName, "*");
|
||||
@@ -1861,24 +1869,51 @@ func parserParseParamList(p: *Parser) -> *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; }
|
||||
discard parserAdvance(p);
|
||||
let tp0: LexToken = parserExpect(p, tkIdent, "expected type param");
|
||||
d.typeParam0 = tp0.text;
|
||||
d.typeParamCount = 1;
|
||||
var typeCount: int = 0;
|
||||
var first: bool = true;
|
||||
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) {
|
||||
let bound0: LexToken = parserExpect(p, tkIdent, "expected trait bound name");
|
||||
d.typeParam0Bound = bound0.text;
|
||||
}
|
||||
if parserMatch(p, tkComma) {
|
||||
let tp1: LexToken = parserExpect(p, tkIdent, "expected type param");
|
||||
d.typeParam1 = tp1.text;
|
||||
d.typeParamCount = 2;
|
||||
} else if typeCount == 1 {
|
||||
d.typeParam1 = tpTok.text;
|
||||
typeCount = 2;
|
||||
if parserMatch(p, tkColon) {
|
||||
let bound1: LexToken = parserExpect(p, tkIdent, "expected trait bound name");
|
||||
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 '>'");
|
||||
}
|
||||
|
||||
|
||||
+252
-2
@@ -30,6 +30,26 @@ struct Sema {
|
||||
movedName5: String;
|
||||
movedName6: 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
|
||||
currentClosureExpr: *Expr; // current closure being analyzed (for capture tracking)
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1221,6 +1443,30 @@ func Sema_CheckStmt(sema: *Sema, stmt: *Stmt) {
|
||||
sym.isPublic = false;
|
||||
sym.decl = null as *Decl;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
if sema.currentRetType != tyVoid && sema.currentRetType != tyUnknown {
|
||||
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);
|
||||
} else {
|
||||
s.currentRetType = tyVoid;
|
||||
s.checkedFunc = false;
|
||||
s.movedCount = 0;
|
||||
}
|
||||
|
||||
// Enable borrow checking for @[Checked] functions
|
||||
@@ -2084,6 +2330,9 @@ func Sema_Analyze(mod: *Module) -> *Sema {
|
||||
s.checkedFunc = decl.isChecked != 0;
|
||||
let wasRelease: bool = s.releaseFunc;
|
||||
s.releaseFunc = decl.isRelease != 0;
|
||||
s.movedCount = 0;
|
||||
// C.1: lifetime elision before walking the body
|
||||
Sema_ApplyLifetimeElision(s, decl);
|
||||
|
||||
// Check body statements
|
||||
var stmt: *Stmt = decl.refBody.firstStmt;
|
||||
@@ -2094,6 +2343,7 @@ func Sema_Analyze(mod: *Module) -> *Sema {
|
||||
|
||||
s.checkedFunc = wasChecked;
|
||||
s.releaseFunc = wasRelease;
|
||||
Sema_ClearLifetimes(s);
|
||||
s.scope = prevScope;
|
||||
}
|
||||
decl = decl.childDecl2;
|
||||
|
||||
@@ -144,6 +144,9 @@ const tkCase: int = 108;
|
||||
const tkDefault: int = 109;
|
||||
const tkUnsafe: int = 110;
|
||||
|
||||
// Lifetime parameter token: 'a, 'b, ... (not a char literal)
|
||||
const tkLifetime: int = 111;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Token struct
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -339,6 +342,7 @@ func Token_KindName(kind: int) -> String {
|
||||
if kind == tkHashDate { return "#date"; }
|
||||
if kind == tkHashTime { return "#time"; }
|
||||
if kind == tkHashModule { return "#module"; }
|
||||
if kind == tkLifetime { return "lifetime"; }
|
||||
if kind == tkNewLine { return "newline"; }
|
||||
if kind == tkEndOfFile { return "end of file"; }
|
||||
return "unknown token";
|
||||
|
||||
@@ -248,5 +248,126 @@ func Main() -> int {
|
||||
let val: int = (*r).x;
|
||||
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)
|
||||
@@ -0,0 +1,7 @@
|
||||
[Package]
|
||||
Name = "elision_multi_input"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1,7 @@
|
||||
error: type errors in project
|
||||
error: lifetime elision failed: return type needs an explicit lifetime (multiple input references); e.g. func F<'a>(a: &'a T, b: &'a U) -> &'a T
|
||||
--> FILE:2:1
|
||||
|
|
||||
2 | func Pick(a: &int, b: &int) -> &int {
|
||||
| ^^^^
|
||||
= help: add an explicit lifetime, e.g. func F<'a>(x: &'a T, y: &'a U) -> &'a T
|
||||
@@ -0,0 +1,9 @@
|
||||
@[Checked]
|
||||
func Pick(a: &int, b: &int) -> &int {
|
||||
return a;
|
||||
}
|
||||
|
||||
@[Checked]
|
||||
func Main() -> int {
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[Package]
|
||||
Name = "return_local_ref"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1,7 @@
|
||||
error: type errors in project
|
||||
error: cannot return reference to local variable
|
||||
--> FILE:4:5
|
||||
|
|
||||
4 | return &x;
|
||||
| ^^^^^^
|
||||
= help: return a value, or return a reference borrowed from a function parameter
|
||||
@@ -0,0 +1,10 @@
|
||||
@[Checked]
|
||||
func Dangle(p: &int) -> &int {
|
||||
var x: int = 42;
|
||||
return &x;
|
||||
}
|
||||
|
||||
@[Checked]
|
||||
func Main() -> int {
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[Package]
|
||||
Name = "stdlib_array"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1,3 @@
|
||||
stdlib_array: ok
|
||||
PASS:
|
||||
stdlib_array
|
||||
@@ -0,0 +1,50 @@
|
||||
// Stdlib golden: Array helpers + Contains/IndexOf/Extend
|
||||
import Std::Io::{PrintLine};
|
||||
import Std::Array::{
|
||||
Array, Array_New, Array_Push, Array_Pop, Array_Clear, Array_IsEmpty,
|
||||
Array_First, Array_Last, Array_Cap, Array_Reserve, Array_Len, Array_Get,
|
||||
Array_Contains, Array_IndexOf, Array_Extend, Array_Free
|
||||
};
|
||||
import Std::Test::{
|
||||
Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_Pass
|
||||
};
|
||||
|
||||
func Main() -> int {
|
||||
var arr: Array<int> = Array_New<int>(2);
|
||||
Array_Reserve<int>(&arr, 8);
|
||||
Test_AssertTrue(Array_Cap<int>(&arr) >= 8);
|
||||
Test_AssertTrue(Array_IsEmpty<int>(&arr));
|
||||
|
||||
Array_Push<int>(&arr, 10);
|
||||
Array_Push<int>(&arr, 20);
|
||||
Array_Push<int>(&arr, 30);
|
||||
|
||||
Test_AssertFalse(Array_IsEmpty<int>(&arr));
|
||||
Test_AssertEqInt(Array_Len<int>(&arr) as int, 3);
|
||||
Test_AssertEqInt(Array_First<int>(&arr), 10);
|
||||
Test_AssertEqInt(Array_Last<int>(&arr), 30);
|
||||
Test_AssertTrue(Array_Contains<int>(&arr, 20));
|
||||
Test_AssertFalse(Array_Contains<int>(&arr, 99));
|
||||
Test_AssertEqInt(Array_IndexOf<int>(&arr, 30), 2);
|
||||
|
||||
let popped: int = Array_Pop<int>(&arr);
|
||||
Test_AssertEqInt(popped, 30);
|
||||
Test_AssertEqInt(Array_Len<int>(&arr) as int, 2);
|
||||
|
||||
var extra: Array<int> = Array_New<int>(2);
|
||||
Array_Push<int>(&extra, 40);
|
||||
Array_Push<int>(&extra, 50);
|
||||
Array_Extend<int>(&arr, &extra);
|
||||
Test_AssertEqInt(Array_Len<int>(&arr) as int, 4);
|
||||
Test_AssertEqInt(Array_Get<int>(&arr, 3), 50);
|
||||
|
||||
Array_Clear<int>(&arr);
|
||||
Test_AssertTrue(Array_IsEmpty<int>(&arr));
|
||||
Test_AssertTrue(Array_Cap<int>(&arr) >= 8);
|
||||
|
||||
Array_Free<int>(&arr);
|
||||
Array_Free<int>(&extra);
|
||||
PrintLine("stdlib_array: ok");
|
||||
Test_Pass("stdlib_array");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[Package]
|
||||
Name = "stdlib_collections"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1,3 @@
|
||||
stdlib_collections: ok
|
||||
PASS:
|
||||
stdlib_collections
|
||||
@@ -0,0 +1,67 @@
|
||||
// Stdlib golden: Map / Set / Result / Option helpers
|
||||
import Std::Io::{PrintLine};
|
||||
import Std::Map::{
|
||||
Map, Map_New, Map_Set, Map_Get, Map_Has, Map_Remove, Map_Clear,
|
||||
Map_Len, Map_IsEmpty, Map_Free
|
||||
};
|
||||
import Std::Set::{
|
||||
Set, Set_New, Set_Add, Set_Has, Set_Remove, Set_Len, Set_IsEmpty, Set_Free
|
||||
};
|
||||
import Std::Result::{
|
||||
Result, Result_NewOk, Result_NewErr, Result_IsOk, Result_IsErr,
|
||||
Result_UnwrapOr, Result_Or, Result_UnwrapErr
|
||||
};
|
||||
import Std::Option::{
|
||||
Option, Option_NewSome, Option_NewNone, Option_IsSome, Option_Or, Option_UnwrapOr
|
||||
};
|
||||
import Std::String::{String_Eq};
|
||||
import Std::Test::{
|
||||
Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_Pass
|
||||
};
|
||||
|
||||
func Main() -> int {
|
||||
var m: Map<int, int> = Map_New<int, int>(16);
|
||||
Map_Set<int, int>(&m, 1, 100);
|
||||
Map_Set<int, int>(&m, 2, 200);
|
||||
Map_Set<int, int>(&m, 3, 300);
|
||||
Test_AssertEqInt(Map_Len<int, int>(&m) as int, 3);
|
||||
Test_AssertTrue(Map_Has<int, int>(&m, 2));
|
||||
Test_AssertTrue(Map_Remove<int, int>(&m, 2));
|
||||
Test_AssertFalse(Map_Has<int, int>(&m, 2));
|
||||
Test_AssertEqInt(Map_Get<int, int>(&m, 1), 100);
|
||||
Test_AssertFalse(Map_Remove<int, int>(&m, 99));
|
||||
Map_Clear<int, int>(&m);
|
||||
Test_AssertTrue(Map_IsEmpty<int, int>(&m));
|
||||
Map_Free<int, int>(&m);
|
||||
|
||||
var s: Set<int> = Set_New<int>(16);
|
||||
Set_Add<int>(&s, 10);
|
||||
Set_Add<int>(&s, 20);
|
||||
Set_Add<int>(&s, 30);
|
||||
Test_AssertTrue(Set_Remove<int>(&s, 20));
|
||||
Test_AssertFalse(Set_Has<int>(&s, 20));
|
||||
Test_AssertTrue(Set_Has<int>(&s, 10));
|
||||
Test_AssertEqInt(Set_Len<int>(&s) as int, 2);
|
||||
Test_AssertFalse(Set_IsEmpty<int>(&s));
|
||||
Set_Free<int>(&s);
|
||||
|
||||
let ok: Result = Result_NewOk(42);
|
||||
let err: Result = Result_NewErr("boom");
|
||||
Test_AssertTrue(Result_IsOk(ok));
|
||||
Test_AssertTrue(Result_IsErr(err));
|
||||
Test_AssertEqInt(Result_UnwrapOr(err, -1), -1);
|
||||
let recovered: Result = Result_Or(err, Result_NewOk(7));
|
||||
Test_AssertEqInt(Result_UnwrapOr(recovered, 0), 7);
|
||||
Test_AssertTrue(String_Eq(Result_UnwrapErr(err), "boom"));
|
||||
|
||||
let some: Option = Option_NewSome(5);
|
||||
let none: Option = Option_NewNone();
|
||||
Test_AssertTrue(Option_IsSome(some));
|
||||
Test_AssertEqInt(Option_UnwrapOr(none, 9), 9);
|
||||
let filled: Option = Option_Or(none, Option_NewSome(3));
|
||||
Test_AssertEqInt(Option_UnwrapOr(filled, 0), 3);
|
||||
|
||||
PrintLine("stdlib_collections: ok");
|
||||
Test_Pass("stdlib_collections");
|
||||
return 0;
|
||||
}
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# Golden behavioral tests for stdlib modules.
|
||||
# Usage: from repo root: tests/stdlib_golden/run.sh [path/to/buxc]
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
BUXC_ARG="${1:-$ROOT/buxc}"
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Resolve to absolute path so `cd` into test packages still finds the binary.
|
||||
if [[ "$BUXC_ARG" = /* ]]; then
|
||||
BUXC="$BUXC_ARG"
|
||||
else
|
||||
BUXC="$(cd "$(dirname "$BUXC_ARG")" && pwd)/$(basename "$BUXC_ARG")"
|
||||
fi
|
||||
|
||||
if [[ ! -x "$BUXC" && ! -f "$BUXC" ]]; then
|
||||
echo "error: buxc not found at $BUXC (run make build first)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
passed=0
|
||||
failed=0
|
||||
skipped=0
|
||||
|
||||
normalize_out() {
|
||||
# Drop absolute paths; trim trailing whitespace/blank lines
|
||||
sed -E \
|
||||
-e "s|$ROOT|ROOT|g" \
|
||||
-e "s|$DIR|DIR|g" \
|
||||
-e 's/[[:space:]]+$//' \
|
||||
| sed -e :a -e '/^\n*$/{$d;N;ba' -e '}'
|
||||
}
|
||||
|
||||
for case_dir in "$DIR"/*/; do
|
||||
name="$(basename "$case_dir")"
|
||||
[[ -f "$case_dir/bux.toml" ]] || continue
|
||||
[[ -f "$case_dir/src/Main.bux" ]] || continue
|
||||
|
||||
if [[ ! -f "$case_dir/expected.out" ]]; then
|
||||
echo " SKIP $name (no expected.out)"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Build + run; capture stdout+stderr
|
||||
out=""
|
||||
if ! out="$(cd "$case_dir" && "$BUXC" run . 2>&1)"; then
|
||||
echo " FAIL $name (build/run non-zero)"
|
||||
printf '%s\n' "$out" | head -40
|
||||
failed=$((failed + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
got="$(printf '%s\n' "$out" | normalize_out)"
|
||||
exp="$(cat "$case_dir/expected.out" | normalize_out)"
|
||||
|
||||
# Match on key status lines (tests may also print build noise)
|
||||
if printf '%s\n' "$got" | grep -Fqx "$(printf '%s' "$exp" | head -1)" 2>/dev/null; then
|
||||
# Prefer full expected lines all present
|
||||
all_ok=1
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
if ! printf '%s\n' "$got" | grep -Fqx "$line"; then
|
||||
all_ok=0
|
||||
break
|
||||
fi
|
||||
done <<< "$exp"
|
||||
if [[ $all_ok -eq 1 ]]; then
|
||||
echo " PASS $name"
|
||||
passed=$((passed + 1))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback: every non-empty expected line appears as substring
|
||||
all_ok=1
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
if ! printf '%s\n' "$got" | grep -Fq "$line"; then
|
||||
all_ok=0
|
||||
break
|
||||
fi
|
||||
done <<< "$exp"
|
||||
|
||||
if [[ $all_ok -eq 1 ]]; then
|
||||
echo " PASS $name"
|
||||
passed=$((passed + 1))
|
||||
else
|
||||
echo " FAIL $name"
|
||||
echo "---- expected lines ----"
|
||||
printf '%s\n' "$exp"
|
||||
echo "---- got (tail) ----"
|
||||
printf '%s\n' "$got" | tail -20
|
||||
echo "--------------"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Stdlib golden tests: $passed passed, $failed failed, $skipped skipped"
|
||||
if [[ $failed -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,7 @@
|
||||
[Package]
|
||||
Name = "stdlib_string"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1,3 @@
|
||||
stdlib_string: ok
|
||||
PASS:
|
||||
stdlib_string
|
||||
@@ -0,0 +1,35 @@
|
||||
// Stdlib golden: String_IsEmpty / IsBlank / Repeat / ReplaceAll
|
||||
import Std::Io::{PrintLine};
|
||||
import Std::String::{
|
||||
String_IsEmpty, String_IsBlank, String_Repeat, String_ReplaceAll,
|
||||
String_Eq, String_Len, String_Contains, String_StartsWith, String_EndsWith
|
||||
};
|
||||
import Std::Test::{
|
||||
Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_AssertEqString, Test_Pass
|
||||
};
|
||||
|
||||
func Main() -> int {
|
||||
Test_AssertTrue(String_IsEmpty(""));
|
||||
Test_AssertFalse(String_IsEmpty("x"));
|
||||
Test_AssertTrue(String_IsBlank(""));
|
||||
Test_AssertTrue(String_IsBlank(" \t\n"));
|
||||
Test_AssertFalse(String_IsBlank(" x "));
|
||||
|
||||
Test_AssertEqString(String_Repeat(".", 5), ".....");
|
||||
Test_AssertEqInt(String_Len(String_Repeat("ab", 3)) as int, 6);
|
||||
Test_AssertEqString(String_Repeat("x", 0), "");
|
||||
Test_AssertEqString(String_Repeat("ok", 1), "ok");
|
||||
|
||||
let multi: String = String_ReplaceAll("a-b-a-b-a", "a", "X");
|
||||
Test_AssertEqString(multi, "X-b-X-b-X");
|
||||
let safe: String = String_ReplaceAll("..", ".", "x.");
|
||||
Test_AssertEqString(safe, "x.x.");
|
||||
|
||||
Test_AssertTrue(String_Contains("hello", "ell"));
|
||||
Test_AssertTrue(String_StartsWith("hello", "he"));
|
||||
Test_AssertTrue(String_EndsWith("hello", "lo"));
|
||||
|
||||
PrintLine("stdlib_string: ok");
|
||||
Test_Pass("stdlib_string");
|
||||
return 0;
|
||||
}
|
||||
+259
-43
@@ -4,10 +4,10 @@
|
||||
# Usage: bux-lsp
|
||||
# The editor spawns this binary and communicates via stdin/stdout.
|
||||
#
|
||||
# Hover uses real bootstrap sema types when possible (globals + stdlib);
|
||||
# completion/outline still use a fast lightweight scan.
|
||||
# Hover uses real bootstrap sema types when possible (globals + stdlib).
|
||||
# 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
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -88,6 +88,17 @@ type
|
||||
detail: string ## signature / type annotation
|
||||
container: string ## optional parent (module / type)
|
||||
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
|
||||
uri: string
|
||||
content: string
|
||||
@@ -97,6 +108,8 @@ type
|
||||
## Full-project type index for hover (includes stdlib after sema enrich)
|
||||
typeIndex: Table[string, string] ## name → type / signature string
|
||||
kindIndex: Table[string, string] ## name → kind label
|
||||
## Position-sensitive locals (filled by enrichWithSema)
|
||||
locals: seq[LocalBinding]
|
||||
|
||||
var
|
||||
documents = initTable[string, DocumentState]()
|
||||
@@ -355,8 +368,14 @@ proc typeExprToStr(te: TypeExpr): string =
|
||||
of tekOwn:
|
||||
result = "own " & typeExprToStr(te.pointerPointee)
|
||||
of tekRef:
|
||||
if te.refLifetime.len > 0:
|
||||
result = "&" & te.refLifetime & " " & typeExprToStr(te.pointerPointee)
|
||||
else:
|
||||
result = "&" & typeExprToStr(te.pointerPointee)
|
||||
of tekMutRef:
|
||||
if te.refLifetime.len > 0:
|
||||
result = "&" & te.refLifetime & " mut " & typeExprToStr(te.pointerPointee)
|
||||
else:
|
||||
result = "&mut " & typeExprToStr(te.pointerPointee)
|
||||
of tekSlice:
|
||||
result = typeExprToStr(te.sliceElement) & "[]"
|
||||
@@ -590,56 +609,173 @@ proc enrichWithSema(doc: DocumentState) =
|
||||
else:
|
||||
indexDecl(d)
|
||||
|
||||
# Walk this file's AST for local lets with explicit types (function bodies)
|
||||
proc walkBlock(blk: Block, container: string) =
|
||||
# --- Position-sensitive locals + inferred let types ---
|
||||
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
|
||||
let endLine = max(scopeEnd, blockEndLine(blk))
|
||||
for stmt in blk.stmts:
|
||||
case stmt.kind
|
||||
of skLet:
|
||||
let n = stmt.stmtLetName
|
||||
if n.len == 0: continue
|
||||
var typStr = ""
|
||||
var typ: Type = makeUnknown()
|
||||
var inferred = false
|
||||
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 detail = if typStr.len > 0: kw & " " & n & ": " & typStr else: kw & " " & n
|
||||
let loc = stmt.loc
|
||||
let line = max(0, int(loc.line) - 1)
|
||||
let col = max(0, int(loc.column) - 1)
|
||||
# Prefer sema-enriched detail if name already global; else add local
|
||||
if not doc.symbols.hasKey(n) or not doc.symbols[n].fromSema:
|
||||
let typStr = if typ != nil and not typ.isUnknown: typ.toString else: ""
|
||||
let detail =
|
||||
if typStr.len > 0: kw & " " & n & ": " & typStr
|
||||
else: kw & " " & n
|
||||
let line = max(0, int(stmt.loc.line) - 1)
|
||||
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(
|
||||
line: line, col: col, kind: "variable", detail: detail,
|
||||
container: container, fromSema: typStr.len > 0)
|
||||
if n notin doc.ordered:
|
||||
doc.ordered.add(n)
|
||||
if typStr.len > 0:
|
||||
doc.typeIndex[n] = detail
|
||||
doc.kindIndex[n] = "variable"
|
||||
# Define in scope for subsequent inference
|
||||
let sym = Symbol(kind: skVar, name: n, typ: typ,
|
||||
isMutable: stmt.stmtLetMut, isOwn: false)
|
||||
discard sc.define(sym)
|
||||
of skExpr:
|
||||
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:
|
||||
walkBlock(stmt.stmtIfThen, container)
|
||||
walkBlock(stmt.stmtIfElse, container)
|
||||
var thenSc = newScope(sc)
|
||||
collectLocals(sema, stmt.stmtIfThen, thenSc,
|
||||
blockEndLine(stmt.stmtIfThen), container)
|
||||
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:
|
||||
walkBlock(stmt.stmtWhileBody, container)
|
||||
of skFor:
|
||||
walkBlock(stmt.stmtForBody, container)
|
||||
var wSc = newScope(sc)
|
||||
collectLocals(sema, stmt.stmtWhileBody, wSc,
|
||||
blockEndLine(stmt.stmtWhileBody), container)
|
||||
of skDoWhile:
|
||||
var dSc = newScope(sc)
|
||||
collectLocals(sema, stmt.stmtDoWhileBody, dSc,
|
||||
blockEndLine(stmt.stmtDoWhileBody), container)
|
||||
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:
|
||||
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:
|
||||
if d.kind == dkFunc and d.declFuncBody != nil:
|
||||
walkBlock(d.declFuncBody, d.declFuncName)
|
||||
if d.kind == dkFunc:
|
||||
collectFuncLocals(semaMut, d)
|
||||
elif d.kind == dkModule:
|
||||
for sub in d.declModuleItems:
|
||||
if sub.kind == dkFunc and sub.declFuncBody != nil:
|
||||
walkBlock(sub.declFuncBody, sub.declFuncName)
|
||||
if sub.kind == dkFunc:
|
||||
collectFuncLocals(semaMut, sub)
|
||||
|
||||
except:
|
||||
discard # sema failures must not crash the LSP
|
||||
@@ -862,27 +998,55 @@ proc handleCompletion(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
return
|
||||
|
||||
ensureAnalyzed(doc)
|
||||
if doc.locals.len == 0 and doc.content.len > 0:
|
||||
enrichWithSema(doc)
|
||||
let prefix = findWordAt(doc.content, lineNum, col)
|
||||
|
||||
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:
|
||||
if offered.contains(name): continue
|
||||
if prefix == "" or name.toLowerAscii().startsWith(prefix.toLowerAscii()):
|
||||
offered.incl(name)
|
||||
items.add(%*{
|
||||
"label": name,
|
||||
"kind": completionKind(info.kind),
|
||||
"detail": info.detail,
|
||||
"sortText": "1_" & name,
|
||||
"documentation": {"kind": "markdown", "value": "```bux\n" & info.detail & "\n```\n\n_" & info.kind & "_"}
|
||||
})
|
||||
|
||||
# Also offer workspace symbols (other open / scanned files)
|
||||
for name, ws in workspaceSymbols.pairs:
|
||||
if doc.symbols.hasKey(name):
|
||||
continue
|
||||
if offered.contains(name): continue
|
||||
if prefix == "" or name.toLowerAscii().startsWith(prefix.toLowerAscii()):
|
||||
offered.incl(name)
|
||||
items.add(%*{
|
||||
"label": name,
|
||||
"kind": completionKind(ws.info.kind),
|
||||
"detail": ws.info.detail & " (workspace)",
|
||||
"sortText": "2_" & name,
|
||||
"documentation": {"kind": "markdown", "value": "```bux\n" & ws.info.detail & "\n```"}
|
||||
})
|
||||
|
||||
@@ -896,11 +1060,32 @@ proc handleCompletion(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
items.add(%*{
|
||||
"label": kw,
|
||||
"kind": 14,
|
||||
"detail": "keyword"
|
||||
"detail": "keyword",
|
||||
"sortText": "3_" & kw
|
||||
})
|
||||
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -917,13 +1102,26 @@ proc handleDefinition(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
return
|
||||
|
||||
ensureAnalyzed(doc)
|
||||
if doc.locals.len == 0 and doc.content.len > 0:
|
||||
enrichWithSema(doc)
|
||||
|
||||
let word = findWordAt(doc.content, lineNum, col)
|
||||
if word.len == 0:
|
||||
sendResponse(stream, id, %*[])
|
||||
return
|
||||
|
||||
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]
|
||||
locs.add(%*{
|
||||
"uri": uri,
|
||||
@@ -949,6 +1147,7 @@ proc handleDefinition(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
|
||||
proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
## 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 position = paramsNode["position"]
|
||||
let lineNum = position["line"].getInt()
|
||||
@@ -961,7 +1160,7 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
|
||||
ensureAnalyzed(doc)
|
||||
# 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)
|
||||
|
||||
let lines = doc.content.split("\n")
|
||||
@@ -983,23 +1182,39 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
var detail = ""
|
||||
var kind = ""
|
||||
var found = false
|
||||
var fromSema = false
|
||||
var inferred = false
|
||||
var scopeNote = ""
|
||||
|
||||
# Prefer file-local symbol (may be sema-upgraded)
|
||||
if doc.symbols.hasKey(word):
|
||||
# 1) Position-sensitive local / parameter
|
||||
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]
|
||||
detail = info.detail
|
||||
kind = info.kind
|
||||
found = true
|
||||
# Prefer pure sema typeIndex when richer
|
||||
fromSema = info.fromSema
|
||||
if doc.typeIndex.hasKey(word) and doc.typeIndex[word].len >= detail.len:
|
||||
detail = doc.typeIndex[word]
|
||||
if doc.kindIndex.hasKey(word):
|
||||
kind = doc.kindIndex[word]
|
||||
elif doc.typeIndex.hasKey(word):
|
||||
fromSema = true
|
||||
elif not found and doc.typeIndex.hasKey(word):
|
||||
detail = doc.typeIndex[word]
|
||||
kind = if doc.kindIndex.hasKey(word): doc.kindIndex[word] else: "symbol"
|
||||
found = true
|
||||
elif workspaceSymbols.hasKey(word):
|
||||
fromSema = true
|
||||
elif not found and workspaceSymbols.hasKey(word):
|
||||
let info = workspaceSymbols[word].info
|
||||
detail = info.detail
|
||||
kind = info.kind
|
||||
@@ -1010,10 +1225,12 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
return
|
||||
|
||||
var md = "```bux\n" & detail & "\n```\n\n_" & kind & "_"
|
||||
if doc.symbols.hasKey(word) and doc.symbols[word].fromSema:
|
||||
md &= " · sema"
|
||||
elif doc.typeIndex.hasKey(word):
|
||||
if scopeNote.len > 0:
|
||||
md &= scopeNote
|
||||
if fromSema:
|
||||
md &= " · sema"
|
||||
if inferred:
|
||||
md &= " · inferred"
|
||||
|
||||
sendResponse(stream, id, %*{
|
||||
"contents": {"kind": "markdown", "value": md},
|
||||
@@ -1022,7 +1239,6 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
"end": {"line": lineNum, "character": endC}
|
||||
}
|
||||
})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Document symbols (outline)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1088,7 +1304,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
||||
"hoverProvider": 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:
|
||||
rootPath = paramsNode["rootPath"].getStr()
|
||||
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# Smoke: hover on inferred let + parameter via bux-lsp JSON-RPC.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
LSP="$ROOT/tools/bux-lsp"
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
if [[ ! -x "$LSP" ]]; then
|
||||
echo "building bux-lsp..."
|
||||
(cd "$ROOT" && make lsp >/dev/null)
|
||||
fi
|
||||
|
||||
cat > "$TMP/Main.bux" <<'EOF'
|
||||
func Add(a: int, b: int) -> int {
|
||||
let sum = a + b;
|
||||
return sum;
|
||||
}
|
||||
func Main() -> int {
|
||||
let n = 10;
|
||||
return Add(n, 2);
|
||||
}
|
||||
EOF
|
||||
|
||||
rpc() {
|
||||
local body="$1"
|
||||
local len
|
||||
len=$(printf '%s' "$body" | wc -c)
|
||||
printf 'Content-Length: %s\r\n\r\n%s' "$len" "$body"
|
||||
}
|
||||
|
||||
CONTENT_JSON=$(python3 -c 'import json,sys; print(json.dumps(open(sys.argv[1]).read()))' "$TMP/Main.bux")
|
||||
URI="file://$TMP/Main.bux"
|
||||
|
||||
{
|
||||
rpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file://'"$TMP"'"}}'
|
||||
rpc '{"jsonrpc":"2.0","method":"initialized","params":{}}'
|
||||
rpc '{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"'"$URI"'","languageId":"bux","version":1,"text":'"$CONTENT_JSON"'}}}'
|
||||
# hover on `sum` (line 1)
|
||||
rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/hover","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":1,"character":8}}}'
|
||||
# hover on param a (line 0)
|
||||
rpc '{"jsonrpc":"2.0","id":3,"method":"textDocument/hover","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":0,"character":9}}}'
|
||||
# hover on n in Main (line 5)
|
||||
rpc '{"jsonrpc":"2.0","id":4,"method":"textDocument/hover","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":5,"character":8}}}'
|
||||
rpc '{"jsonrpc":"2.0","id":5,"method":"shutdown","params":null}'
|
||||
rpc '{"jsonrpc":"2.0","method":"exit","params":null}'
|
||||
} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt"
|
||||
|
||||
echo "---- hover responses (excerpt) ----"
|
||||
grep -o '"value":"[^"]*"' "$TMP/out.txt" | head -20 || true
|
||||
|
||||
# Must have typed sum / n and param a somewhere in output
|
||||
ok=1
|
||||
if ! grep -q 'sum' "$TMP/out.txt"; then
|
||||
echo "FAIL: no hover for sum"
|
||||
ok=0
|
||||
fi
|
||||
if ! grep -Eq 'let sum: int|sum: int' "$TMP/out.txt"; then
|
||||
echo "WARN: sum type not clearly int (may still pass if detail present)"
|
||||
# Soft fail only if completely missing inferred path
|
||||
if ! grep -q 'inferred' "$TMP/out.txt" && ! grep -q 'let sum' "$TMP/out.txt"; then
|
||||
ok=0
|
||||
fi
|
||||
fi
|
||||
if ! grep -Eq 'param a|a: int' "$TMP/out.txt"; then
|
||||
echo "FAIL: expected param a hover"
|
||||
ok=0
|
||||
fi
|
||||
if ! grep -Eq 'let n: int|n: int' "$TMP/out.txt"; then
|
||||
echo "WARN: n type not clearly int"
|
||||
fi
|
||||
|
||||
if [[ $ok -eq 0 ]]; then
|
||||
echo "---- full output ----"
|
||||
cat "$TMP/out.txt"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: LSP hover smoke (locals + params + inferred lets)"
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# Smoke: registry search + add + install + build with greet package (E.1)
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
BUXC="$ROOT/buxc"
|
||||
export BUX_REGISTRY="$ROOT/config/registry.toml"
|
||||
|
||||
if [[ ! -x "$BUXC" ]]; then
|
||||
(cd "$ROOT" && make build >/dev/null)
|
||||
fi
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
echo "=== bux search greet ==="
|
||||
"$BUXC" search greet | tee "$TMP/search.out"
|
||||
grep -q greet "$TMP/search.out"
|
||||
|
||||
echo "=== create consumer project ==="
|
||||
mkdir -p "$TMP/app/src"
|
||||
cat > "$TMP/app/bux.toml" <<'EOF'
|
||||
[Package]
|
||||
Name = "registry_consumer"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
EOF
|
||||
|
||||
cat > "$TMP/app/src/Main.bux" <<'EOF'
|
||||
import Std::Io::{PrintLine};
|
||||
import Std::String::{String_Eq};
|
||||
import Std::Test::{Test_AssertTrue, Test_Pass};
|
||||
|
||||
func Main() -> int {
|
||||
let msg: String = Greet_Hello("Bux");
|
||||
Test_AssertTrue(String_Eq(msg, "Hello, Bux!"));
|
||||
Test_AssertTrue(String_Eq(Greet_Version(), "0.1.1"));
|
||||
PrintLine(msg);
|
||||
Test_Pass("registry_consumer");
|
||||
return 0;
|
||||
}
|
||||
EOF
|
||||
|
||||
cd "$TMP/app"
|
||||
export BUX_STDLIB="$ROOT/lib"
|
||||
|
||||
echo "=== bux add greet ==="
|
||||
"$BUXC" add greet
|
||||
grep -q greet bux.toml
|
||||
cat bux.toml
|
||||
|
||||
echo "=== bux install ==="
|
||||
"$BUXC" install
|
||||
test -f bux.lock
|
||||
grep -q greet bux.lock
|
||||
cat bux.lock
|
||||
|
||||
echo "=== bux run ==="
|
||||
"$BUXC" run . | tee "$TMP/run.out"
|
||||
grep -q "Hello, Bux!" "$TMP/run.out"
|
||||
|
||||
echo "PASS: registry smoke (search + add + install + build)"
|
||||
@@ -0,0 +1,93 @@
|
||||
## Smoke test for position-sensitive locals + inferred let types.
|
||||
## Run: nim r --path:../bootstrap tools/test_lsp_locals.nim
|
||||
import std/[os, strutils, tables, unittest]
|
||||
import lexer, parser, ast, sema, types, scope
|
||||
|
||||
# Minimal mirror of LSP collect (keeps the test free of JSON-RPC)
|
||||
|
||||
proc typeOfLet(sema: var Sema, stmt: Stmt, sc: Scope): tuple[t: Type, inferred: bool] =
|
||||
result.inferred = false
|
||||
result.t = makeUnknown()
|
||||
if stmt.stmtLetType != nil:
|
||||
result.t = sema.resolveType(stmt.stmtLetType)
|
||||
if (result.t == nil or result.t.isUnknown) and stmt.stmtLetInit != nil:
|
||||
result.t = sema.checkExprForLsp(stmt.stmtLetInit, sc)
|
||||
result.inferred = true
|
||||
elif stmt.stmtLetType == nil and stmt.stmtLetInit != nil:
|
||||
result.t = sema.checkExprForLsp(stmt.stmtLetInit, sc)
|
||||
result.inferred = true
|
||||
|
||||
suite "LSP locals / inference":
|
||||
test "inferred let int from literal":
|
||||
let src = """
|
||||
func Main() -> int {
|
||||
let x = 42;
|
||||
return x;
|
||||
}
|
||||
"""
|
||||
let lexRes = tokenize(src, "t.bux")
|
||||
check(not lexRes.hasErrors)
|
||||
let parseRes = parse(lexRes.tokens, "t.bux")
|
||||
check(parseRes.diagnostics.len == 0)
|
||||
var (res, semaCtx) = analyzeFull(parseRes.module)
|
||||
discard res
|
||||
var found = false
|
||||
for d in parseRes.module.items:
|
||||
if d.kind != dkFunc: continue
|
||||
var sc = newScope(semaCtx.globalScope)
|
||||
for stmt in d.declFuncBody.stmts:
|
||||
if stmt.kind == skLet and stmt.stmtLetName == "x":
|
||||
let (t, inf) = typeOfLet(semaCtx, stmt, sc)
|
||||
check(inf)
|
||||
check(t.toString == "int" or t.kind == tkInt)
|
||||
found = true
|
||||
check(found)
|
||||
|
||||
test "explicit type not marked inferred":
|
||||
let src = """
|
||||
func Main() -> int {
|
||||
let s: String = "hi";
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
let lexRes = tokenize(src, "t.bux")
|
||||
let parseRes = parse(lexRes.tokens, "t.bux")
|
||||
var (res, semaCtx) = analyzeFull(parseRes.module)
|
||||
discard res
|
||||
for d in parseRes.module.items:
|
||||
if d.kind != dkFunc: continue
|
||||
var sc = newScope(semaCtx.globalScope)
|
||||
for stmt in d.declFuncBody.stmts:
|
||||
if stmt.kind == skLet and stmt.stmtLetName == "s":
|
||||
let (t, inf) = typeOfLet(semaCtx, stmt, sc)
|
||||
check(not inf)
|
||||
check(t.toString == "String" or t.kind == tkStr)
|
||||
|
||||
test "shadowed local: outer then inner":
|
||||
let src = """
|
||||
func Main() -> int {
|
||||
let x = 1;
|
||||
if true {
|
||||
let x = 2;
|
||||
return x;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
"""
|
||||
let lexRes = tokenize(src, "t.bux")
|
||||
let parseRes = parse(lexRes.tokens, "t.bux")
|
||||
check(parseRes.diagnostics.len == 0)
|
||||
# Both lets parse; inner is nested under if
|
||||
var outer, inner: bool
|
||||
for d in parseRes.module.items:
|
||||
if d.kind != dkFunc: continue
|
||||
for stmt in d.declFuncBody.stmts:
|
||||
if stmt.kind == skLet and stmt.stmtLetName == "x":
|
||||
outer = true
|
||||
if stmt.kind == skIf:
|
||||
for s2 in stmt.stmtIfThen.stmts:
|
||||
if s2.kind == skLet and s2.stmtLetName == "x":
|
||||
inner = true
|
||||
check(outer and inner)
|
||||
|
||||
echo "LSP locals unit checks done"
|
||||
Reference in New Issue
Block a user