feat: Linux/cloud platform stack (TLS, registry, static/cross, selfhost PM)
ci / build (ubuntu) (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
ci / build (ubuntu) (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
Ship the QUALITY_PLAN platform focus: thin/minimal runtime, --static/--target, Nexus HTTPS/mTLS with graceful stop, lock checksums + install --locked, selfhost registry (search/add/HTTP), containers, and CI smokes for cloud path.
This commit is contained in:
+222
-27
@@ -1,4 +1,4 @@
|
||||
import std/[os, strutils, terminal, strformat, osproc, sets, algorithm, tables]
|
||||
import std/[os, strutils, terminal, strformat, osproc, sets, algorithm, tables, sha1]
|
||||
import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend
|
||||
import source_location
|
||||
import fmt
|
||||
@@ -12,11 +12,19 @@ type
|
||||
cmOn
|
||||
cmOff
|
||||
|
||||
## Which C runtime shim to link (session 75 — Linux / cloud / embedded).
|
||||
RuntimeFlavor* = enum
|
||||
rfFull ## rt/runtime.c — POSIX + OpenSSL
|
||||
rfMinimal ## rt/runtime_minimal.c — thin, static/container/embed friendly
|
||||
rfWin ## rt/runtime_win.c — Windows/MinGW (historical)
|
||||
|
||||
GlobalOptions* = object
|
||||
color*: ColorMode
|
||||
quiet*: bool
|
||||
verbose*: bool
|
||||
release*: bool ## --release: -O2, no -g / no #line (E.4 dual)
|
||||
release*: bool ## --release: -O2, no -g / no #line (E.4 dual)
|
||||
staticLink*: bool ## --static: fully-static binary (implies thin runtime unless full)
|
||||
target*: string ## --target <triple>: cross-compile (e.g. aarch64-linux-gnu)
|
||||
|
||||
proc printUsage*() =
|
||||
echo """Bux Programming Language (bootstrap compiler)
|
||||
@@ -44,22 +52,32 @@ 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
|
||||
install --locked Verify bux.lock only (CI; no re-resolve)
|
||||
build --release Optimized build (-O2, no debug / #line)
|
||||
build --static Fully-static link (uses minimal runtime; no OpenSSL)
|
||||
build --target T Cross-compile triple (prefers T-gcc, else clang -target)
|
||||
|
||||
Registry:
|
||||
Registry / toolchain env:
|
||||
BUX_REGISTRY Local path or http(s):// URL to registry.toml
|
||||
BUX_REGISTRY_REFRESH=1 Force re-download of HTTP index cache
|
||||
BUX_REGISTRY_INSECURE=1 Allow self-signed HTTPS registry (dev/smoke)
|
||||
BUX_CFLAGS Extra flags appended to the C compiler line
|
||||
BUX_CC C compiler binary (overrides --target pick)
|
||||
BUX_RUNTIME full|minimal|thin|embed|win (default: full on Unix)
|
||||
BUX_STATIC=1 Same as --static
|
||||
|
||||
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
|
||||
--static Fully-static link + thin runtime (containers / distroless)
|
||||
--target <triple> Cross-compile (e.g. aarch64-linux-gnu)
|
||||
"""
|
||||
|
||||
proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq[string], ok: bool] =
|
||||
result.opts = GlobalOptions(color: cmAuto, quiet: false, verbose: false, release: false)
|
||||
result.opts = GlobalOptions(color: cmAuto, quiet: false, verbose: false,
|
||||
release: false, staticLink: false, target: "")
|
||||
result.rest = @[]
|
||||
result.ok = true
|
||||
var i = 0
|
||||
@@ -85,6 +103,17 @@ proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq
|
||||
result.opts.verbose = true
|
||||
elif arg == "--release":
|
||||
result.opts.release = true
|
||||
elif arg == "--static":
|
||||
result.opts.staticLink = true
|
||||
elif arg == "--target":
|
||||
if i + 1 >= args.len:
|
||||
stderr.writeLine("error: --target requires a triple (e.g. aarch64-linux-gnu)")
|
||||
result.ok = false
|
||||
return
|
||||
inc i
|
||||
result.opts.target = args[i]
|
||||
elif arg.startsWith("--target="):
|
||||
result.opts.target = arg["--target=".len .. ^1]
|
||||
else:
|
||||
result.rest.add(arg)
|
||||
inc i
|
||||
@@ -95,6 +124,80 @@ proc shouldUseColor(opts: GlobalOptions): bool =
|
||||
of cmOff: false
|
||||
of cmAuto: terminal.isatty(stdout)
|
||||
|
||||
proc wantStaticLink(opts: GlobalOptions): bool =
|
||||
## --static or BUX_STATIC=1
|
||||
if opts.staticLink: return true
|
||||
let e = getEnv("BUX_STATIC")
|
||||
result = e == "1" or e.toLowerAscii() in ["true", "yes", "on"]
|
||||
|
||||
proc resolveRuntimeFlavor(opts: GlobalOptions): RuntimeFlavor =
|
||||
## Linux/cloud/embed first. Windows is not a product target (rfWin historical).
|
||||
let e = getEnv("BUX_RUNTIME").toLowerAscii()
|
||||
case e
|
||||
of "full", "posix":
|
||||
return rfFull
|
||||
of "minimal", "thin", "embed", "embedded", "freestanding":
|
||||
return rfMinimal
|
||||
of "win", "windows":
|
||||
return rfWin
|
||||
of "":
|
||||
discard
|
||||
else:
|
||||
# Unknown value → fall through to defaults
|
||||
discard
|
||||
when defined(windows):
|
||||
return rfWin
|
||||
# Fully-static containers: OpenSSL static is painful → thin runtime default
|
||||
if wantStaticLink(opts):
|
||||
return rfMinimal
|
||||
# Cross without explicit full: prefer thin (host may lack target libcrypto)
|
||||
if opts.target.len > 0:
|
||||
return rfMinimal
|
||||
return rfFull
|
||||
|
||||
proc runtimeFileName(flavor: RuntimeFlavor): string =
|
||||
case flavor
|
||||
of rfFull: "runtime.c"
|
||||
of rfMinimal: "runtime_minimal.c"
|
||||
of rfWin: "runtime_win.c"
|
||||
|
||||
proc isThinRuntime(flavor: RuntimeFlavor): bool =
|
||||
flavor in {rfMinimal, rfWin}
|
||||
|
||||
proc findOnPath(bin: string): bool =
|
||||
## True if `bin` resolves as an executable on PATH (or is an absolute path).
|
||||
if bin.len == 0: return false
|
||||
if '/' in bin or '\\' in bin:
|
||||
return fileExists(bin)
|
||||
let (outp, code) = execCmdEx(&"command -v {quoteShell(bin)} 2>/dev/null")
|
||||
result = code == 0 and outp.strip().len > 0
|
||||
|
||||
proc resolveCCompiler(opts: GlobalOptions): string =
|
||||
## Prefer BUX_CC, then <triple>-gcc for --target, then clang -target, else host cc.
|
||||
let envCc = getEnv("BUX_CC")
|
||||
if envCc.len > 0:
|
||||
return envCc
|
||||
if opts.target.len > 0:
|
||||
let tripleGcc = opts.target & "-gcc"
|
||||
if findOnPath(tripleGcc):
|
||||
return tripleGcc
|
||||
if findOnPath("clang"):
|
||||
return "clang"
|
||||
# Fall through — user may still have a named cross compiler elsewhere
|
||||
return tripleGcc
|
||||
when defined(windows):
|
||||
return "gcc"
|
||||
else:
|
||||
return "cc"
|
||||
|
||||
proc cTargetFlags(opts: GlobalOptions, ccBin: string): string =
|
||||
## Extra flags for cross: clang needs -target; *-gcc is already a cross binary.
|
||||
if opts.target.len == 0: return ""
|
||||
let base = ccBin.extractFilename.toLowerAscii()
|
||||
if base == "clang" or base.startsWith("clang-"):
|
||||
return " -target " & opts.target
|
||||
""
|
||||
|
||||
proc printError(msg: string, useColor: bool) =
|
||||
if useColor:
|
||||
stdout.setForegroundColor(fgRed)
|
||||
@@ -487,9 +590,87 @@ proc cmdSearch*(args: seq[string], opts: GlobalOptions): int =
|
||||
echo &" {p.name} {p.version} — {desc}"
|
||||
return 0
|
||||
|
||||
|
||||
proc packageChecksum*(dir: string): string =
|
||||
## Deterministic sha1 of all `*.bux` under dir (sorted paths + contents).
|
||||
## Used for bux.lock Checksum — cloud install reproducibility (session 79).
|
||||
if dir.len == 0 or not dirExists(dir):
|
||||
return ""
|
||||
var files: seq[string] = @[]
|
||||
for f in walkDirRec(dir):
|
||||
if f.endsWith(".bux"):
|
||||
files.add(f)
|
||||
files.sort(system.cmp)
|
||||
var blob = ""
|
||||
for f in files:
|
||||
let rel = relativePath(f, dir)
|
||||
blob.add(rel)
|
||||
blob.add("\n")
|
||||
try:
|
||||
blob.add(readFile(f))
|
||||
except CatchableError:
|
||||
discard
|
||||
blob.add("\0")
|
||||
result = toLowerAscii($secureHash(blob))
|
||||
|
||||
proc verifyLockedInstall*(root: string, useColor: bool, opts: GlobalOptions): int =
|
||||
## `bux install --locked`: require bux.lock and verify path deps + checksums.
|
||||
let lockPath = root / "bux.lock"
|
||||
if not fileExists(lockPath):
|
||||
printError("install --locked: bux.lock missing (run `bux install` first)", useColor)
|
||||
return 1
|
||||
let lock = loadLockfile(lockPath)
|
||||
if lock.entries.len == 0:
|
||||
if not opts.quiet:
|
||||
printInfo("install --locked: empty lock (no dependencies)", useColor)
|
||||
return 0
|
||||
for e in lock.entries:
|
||||
let src = e.source
|
||||
if src.startsWith("http://") or src.startsWith("https://") or src.endsWith(".git"):
|
||||
# Git: ensure cache dir exists
|
||||
let depDir = getHomeDir() / ".bux" / "packages" / e.name
|
||||
if not dirExists(depDir):
|
||||
printError(&"install --locked: git package '{e.name}' not cached at {depDir}", useColor)
|
||||
printError("hint: run `bux install` once to clone, then commit bux.lock", useColor)
|
||||
return 1
|
||||
if e.checksum.len > 0:
|
||||
let got = packageChecksum(depDir)
|
||||
if got != e.checksum:
|
||||
printError(&"install --locked: checksum mismatch for '{e.name}'", useColor)
|
||||
printError(&" lock: {e.checksum}", useColor)
|
||||
printError(&" got: {got}", useColor)
|
||||
return 1
|
||||
else:
|
||||
# Path source (absolute or relative)
|
||||
let absPath = if src.isAbsolute: src else: root / src
|
||||
if not dirExists(absPath):
|
||||
printError(&"install --locked: path '{e.name}' missing: {absPath}", useColor)
|
||||
return 1
|
||||
if e.checksum.len > 0:
|
||||
let got = packageChecksum(absPath)
|
||||
if got != e.checksum:
|
||||
printError(&"install --locked: checksum mismatch for '{e.name}'", useColor)
|
||||
printError(&" lock: {e.checksum}", useColor)
|
||||
printError(&" got: {got}", useColor)
|
||||
return 1
|
||||
if not opts.quiet:
|
||||
printInfo(&"locked ok: {e.name} {e.version}", useColor)
|
||||
if not opts.quiet:
|
||||
printInfo(&"install --locked: {lock.entries.len} package(s) verified", useColor)
|
||||
return 0
|
||||
|
||||
proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
||||
let useColor = shouldUseColor(opts)
|
||||
var lockedOnly = false
|
||||
for a in args:
|
||||
if a == "--locked":
|
||||
lockedOnly = true
|
||||
elif a.startsWith("-"):
|
||||
printError(&"unknown install option '{a}'", useColor)
|
||||
return 1
|
||||
let root = getCurrentDir()
|
||||
if lockedOnly:
|
||||
return verifyLockedInstall(root, useColor, opts)
|
||||
let manifestPath = root / "bux.toml"
|
||||
if not fileExists(manifestPath):
|
||||
printError("no bux.toml found", useColor)
|
||||
@@ -510,11 +691,12 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
||||
return 1
|
||||
# Read dependency manifest
|
||||
let depManifestPath = absPath / "bux.toml"
|
||||
let csum = packageChecksum(absPath)
|
||||
if fileExists(depManifestPath):
|
||||
let depMan = loadManifest(depManifestPath)
|
||||
lock.entries.add(LockEntry(name: dep.name, version: depMan.version, source: absPath))
|
||||
lock.entries.add(LockEntry(name: dep.name, version: depMan.version, source: absPath, checksum: csum))
|
||||
else:
|
||||
lock.entries.add(LockEntry(name: dep.name, version: "0.0.0", source: absPath))
|
||||
lock.entries.add(LockEntry(name: dep.name, version: "0.0.0", source: absPath, checksum: csum))
|
||||
if not opts.quiet:
|
||||
printInfo(&"Resolved path dependency '{dep.name}' from {absPath}", useColor)
|
||||
of dkGit:
|
||||
@@ -530,7 +712,8 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
||||
if not opts.quiet:
|
||||
printInfo(&"Using cached '{dep.name}' from {depDir}", useColor)
|
||||
# Lock stores git URL; build loads from cache by name
|
||||
lock.entries.add(LockEntry(name: dep.name, version: dep.gitVersion, source: dep.gitUrl))
|
||||
let csumGit = packageChecksum(depDir)
|
||||
lock.entries.add(LockEntry(name: dep.name, version: dep.gitVersion, source: dep.gitUrl, checksum: csumGit))
|
||||
of dkVersion:
|
||||
# Registry lookup (E.1)
|
||||
if reg.path.len == 0:
|
||||
@@ -541,7 +724,8 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
||||
printError(&"package '{dep.name}' not found in registry", useColor)
|
||||
return 1
|
||||
if pkg.resolvedPath.len > 0 and dirExists(pkg.resolvedPath):
|
||||
lock.entries.add(LockEntry(name: dep.name, version: pkg.version, source: pkg.resolvedPath))
|
||||
let csum = packageChecksum(pkg.resolvedPath)
|
||||
lock.entries.add(LockEntry(name: dep.name, version: pkg.version, source: pkg.resolvedPath, checksum: csum))
|
||||
if not opts.quiet:
|
||||
printInfo(&"Resolved '{dep.name}' {pkg.version} → {pkg.resolvedPath}", useColor)
|
||||
elif isGitSource(pkg.source):
|
||||
@@ -553,7 +737,8 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
|
||||
if code != 0:
|
||||
printError(&"failed to clone {pkg.source}: {outp}", useColor)
|
||||
return 1
|
||||
lock.entries.add(LockEntry(name: dep.name, version: pkg.version, source: pkg.source))
|
||||
let csumG = packageChecksum(depDir)
|
||||
lock.entries.add(LockEntry(name: dep.name, version: pkg.version, source: pkg.source, checksum: csumG))
|
||||
if not opts.quiet:
|
||||
printInfo(&"Resolved '{dep.name}' {pkg.version} → git {pkg.source}", useColor)
|
||||
else:
|
||||
@@ -750,14 +935,25 @@ proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl] =
|
||||
proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
|
||||
var opts = opts
|
||||
var pathArgs: seq[string] = @[]
|
||||
for a in args:
|
||||
var i = 0
|
||||
while i < args.len:
|
||||
let a = args[i]
|
||||
if a == "--release":
|
||||
opts.release = true
|
||||
elif a == "--static":
|
||||
opts.staticLink = true
|
||||
elif a == "--target":
|
||||
if i + 1 < args.len:
|
||||
inc i
|
||||
opts.target = args[i]
|
||||
elif a.startsWith("--target="):
|
||||
opts.target = a["--target=".len .. ^1]
|
||||
elif a.startsWith("-"):
|
||||
# ignore unknown flags for forward-compat; keep path-like later
|
||||
# ignore unknown flags for forward-compat
|
||||
discard
|
||||
else:
|
||||
pathArgs.add(a)
|
||||
inc i
|
||||
let useColor = shouldUseColor(opts)
|
||||
let root = if pathArgs.len > 0: absolutePath(pathArgs[0]) else: getCurrentDir()
|
||||
let (pctx, status) = prepareProject(root, useColor, opts)
|
||||
@@ -805,11 +1001,11 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
|
||||
return 1
|
||||
|
||||
let baseDir = stdlibDir.parentDir()
|
||||
# Windows / BUX_RUNTIME=win → minimal runtime (no pthread/OpenSSL).
|
||||
# Full POSIX runtime is rt/runtime.c.
|
||||
let forceWinRt = getEnv("BUX_RUNTIME") == "win" or getEnv("BUX_RUNTIME") == "windows"
|
||||
let useWinRt = forceWinRt or (when defined(windows): true else: false)
|
||||
let runtimeName = if useWinRt: "runtime_win.c" else: "runtime.c"
|
||||
# Runtime pick: full POSIX | minimal (Linux static/embed) | win (historical).
|
||||
# See resolveRuntimeFlavor — BUX_RUNTIME, --static, --target.
|
||||
let flavor = resolveRuntimeFlavor(opts)
|
||||
let thinRt = isThinRuntime(flavor)
|
||||
let runtimeName = runtimeFileName(flavor)
|
||||
let runtimeSrc = baseDir / "rt" / runtimeName
|
||||
if fileExists(runtimeSrc):
|
||||
copyFile(runtimeSrc, runtimeDst)
|
||||
@@ -830,26 +1026,25 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
|
||||
let outputFile = buildDir / (outputName & exeSuffix)
|
||||
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
|
||||
# Host C toolchain + link flags
|
||||
let envCc = getEnv("BUX_CC")
|
||||
let ccBin =
|
||||
if envCc.len > 0: envCc
|
||||
else:
|
||||
when defined(windows): "gcc"
|
||||
else: "cc"
|
||||
var cflags = if extraCflags.len > 0: optFlags & " " & extraCflags else: optFlags
|
||||
let doStatic = wantStaticLink(opts)
|
||||
if doStatic:
|
||||
cflags = cflags & " -static"
|
||||
# Host / cross C toolchain + link flags
|
||||
let ccBin = resolveCCompiler(opts)
|
||||
cflags = cflags & cTargetFlags(opts, ccBin)
|
||||
let ldStable =
|
||||
when defined(linux):
|
||||
if useWinRt: "" else: " -Wl,--build-id=none"
|
||||
if thinRt: "" else: " -Wl,--build-id=none"
|
||||
else:
|
||||
""
|
||||
# Note: -l libs must come *after* .c/.o inputs (GNU ld left-to-right).
|
||||
let (hostCflags, hostLibs) =
|
||||
if useWinRt:
|
||||
if thinRt:
|
||||
# gc-sections drops mono stdlib that is never called (crypto/tasks, …)
|
||||
(" -ffunction-sections -fdata-sections", " -Wl,--gc-sections -lm")
|
||||
else:
|
||||
(" -pthread" & ldStable, " -lm -lcrypto")
|
||||
(" -pthread" & ldStable, " -lm -lssl -lcrypto")
|
||||
let ccCmd = &"{ccBin} {cflags}{hostCflags} -o {outputFile} {cFile} {runtimeDst} {ioDst}{hostLibs} 2>&1"
|
||||
if opts.verbose:
|
||||
printInfo(&"running: {ccCmd}", useColor)
|
||||
|
||||
@@ -98,6 +98,7 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type
|
||||
proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string
|
||||
proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type
|
||||
proc substituteType(ctx: var LowerCtx, te: TypeExpr, subst: Table[string, Type]): Type
|
||||
proc markCrossFuncPtrMoves(ctx: var LowerCtx, call: Expr)
|
||||
|
||||
proc resolvePtrAlias(ctx: LowerCtx, name: string): string =
|
||||
## Follow `p → bag` aliases (depth-limited).
|
||||
@@ -173,9 +174,174 @@ proc markMovedOutFromAst(ctx: var LowerCtx, expr: Expr) =
|
||||
of ekTuple:
|
||||
for e in expr.exprTupleElements:
|
||||
ctx.markMovedOutFromAst(e)
|
||||
of ekCall:
|
||||
# Cross-function: Take(&bag) may move fields of bag (session 76)
|
||||
ctx.markCrossFuncPtrMoves(expr)
|
||||
for a in expr.exprCallArgs:
|
||||
ctx.markMovedOutFromAst(a)
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
proc argAmpOwner(ctx: LowerCtx, arg: Expr): string =
|
||||
## If arg is `&local` (or cast of that), return the owner local name.
|
||||
## Also: bare pointer local that aliases an owner (`p` where p→bag).
|
||||
if arg == nil: return ""
|
||||
var e = arg
|
||||
while e != nil and e.kind == ekCast:
|
||||
e = e.exprCastOperand
|
||||
if e != nil and e.kind == ekUnary and e.exprUnaryOp == tkAmp:
|
||||
var op = e.exprUnaryOperand
|
||||
while op != nil and op.kind == ekCast:
|
||||
op = op.exprCastOperand
|
||||
if op != nil and op.kind == ekIdent and op.exprIdent.len > 0:
|
||||
return ctx.resolvePtrAlias(op.exprIdent)
|
||||
return ""
|
||||
if e != nil and e.kind == ekIdent and e.exprIdent.len > 0:
|
||||
let owner = ctx.resolvePtrAlias(e.exprIdent)
|
||||
if owner != e.exprIdent:
|
||||
return owner
|
||||
""
|
||||
|
||||
proc fieldPathFromParam(expr: Expr, param: string): seq[string] =
|
||||
## If `expr` is `param.a.b` / `(*param).a` / `param` auto-deref field chain,
|
||||
## return path `["a","b"]`. Empty if not rooted at param.
|
||||
result = @[]
|
||||
if expr == nil or param.len == 0: return
|
||||
var path: seq[string] = @[]
|
||||
var e = expr
|
||||
while e != nil and e.kind == ekField:
|
||||
path.insert(e.exprFieldName, 0)
|
||||
e = e.exprFieldObj
|
||||
while e != nil and e.kind == ekUnary and e.exprUnaryOp == tkStar:
|
||||
e = e.exprUnaryOperand
|
||||
if e != nil and e.kind == ekIdent and e.exprIdent == param and path.len > 0:
|
||||
result = path
|
||||
|
||||
proc scanExprParamMoves(e: Expr, param: string, paths: var HashSet[string], whole: var bool)
|
||||
proc scanBlockParamMoves(blk: Block, param: string, paths: var HashSet[string], whole: var bool)
|
||||
|
||||
proc scanExprParamMoves(e: Expr, param: string, paths: var HashSet[string], whole: var bool) =
|
||||
## Detect ownership moves of pointee fields through pointer param `param`.
|
||||
if e == nil or param.len == 0: return
|
||||
case e.kind
|
||||
of ekField:
|
||||
let path = fieldPathFromParam(e, param)
|
||||
if path.len > 0:
|
||||
paths.incl(path.join("."))
|
||||
of ekUnary:
|
||||
if e.exprUnaryOp == tkStar and e.exprUnaryOperand != nil and
|
||||
e.exprUnaryOperand.kind == ekIdent and
|
||||
e.exprUnaryOperand.exprIdent == param:
|
||||
whole = true
|
||||
else:
|
||||
scanExprParamMoves(e.exprUnaryOperand, param, paths, whole)
|
||||
of ekStructInit:
|
||||
for f in e.exprStructInitFields:
|
||||
scanExprParamMoves(f.value, param, paths, whole)
|
||||
of ekTuple:
|
||||
for el in e.exprTupleElements:
|
||||
scanExprParamMoves(el, param, paths, whole)
|
||||
of ekCall:
|
||||
if e.exprCallCallee != nil:
|
||||
scanExprParamMoves(e.exprCallCallee, param, paths, whole)
|
||||
for a in e.exprCallArgs:
|
||||
scanExprParamMoves(a, param, paths, whole)
|
||||
of ekBinary:
|
||||
scanExprParamMoves(e.exprBinaryLeft, param, paths, whole)
|
||||
scanExprParamMoves(e.exprBinaryRight, param, paths, whole)
|
||||
of ekAssign:
|
||||
# `let x = p.items` style via assign value
|
||||
scanExprParamMoves(e.exprAssignValue, param, paths, whole)
|
||||
of ekBlock:
|
||||
if e.exprBlock != nil:
|
||||
scanBlockParamMoves(e.exprBlock, param, paths, whole)
|
||||
of ekCast:
|
||||
scanExprParamMoves(e.exprCastOperand, param, paths, whole)
|
||||
else:
|
||||
discard
|
||||
|
||||
proc scanStmtParamMoves(s: Stmt, param: string, paths: var HashSet[string], whole: var bool) =
|
||||
if s == nil: return
|
||||
case s.kind
|
||||
of skReturn:
|
||||
scanExprParamMoves(s.stmtReturnValue, param, paths, whole)
|
||||
of skLet:
|
||||
scanExprParamMoves(s.stmtLetInit, param, paths, whole)
|
||||
of skExpr:
|
||||
scanExprParamMoves(s.stmtExpr, param, paths, whole)
|
||||
of skIf:
|
||||
scanExprParamMoves(s.stmtIfCond, param, paths, whole)
|
||||
if s.stmtIfThen != nil: scanBlockParamMoves(s.stmtIfThen, param, paths, whole)
|
||||
if s.stmtIfElse != nil: scanBlockParamMoves(s.stmtIfElse, param, paths, whole)
|
||||
for br in s.stmtIfElseIfs:
|
||||
scanExprParamMoves(br.cond, param, paths, whole)
|
||||
if br.blk != nil: scanBlockParamMoves(br.blk, param, paths, whole)
|
||||
of skWhile:
|
||||
scanExprParamMoves(s.stmtWhileCond, param, paths, whole)
|
||||
if s.stmtWhileBody != nil: scanBlockParamMoves(s.stmtWhileBody, param, paths, whole)
|
||||
of skFor:
|
||||
scanExprParamMoves(s.stmtForIter, param, paths, whole)
|
||||
if s.stmtForBody != nil: scanBlockParamMoves(s.stmtForBody, param, paths, whole)
|
||||
of skMatch:
|
||||
scanExprParamMoves(s.stmtMatchSubject, param, paths, whole)
|
||||
for arm in s.stmtMatchArms:
|
||||
if arm.body != nil:
|
||||
scanExprParamMoves(arm.body, param, paths, whole)
|
||||
else:
|
||||
discard
|
||||
|
||||
proc scanBlockParamMoves(blk: Block, param: string, paths: var HashSet[string], whole: var bool) =
|
||||
if blk == nil: return
|
||||
for st in blk.stmts:
|
||||
scanStmtParamMoves(st, param, paths, whole)
|
||||
|
||||
proc paramIsPointer(p: Param): bool =
|
||||
## True if the parameter type is a pointer (`*T` / `&T` / `own` pointer-ish).
|
||||
if p.ptype == nil: return false
|
||||
p.ptype.kind in {tekPointer, tekOwn}
|
||||
|
||||
proc markCrossFuncPtrMoves(ctx: var LowerCtx, call: Expr) =
|
||||
## Session 76: `TakeItems(&bag)` where TakeItems moves `p.items` → mark bag.
|
||||
if call == nil or call.kind != ekCall: return
|
||||
var calleeName = ""
|
||||
if call.exprCallCallee == nil: return
|
||||
case call.exprCallCallee.kind
|
||||
of ekIdent:
|
||||
calleeName = call.exprCallCallee.exprIdent
|
||||
if ctx.importTable.hasKey(calleeName):
|
||||
calleeName = ctx.importTable[calleeName]
|
||||
of ekPath:
|
||||
calleeName = call.exprCallCallee.exprPath.join("_")
|
||||
of ekGenericCall:
|
||||
calleeName = call.exprCallCallee.exprGenericCallee
|
||||
else:
|
||||
return
|
||||
if calleeName.len == 0: return
|
||||
let sym = ctx.globalScope.lookup(calleeName)
|
||||
if sym == nil or sym.decl == nil or sym.decl.kind != dkFunc: return
|
||||
let decl = sym.decl
|
||||
if decl.declFuncBody == nil: return
|
||||
for i, arg in call.exprCallArgs:
|
||||
if i >= decl.declFuncParams.len: break
|
||||
let fp = decl.declFuncParams[i]
|
||||
if not paramIsPointer(fp): continue
|
||||
let owner = ctx.argAmpOwner(arg)
|
||||
if owner.len == 0: continue
|
||||
if not ctx.hasPendingDrop(owner): continue
|
||||
var paths = initHashSet[string]()
|
||||
var whole = false
|
||||
scanBlockParamMoves(decl.declFuncBody, fp.name, paths, whole)
|
||||
if not whole and paths.len == 0: continue
|
||||
if whole:
|
||||
ctx.markMovedOutLocal(owner)
|
||||
else:
|
||||
if not ctx.partialMovedFields.hasKey(owner):
|
||||
ctx.partialMovedFields[owner] = initHashSet[string]()
|
||||
for path in paths:
|
||||
ctx.partialMovedFields[owner].incl(path)
|
||||
ctx.markMovedOutLocal(owner)
|
||||
|
||||
proc shouldSkipDrop(ctx: LowerCtx, dropNode: HirNode, skipName: string): bool =
|
||||
## Skip Drop for explicit skipName or any moved-out local.
|
||||
let target = dropTargetName(dropNode)
|
||||
@@ -1461,6 +1627,8 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
return hirBinary(expr.exprBinaryOp, left, right, typ, loc)
|
||||
|
||||
of ekCall:
|
||||
# Cross-function pointer ownership (before any lowering side effects)
|
||||
ctx.markCrossFuncPtrMoves(expr)
|
||||
# Method call desugaring: obj.method(args) → Type_method(obj, args)
|
||||
if expr.exprCallCallee.kind == ekField:
|
||||
let methodName = expr.exprCallCallee.exprFieldName
|
||||
@@ -2006,6 +2174,8 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
|
||||
case stmt.kind
|
||||
of skExpr:
|
||||
if stmt.stmtExpr != nil:
|
||||
ctx.markMovedOutFromAst(stmt.stmtExpr)
|
||||
return ctx.flushPending(ctx.lowerExpr(stmt.stmtExpr))
|
||||
|
||||
of skLet:
|
||||
|
||||
@@ -1006,9 +1006,16 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
|
||||
let pat = exprToPattern(arg)
|
||||
if pat == nil: return nil
|
||||
return Expr(kind: ekMacroPat, loc: arg.loc, exprMacroPat: pat)
|
||||
of mfkExpr, mfkTt:
|
||||
of mfkExpr:
|
||||
if arg.kind in {ekMacroStmt, ekMacroPat}: return nil
|
||||
return arg
|
||||
of mfkTt:
|
||||
# Session 76: token-tree is a *superset* of expr — any single
|
||||
# well-formed AST fragment the call parser already produced:
|
||||
# expr, block, ident, literal, path, call, stmt, or pat wrapper.
|
||||
# (True delimiter-balanced raw tokens remain future work.)
|
||||
if arg == nil: return nil
|
||||
return arg
|
||||
|
||||
proc fragMatches(k: MacroFragKind, arg: Expr): bool =
|
||||
## Kind constraint at match time (after arg expand).
|
||||
|
||||
+10
-5
@@ -44,12 +44,13 @@ proc resolvePackageSource(pkg: var RegistryPackage, indexDir: string) =
|
||||
p = p[2 .. ^1]
|
||||
if not p.isAbsolute:
|
||||
p = indexDir / p
|
||||
pkg.resolvedPath = p.absolutePath
|
||||
# Collapse ../ segments for cleaner lockfiles (session 82)
|
||||
pkg.resolvedPath = expandFilename(p)
|
||||
elif pkg.source.startsWith("path:"):
|
||||
var p = pkg.source["path:".len .. ^1]
|
||||
if not p.isAbsolute:
|
||||
p = indexDir / p
|
||||
pkg.resolvedPath = p.absolutePath
|
||||
pkg.resolvedPath = expandFilename(p)
|
||||
pkg.source = "file:" & pkg.resolvedPath
|
||||
|
||||
proc parseRegistryToml(content, indexPath: string): seq[RegistryPackage] =
|
||||
@@ -115,14 +116,18 @@ proc fetchRegistryUrl*(url: string): string =
|
||||
if not force and fileExists(cachePath) and cachedUrl == url:
|
||||
return cachePath.absolutePath
|
||||
|
||||
# Prefer curl; fall back to wget
|
||||
# Prefer curl; fall back to wget.
|
||||
# BUX_REGISTRY_INSECURE=1 → allow self-signed HTTPS (dev / smoke only).
|
||||
let insecure = getEnv("BUX_REGISTRY_INSECURE").len > 0
|
||||
var ok = false
|
||||
if findExe("curl").len > 0:
|
||||
let cmd = &"curl -fsSL --max-time 30 -o {quoteShell(cachePath)} {quoteShell(url)}"
|
||||
let kflag = if insecure: " -k" else: ""
|
||||
let cmd = &"curl -fsSL{kflag} --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 nflag = if insecure: " --no-check-certificate" else: ""
|
||||
let cmd = &"wget -q{nflag} -T 30 -O {quoteShell(cachePath)} {quoteShell(url)}"
|
||||
let (_, code) = execCmdEx(cmd)
|
||||
ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0
|
||||
else:
|
||||
|
||||
+20
-1
@@ -745,7 +745,20 @@ proc evalExpr(sema: Sema, expr: Expr, locals: Table[string, CtValue]): CtValue =
|
||||
of ekLiteral:
|
||||
case expr.exprLit.kind
|
||||
of tkIntLiteral:
|
||||
return CtValue(kind: ctkInt, intVal: parseBiggestInt(expr.exprLit.text))
|
||||
# Support 0x / 0b / 0o prefixes (parseBiggestInt is decimal-only).
|
||||
let lit = expr.exprLit.text
|
||||
try:
|
||||
if lit.len >= 3 and lit[0] == '0':
|
||||
let p = lit[1].toLowerAscii()
|
||||
if p == 'x':
|
||||
return CtValue(kind: ctkInt, intVal: BiggestInt(parseHexInt(lit[2 .. ^1])))
|
||||
elif p == 'b':
|
||||
return CtValue(kind: ctkInt, intVal: BiggestInt(parseBinInt(lit[2 .. ^1])))
|
||||
elif p == 'o':
|
||||
return CtValue(kind: ctkInt, intVal: BiggestInt(parseOctInt(lit[2 .. ^1])))
|
||||
return CtValue(kind: ctkInt, intVal: parseBiggestInt(lit))
|
||||
except ValueError:
|
||||
return CtValue(kind: ctkVoid)
|
||||
of tkBoolLiteral:
|
||||
return CtValue(kind: ctkBool, boolVal: expr.exprLit.text == "true")
|
||||
of tkStringLiteral:
|
||||
@@ -786,6 +799,12 @@ proc evalExpr(sema: Sema, expr: Expr, locals: Table[string, CtValue]): CtValue =
|
||||
of tkPercent:
|
||||
if right.intVal != 0:
|
||||
return CtValue(kind: ctkInt, intVal: left.intVal mod right.intVal)
|
||||
# Bitwise (session 75 — embedded CRC / flag tables at compile time)
|
||||
of tkCaret: return CtValue(kind: ctkInt, intVal: left.intVal xor right.intVal)
|
||||
of tkAmp: return CtValue(kind: ctkInt, intVal: left.intVal and right.intVal)
|
||||
of tkPipe: return CtValue(kind: ctkInt, intVal: left.intVal or right.intVal)
|
||||
of tkShl: return CtValue(kind: ctkInt, intVal: left.intVal shl right.intVal)
|
||||
of tkShr: return CtValue(kind: ctkInt, intVal: left.intVal shr right.intVal)
|
||||
of tkEq: return CtValue(kind: ctkBool, boolVal: left.intVal == right.intVal)
|
||||
of tkNe: return CtValue(kind: ctkBool, boolVal: left.intVal != right.intVal)
|
||||
of tkLt: return CtValue(kind: ctkBool, boolVal: left.intVal < right.intVal)
|
||||
|
||||
Reference in New Issue
Block a user