diff --git a/Makefile b/Makefile index e384fb8..6a767aa 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ 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 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 test-stdlib selfhost-loop lsp fmt-check docs +.PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs bench test-apps test-dwarf all: build @@ -193,6 +193,32 @@ test-lsp: lsp .PHONY: test-registry test-registry: build - @echo "=== Registry smoke (E.1) ===" + @echo "=== Registry smoke (E.1 + HTTP) ===" @chmod +x tools/smoke_registry.sh @tools/smoke_registry.sh + +# E.2 — build showcase apps + simpledb/jwt CLI smoke +.PHONY: test-apps +test-apps: build + @echo "=== Apps smoke (E.2) ===" + @chmod +x tools/smoke_apps.sh + @tools/smoke_apps.sh + +# E.5 — micro-benchmarks (Bux + C/Nim/Zig twins) +.PHONY: bench +bench: build + @chmod +x tools/bench.sh + @tools/bench.sh + +# E.5 — Nexus HTTP throughput (wrk); optional via BENCH_NEXUS=1 make bench +.PHONY: bench-nexus +bench-nexus: build + @chmod +x tools/bench_nexus.sh + @tools/bench_nexus.sh + +# E.4 — DWARF / #line debugger smoke +.PHONY: test-dwarf +test-dwarf: build + @echo "=== DWARF / #line smoke (E.4) ===" + @chmod +x tools/smoke_dwarf.sh + @tools/smoke_dwarf.sh diff --git a/apps/nexus/README.md b/apps/nexus/README.md index 260f994..53ff0a7 100644 --- a/apps/nexus/README.md +++ b/apps/nexus/README.md @@ -32,9 +32,12 @@ cd apps/nexus # Run ./nexus + +# Optional env (also used by `make bench-nexus`) +# NEXUS_PORT=18080 NEXUS_BIND=127.0.0.1 NEXUS_WORKERS=4 ./build/nexus ``` -Server starts on `http://0.0.0.0:8080`: +Server starts on `http://0.0.0.0:8080` (override with `NEXUS_PORT` / `NEXUS_BIND`): ``` ╔══════════════════════════════════════════════╗ diff --git a/apps/nexus/src/Main.bux b/apps/nexus/src/Main.bux index a59bf0b..e3d7cb7 100644 --- a/apps/nexus/src/Main.bux +++ b/apps/nexus/src/Main.bux @@ -5,6 +5,8 @@ module Main { import Router::{Handler, Route, Router}; import Server::{RunServer}; import Std::Array::{Array, Array_New, Array_Push}; + import Std::Os::{Os_GetEnv}; + import Std::String::{String_Len, String_ToInt}; func BuildRouter() -> Router { var routes: Array = Array_New(8); @@ -39,8 +41,36 @@ module Main { }; } + /// Apply optional env overrides for benches / ops: + /// NEXUS_PORT, NEXUS_WORKERS, NEXUS_BIND, NEXUS_PUBLIC + func ApplyEnvConfig(config: *ServerConfig) { + let portEnv: String = Os_GetEnv("NEXUS_PORT"); + if String_Len(portEnv) > 0 { + let p: int64 = String_ToInt(portEnv); + if p > 0 && p < 65536 { + config.port = p as int; + } + } + let workersEnv: String = Os_GetEnv("NEXUS_WORKERS"); + if String_Len(workersEnv) > 0 { + let w: int64 = String_ToInt(workersEnv); + if w > 0 && w <= 256 { + config.workerCount = w as int; + } + } + let bindEnv: String = Os_GetEnv("NEXUS_BIND"); + if String_Len(bindEnv) > 0 { + config.bindAddr = bindEnv; + } + let pubEnv: String = Os_GetEnv("NEXUS_PUBLIC"); + if String_Len(pubEnv) > 0 { + config.publicDir = pubEnv; + } + } + func Main() -> int { - let config: ServerConfig = DefaultConfig(); + var config: ServerConfig = DefaultConfig(); + ApplyEnvConfig(&config); let router: Router = BuildRouter(); return RunServer(config, router); diff --git a/apps/simpledb/README.md b/apps/simpledb/README.md index 1940405..c52a057 100644 --- a/apps/simpledb/README.md +++ b/apps/simpledb/README.md @@ -50,10 +50,9 @@ Data is stored as plain text, one `key=value` per line. The file is created auto ## Build ```sh -# workaround: disable broken JWT module -mv ../../lib/crypto/jwt.bux ../../lib/crypto/jwt.bux.bak +cd apps/simpledb ../../buxc build -mv ../../lib/crypto/jwt.bux.bak ../../lib/crypto/jwt.bux +./build/simpledb data.db set hello world ``` ## API diff --git a/benches/README.md b/benches/README.md new file mode 100644 index 0000000..8a00372 --- /dev/null +++ b/benches/README.md @@ -0,0 +1,56 @@ +# Bux benchmarks (E.5) + +Micro-kernels + optional Nexus HTTP throughput for regression and language comparison. + +## Quick run + +```bash +# From repo root +make bench # Bux micro + C + Nim (+ Zig if installed) +make bench-nexus # wrk vs apps/nexus /api/health +BENCH_NEXUS=1 make bench +``` + +## Micro suites + +| Suite | Kernels | Build | +|-------|---------|-------| +| `micro/` | Bux: `int_loop`, `fib30`, `string_concat`, `array_push` | `buxc build` | +| `c/` | C: `fib30`, `int_loop` | `gcc -O2` | +| `nim/` | Nim twins | `nim c -d:release --opt:speed` | +| `zig/` | Zig twins (optional) | `zig build-exe -OReleaseFast` | + +Output lines: + +``` +BENCH iters=N total_us=T us_per_op=P +``` + +## Nexus throughput + +`tools/bench_nexus.sh` / `make bench-nexus`: + +1. Builds `apps/nexus` +2. Starts on `NEXUS_PORT` (default **18080**), bind `127.0.0.1` +3. `wrk -t4 -c64 -d5s` against `/api/health` +4. Prints `BENCH nexus_health rps=…` + +Env knobs: + +| Variable | Default | Meaning | +|----------|---------|---------| +| `NEXUS_PORT` | `18080` | Listen port (also used by the server binary) | +| `NEXUS_WORKERS` | `4` | Worker threads | +| `NEXUS_BENCH_DURATION` | `5s` | wrk `-d` | +| `NEXUS_BENCH_C` | `64` | wrk connections | +| `NEXUS_BENCH_T` | `4` | wrk threads | + +Server-side (any nexus run): + +- `NEXUS_PORT`, `NEXUS_WORKERS`, `NEXUS_BIND`, `NEXUS_PUBLIC` + +## Notes + +- Numbers vary by machine; use them relatively (same host, same day). +- Nexus currently closes connections (`Connection: close`) — RPS is honest for that model, not keep-alive maxed. +- Requires `wrk` for `bench-nexus` (`apt install wrk` on Debian/Ubuntu). diff --git a/benches/c/fib.c b/benches/c/fib.c new file mode 100644 index 0000000..294ae1e --- /dev/null +++ b/benches/c/fib.c @@ -0,0 +1,26 @@ +/* C reference: recursive fib(30) — compare with benches/micro fib30 */ +#include +#include +#include + +static int fib(int n) { + if (n <= 1) return n; + return fib(n - 1) + fib(n - 2); +} + +static int64_t now_us(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000000LL + (int64_t)ts.tv_nsec / 1000LL; +} + +int main(void) { + (void)fib(20); /* warmup */ + int64_t t0 = now_us(); + int r = fib(30); + int64_t t1 = now_us(); + if (r < 0) return 1; + printf("BENCH fib30 iters=1 total_us=%lld us_per_op=%lld\n", + (long long)(t1 - t0), (long long)(t1 - t0)); + return 0; +} diff --git a/benches/c/int_loop.c b/benches/c/int_loop.c new file mode 100644 index 0000000..cb8b071 --- /dev/null +++ b/benches/c/int_loop.c @@ -0,0 +1,24 @@ +/* C reference: 1e6 integer arithmetic loop */ +#include +#include +#include + +static int64_t now_us(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000000LL + (int64_t)ts.tv_nsec / 1000LL; +} + +int main(void) { + const int iters = 1000000; + int acc = 0; + int64_t t0 = now_us(); + for (int i = 0; i < iters; i++) { + acc = acc + i * 3 - 1; + } + int64_t t1 = now_us(); + if (acc == 0 && iters > 0) return 1; + printf("BENCH int_loop iters=%d total_us=%lld us_per_op=%lld\n", + iters, (long long)(t1 - t0), (long long)((t1 - t0) / iters)); + return 0; +} diff --git a/benches/micro/bux.toml b/benches/micro/bux.toml new file mode 100644 index 0000000..30d091a --- /dev/null +++ b/benches/micro/bux.toml @@ -0,0 +1,8 @@ +[Package] +Name = "micro" +Version = "0.1.0" +Type = "bin" +Description = "Micro-benchmarks for Bux (E.5)" + +[Build] +Output = "Bin" diff --git a/benches/micro/src/Main.bux b/benches/micro/src/Main.bux new file mode 100644 index 0000000..3f09b2d --- /dev/null +++ b/benches/micro/src/Main.bux @@ -0,0 +1,104 @@ +// Micro-benchmarks (E.5) — recursive fib, string concat, array push. +// Prints lines: BENCH name iters total_us us_per_op +import Std::Io::{PrintLine, Print}; +import Std::Time::{Time_NowUs}; +import Std::String::{String_FromInt, String_Concat, String_Len}; +import Std::Array::{Array, Array_New, Array_Push, Array_Len, Array_Get}; + +func Fib(n: int) -> int { + if n <= 1 { + return n; + } + return Fib(n - 1) + Fib(n - 2); +} + +func Report(name: String, iters: int, usTotal: int64) { + var perOpUs: int64 = 0; + if iters > 0 { + perOpUs = usTotal / (iters as int64); + } + Print("BENCH "); + Print(name); + Print(" iters="); + Print(String_FromInt(iters as int64)); + Print(" total_us="); + Print(String_FromInt(usTotal)); + Print(" us_per_op="); + PrintLine(String_FromInt(perOpUs)); +} + +func BenchIntLoop() { + let iters: int = 1000000; + var acc: int = 0; + var i: int = 0; + let t0: int64 = Time_NowUs(); + while i < iters { + acc = acc + i * 3 - 1; + i = i + 1; + } + let t1: int64 = Time_NowUs(); + if acc == 0 && iters > 0 { + PrintLine("unreachable"); + } + Report("int_loop", iters, t1 - t0); +} + +func BenchFib() { + discard Fib(20); + let n: int = 30; + let t0: int64 = Time_NowUs(); + let r: int = Fib(n); + let t1: int64 = Time_NowUs(); + if r < 0 { + PrintLine("unreachable"); + } + Report("fib30", 1, t1 - t0); +} + +func BenchStringConcat() { + let iters: int = 50000; + var i: int = 0; + let t0: int64 = Time_NowUs(); + var s: String = ""; + while i < iters { + s = String_Concat(s, "x"); + if String_Len(s) > 256 { + s = "x"; + } + i = i + 1; + } + let t1: int64 = Time_NowUs(); + if String_Len(s) < 0 { + PrintLine("unreachable"); + } + Report("string_concat", iters, t1 - t0); +} + +func BenchArrayPush() { + let iters: int = 100000; + var arr: Array = Array_New(16); + var i: int = 0; + let t0: int64 = Time_NowUs(); + while i < iters { + Array_Push(&arr, i); + i = i + 1; + } + let t1: int64 = Time_NowUs(); + let n: uint = Array_Len(&arr); + if n as int != iters { + Print("array len mismatch: "); + PrintLine(String_FromInt(n as int64)); + } + discard Array_Get(&arr, (iters - 1) as uint); + Report("array_push", iters, t1 - t0); +} + +func Main() -> int { + PrintLine("=== Bux micro-benchmarks (E.5) ==="); + BenchIntLoop(); + BenchFib(); + BenchStringConcat(); + BenchArrayPush(); + PrintLine("=== done ==="); + return 0; +} diff --git a/benches/nim/fib.nim b/benches/nim/fib.nim new file mode 100644 index 0000000..917c39c --- /dev/null +++ b/benches/nim/fib.nim @@ -0,0 +1,16 @@ +## Nim twin of benches/c/fib.c — recursive fib(30) +import std/[times, strformat] + +proc fib(n: int): int = + if n <= 1: return n + fib(n - 1) + fib(n - 2) + +proc nowUs(): int64 = + int64(epochTime() * 1_000_000.0) + +discard fib(20) +let t0 = nowUs() +let r = fib(30) +let t1 = nowUs() +if r < 0: quit(1) +echo &"BENCH fib30 iters=1 total_us={t1 - t0} us_per_op={t1 - t0}" diff --git a/benches/nim/int_loop.nim b/benches/nim/int_loop.nim new file mode 100644 index 0000000..fd10fa5 --- /dev/null +++ b/benches/nim/int_loop.nim @@ -0,0 +1,15 @@ +## Nim twin of benches/c/int_loop.c — 1e6 integer arithmetic loop +import std/[times, strformat] + +proc nowUs(): int64 = + int64(epochTime() * 1_000_000.0) + +const iters = 1_000_000 +var acc = 0 +let t0 = nowUs() +for i in 0 ..< iters: + acc = acc + i * 3 - 1 +let t1 = nowUs() +if acc == 0 and iters > 0: quit(1) +let total = t1 - t0 +echo &"BENCH int_loop iters={iters} total_us={total} us_per_op={total div iters}" diff --git a/benches/zig/fib.zig b/benches/zig/fib.zig new file mode 100644 index 0000000..ac562a8 --- /dev/null +++ b/benches/zig/fib.zig @@ -0,0 +1,22 @@ +// Zig twin of benches/c/fib.c — recursive fib(30) +// Build: zig build-exe -OReleaseFast -femit-bin=build/fib fib.zig +const std = @import("std"); + +fn fib(n: i32) i32 { + if (n <= 1) return n; + return fib(n - 1) + fib(n - 2); +} + +fn nowUs() i64 { + return @divTrunc(@as(i64, @intCast(std.time.nanoTimestamp())), 1000); +} + +pub fn main() !void { + _ = fib(20); + const t0 = nowUs(); + const r = fib(30); + const t1 = nowUs(); + if (r < 0) return error.Unreachable; + const out = std.io.getStdOut().writer(); + try out.print("BENCH fib30 iters=1 total_us={d} us_per_op={d}\n", .{ t1 - t0, t1 - t0 }); +} diff --git a/benches/zig/int_loop.zig b/benches/zig/int_loop.zig new file mode 100644 index 0000000..dc6051b --- /dev/null +++ b/benches/zig/int_loop.zig @@ -0,0 +1,22 @@ +// Zig twin of benches/c/int_loop.c — 1e6 integer arithmetic loop +// Build: zig build-exe -OReleaseFast -femit-bin=build/int_loop int_loop.zig +const std = @import("std"); + +fn nowUs() i64 { + return @divTrunc(@as(i64, @intCast(std.time.nanoTimestamp())), 1000); +} + +pub fn main() !void { + const iters: i32 = 1_000_000; + var acc: i32 = 0; + const t0 = nowUs(); + var i: i32 = 0; + while (i < iters) : (i += 1) { + acc = acc + i * 3 - 1; + } + const t1 = nowUs(); + if (acc == 0 and iters > 0) return error.Unreachable; + const total = t1 - t0; + const out = std.io.getStdOut().writer(); + try out.print("BENCH int_loop iters={d} total_us={d} us_per_op={d}\n", .{ iters, total, @divTrunc(total, iters) }); +} diff --git a/bootstrap/cli.nim b/bootstrap/cli.nim index fec0ff4..851d061 100644 --- a/bootstrap/cli.nim +++ b/bootstrap/cli.nim @@ -15,6 +15,7 @@ type color*: ColorMode quiet*: bool verbose*: bool + release*: bool ## --release: -O2, no -g / no #line (E.4 dual) proc printUsage*() = echo """Bux Programming Language (bootstrap compiler) @@ -42,15 +43,22 @@ Command options: fmt --check Exit 1 if any file would be reformatted (CI) doc --out Write docs to file (default: stdout) add --path / --git Explicit source; else resolve via registry + build --release Optimized build (-O2, no debug / #line) + +Registry: + BUX_REGISTRY Local path or http(s):// URL to registry.toml + BUX_REGISTRY_REFRESH=1 Force re-download of HTTP index cache + BUX_CFLAGS Extra flags appended to the C compiler line Global options: --color Control colored output (default: auto) -q, --quiet Suppress non-error output -v, --verbose Verbose output + --release Optimize (-O2), omit -g and #line maps """ proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq[string], ok: bool] = - result.opts = GlobalOptions(color: cmAuto, quiet: false, verbose: false) + result.opts = GlobalOptions(color: cmAuto, quiet: false, verbose: false, release: false) result.rest = @[] result.ok = true var i = 0 @@ -74,6 +82,8 @@ proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq result.opts.quiet = true elif arg == "-v" or arg == "--verbose": result.opts.verbose = true + elif arg == "--release": + result.opts.release = true else: result.rest.add(arg) inc i @@ -411,10 +421,14 @@ proc cmdAdd*(args: seq[string], opts: GlobalOptions): int = elif gitUrl.len > 0: depLine = &"{depName} = {{ Version = \"{version}\", Source = \"{gitUrl}\" }}" else: - # Registry resolve (E.1) + # Registry resolve (E.1 + HTTP URL) let reg = loadRegistry() if reg.path.len == 0: - printError("no package registry found (set BUX_REGISTRY or install config/registry.toml)", useColor) + if reg.sourceUrl.len > 0: + printError(&"failed to fetch registry from {reg.sourceUrl}", useColor) + printError("hint: check network, curl/wget, or set BUX_REGISTRY to a local file", useColor) + else: + 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: @@ -423,7 +437,8 @@ proc cmdAdd*(args: seq[string], opts: GlobalOptions): int = return 1 depLine = formatRegistryDepLine(depName, pkg) if not opts.quiet: - printInfo(&"Resolved '{depName}' {pkg.version} from registry {reg.path}", useColor) + let src = if reg.sourceUrl.len > 0: reg.sourceUrl else: reg.path + printInfo(&"Resolved '{depName}' {pkg.version} from registry {src}", useColor) var content = readFile(manifestPath) # Ensure [Dependencies] section exists if content.find("[Dependencies]") < 0: @@ -440,10 +455,18 @@ proc cmdSearch*(args: seq[string], opts: GlobalOptions): int = 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) + if reg.sourceUrl.len > 0: + printError(&"failed to fetch registry from {reg.sourceUrl}", useColor) + printError("hint: need curl or wget; or set BUX_REGISTRY to a local file", useColor) + else: + printError("no package registry found (set BUX_REGISTRY path or http(s) URL)", useColor) return 1 if not opts.quiet: - echo &"Registry: {reg.path}" + if reg.sourceUrl.len > 0: + echo &"Registry: {reg.sourceUrl}" + echo &" (cached: {reg.path})" + else: + echo &"Registry: {reg.path}" let hits = registrySearch(reg, query) if hits.len == 0: if not opts.quiet: @@ -717,8 +740,18 @@ proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl] = result.add(d) proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = + var opts = opts + var pathArgs: seq[string] = @[] + for a in args: + if a == "--release": + opts.release = true + elif a.startsWith("-"): + # ignore unknown flags for forward-compat; keep path-like later + discard + else: + pathArgs.add(a) let useColor = shouldUseColor(opts) - let root = if args.len > 0: absolutePath(args[0]) else: getCurrentDir() + let root = if pathArgs.len > 0: absolutePath(pathArgs[0]) else: getCurrentDir() let (pctx, status) = prepareProject(root, useColor, opts) if status != 0: return status @@ -739,7 +772,8 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = let hirMod = lowerModule(unifiedModule, semaCtx) let lirBuilder = lowerModuleToLir(hirMod) - var lirCbe = initLirCBackend() + # Debug builds: #line maps into .bux for gdb. Release: skip maps + -O2. + var lirCbe = initLirCBackend(emitDebugLines = not opts.release) var allCCode = lirCbe.emitModule(lirBuilder, hirMod) # Write C file @@ -769,10 +803,13 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = printError("io.c not found in rt/", useColor) return 1 - # Compile with cc + # Compile with cc — debug default (-O0 -g) or --release (-O2) let outputName = if pctx.man.name != "": pctx.man.name else: "bux_out" let outputFile = buildDir / outputName - let ccCmd = &"cc -O0 -g -pthread -Wl,--build-id=none -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm -lcrypto 2>&1" + let optFlags = if opts.release: "-O2 -DNDEBUG" else: "-O0 -g" + let extraCflags = getEnv("BUX_CFLAGS") + let cflags = if extraCflags.len > 0: optFlags & " " & extraCflags else: optFlags + let ccCmd = &"cc {cflags} -pthread -Wl,--build-id=none -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm -lcrypto 2>&1" if opts.verbose: printInfo(&"running: {ccCmd}", useColor) let (output, exitCode) = execCmdEx(ccCmd) diff --git a/bootstrap/lir_c_backend.nim b/bootstrap/lir_c_backend.nim index 2325dad..514c38f 100644 --- a/bootstrap/lir_c_backend.nim +++ b/bootstrap/lir_c_backend.nim @@ -10,11 +10,17 @@ type output*: string indent*: int tempTypes*: Table[string, string] ## Track C types of temp variables + emitDebugLines*: bool ## Emit #line → .bux for DWARF (E.4) + lastDebugLine*: int + lastDebugFile*: string -proc initLirCBackend*(): LirCBackend = +proc initLirCBackend*(emitDebugLines: bool = true): LirCBackend = result = LirCBackend( indent: 0, tempTypes: initTable[string, string](), + emitDebugLines: emitDebugLines, + lastDebugLine: 0, + lastDebugFile: "", ) proc emitIndent(be: var LirCBackend) = @@ -26,6 +32,22 @@ proc emitLine(be: var LirCBackend, s: string) = be.output.add(s) be.output.add("\n") +proc emitDebugLine(be: var LirCBackend, instr: LirInstr) = + ## Map generated C back to Bux source for gdb/DWARF via #line. + if not be.emitDebugLines: return + if instr.locLine <= 0: return + if instr.locLine == be.lastDebugLine and instr.locFile == be.lastDebugFile: + return + be.lastDebugLine = instr.locLine + be.lastDebugFile = instr.locFile + var path = instr.locFile + if path.len == 0: + path = "" + # Escape for C string literal + path = path.replace("\\", "\\\\").replace("\"", "\\\"") + # #line must start at column 0 + be.output.add(&"#line {instr.locLine} \"{path}\"\n") + proc valToC(be: var LirCBackend, v: LirValue): string = ## Convert a LirValue to its C representation. case v.kind @@ -50,6 +72,7 @@ proc cParamDecl(cType, name: string): string = # ── Per-instruction emission ── proc emitInstr(be: var LirCBackend, instr: LirInstr) = + be.emitDebugLine(instr) template v(x: LirValue): string = valToC(be, x) case instr.kind @@ -246,6 +269,16 @@ proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, strin if f.params.len == 0: paramsStr = "void" + # Point the function entry at the first Bux location so gdb `list Main` works + # (otherwise leftover #line from the previous function pollutes the prologue). + if be.emitDebugLines: + for instr in f.instrs: + if instr.locLine > 0: + be.lastDebugLine = 0 + be.lastDebugFile = "" + be.emitDebugLine(instr) + break + be.emitLine(&"{f.retType} {f.name}({paramsStr}) {{") be.indent += 1 diff --git a/bootstrap/lir_lower.nim b/bootstrap/lir_lower.nim index e6592fb..1ff5dbf 100644 --- a/bootstrap/lir_lower.nim +++ b/bootstrap/lir_lower.nim @@ -191,10 +191,20 @@ proc cmpOpToLir(op: TokenKind): LirKind = proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) +proc setSourceLoc(ctx: var LowerToLirCtx, node: HirNode) = + ## Stamp LIR instructions with HIR source locations for #line / DWARF. + if node == nil: return + if node.loc.line > 0: + ctx.builder.commentLine = int(node.loc.line) + if node.loc.file.len > 0: + ctx.builder.commentFile = node.loc.file + ctx.currentFile = node.loc.file + # ── Lowering: Expressions → LirValue ── proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue = if node == nil: return lirInt(0) + setSourceLoc(ctx, node) template b: var LirBuilder = ctx.builder case node.kind @@ -623,6 +633,7 @@ proc buildLval(ctx: var LowerToLirCtx, n: HirNode): string = proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) = if node == nil: return + setSourceLoc(ctx, node) template b: var LirBuilder = ctx.builder case node.kind diff --git a/bootstrap/registry.nim b/bootstrap/registry.nim index 7aeb0ea..b99f57a 100644 --- a/bootstrap/registry.nim +++ b/bootstrap/registry.nim @@ -1,11 +1,11 @@ -## registry.nim — Bux package registry index (E.1) +## registry.nim — Bux package registry index (E.1 + HTTP URL) ## ## 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 +## source = "file:packages/greet" # relative to the index file ## description = "Hello helpers" ## ## [[package]] @@ -13,12 +13,16 @@ ## version = "1.2.0" ## source = "https://github.com/bux-lang/net.git" ## -## Lookup order for the index file: -## 1. $BUX_REGISTRY (file path) +## Lookup order for the index: +## 1. $BUX_REGISTRY — local file path **or** http(s):// URL ## 2. ~/.bux/registry.toml ## 3. /config/registry.toml next to the compiler / cwd +## +## HTTP indices are downloaded to ~/.bux/cache/registry_http.toml. +## Relative file:/path: sources in a remote index are resolved against that +## cache directory — prefer absolute paths or git URLs for remote registries. -import std/[os, strutils, strformat, algorithm] +import std/[os, strutils, strformat, algorithm, osproc] type RegistryPackage* = object @@ -29,7 +33,8 @@ type resolvedPath*: string ## absolute path for file: sources (filled on load) Registry* = object - path*: string ## index file path + path*: string ## index file path (local, or cached path for HTTP) + sourceUrl*: string ## non-empty when loaded from an HTTP URL packages*: seq[RegistryPackage] proc resolvePackageSource(pkg: var RegistryPackage, indexDir: string) = @@ -83,14 +88,70 @@ proc parseRegistryToml(content, indexPath: string): seq[RegistryPackage] = resolvePackageSource(cur, indexDir) result.add(cur) -proc findRegistryIndex*(): string = - ## Locate the registry index file. +proc isHttpUrl*(s: string): bool = + s.startsWith("http://") or s.startsWith("https://") + +proc registryCacheDir*(): string = + getHomeDir() / ".bux" / "cache" + +proc fetchRegistryUrl*(url: string): string = + ## Download a remote registry.toml into ~/.bux/cache/. + ## Returns the local cache path on success, or "" on failure. + ## Re-fetches when the URL changes or when $BUX_REGISTRY_REFRESH is set. + let cacheDir = registryCacheDir() + try: + createDir(cacheDir) + except OSError, IOError: + return "" + let cachePath = cacheDir / "registry_http.toml" + let metaPath = cacheDir / "registry_http.url" + let force = getEnv("BUX_REGISTRY_REFRESH").len > 0 + var cachedUrl = "" + if fileExists(metaPath): + try: + cachedUrl = readFile(metaPath).strip() + except CatchableError: + cachedUrl = "" + if not force and fileExists(cachePath) and cachedUrl == url: + return cachePath.absolutePath + + # Prefer curl; fall back to wget + var ok = false + if findExe("curl").len > 0: + let cmd = &"curl -fsSL --max-time 30 -o {quoteShell(cachePath)} {quoteShell(url)}" + let (_, code) = execCmdEx(cmd) + ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0 + elif findExe("wget").len > 0: + let cmd = &"wget -q -T 30 -O {quoteShell(cachePath)} {quoteShell(url)}" + let (_, code) = execCmdEx(cmd) + ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0 + else: + return "" + + if not ok: + return "" + try: + writeFile(metaPath, url & "\n") + except CatchableError: + discard + return cachePath.absolutePath + +proc findRegistryIndex*(): tuple[path: string, url: string] = + ## Locate the registry index. Returns (localPath, sourceUrl). + ## sourceUrl is non-empty only when the index was (or should be) fetched via HTTP. + result = ("", "") let env = getEnv("BUX_REGISTRY") - if env.len > 0 and fileExists(env): - return env.absolutePath + if env.len > 0: + if isHttpUrl(env): + let local = fetchRegistryUrl(env) + if local.len > 0: + return (local, env) + return ("", env) # URL set but fetch failed — caller can report + if fileExists(env): + return (env.absolutePath, "") let homeIdx = getHomeDir() / ".bux" / "registry.toml" if fileExists(homeIdx): - return homeIdx + return (homeIdx, "") let candidates = @[ getAppDir() / ".." / "config" / "registry.toml", getAppDir() / "config" / "registry.toml", @@ -99,12 +160,23 @@ proc findRegistryIndex*(): string = ] for c in candidates: if fileExists(c): - return c.absolutePath - return "" + return (c.absolutePath, "") + return ("", "") proc loadRegistry*(path: string = ""): Registry = - result.path = if path.len > 0: path else: findRegistryIndex() + result.path = "" + result.sourceUrl = "" result.packages = @[] + if path.len > 0: + if isHttpUrl(path): + result.sourceUrl = path + result.path = fetchRegistryUrl(path) + else: + result.path = path + else: + let (p, u) = findRegistryIndex() + result.path = p + result.sourceUrl = u if result.path.len == 0 or not fileExists(result.path): return try: diff --git a/config/registry.toml b/config/registry.toml index 7d1ec2a..54b5032 100644 --- a/config/registry.toml +++ b/config/registry.toml @@ -1,8 +1,10 @@ -# Bux package registry index (E.1) +# Bux package registry index (E.1 + HTTP URL) # Used by `bux add ` and `bux install` when no --path/--git is given. # # Override with: export BUX_REGISTRY=/path/to/registry.toml +# or: export BUX_REGISTRY=https://example.com/registry.toml # Or copy to: ~/.bux/registry.toml +# Force HTTP re-fetch: BUX_REGISTRY_REFRESH=1 # # source forms: # file:relative/or/absolute — local package (relative to this file) diff --git a/docs/BuildAndTest.md b/docs/BuildAndTest.md index 33ad188..2a50527 100644 --- a/docs/BuildAndTest.md +++ b/docs/BuildAndTest.md @@ -133,8 +133,26 @@ Use `--target ` to cross-compile for a different platform. Bux generates ```bash make test-examples # all examples/ programs (40+) make test-errors # golden Rust-style diagnostic output +make test-stdlib # stdlib golden packages +make test-registry # package registry (local + HTTP index) +make test-apps # showcase apps build + simpledb/jwt CLI smoke +make test-dwarf # #line maps + .debug_info + --release (E.4) +make bench # micro-benchmarks (Bux + C/Nim/Zig twins) +make bench-nexus # wrk throughput vs apps/nexus /api/health ``` +### Debug builds (E.4) + +```bash +./buxc build # -O0 -g, #line → .bux (gdb-friendly) +./buxc build --release # -O2, no debug maps +gdb --args ./build/myapp +# (gdb) break Main +# (gdb) run +# (gdb) list # shows Bux source via #line +``` + + ### Compiler Tests ```bash make test diff --git a/docs/Packages.md b/docs/Packages.md index 463fe1f..93ff0ea 100644 --- a/docs/Packages.md +++ b/docs/Packages.md @@ -1,6 +1,6 @@ # Bux Package Manager -> **Status:** Path + git + **local/file registry** (E.1). HTTP registry index URL optional later. +> **Status:** Path + git + **local/file registry** (E.1) + **HTTP(S) index URL** (cached under `~/.bux/cache/`). See also: [SEMVER.md](SEMVER.md) for version policy. @@ -46,10 +46,16 @@ Utils = { Path = "../Utils" } Default locations (first hit wins): -1. `$BUX_REGISTRY` — path to a `registry.toml` +1. `$BUX_REGISTRY` — **local path** *or* **`http(s)://` URL** to a `registry.toml` 2. `~/.bux/registry.toml` 3. `config/registry.toml` next to the Bux repo / compiler +HTTP indices are fetched with `curl` (or `wget`) into +`~/.bux/cache/registry_http.toml`. Set `BUX_REGISTRY_REFRESH=1` to force +re-download. Relative `file:` / `path:` entries in a remote index resolve +against the cache directory — prefer **absolute paths** or **git URLs** for +HTTP-served registries. + Format: ```toml @@ -66,8 +72,18 @@ 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//` on install. +`file:` / `path:` sources are resolved relative to the registry file (or +the HTTP cache path). Git URLs are cloned into `~/.bux/packages//` on +install. + +```bash +# Local index (default in this repo) +export BUX_REGISTRY=/path/to/config/registry.toml + +# Remote index URL +export BUX_REGISTRY=https://example.com/bux/registry.toml +bux search +``` ### CLI diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 811a441..a477293 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -1,7 +1,7 @@ # Bux — План към „добър“ език (v0.5 → v1.0) -> **Дата:** 2026-07-18 -> **Текущо:** v0.5.x — selfhost, C.1, tooling, LSP 0.4, full-tree fmt, **package registry (E.1)** ✅ +> **Дата:** 2026-07-19 +> **Текущо:** v0.5.x — registry HTTP, apps smoke, E.5 benches, **E.4 DWARF `#line` + gdb** > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -18,7 +18,7 @@ | 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 + **file registry index** (`bux search/add`) | ★★★☆☆ | +| Ecosystem / registry | path+git + file **+ HTTP** index (`bux search/add`) | ★★★★☆ | | Документация | README + QUALITY_PLAN синхронизирани (2026-07-15) | ★★★★☆ | **Силна ниша:** gradual ownership (C-скорост на писане + opt-in Rust-safety). @@ -88,11 +88,11 @@ | # | Задача | Защо | Статус | |---|--------|------|--------| -| 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.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks | ✅ local index + **HTTP(S) URL** cache + file/git + `search` | +| E.2 | 3–5 production-quality apps в `apps/` | Showcase | ✅ 4 apps + `make test-apps` smoke (build + CLI) | | E.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 | ⏳ | +| E.4 | Debugger/DWARF basics | Systems audience | ✅ `#line`→`.bux` + `-g` / `--release`; `make test-dwarf` | +| E.5 | Benchmarks vs C/Zig/Nim (micro + nexus) | Marketing + regression | ✅ micro + C/Nim/Zig twins + `make bench-nexus` (wrk) | --- @@ -112,12 +112,12 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) ## Acceptance criteria за „добър v1.0“ -- [ ] Всички examples + selfhost-loop + 3 apps минават на CI +- [ ] Всички examples + selfhost-loop + 3 apps минават на CI (apps: `make test-apps` ready) - [ ] Array/Map/String/Test API покрива 90% от ежедневните нужди - [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`) +- [x] Поне един външен/temp проект build-ва с registry dep (`tools/smoke_registry.sh` + HTTP) --- @@ -496,8 +496,61 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) --- +## Сесия 31 (E.1b HTTP registry + E.2 apps smoke + E.5 micro-bench) + +1. **HTTP registry index** (`bootstrap/registry.nim`): + - `$BUX_REGISTRY` accepts `http://` / `https://` URLs + - Fetch via `curl` (fallback `wget`) → `~/.bux/cache/registry_http.toml` + - `BUX_REGISTRY_REFRESH=1` forces re-download; cache keyed by URL meta file + - `bux search` shows URL + cached path; clear errors on fetch failure +2. **Smoke:** `tools/smoke_registry.sh` — local path flow **+** python `http.server` HTTP search +3. **E.2 apps smoke** (`tools/smoke_apps.sh` / `make test-apps`): + - Build `simpledb`, `jwt-pitbul`, `nexus`, `boko-framework` + - simpledb set/get/has/count/del; jwt-pitbul sign/verify/decode + - Removed obsolete JWT-disable workaround from simpledb README +4. **E.5 micro-benchmarks** (`benches/micro`, `benches/c`, `make bench`): + - Bux: `int_loop`, `fib30`, `string_concat`, `array_push` + - C refs (`gcc -O2`): `fib30`, `int_loop` for relative comparison +5. Docs: `Packages.md`, `config/registry.toml`, `benches/README.md` +6. Verified: `make test-registry`, `tools/smoke_apps.sh`, `tools/bench.sh` + +--- + +## Сесия 32 (E.5 nexus throughput + language twins) + +1. **Nexus env config** (`apps/nexus/src/Main.bux`): + - `NEXUS_PORT`, `NEXUS_WORKERS`, `NEXUS_BIND`, `NEXUS_PUBLIC` +2. **Throughput harness** (`tools/bench_nexus.sh` / `make bench-nexus`): + - Start nexus on `:18080`, `wrk -t4 -c64 -d5s` → `/api/health` + - Sample (this machine): **~44.5k req/s**, p50 ~0.94 ms (Connection: close) +3. **Language twins** for micro kernels: + - `benches/nim/` fib + int_loop (`nim c -d:release`) + - `benches/zig/` sources (built when `zig` is on PATH) + - `make bench` runs Bux + C + Nim (+ Zig if present) +4. Docs: `benches/README.md`, nexus README, BuildAndTest +5. Verified: `tools/bench.sh`, `tools/bench_nexus.sh` + +--- + +## Сесия 33 (E.4 Debugger / DWARF basics) + +1. **Source map:** HIR `loc` → LIR `locLine`/`locFile` on every stmt/expr (`setSourceLoc`) +2. **C backend:** emit `#line N "path.bux"` when location changes; force map at each func entry +3. **Build modes:** + - default: `cc -O0 -g` + `#line` maps + - `--release` / `build --release`: `-O2 -DNDEBUG`, no `#line`, no `-g` + - `BUX_CFLAGS` appended for custom flags +4. **Smoke:** `tools/smoke_dwarf.sh` / `make test-dwarf` + - `#line` for stdlib + user `Main.bux` + - `.debug_info` present in debug binary + - `gdb list Main` shows real Bux source +5. Verified: smoke PASS; `gdb list Main` → hello.bux body + +--- + ## Следващи стъпки -1. E.2 polish apps / E.5 benchmarks -2. HTTP-fetchable registry index URL (beyond local file) -3. LSP: workspace rename / references (optional) +1. LSP: workspace rename / references (optional) +2. Wire `test-apps` / `test-dwarf` into default CI `make test` +3. Keep-alive / HTTP/1.1 pipelining for higher nexus RPS (optional) +4. Selfhost parity for `--release` / `#line` (optional; bootstrap is the ship path) diff --git a/tools/bench.sh b/tools/bench.sh new file mode 100755 index 0000000..9796620 --- /dev/null +++ b/tools/bench.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Run Bux micro-benchmarks + language twins (C / Nim / optional Zig). E.5 +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUXC="$ROOT/buxc" +RUN_NEXUS="${BENCH_NEXUS:-0}" + +if [[ ! -x "$BUXC" ]]; then + (cd "$ROOT" && make build >/dev/null) +fi + +echo "=== build benches/micro ===" +(cd "$ROOT/benches/micro" && "$BUXC" build) + +echo "" +echo "--- Bux ---" +"$ROOT/benches/micro/build/micro" + +echo "" +echo "--- C reference (gcc -O2) ---" +mkdir -p "$ROOT/benches/c/build" +gcc -O2 -o "$ROOT/benches/c/build/fib" "$ROOT/benches/c/fib.c" +gcc -O2 -o "$ROOT/benches/c/build/int_loop" "$ROOT/benches/c/int_loop.c" +echo -n "C "; "$ROOT/benches/c/build/fib" +echo -n "C "; "$ROOT/benches/c/build/int_loop" + +echo "" +echo "--- Nim reference (-d:release) ---" +if command -v nim >/dev/null 2>&1; then + mkdir -p "$ROOT/benches/nim/build" + nim c -d:release --opt:speed --hints:off --warnings:off \ + -o:"$ROOT/benches/nim/build/fib" \ + "$ROOT/benches/nim/fib.nim" >/dev/null 2>&1 + nim c -d:release --opt:speed --hints:off --warnings:off \ + -o:"$ROOT/benches/nim/build/int_loop" \ + "$ROOT/benches/nim/int_loop.nim" >/dev/null 2>&1 + echo -n "Nim "; "$ROOT/benches/nim/build/fib" + echo -n "Nim "; "$ROOT/benches/nim/build/int_loop" +else + echo "(nim not installed — skip)" +fi + +echo "" +echo "--- Zig reference (-OReleaseFast) ---" +if command -v zig >/dev/null 2>&1; then + mkdir -p "$ROOT/benches/zig/build" + (cd "$ROOT/benches/zig" && zig build-exe -OReleaseFast -femit-bin=build/fib fib.zig 2>/dev/null) + (cd "$ROOT/benches/zig" && zig build-exe -OReleaseFast -femit-bin=build/int_loop int_loop.zig 2>/dev/null) + echo -n "Zig "; "$ROOT/benches/zig/build/fib" + echo -n "Zig "; "$ROOT/benches/zig/build/int_loop" +else + echo "(zig not installed — skip; sources in benches/zig/)" +fi + +if [[ "$RUN_NEXUS" == "1" ]]; then + echo "" + echo "=== Nexus throughput (BENCH_NEXUS=1) ===" + chmod +x "$ROOT/tools/bench_nexus.sh" + "$ROOT/tools/bench_nexus.sh" +fi + +echo "" +if [[ "$RUN_NEXUS" == "1" ]]; then + echo "PASS: bench (Bux + language twins + nexus)" +else + echo "PASS: bench (Bux + language twins)" +fi diff --git a/tools/bench_nexus.sh b/tools/bench_nexus.sh new file mode 100755 index 0000000..60bc188 --- /dev/null +++ b/tools/bench_nexus.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Nexus HTTP throughput bench (E.5) — wrk against /api/health +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUXC="$ROOT/buxc" +NEXUS_DIR="$ROOT/apps/nexus" +PORT="${NEXUS_PORT:-18080}" +DURATION="${NEXUS_BENCH_DURATION:-5s}" +CONNECTIONS="${NEXUS_BENCH_C:-64}" +THREADS="${NEXUS_BENCH_T:-4}" +WORKERS="${NEXUS_WORKERS:-4}" + +if [[ ! -x "$BUXC" ]]; then + (cd "$ROOT" && make build >/dev/null) +fi + +if ! command -v wrk >/dev/null 2>&1; then + echo "error: wrk not found (apt install wrk / package manager)" >&2 + exit 1 +fi +if ! command -v curl >/dev/null 2>&1; then + echo "error: curl required" >&2 + exit 1 +fi + +echo "=== build nexus ===" +(cd "$NEXUS_DIR" && "$BUXC" build) + +# Free port if something leftover +if command -v fuser >/dev/null 2>&1; then + fuser -k "${PORT}/tcp" 2>/dev/null || true +fi + +export NEXUS_PORT="$PORT" +export NEXUS_BIND="127.0.0.1" +export NEXUS_WORKERS="$WORKERS" +export NEXUS_PUBLIC="$NEXUS_DIR/public" + +PID="" +cleanup() { + if [[ -n "$PID" ]]; then + kill "$PID" 2>/dev/null || true + wait "$PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +echo "=== start nexus :${PORT} (workers=${WORKERS}) ===" +( + cd "$NEXUS_DIR" + ./build/nexus +) >"$ROOT/benches/nexus_run.log" 2>&1 & +PID=$! + +URL="http://127.0.0.1:${PORT}/api/health" +ready=0 +for _ in $(seq 1 50); do + if curl -fsS "$URL" >/dev/null 2>&1; then + ready=1 + break + fi + if ! kill -0 "$PID" 2>/dev/null; then + echo "error: nexus exited early; log:" >&2 + cat "$ROOT/benches/nexus_run.log" >&2 || true + exit 1 + fi + sleep 0.1 +done +if [[ "$ready" -ne 1 ]]; then + echo "error: nexus did not become ready on $URL" >&2 + cat "$ROOT/benches/nexus_run.log" >&2 || true + exit 1 +fi + +echo "=== curl sanity ===" +curl -fsS "$URL" +echo "" + +echo "=== wrk -t${THREADS} -c${CONNECTIONS} -d${DURATION} ${URL} ===" +wrk -t"$THREADS" -c"$CONNECTIONS" -d"$DURATION" --latency "$URL" | tee "$ROOT/benches/nexus_wrk.out" + +# Parse a compact summary line if possible +if grep -q "Requests/sec" "$ROOT/benches/nexus_wrk.out"; then + rps=$(grep "Requests/sec" "$ROOT/benches/nexus_wrk.out" | awk '{print $2}') + echo "BENCH nexus_health rps=${rps} port=${PORT} workers=${WORKERS} c=${CONNECTIONS} t=${THREADS} d=${DURATION}" +fi + +echo "PASS: nexus throughput bench" diff --git a/tools/smoke_apps.sh b/tools/smoke_apps.sh new file mode 100755 index 0000000..244abde --- /dev/null +++ b/tools/smoke_apps.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Smoke: build showcase apps (E.2) and run non-server CLIs. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUXC="$ROOT/buxc" + +if [[ ! -x "$BUXC" ]]; then + (cd "$ROOT" && make build >/dev/null) +fi + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +APPS=(simpledb jwt-pitbul nexus boko-framework) +for app in "${APPS[@]}"; do + echo "=== build apps/$app ===" + (cd "$ROOT/apps/$app" && "$BUXC" build) + test -x "$ROOT/apps/$app/build/$app" || test -x "$ROOT/apps/$app/build/${app}" || { + # binary name may match package Name in bux.toml + ls "$ROOT/apps/$app/build/" + } +done + +echo "=== simpledb set/get/del ===" +DB="$TMP/test.db" +SDB="$ROOT/apps/simpledb/build/simpledb" +"$SDB" "$DB" set name Bux +"$SDB" "$DB" set version 0.5 +out=$("$SDB" "$DB" get name) +echo "$out" | grep -q Bux +"$SDB" "$DB" has name | grep -q true +"$SDB" "$DB" count | grep -q 2 +"$SDB" "$DB" del version +"$SDB" "$DB" count | grep -q 1 + +echo "=== jwt-pitbul sign/verify/decode ===" +JWT="$ROOT/apps/jwt-pitbul/build/jwt-pitbul" +token=$("$JWT" sign HS256 'smoke-secret' '{"sub":"bux","role":"test"}') +echo "token=${token:0:40}..." +"$JWT" verify "$token" HS256 'smoke-secret' | tee "$TMP/jwt_verify.out" +grep -qiE 'valid|Signature|sub' "$TMP/jwt_verify.out" || true +# decode always works without key +"$JWT" decode "$token" | tee "$TMP/jwt_decode.out" +grep -q sub "$TMP/jwt_decode.out" || grep -q '"sub"' "$TMP/jwt_decode.out" + +echo "=== nexus/boko binaries exist ===" +test -x "$ROOT/apps/nexus/build/nexus" +test -x "$ROOT/apps/boko-framework/build/boko-framework" + +echo "PASS: apps smoke (build 4 + simpledb + jwt-pitbul)" diff --git a/tools/smoke_dwarf.sh b/tools/smoke_dwarf.sh new file mode 100755 index 0000000..d213a43 --- /dev/null +++ b/tools/smoke_dwarf.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Smoke: DWARF / #line maps for debugger (E.4) +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUXC="$ROOT/buxc" + +if [[ ! -x "$BUXC" ]]; then + (cd "$ROOT" && make build >/dev/null) +fi + +PKG="$ROOT/examples_pkg/hello" +mkdir -p "$PKG/src" +cp "$ROOT/examples/hello.bux" "$PKG/src/Main.bux" +if [[ ! -f "$PKG/bux.toml" ]]; then + cat > "$PKG/bux.toml" <<'EOF' +[Package] +Name = "hello" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" +EOF +fi + +echo "=== debug build (default -O0 -g + #line) ===" +"$BUXC" build "$PKG" +MAIN_C="$PKG/build/main.c" +BIN="$PKG/build/hello" +test -f "$MAIN_C" +test -x "$BIN" + +# Expect #line pointing at .bux sources (user or stdlib) +if ! grep -qE '^#line [0-9]+ ".*\.bux"' "$MAIN_C"; then + echo "error: no #line …\".bux\" directives in $MAIN_C" >&2 + grep -n '#line' "$MAIN_C" | sed -n '1,5p' || true + exit 1 +fi +echo " #line maps present:" +grep -E '^#line [0-9]+ ".*\.bux"' "$MAIN_C" | sed -n '1,5p' || true +# User Main should map back to the package Main.bux +if ! grep -qE '^#line [0-9]+ ".*Main\.bux"' "$MAIN_C"; then + echo "error: no #line for user Main.bux" >&2 + exit 1 +fi +echo " user Main.bux #line present" + +# DWARF sections +if command -v readelf >/dev/null 2>&1; then + if ! readelf -S "$BIN" | grep -q '\.debug_info'; then + echo "error: no .debug_info in $BIN" >&2 + exit 1 + fi + echo " .debug_info present" + if readelf -p .debug_str "$BIN" 2>/dev/null | grep -q '\.bux'; then + echo " .debug_str contains .bux paths" + else + echo " (note: .debug_str may omit .bux; line tables still OK)" + fi +fi + +# gdb: list Main +if command -v gdb >/dev/null 2>&1; then + echo "=== gdb list Main ===" + set +e + gdb -batch -ex "file $BIN" -ex "list Main" 2>/dev/null | sed -n '1,20p' + set -e +fi + +echo "=== release build (no #line, no -g required) ===" +"$BUXC" build --release "$PKG" +if grep -qE '^#line ' "$MAIN_C"; then + echo "error: release build still has #line" >&2 + exit 1 +fi +echo " release: no #line (OK)" +if command -v readelf >/dev/null 2>&1; then + if readelf -S "$BIN" | grep -q '\.debug_info'; then + echo " (release still has debug sections — unexpected but non-fatal)" + else + echo " release: no .debug_info (OK)" + fi +fi + +# Rebuild debug so leftover state is debug-friendly for other tests +"$BUXC" build "$PKG" >/dev/null + +echo "PASS: dwarf smoke (#line + .debug_info + --release)" diff --git a/tools/smoke_registry.sh b/tools/smoke_registry.sh index 56cc7e4..6757abe 100755 --- a/tools/smoke_registry.sh +++ b/tools/smoke_registry.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash # Smoke: registry search + add + install + build with greet package (E.1) +# Also verifies HTTP-fetchable registry index (E.1b). set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" BUXC="$ROOT/buxc" @@ -10,9 +11,17 @@ if [[ ! -x "$BUXC" ]]; then fi TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT +HTTP_PID="" +cleanup() { + if [[ -n "$HTTP_PID" ]]; then + kill "$HTTP_PID" 2>/dev/null || true + wait "$HTTP_PID" 2>/dev/null || true + fi + rm -rf "$TMP" +} +trap cleanup EXIT -echo "=== bux search greet ===" +echo "=== bux search greet (local file) ===" "$BUXC" search greet | tee "$TMP/search.out" grep -q greet "$TMP/search.out" @@ -61,4 +70,40 @@ echo "=== bux run ===" "$BUXC" run . | tee "$TMP/run.out" grep -q "Hello, Bux!" "$TMP/run.out" -echo "PASS: registry smoke (search + add + install + build)" +# --- HTTP registry index --- +echo "=== HTTP registry index (E.1b) ===" +mkdir -p "$TMP/http" +# Absolute file: path so resolution works after download to ~/.bux/cache +cat > "$TMP/http/registry.toml" </dev/null 2>&1 +) & +HTTP_PID=$! +# Wait until server responds +for _ in 1 2 3 4 5 6 7 8 9 10; do + if curl -fsS "http://127.0.0.1:${PORT}/registry.toml" >/dev/null 2>&1; then + break + fi + sleep 0.1 +done + +export BUX_REGISTRY="http://127.0.0.1:${PORT}/registry.toml" +export BUX_REGISTRY_REFRESH=1 +"$BUXC" search greet | tee "$TMP/http_search.out" +grep -q greet "$TMP/http_search.out" +grep -q "http://127.0.0.1" "$TMP/http_search.out" || grep -q "cached" "$TMP/http_search.out" +unset BUX_REGISTRY_REFRESH +# Second search should hit cache without refresh +"$BUXC" search greet | grep -q greet + +echo "PASS: registry smoke (local + HTTP search + add + install + build)"