feat: HTTP registry, app/bench harnesses, and DWARF #line maps

Ship QUALITY_PLAN sessions 31–33: fetchable package index URLs, showcase
app and micro/nexus benchmarks, and debugger-friendly C codegen.

- Registry: BUX_REGISTRY accepts http(s) URLs (curl/wget → ~/.bux/cache)
- E.2: make test-apps smoke for nexus/boko/simpledb/jwt-pitbul
- E.5: benches/micro + C/Nim/Zig twins; make bench-nexus (wrk)
- E.4: HIR locs → #line .bux; default -O0 -g; --release -O2; make test-dwarf
This commit is contained in:
2026-07-19 22:46:45 +03:00
parent 53b43b0f79
commit eb81856565
26 changed files with 983 additions and 52 deletions
+28 -2
View File
@@ -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 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 all: build
@@ -193,6 +193,32 @@ test-lsp: lsp
.PHONY: test-registry .PHONY: test-registry
test-registry: build test-registry: build
@echo "=== Registry smoke (E.1) ===" @echo "=== Registry smoke (E.1 + HTTP) ==="
@chmod +x tools/smoke_registry.sh @chmod +x tools/smoke_registry.sh
@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
+4 -1
View File
@@ -32,9 +32,12 @@ cd apps/nexus
# Run # Run
./nexus ./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`):
``` ```
╔══════════════════════════════════════════════╗ ╔══════════════════════════════════════════════╗
+31 -1
View File
@@ -5,6 +5,8 @@ module Main {
import Router::{Handler, Route, Router}; import Router::{Handler, Route, Router};
import Server::{RunServer}; import Server::{RunServer};
import Std::Array::{Array, Array_New, Array_Push}; import Std::Array::{Array, Array_New, Array_Push};
import Std::Os::{Os_GetEnv};
import Std::String::{String_Len, String_ToInt};
func BuildRouter() -> Router { func BuildRouter() -> Router {
var routes: Array<Route> = Array_New<Route>(8); var routes: Array<Route> = Array_New<Route>(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 { func Main() -> int {
let config: ServerConfig = DefaultConfig(); var config: ServerConfig = DefaultConfig();
ApplyEnvConfig(&config);
let router: Router = BuildRouter(); let router: Router = BuildRouter();
return RunServer(config, router); return RunServer(config, router);
+2 -3
View File
@@ -50,10 +50,9 @@ Data is stored as plain text, one `key=value` per line. The file is created auto
## Build ## Build
```sh ```sh
# workaround: disable broken JWT module cd apps/simpledb
mv ../../lib/crypto/jwt.bux ../../lib/crypto/jwt.bux.bak
../../buxc build ../../buxc build
mv ../../lib/crypto/jwt.bux.bak ../../lib/crypto/jwt.bux ./build/simpledb data.db set hello world
``` ```
## API ## API
+56
View File
@@ -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 <name> 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).
+26
View File
@@ -0,0 +1,26 @@
/* C reference: recursive fib(30) — compare with benches/micro fib30 */
#include <stdio.h>
#include <stdint.h>
#include <time.h>
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;
}
+24
View File
@@ -0,0 +1,24 @@
/* C reference: 1e6 integer arithmetic loop */
#include <stdio.h>
#include <stdint.h>
#include <time.h>
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;
}
+8
View File
@@ -0,0 +1,8 @@
[Package]
Name = "micro"
Version = "0.1.0"
Type = "bin"
Description = "Micro-benchmarks for Bux (E.5)"
[Build]
Output = "Bin"
+104
View File
@@ -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<int> = Array_New<int>(16);
var i: int = 0;
let t0: int64 = Time_NowUs();
while i < iters {
Array_Push<int>(&arr, i);
i = i + 1;
}
let t1: int64 = Time_NowUs();
let n: uint = Array_Len<int>(&arr);
if n as int != iters {
Print("array len mismatch: ");
PrintLine(String_FromInt(n as int64));
}
discard Array_Get<int>(&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;
}
+16
View File
@@ -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}"
+15
View File
@@ -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}"
+22
View File
@@ -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 });
}
+22
View File
@@ -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) });
}
+47 -10
View File
@@ -15,6 +15,7 @@ type
color*: ColorMode color*: ColorMode
quiet*: bool quiet*: bool
verbose*: bool verbose*: bool
release*: bool ## --release: -O2, no -g / no #line (E.4 dual)
proc printUsage*() = proc printUsage*() =
echo """Bux Programming Language (bootstrap compiler) echo """Bux Programming Language (bootstrap compiler)
@@ -42,15 +43,22 @@ Command options:
fmt --check Exit 1 if any file would be reformatted (CI) fmt --check Exit 1 if any file would be reformatted (CI)
doc --out <file> Write docs to file (default: stdout) doc --out <file> Write docs to file (default: stdout)
add --path / --git Explicit source; else resolve via registry 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: Global options:
--color <auto|on|off> Control colored output (default: auto) --color <auto|on|off> Control colored output (default: auto)
-q, --quiet Suppress non-error output -q, --quiet Suppress non-error output
-v, --verbose Verbose 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] = 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.rest = @[]
result.ok = true result.ok = true
var i = 0 var i = 0
@@ -74,6 +82,8 @@ proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq
result.opts.quiet = true result.opts.quiet = true
elif arg == "-v" or arg == "--verbose": elif arg == "-v" or arg == "--verbose":
result.opts.verbose = true result.opts.verbose = true
elif arg == "--release":
result.opts.release = true
else: else:
result.rest.add(arg) result.rest.add(arg)
inc i inc i
@@ -411,10 +421,14 @@ proc cmdAdd*(args: seq[string], opts: GlobalOptions): int =
elif gitUrl.len > 0: elif gitUrl.len > 0:
depLine = &"{depName} = {{ Version = \"{version}\", Source = \"{gitUrl}\" }}" depLine = &"{depName} = {{ Version = \"{version}\", Source = \"{gitUrl}\" }}"
else: else:
# Registry resolve (E.1) # Registry resolve (E.1 + HTTP URL)
let reg = loadRegistry() let reg = loadRegistry()
if reg.path.len == 0: 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 return 1
let pkg = registryLookup(reg, depName, version) let pkg = registryLookup(reg, depName, version)
if pkg.name.len == 0: if pkg.name.len == 0:
@@ -423,7 +437,8 @@ proc cmdAdd*(args: seq[string], opts: GlobalOptions): int =
return 1 return 1
depLine = formatRegistryDepLine(depName, pkg) depLine = formatRegistryDepLine(depName, pkg)
if not opts.quiet: 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) var content = readFile(manifestPath)
# Ensure [Dependencies] section exists # Ensure [Dependencies] section exists
if content.find("[Dependencies]") < 0: 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 query = if args.len > 0: args[0] else: ""
let reg = loadRegistry() let reg = loadRegistry()
if reg.path.len == 0: 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 return 1
if not opts.quiet: 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) let hits = registrySearch(reg, query)
if hits.len == 0: if hits.len == 0:
if not opts.quiet: if not opts.quiet:
@@ -717,8 +740,18 @@ proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl] =
result.add(d) result.add(d)
proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = 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 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) let (pctx, status) = prepareProject(root, useColor, opts)
if status != 0: if status != 0:
return status return status
@@ -739,7 +772,8 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
let hirMod = lowerModule(unifiedModule, semaCtx) let hirMod = lowerModule(unifiedModule, semaCtx)
let lirBuilder = lowerModuleToLir(hirMod) 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) var allCCode = lirCbe.emitModule(lirBuilder, hirMod)
# Write C file # Write C file
@@ -769,10 +803,13 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
printError("io.c not found in rt/", useColor) printError("io.c not found in rt/", useColor)
return 1 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 outputName = if pctx.man.name != "": pctx.man.name else: "bux_out"
let outputFile = buildDir / outputName 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: if opts.verbose:
printInfo(&"running: {ccCmd}", useColor) printInfo(&"running: {ccCmd}", useColor)
let (output, exitCode) = execCmdEx(ccCmd) let (output, exitCode) = execCmdEx(ccCmd)
+34 -1
View File
@@ -10,11 +10,17 @@ type
output*: string output*: string
indent*: int indent*: int
tempTypes*: Table[string, string] ## Track C types of temp variables 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( result = LirCBackend(
indent: 0, indent: 0,
tempTypes: initTable[string, string](), tempTypes: initTable[string, string](),
emitDebugLines: emitDebugLines,
lastDebugLine: 0,
lastDebugFile: "",
) )
proc emitIndent(be: var LirCBackend) = proc emitIndent(be: var LirCBackend) =
@@ -26,6 +32,22 @@ proc emitLine(be: var LirCBackend, s: string) =
be.output.add(s) be.output.add(s)
be.output.add("\n") 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 = "<bux>"
# 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 = proc valToC(be: var LirCBackend, v: LirValue): string =
## Convert a LirValue to its C representation. ## Convert a LirValue to its C representation.
case v.kind case v.kind
@@ -50,6 +72,7 @@ proc cParamDecl(cType, name: string): string =
# ── Per-instruction emission ── # ── Per-instruction emission ──
proc emitInstr(be: var LirCBackend, instr: LirInstr) = proc emitInstr(be: var LirCBackend, instr: LirInstr) =
be.emitDebugLine(instr)
template v(x: LirValue): string = valToC(be, x) template v(x: LirValue): string = valToC(be, x)
case instr.kind case instr.kind
@@ -246,6 +269,16 @@ proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, strin
if f.params.len == 0: if f.params.len == 0:
paramsStr = "void" 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.emitLine(&"{f.retType} {f.name}({paramsStr}) {{")
be.indent += 1 be.indent += 1
+11
View File
@@ -191,10 +191,20 @@ proc cmpOpToLir(op: TokenKind): LirKind =
proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue
proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) 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 ── # ── Lowering: Expressions → LirValue ──
proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue = proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
if node == nil: return lirInt(0) if node == nil: return lirInt(0)
setSourceLoc(ctx, node)
template b: var LirBuilder = ctx.builder template b: var LirBuilder = ctx.builder
case node.kind case node.kind
@@ -623,6 +633,7 @@ proc buildLval(ctx: var LowerToLirCtx, n: HirNode): string =
proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) = proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
if node == nil: return if node == nil: return
setSourceLoc(ctx, node)
template b: var LirBuilder = ctx.builder template b: var LirBuilder = ctx.builder
case node.kind case node.kind
+86 -14
View File
@@ -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): ## Index format (TOML-ish, one package per [[package]] table):
## ##
## [[package]] ## [[package]]
## name = "greet" ## name = "greet"
## version = "0.1.0" ## 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" ## description = "Hello helpers"
## ##
## [[package]] ## [[package]]
@@ -13,12 +13,16 @@
## version = "1.2.0" ## version = "1.2.0"
## source = "https://github.com/bux-lang/net.git" ## source = "https://github.com/bux-lang/net.git"
## ##
## Lookup order for the index file: ## Lookup order for the index:
## 1. $BUX_REGISTRY (file path) ## 1. $BUX_REGISTRY — local file path **or** http(s):// URL
## 2. ~/.bux/registry.toml ## 2. ~/.bux/registry.toml
## 3. <repo>/config/registry.toml next to the compiler / cwd ## 3. <repo>/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 type
RegistryPackage* = object RegistryPackage* = object
@@ -29,7 +33,8 @@ type
resolvedPath*: string ## absolute path for file: sources (filled on load) resolvedPath*: string ## absolute path for file: sources (filled on load)
Registry* = object 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] packages*: seq[RegistryPackage]
proc resolvePackageSource(pkg: var RegistryPackage, indexDir: string) = proc resolvePackageSource(pkg: var RegistryPackage, indexDir: string) =
@@ -83,14 +88,70 @@ proc parseRegistryToml(content, indexPath: string): seq[RegistryPackage] =
resolvePackageSource(cur, indexDir) resolvePackageSource(cur, indexDir)
result.add(cur) result.add(cur)
proc findRegistryIndex*(): string = proc isHttpUrl*(s: string): bool =
## Locate the registry index file. 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") let env = getEnv("BUX_REGISTRY")
if env.len > 0 and fileExists(env): if env.len > 0:
return env.absolutePath 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" let homeIdx = getHomeDir() / ".bux" / "registry.toml"
if fileExists(homeIdx): if fileExists(homeIdx):
return homeIdx return (homeIdx, "")
let candidates = @[ let candidates = @[
getAppDir() / ".." / "config" / "registry.toml", getAppDir() / ".." / "config" / "registry.toml",
getAppDir() / "config" / "registry.toml", getAppDir() / "config" / "registry.toml",
@@ -99,12 +160,23 @@ proc findRegistryIndex*(): string =
] ]
for c in candidates: for c in candidates:
if fileExists(c): if fileExists(c):
return c.absolutePath return (c.absolutePath, "")
return "" return ("", "")
proc loadRegistry*(path: string = ""): Registry = proc loadRegistry*(path: string = ""): Registry =
result.path = if path.len > 0: path else: findRegistryIndex() result.path = ""
result.sourceUrl = ""
result.packages = @[] 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): if result.path.len == 0 or not fileExists(result.path):
return return
try: try:
+3 -1
View File
@@ -1,8 +1,10 @@
# Bux package registry index (E.1) # Bux package registry index (E.1 + HTTP URL)
# Used by `bux add <name>` and `bux install` when no --path/--git is given. # Used by `bux add <name>` and `bux install` when no --path/--git is given.
# #
# Override with: export BUX_REGISTRY=/path/to/registry.toml # Override with: export BUX_REGISTRY=/path/to/registry.toml
# or: export BUX_REGISTRY=https://example.com/registry.toml
# Or copy to: ~/.bux/registry.toml # Or copy to: ~/.bux/registry.toml
# Force HTTP re-fetch: BUX_REGISTRY_REFRESH=1
# #
# source forms: # source forms:
# file:relative/or/absolute — local package (relative to this file) # file:relative/or/absolute — local package (relative to this file)
+18
View File
@@ -133,8 +133,26 @@ Use `--target <triple>` to cross-compile for a different platform. Bux generates
```bash ```bash
make test-examples # all examples/ programs (40+) make test-examples # all examples/ programs (40+)
make test-errors # golden Rust-style diagnostic output 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 ### Compiler Tests
```bash ```bash
make test make test
+20 -4
View File
@@ -1,6 +1,6 @@
# Bux Package Manager # 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. See also: [SEMVER.md](SEMVER.md) for version policy.
@@ -46,10 +46,16 @@ Utils = { Path = "../Utils" }
Default locations (first hit wins): 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` 2. `~/.bux/registry.toml`
3. `config/registry.toml` next to the Bux repo / compiler 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: Format:
```toml ```toml
@@ -66,8 +72,18 @@ source = "https://github.com/example/bux-net.git"
description = "TCP helpers" description = "TCP helpers"
``` ```
`file:` / `path:` sources are resolved relative to the registry file. `file:` / `path:` sources are resolved relative to the registry file (or
Git URLs are cloned into `~/.bux/packages/<name>/` on install. the HTTP cache path). Git URLs are cloned into `~/.bux/packages/<name>/` 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 ### CLI
+65 -12
View File
@@ -1,7 +1,7 @@
# Bux — План към „добър“ език (v0.5 → v1.0) # Bux — План към „добър“ език (v0.5 → v1.0)
> **Дата:** 2026-07-18 > **Дата:** 2026-07-19
> **Текущо:** v0.5.x — selfhost, C.1, tooling, LSP 0.4, full-tree fmt, **package registry (E.1)** ✅ > **Текущо:** v0.5.x — registry HTTP, apps smoke, E.5 benches, **E.4 DWARF `#line` + gdb**
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
--- ---
@@ -18,7 +18,7 @@
| Concurrency | M:N tasks + channels + async | ★★★★☆ | | Concurrency | M:N tasks + channels + async | ★★★★☆ |
| Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ | | Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ |
| Tooling | `test-errors`, LSP diagnostics + hover/def/outline | ★★★★☆ | | Tooling | `test-errors`, LSP diagnostics + hover/def/outline | ★★★★☆ |
| Ecosystem / registry | path+git + **file registry index** (`bux search/add`) | ★★★☆ | | Ecosystem / registry | path+git + file **+ HTTP** index (`bux search/add`) | ★★★☆ |
| Документация | README + QUALITY_PLAN синхронизирани (2026-07-15) | ★★★★☆ | | Документация | README + QUALITY_PLAN синхронизирани (2026-07-15) | ★★★★☆ |
**Силна ниша:** gradual ownership (C-скорост на писане + opt-in Rust-safety). **Силна ниша:** gradual ownership (C-скорост на писане + opt-in Rust-safety).
@@ -88,11 +88,11 @@
| # | Задача | Защо | Статус | | # | Задача | Защо | Статус |
|---|--------|------|--------| |---|--------|------|--------|
| E.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks | ✅ local index + file/git sources + `search` | | E.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks | ✅ local index + **HTTP(S) URL** cache + file/git + `search` |
| E.2 | 35 production-quality apps в `apps/` | Showcase | ⏳ partial (`nexus`, `boko`, `simpledb`, `jwt-pitbul`) | | E.2 | 35 production-quality apps в `apps/` | Showcase | ✅ 4 apps + `make test-apps` smoke (build + CLI) |
| E.3 | Language freeze + semver policy | Trust | ✅ draft `docs/SEMVER.md` | | E.3 | Language freeze + semver policy | Trust | ✅ draft `docs/SEMVER.md` |
| E.4 | Debugger/DWARF basics | Systems audience | | | 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 | | | 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“ ## 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% от ежедневните нужди - [ ] Array/Map/String/Test API покрива 90% от ежедневните нужди
- [x] `@[Checked]` хваща use-after-move + double `&mut` + dangling return / elision fail - [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] `bux test` + `bux fmt` + `bux check` са default developer loop (`--filter` / `--check` shipped)
- [x] LanguageRef синхронизиран с компилатора (incl. C.1 elision) - [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.94ms (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 1. LSP: workspace rename / references (optional)
2. HTTP-fetchable registry index URL (beyond local file) 2. Wire `test-apps` / `test-dwarf` into default CI `make test`
3. LSP: workspace rename / references (optional) 3. Keep-alive / HTTP/1.1 pipelining for higher nexus RPS (optional)
4. Selfhost parity for `--release` / `#line` (optional; bootstrap is the ship path)
Executable
+67
View File
@@ -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
+88
View File
@@ -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"
+50
View File
@@ -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)"
+88
View File
@@ -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)"
+48 -3
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Smoke: registry search + add + install + build with greet package (E.1) # Smoke: registry search + add + install + build with greet package (E.1)
# Also verifies HTTP-fetchable registry index (E.1b).
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BUXC="$ROOT/buxc" BUXC="$ROOT/buxc"
@@ -10,9 +11,17 @@ if [[ ! -x "$BUXC" ]]; then
fi fi
TMP=$(mktemp -d) 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" "$BUXC" search greet | tee "$TMP/search.out"
grep -q greet "$TMP/search.out" grep -q greet "$TMP/search.out"
@@ -61,4 +70,40 @@ echo "=== bux run ==="
"$BUXC" run . | tee "$TMP/run.out" "$BUXC" run . | tee "$TMP/run.out"
grep -q "Hello, Bux!" "$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" <<EOF
[[package]]
name = "greet"
version = "0.1.1"
source = "file:$ROOT/registry/packages/greet"
description = "HTTP-served greet package"
EOF
# Free port via python
PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')
(
cd "$TMP/http"
python3 -m http.server "$PORT" --bind 127.0.0.1 >/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)"