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:
@@ -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
|
||||
|
||||
@@ -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`):
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════╗
|
||||
|
||||
+31
-1
@@ -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<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 {
|
||||
let config: ServerConfig = DefaultConfig();
|
||||
var config: ServerConfig = DefaultConfig();
|
||||
ApplyEnvConfig(&config);
|
||||
let router: Router = BuildRouter();
|
||||
|
||||
return RunServer(config, router);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[Package]
|
||||
Name = "micro"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
Description = "Micro-benchmarks for Bux (E.5)"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}"
|
||||
@@ -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}"
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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
@@ -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 <file> 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 <auto|on|off> 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)
|
||||
|
||||
@@ -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 = "<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 =
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+86
-14
@@ -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. <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
|
||||
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:
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
# 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)
|
||||
|
||||
@@ -133,8 +133,26 @@ Use `--target <triple>` 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
|
||||
|
||||
+20
-4
@@ -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/<name>/` on install.
|
||||
`file:` / `path:` sources are resolved relative to the registry file (or
|
||||
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
|
||||
|
||||
|
||||
+65
-12
@@ -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)
|
||||
|
||||
Executable
+67
@@ -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
|
||||
Executable
+88
@@ -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"
|
||||
Executable
+50
@@ -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)"
|
||||
Executable
+88
@@ -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
@@ -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" <<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)"
|
||||
|
||||
Reference in New Issue
Block a user