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:
+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:
|
||||
|
||||
Reference in New Issue
Block a user