From a939f74b1b737ddd10c9562068b5605961644e3a Mon Sep 17 00:00:00 2001 From: dimgigov Date: Tue, 21 Jul 2026 12:29:53 +0300 Subject: [PATCH] feat: field-move Drop (partial/nested/ptr), stmt/pat macros, Windows runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sessions 70–74: partialMovedPaths + ptrAliases in bootstrap/selfhost CBE, remaining drops after field moves, macro stmt/pat fragments, runtime_win.c and MinGW hello CI, examples + drop-move smoke coverage, QUALITY_PLAN update. --- .github/workflows/ci.yml | 42 +- Makefile | 4 +- bootstrap/ast.nim | 10 + bootstrap/cli.nim | 41 +- bootstrap/hir_lower.nim | 239 +++++++++- bootstrap/macroexpand.nim | 169 ++++++- bootstrap/parser.nim | 28 +- bootstrap/sema.nim | 4 + docs/BuildAndTest.md | 10 +- docs/LanguageRef.md | 84 +++- docs/QUALITY_PLAN.md | 102 ++++- examples/macro_stmt_pat.bux | 84 ++++ examples/move_field_nested.bux | 91 ++++ examples/move_field_ptr.bux | 131 ++++++ examples/move_field_remaining.bux | 87 ++++ rt/runtime_win.c | 733 ++++++++++++++++++++++++++++++ src/ast.bux | 10 +- src/c_backend.bux | 530 +++++++++++++++++++-- src/macroexpand.bux | 189 +++++++- src/parser.bux | 39 +- tools/smoke_drop_move.sh | 94 +++- tools/smoke_windows_hello.sh | 68 +++ 22 files changed, 2672 insertions(+), 117 deletions(-) create mode 100644 examples/macro_stmt_pat.bux create mode 100644 examples/move_field_nested.bux create mode 100644 examples/move_field_ptr.bux create mode 100644 examples/move_field_remaining.bux create mode 100644 rt/runtime_win.c create mode 100755 tools/smoke_windows_hello.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 940eb86..1ff833f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -310,14 +310,12 @@ jobs: make test-unit make test-examples-smoke - # ── Windows smoke (bootstrap + pure Nim unit tests) ───────────────────── - # Full bux→C examples need POSIX runtime (ucontext / pthread sockets in - # rt/runtime.c) — not ported yet. This job still catches Nim/bootstrap - # regressions on Windows (prebuilt Nim zip = fast install). + # ── Windows smoke (bootstrap + unit + MinGW hello) ────────────────────── + # Uses rt/runtime_win.c (no pthread/OpenSSL). Full POSIX runtime is Unix-only. windows: name: windows smoke runs-on: windows-latest - timeout-minutes: 30 + timeout-minutes: 35 defaults: run: shell: bash @@ -345,6 +343,26 @@ jobs: echo "$PWD/${{ env.NIM_INSTALL_DIR }}/bin" >> "$GITHUB_PATH" echo "$HOME/.nimble/bin" >> "$GITHUB_PATH" + - name: Install MinGW (gcc) + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: false + install: mingw-w64-x86_64-gcc + path-type: inherit + + - name: Add MinGW64 to PATH + run: | + # setup-msys2 installs under C:/msys64 by default + for d in /c/msys64/mingw64/bin /mingw64/bin "$HOME/msys64/mingw64/bin"; do + if [[ -x "$d/gcc.exe" || -x "$d/gcc" ]]; then + echo "$d" >> "$GITHUB_PATH" + export PATH="$d:$PATH" + break + fi + done + gcc --version | head -1 + - name: Cache nimcache (bootstrap + unit) uses: actions/cache@v4 with: @@ -357,6 +375,8 @@ jobs: run: | set -e nim -v + # MinGW gcc on PATH (from setup-msys2) + gcc --version | head -1 # Windows produces buxc.exe; keep name predictable for the smoke steps nim c --nimcache:nimcache -o:buxc.exe -d:release --opt:size bootstrap/main.nim ./buxc.exe --version @@ -380,7 +400,17 @@ jobs: rm -rf _test_tmp_pkg ./buxc.exe new _test_tmp_pkg ./buxc.exe --version - echo "windows smoke: PASS (bootstrap + unit + CLI)" + echo "unit+CLI: PASS" + + - name: hello smoke (MinGW + runtime_win) + run: | + set -e + unset BUX_DEBUG_FILE || true + export BUX_STDLIB="$PWD/lib" + # bootstrap on Windows always picks runtime_win.c + chmod +x tools/smoke_windows_hello.sh + tools/smoke_windows_hello.sh + echo "windows smoke: PASS (bootstrap + unit + CLI + hello)" # ── Single required status for branch protection ──────────────────────── ci-gate: diff --git a/Makefile b/Makefile index 0854724..d3bbb7f 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ BUILD_DIR := build # Project-local nimcache so CI can cache compiles (default is ~/.cache/nim). NIMFLAGS ?= --nimcache:nimcache -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 ownership_release 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 move_field move_field_partial c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic +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 ownership_release 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 move_field move_field_partial move_field_remaining move_field_nested move_field_ptr c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat # Platform smoke (macOS CI): full EXAMPLES still runs on Linux. -EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic +EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial move_field_remaining move_field_nested move_field_ptr c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat .PHONY: all build dev debug test clean clean-all test-examples test-examples-smoke selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs bench test-apps test-dwarf test-selfhost-smoke test-unit ensure-buxc diff --git a/bootstrap/ast.nim b/bootstrap/ast.nim index 5bf219d..56174a8 100644 --- a/bootstrap/ast.nim +++ b/bootstrap/ast.nim @@ -134,6 +134,8 @@ type ekStringInterp ekClosure ekMacroCall ## name!(args) — expanded before sema + ekMacroStmt ## `$s:stmt` arg wrapper (expand only) + ekMacroPat ## `$p:pat` arg wrapper (expand only) MatchArm* = object loc*: SourceLocation @@ -243,6 +245,12 @@ type ## Group lengths for multi-rep: `m!(1,2; 3,4)` → @[2, 2]. ## Empty means a single group of all args. exprMacroGroupLens*: seq[int] + of ekMacroStmt: + ## Statement fragment argument (`$s:stmt`) — only during expand + exprMacroStmt*: Stmt + of ekMacroPat: + ## Pattern fragment argument (`$p:pat`) — only during expand + exprMacroPat*: Pattern # --------------------------------------------------------------------------- # Statements @@ -375,6 +383,8 @@ type mfkTt ## token-tree (MVP: same as expr) mfkLiteral ## int/float/string/char/bool literal only mfkBlock ## block expression `{ … }` + mfkStmt ## one statement (let/if/… or expression-stmt) + mfkPat ## match/let pattern MacroFragment* = object name*: string ## primary / first name (compat) diff --git a/bootstrap/cli.nim b/bootstrap/cli.nim index 768000f..3ab6bf4 100644 --- a/bootstrap/cli.nim +++ b/bootstrap/cli.nim @@ -805,11 +805,16 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = return 1 let baseDir = stdlibDir.parentDir() - let runtimeSrc = baseDir / "rt" / "runtime.c" + # 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" + let runtimeSrc = baseDir / "rt" / runtimeName if fileExists(runtimeSrc): copyFile(runtimeSrc, runtimeDst) else: - printError("runtime.c not found in rt/", useColor) + printError(&"{runtimeName} not found in rt/", useColor) return 1 let ioSrc = baseDir / "rt" / "io.c" @@ -821,13 +826,31 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = # 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 exeSuffix = when defined(windows): ".exe" else: "" + 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 - # --build-id is GNU ld only (breaks Apple ld). Reproducible selfhost-loop uses Linux CI. - let ldStable = when defined(linux): " -Wl,--build-id=none" else: "" - let ccCmd = &"cc {cflags} -pthread{ldStable} -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm -lcrypto 2>&1" + # Host C toolchain + link flags + let envCc = getEnv("BUX_CC") + let ccBin = + if envCc.len > 0: envCc + else: + when defined(windows): "gcc" + else: "cc" + let ldStable = + when defined(linux): + if useWinRt: "" 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: + # gc-sections drops mono stdlib that is never called (crypto/tasks, …) + (" -ffunction-sections -fdata-sections", " -Wl,--gc-sections -lm") + else: + (" -pthread" & ldStable, " -lm -lcrypto") + let ccCmd = &"{ccBin} {cflags}{hostCflags} -o {outputFile} {cFile} {runtimeDst} {ioDst}{hostLibs} 2>&1" if opts.verbose: printInfo(&"running: {ccCmd}", useColor) let (output, exitCode) = execCmdEx(ccCmd) @@ -848,7 +871,11 @@ proc cmdRun*(args: seq[string], opts: GlobalOptions): int = return buildRes let man = loadManifest(root / "bux.toml") let outputName = if man.name != "": man.name else: "bux_out" - let outputFile = root / "build" / outputName + let exeSuffix = when defined(windows): ".exe" else: "" + var outputFile = root / "build" / (outputName & exeSuffix) + if not fileExists(outputFile): + # Fallback without suffix (cross-env / older builds) + outputFile = root / "build" / outputName if not fileExists(outputFile): printError("executable not found after build", useColor) return 1 diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index e9def44..7601cf8 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -38,6 +38,14 @@ type ## Locals whose value was moved into another owner (struct field, let, return). ## Auto-Drop is skipped for these (session 37 — field-move ownership). movedOutLocals*: HashSet[string] + ## Partial field moves: local → dotted paths moved out by value + ## (e.g. "items", "inner.items" for nested `a.b.c` — session 70/73). + ## When parent Type_Drop is skipped, remaining droppable fields still Drop. + ## Whole-local moves leave this empty → full skip, no field drops. + partialMovedFields*: Table[string, HashSet[string]] + ## Pointer aliases: local pointer name → pointee local (`p = &bag` → p→bag). + ## Used so `p.items` / `(*p).items` mark the owner local (session 74). + ptrAliases*: Table[string, string] proc freshName(ctx: var LowerCtx): string = inc ctx.varCounter @@ -85,23 +93,80 @@ proc markMovedOutLocal(ctx: var LowerCtx, name: string) = if name.len > 0 and ctx.hasPendingDrop(name): ctx.movedOutLocals.incl(name) -# Forward decls (used by markMovedOutFromAst before their full definitions) +# Forward decls (used by markMovedOutFromAst / remainingFieldDrops before defs) 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 resolvePtrAlias(ctx: LowerCtx, name: string): string = + ## Follow `p → bag` aliases (depth-limited). + result = name + var guard = 0 + while result.len > 0 and ctx.ptrAliases.hasKey(result) and guard < 8: + result = ctx.ptrAliases[result] + inc guard + +proc fieldPathFromAst(ctx: LowerCtx, expr: Expr): tuple[base: string, path: seq[string]] = + ## Walk `a.b.c` / `(*p).b.c` / `p.b` (auto-deref) → owner local + path. + ## Resolves pointer aliases (`p = &bag` → owner is `bag`). + result = ("", @[]) + if expr == nil: return + var path: seq[string] = @[] + var e = expr + while e != nil and e.kind == ekField: + path.insert(e.exprFieldName, 0) + e = e.exprFieldObj + # Peel explicit derefs: (*p).x or (**pp).x + while e != nil and e.kind == ekUnary and e.exprUnaryOp == tkStar: + e = e.exprUnaryOperand + if e != nil and e.kind == ekIdent and e.exprIdent.len > 0 and path.len > 0: + let owner = ctx.resolvePtrAlias(e.exprIdent) + result = (owner, path) + +proc pathKey(path: seq[string]): string = + path.join(".") + +proc recordPtrAliasFromAst(ctx: var LowerCtx, ptrName: string, init: Expr) = + ## If `init` is `&local` (possibly with paren/cast noise), record ptr→local. + if ptrName.len == 0 or init == nil: return + var e = init + # Skip simple casts + 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: + ctx.ptrAliases[ptrName] = op.exprIdent proc markMovedOutFromAst(ctx: var LowerCtx, expr: Expr) = ## Mark droppable locals used by-value in ownership-taking contexts. - ## Partial field moves: `return bag.items` / `let x = bag.items` mark `bag` - ## so auto-Drop of the parent is skipped — **only when the field type itself - ## is droppable** (not `return bag.tag` for an int field). + ## Partial field moves: `return bag.items` / `return outer.inner.items` / + ## `return p.items` (p = &bag) mark the **owner** local so auto-Drop of the + ## parent is skipped. Records dotted path so remaining fields still Drop + ## (sessions 70/73/74). if expr == nil: return case expr.kind of ekIdent: - ctx.markMovedOutLocal(expr.exprIdent) + ctx.markMovedOutLocal(ctx.resolvePtrAlias(expr.exprIdent)) of ekField: let fieldTy = ctx.resolveExprType(expr) if ctx.autoDropFuncName(fieldTy).len > 0: - ctx.markMovedOutFromAst(expr.exprFieldObj) + let (base, path) = ctx.fieldPathFromAst(expr) + if base.len > 0 and path.len > 0: + if not ctx.partialMovedFields.hasKey(base): + ctx.partialMovedFields[base] = initHashSet[string]() + ctx.partialMovedFields[base].incl(pathKey(path)) + ctx.markMovedOutLocal(base) + # Nested path recorded as a whole — do not recurse (would mis-mark intermediates) + of ekUnary: + # Moving `*p` by value (whole pointee) — mark owner local if known + if expr.exprUnaryOp == tkStar and expr.exprUnaryOperand != nil and + expr.exprUnaryOperand.kind == ekIdent: + let owner = ctx.resolvePtrAlias(expr.exprUnaryOperand.exprIdent) + ctx.markMovedOutLocal(owner) of ekStructInit: for f in expr.exprStructInitFields: ctx.markMovedOutFromAst(f.value) @@ -119,6 +184,136 @@ proc shouldSkipDrop(ctx: LowerCtx, dropNode: HirNode, skipName: string): bool = if target in ctx.movedOutLocals: return true false +proc structFieldsOf(ctx: var LowerCtx, te: TypeExpr, typeName: string): seq[tuple[name: string, typ: Type]] = + ## Resolve struct fields for a named / monomorphized type. + result = @[] + var declName = if te != nil: te.typeName else: "" + if declName.len == 0: declName = typeName + let sym = ctx.globalScope.lookup(declName) + if sym != nil and sym.decl != nil and sym.decl.kind == dkStruct: + for f in sym.decl.declStructFields: + if f.ftype == nil: continue + var fieldTy: Type + if te != nil and te.typeArgs.len > 0 and ctx.genericStructs.hasKey(declName): + var subst = initTable[string, Type]() + let gdecl = ctx.genericStructs[declName] + for j, tp in gdecl.declStructTypeParams: + if j < te.typeArgs.len: + subst[tp.name] = ctx.resolveTypeExpr(te.typeArgs[j]) + fieldTy = substituteType(ctx, f.ftype, subst) + else: + fieldTy = ctx.resolveTypeExpr(f.ftype) + result.add((f.name, fieldTy)) + return + if ctx.structInstMap.hasKey(typeName): + for es in ctx.extraStructs: + if es.name == typeName: + for f in es.fields: + result.add((f.name, f.typ)) + return + # Also try mangled typeName as decl name + let sym2 = ctx.globalScope.lookup(typeName) + if sym2 != nil and sym2.decl != nil and sym2.decl.kind == dkStruct: + for f in sym2.decl.declStructFields: + if f.ftype == nil: continue + result.add((f.name, ctx.resolveTypeExpr(f.ftype))) + +proc makeFieldPtrAt(ctx: var LowerCtx, base: HirNode, rootTe: TypeExpr, + rootTypeName: string, path: seq[string], fieldTy: Type, + loc: SourceLocation): HirNode = + ## `&(base.a.b)` with typed intermediate field accesses (needed by LIR/C). + if path.len == 0: + return hirUnary(tkAmp, base, makePointer(fieldTy), loc) + if path.len == 1: + return HirNode(kind: hFieldPtr, fieldPtrBase: base, fieldName: path[0], + typ: makePointer(fieldTy), loc: loc) + # Build typed prefix: base.a.b for path [a,b,c] → access a, then b; ptr on c + var cur = base + var curTe = rootTe + var curTypeName = rootTypeName + for i in 0 ..< path.len - 1: + let fields = ctx.structFieldsOf(curTe, curTypeName) + var nextTy: Type = makeUnknown() + for f in fields: + if f.name == path[i]: + nextTy = f.typ + break + cur = HirNode(kind: hFieldAccess, fieldAccessBase: cur, + fieldAccessName: path[i], typ: nextTy, loc: loc) + if nextTy != nil and nextTy.kind == tkNamed: + curTypeName = nextTy.name + curTe = TypeExpr(kind: tekNamed, typeName: nextTy.name) + else: + curTe = nil + curTypeName = "" + return HirNode(kind: hFieldPtr, fieldPtrBase: cur, fieldName: path[^1], + typ: makePointer(fieldTy), loc: loc) + +proc remainingDropsAt(ctx: var LowerCtx, baseHir: HirNode, typeName: string, + te: TypeExpr, prefix: seq[string], + moved: HashSet[string], loc: SourceLocation, + rootTe: TypeExpr, rootTypeName: string): seq[HirNode] = + ## Emit Drops for fields of `typeName` under `baseHir`+`prefix`, respecting + ## dotted moved paths (exact = fully moved; prefix = recurse nested). + ## `rootTe`/`rootTypeName` are the original local's type (for path typing). + result = @[] + let fields = ctx.structFieldsOf(te, typeName) + for f in fields: + var fpath = prefix + fpath.add(f.name) + let key = pathKey(fpath) + # Fully moved this field + if key in moved: + continue + # Nested partial: some path starts with key + "." + var nestedMoved = false + for m in moved: + if m.startsWith(key & "."): + nestedMoved = true + break + if nestedMoved: + let fty = f.typ + if fty == nil or fty.kind != tkNamed: continue + var fte = TypeExpr(kind: tekNamed, typeName: fty.name) + result.add(ctx.remainingDropsAt(baseHir, fty.name, fte, fpath, moved, loc, + rootTe, rootTypeName)) + continue + # Unrelated field — full Drop if droppable + let dropFn = ctx.autoDropFuncName(f.typ) + if dropFn.len == 0: continue + let fieldPtr = ctx.makeFieldPtrAt(baseHir, rootTe, rootTypeName, fpath, f.typ, loc) + result.add(hirCall(dropFn, @[fieldPtr], makeVoid(), loc)) + +proc remainingFieldDrops(ctx: var LowerCtx, localName: string, loc: SourceLocation): seq[HirNode] = + ## After a partial field move out of `localName`, Drop every *other* droppable + ## field (including nested remaining after `a.b.c` moves). + result = @[] + if localName.len == 0 or not ctx.partialMovedFields.hasKey(localName): + return + let moved = ctx.partialMovedFields[localName] + if not ctx.varTypeExprs.hasKey(localName): + return + let te = ctx.varTypeExprs[localName] + if te == nil or te.kind != tekNamed: + return + let localTy = ctx.resolveTypeExpr(te) + if localTy == nil or localTy.kind != tkNamed: + return + let base = hirVar(localName, localTy, loc) + result = ctx.remainingDropsAt(base, localTy.name, te, @[], moved, loc, te, localTy.name) + +proc emitDropOrPartial(ctx: var LowerCtx, stmts: var seq[HirNode], dropNode: HirNode, + skipName: string) = + ## Emit Type_Drop, or remaining field Drops after a partial move. + if not ctx.shouldSkipDrop(dropNode, skipName): + stmts.add(dropNode) + return + let target = dropTargetName(dropNode) + if target.len > 0 and target in ctx.partialMovedFields: + let loc = if dropNode != nil: dropNode.loc else: SourceLocation() + for d in ctx.remainingFieldDrops(target, loc): + stmts.add(d) + proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string = ## Return `Type_Drop` if this type should be auto-dropped, else "". ## Also monomorphizes generic Drop/Free helpers for stdlib collections. @@ -195,8 +390,6 @@ proc patternLiteralNode(pat: Pattern, loc: SourceLocation): HirNode = return nil return hirLit(pat.patLit, litTokenType(pat.patLit), loc) -proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type - proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern, subjectEnumName: string, subjectHasData: bool, loc: SourceLocation): HirNode = @@ -527,6 +720,8 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx = result.patternBoundNames = initHashSet[string]() result.patternRenames = initTable[string, string]() result.movedOutLocals = initHashSet[string]() + result.partialMovedFields = initTable[string, HashSet[string]]() + result.ptrAliases = initTable[string, string]() proc sanitizeFatPart(s: string): string = result = s.replace("const char*", "cstr").replace("unsigned int", "uint") @@ -1466,6 +1661,9 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = return HirNode(kind: hAssign, assignOp: tkAssign, assignTarget: loadTarget, assignValue: value, typ: makeVoid(), loc: loc) + # Pointer alias update: `p = &bag` + if expr.exprAssignTarget.kind == ekIdent and expr.exprAssignValue != nil: + ctx.recordPtrAliasFromAst(expr.exprAssignTarget.exprIdent, expr.exprAssignValue) let target = ctx.lowerExpr(expr.exprAssignTarget) let value = ctx.lowerExpr(expr.exprAssignValue) return HirNode(kind: hAssign, assignOp: expr.exprAssignOp, @@ -1842,6 +2040,9 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode = if initHir != nil: let store = hirStore(varNode, initHir, loc) stmts.add(store) + # Pointer alias: `let p = &bag` so later `p.items` marks bag (session 74) + if stmt.stmtLetInit != nil: + ctx.recordPtrAliasFromAst(stmt.stmtLetName, stmt.stmtLetInit) # Move: `let a = b` takes ownership of droppable local `b` if stmt.stmtLetInit != nil: ctx.markMovedOutFromAst(stmt.stmtLetInit) @@ -1879,8 +2080,7 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode = retVal = hirVar(tmp, retTy, loc) # Add defers in reverse order (LIFO); snapshot full stack for every return path for i in countdown(ctx.deferStmts.len - 1, 0): - if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop): - stmts.add(ctx.deferStmts[i]) + ctx.emitDropOrPartial(stmts, ctx.deferStmts[i], skipDrop) stmts.add(hirReturn(retVal, loc)) return hirBlock(stmts, nil, makeVoid(), loc) @@ -2201,8 +2401,7 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode = let lastAlwaysReturns = stmts.len > 0 and blockAlwaysReturns(stmts[^1]) if ctx.deferStmts.len > deferBase and not lastAlwaysReturns: for i in countdown(ctx.deferStmts.len - 1, deferBase): - if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop): - stmts.add(ctx.deferStmts[i]) + ctx.emitDropOrPartial(stmts, ctx.deferStmts[i], skipDrop) ctx.deferStmts.setLen(deferBase) elif ctx.deferStmts.len > deferBase and lastAlwaysReturns: # Return path already owns these drops; pop so outer scopes don't re-run them @@ -2258,8 +2457,12 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc = ctx.patternRenames = initTable[string, string]() let oldDefers = ctx.deferStmts let oldMovedOut = ctx.movedOutLocals + let oldPartialMoved = ctx.partialMovedFields + let oldPtrAliases = ctx.ptrAliases ctx.deferStmts = @[] ctx.movedOutLocals = initHashSet[string]() + ctx.partialMovedFields = initTable[string, HashSet[string]]() + ctx.ptrAliases = initTable[string, string]() # Add parameters to varTypeExprs after clearing so they are visible in the body. for p in funcParams: if p.ptype != nil: @@ -2279,10 +2482,14 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc = hasReturn = true if not hasReturn: for i in countdown(ctx.deferStmts.len - 1, 0): - if not ctx.shouldSkipDrop(ctx.deferStmts[i], ""): - body.blockStmts.add(ctx.deferStmts[i]) - ctx.deferStmts = oldDefers - ctx.movedOutLocals = oldMovedOut + ctx.emitDropOrPartial(body.blockStmts, ctx.deferStmts[i], "") + # Always restore — mono of generics (generateMethodInstance → lowerFunc) nests + # inside an outer function. Restoring only when deferStmts.len > 0 wiped the + # caller's Drop stack (PeekTagAndTake lost Array_Drop after Array_Len mono). + ctx.deferStmts = oldDefers + ctx.movedOutLocals = oldMovedOut + ctx.partialMovedFields = oldPartialMoved + ctx.ptrAliases = oldPtrAliases ctx.currentFuncDecl = oldFuncDecl ctx.currentFuncRetType = oldFuncRetType diff --git a/bootstrap/macroexpand.nim b/bootstrap/macroexpand.nim index f5463c8..4fd55d5 100644 --- a/bootstrap/macroexpand.nim +++ b/bootstrap/macroexpand.nim @@ -3,7 +3,7 @@ ## Hygiene: substitute clones args at call-site, graft call-site SourceLocation ## onto expanded template nodes (Ast_QuoteCallSite policy from QUALITY_PLAN). -import std/[tables, sequtils, sets] +import std/[tables, sequtils, sets, strutils] import ast, token, source_location type @@ -193,9 +193,16 @@ proc cloneExpr*(e: Expr): Expr = captureCount: 0, captureNames: @[], captureTypeKinds: @[]) of ekMacroCall: result = Expr(kind: ekMacroCall, loc: e.loc, - exprMacroName: e.exprMacroName, exprMacroArgs: @[]) + exprMacroName: e.exprMacroName, exprMacroArgs: @[], + exprMacroGroupLens: e.exprMacroGroupLens) for a in e.exprMacroArgs: result.exprMacroArgs.add(cloneExpr(a)) + of ekMacroStmt: + result = Expr(kind: ekMacroStmt, loc: e.loc, + exprMacroStmt: cloneStmt(e.exprMacroStmt)) + of ekMacroPat: + result = Expr(kind: ekMacroPat, loc: e.loc, + exprMacroPat: clonePattern(e.exprMacroPat)) proc cloneStmt*(s: Stmt): Stmt = if s == nil: return nil @@ -329,6 +336,10 @@ proc graftExprLoc(e: Expr, loc: SourceLocation) = of ekClosure: graftBlockLoc(e.exprClosureBody, loc) of ekMacroCall: for a in e.exprMacroArgs: graftExprLoc(a, loc) + of ekMacroStmt: + graftStmtLoc(e.exprMacroStmt, loc) + of ekMacroPat: + discard else: discard proc graftStmtLoc(s: Stmt, loc: SourceLocation) = @@ -530,6 +541,46 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr proc substStmt(s: Stmt, env: MacroEnv, callLoc: SourceLocation): Stmt proc substBlock(b: Block, env: MacroEnv, callLoc: SourceLocation): Block proc substStmtsFlat(stmts: seq[Stmt], env: MacroEnv, callLoc: SourceLocation): seq[Stmt] +proc substPattern(p: Pattern, env: MacroEnv, callLoc: SourceLocation): Pattern + +proc substPattern(p: Pattern, env: MacroEnv, callLoc: SourceLocation): Pattern = + ## Substitute `$p:pat` (pkIdent `$name`) with the bound pattern. + if p == nil: return nil + if p.kind == pkIdent and env.singles.hasKey(p.patIdent): + let bound = env.singles[p.patIdent] + if bound != nil and bound.kind == ekMacroPat: + result = clonePattern(bound.exprMacroPat) + if result != nil: result.loc = callLoc + return + case p.kind + of pkRange: + result = Pattern(kind: pkRange, loc: callLoc, + patRangeLo: substPattern(p.patRangeLo, env, callLoc), + patRangeHi: substPattern(p.patRangeHi, env, callLoc), + patRangeInclusive: p.patRangeInclusive) + of pkEnum: + result = Pattern(kind: pkEnum, loc: callLoc, patEnumPath: p.patEnumPath, + patEnumArgs: @[], patEnumNamed: @[]) + for a in p.patEnumArgs: + result.patEnumArgs.add(substPattern(a, env, callLoc)) + for nf in p.patEnumNamed: + result.patEnumNamed.add((nf.name, substPattern(nf.pattern, env, callLoc))) + of pkStruct: + result = Pattern(kind: pkStruct, loc: callLoc, patStructName: p.patStructName, + patStructFields: @[]) + for f in p.patStructFields: + result.patStructFields.add((f.name, substPattern(f.pattern, env, callLoc))) + of pkTuple: + result = Pattern(kind: pkTuple, loc: callLoc, patTupleElements: @[]) + for el in p.patTupleElements: + result.patTupleElements.add(substPattern(el, env, callLoc)) + of pkGuarded: + result = Pattern(kind: pkGuarded, loc: callLoc, + patGuardedInner: substPattern(p.patGuardedInner, env, callLoc), + patGuardedExpr: substExpr(p.patGuardedExpr, env, callLoc)) + else: + result = clonePattern(p) + if result != nil: result.loc = callLoc proc substBlock(b: Block, env: MacroEnv, callLoc: SourceLocation): Block = if b == nil: return nil @@ -611,6 +662,14 @@ proc substStmtsFlat(stmts: seq[Stmt], env: MacroEnv, callLoc: SourceLocation): s if body != nil: for st in body.stmts: result.add(st) + elif s.kind == skExpr and s.stmtExpr != nil and s.stmtExpr.kind == ekIdent and + env.singles.hasKey(s.stmtExpr.exprIdent): + let bound = env.singles[s.stmtExpr.exprIdent] + if bound != nil and bound.kind == ekMacroStmt: + # Splice `$s:stmt` as a real statement (not an expression) + result.add(substStmt(bound.exprMacroStmt, env, callLoc)) + else: + result.add(substStmt(s, env, callLoc)) else: result.add(substStmt(s, env, callLoc)) @@ -659,7 +718,7 @@ proc substStmt(s: Stmt, env: MacroEnv, callLoc: SourceLocation): Stmt = c.stmtMatchSubject = substExpr(c.stmtMatchSubject, env, callLoc) var arms: seq[MatchArm] = @[] for arm in c.stmtMatchArms: - arms.add(MatchArm(loc: callLoc, pattern: arm.pattern, + arms.add(MatchArm(loc: callLoc, pattern: substPattern(arm.pattern, env, callLoc), body: substExpr(arm.body, env, callLoc))) c.stmtMatchArms = arms of skReturn: @@ -775,7 +834,7 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr = c.exprMatchSubject = substExpr(c.exprMatchSubject, env, callLoc) var arms: seq[MatchArm] = @[] for arm in c.exprMatchArms: - arms.add(MatchArm(loc: callLoc, pattern: arm.pattern, + arms.add(MatchArm(loc: callLoc, pattern: substPattern(arm.pattern, env, callLoc), body: substExpr(arm.body, env, callLoc))) c.exprMatchArms = arms of ekStringInterp: @@ -871,14 +930,89 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl], if f.kinds.len > 0: return f.kinds @[f.kind] + proc exprToPattern(arg: Expr): Pattern = + ## Convert a call-site expression into a pattern for `$p:pat`. + if arg == nil: return nil + if arg.kind == ekMacroPat: return clonePattern(arg.exprMacroPat) + case arg.kind + of ekIdent: + if arg.exprIdent == "_": + return Pattern(kind: pkWildcard, loc: arg.loc) + return Pattern(kind: pkIdent, loc: arg.loc, patIdent: arg.exprIdent) + of ekLiteral: + return Pattern(kind: pkLiteral, loc: arg.loc, patLit: arg.exprLit) + of ekPath: + return Pattern(kind: pkEnum, loc: arg.loc, patEnumPath: arg.exprPath, + patEnumArgs: @[], patEnumNamed: @[]) + of ekCall: + # Enum::Variant(args) or Variant(args) + var path: seq[string] = @[] + if arg.exprCallCallee == nil: return nil + case arg.exprCallCallee.kind + of ekIdent: path = @[arg.exprCallCallee.exprIdent] + of ekPath: path = arg.exprCallCallee.exprPath + else: return nil + var pargs: seq[Pattern] = @[] + for a in arg.exprCallArgs: + let ap = exprToPattern(a) + if ap == nil: return nil + pargs.add(ap) + return Pattern(kind: pkEnum, loc: arg.loc, patEnumPath: path, + patEnumArgs: pargs, patEnumNamed: @[]) + of ekTuple: + var elems: seq[Pattern] = @[] + for el in arg.exprTupleElements: + let ep = exprToPattern(el) + if ep == nil: return nil + elems.add(ep) + return Pattern(kind: pkTuple, loc: arg.loc, patTupleElements: elems) + of ekStructInit: + var fields: seq[tuple[name: string, pattern: Pattern]] = @[] + for f in arg.exprStructInitFields: + let fp = exprToPattern(f.value) + if fp == nil: return nil + fields.add((f.name, fp)) + return Pattern(kind: pkStruct, loc: arg.loc, + patStructName: arg.exprStructInitName, patStructFields: fields) + of ekRange: + let lo = exprToPattern(arg.exprRangeLo) + let hi = exprToPattern(arg.exprRangeHi) + if lo == nil or hi == nil: return nil + return Pattern(kind: pkRange, loc: arg.loc, patRangeLo: lo, patRangeHi: hi, + patRangeInclusive: arg.exprRangeInclusive) + else: + return nil + + proc coerceArg(k: MacroFragKind, arg: Expr): Expr = + ## Normalize arg for storage (pat → ekMacroPat). Returns nil if kind fails. + if arg == nil: return nil + case k + of mfkIdent: + if arg.kind != ekIdent: return nil + return arg + of mfkLiteral: + if arg.kind != ekLiteral: return nil + return arg + of mfkBlock: + if arg.kind != ekBlock: return nil + return arg + of mfkStmt: + if arg.kind == ekMacroStmt: return arg + # Expression as expression-statement + if arg.kind in {ekMacroPat}: return nil + return Expr(kind: ekMacroStmt, loc: arg.loc, + exprMacroStmt: Stmt(kind: skExpr, loc: arg.loc, stmtExpr: arg)) + of mfkPat: + let pat = exprToPattern(arg) + if pat == nil: return nil + return Expr(kind: ekMacroPat, loc: arg.loc, exprMacroPat: pat) + of mfkExpr, mfkTt: + if arg.kind in {ekMacroStmt, ekMacroPat}: return nil + return arg + proc fragMatches(k: MacroFragKind, arg: Expr): bool = ## Kind constraint at match time (after arg expand). - if arg == nil: return false - case k - of mfkIdent: arg.kind == ekIdent - of mfkLiteral: arg.kind == ekLiteral - of mfkBlock: arg.kind == ekBlock - of mfkExpr, mfkTt: true + coerceArg(k, arg) != nil var matched: MacroRule var env: MacroEnv @@ -916,10 +1050,11 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl], for c in 0 ..< chunk: let arg = g[i + c] let k = if c < ks.len: ks[c] else: mfkExpr - if not fragMatches(k, arg): + let coerced = coerceArg(k, arg) + if coerced == nil: failed = true break - e.lists[ns[c]].add(arg) + e.lists[ns[c]].add(coerced) if failed: break i += chunk else: @@ -930,10 +1065,11 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl], for c in 0 ..< chunk: let arg = flat[ai] let k = if c < ks.len: ks[c] else: mfkExpr - if not fragMatches(k, arg): + let coerced = coerceArg(k, arg) + if coerced == nil: failed = true break - e.lists[ns[c]].add(arg) + e.lists[ns[c]].add(coerced) inc ai if failed: break else: @@ -954,11 +1090,12 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl], arg = flat[ai] inc ai let k = if ks.len > 0: ks[0] else: frag.kind - if not fragMatches(k, arg): + let coerced = coerceArg(k, arg) + if coerced == nil: failed = true break let n = if ns.len > 0: ns[0] else: frag.name - e.singles[n] = arg + e.singles[n] = coerced if not failed: if useGroups: diff --git a/bootstrap/parser.nim b/bootstrap/parser.nim index 200ab30..3c4735e 100644 --- a/bootstrap/parser.nim +++ b/bootstrap/parser.nim @@ -429,6 +429,26 @@ proc parseAssign(p: var Parser): Expr proc parseExpr(p: var Parser): Expr = p.parseAssign() +proc isMacroStmtStart(p: Parser): bool = + ## Keywords that begin a statement (for `$s:stmt` call-site args). + p.peek() in {tkLet, tkVar, tkIf, tkWhile, tkFor, tkLoop, tkMatch, tkReturn, + tkBreak, tkContinue, tkDefer, tkSwitch, tkDo} + +proc parseMacroArg(p: var Parser): Expr = + ## Macro call argument: + ## - statement keywords → ekMacroStmt + ## - `_` / pattern-only starts → ekMacroPat (also `$p:pat` from expr via coerce) + ## - else expression + let loc = p.currentLoc + if p.isMacroStmtStart(): + let st = p.parseStmt() + return Expr(kind: ekMacroStmt, loc: loc, exprMacroStmt: st) + # Wildcard is not a valid expression; parse as pattern for `$p:pat` + if p.check(tkUnderscore): + let pat = p.parsePattern() + return Expr(kind: ekMacroPat, loc: loc, exprMacroPat: pat) + p.parseExpr() + proc parseStringInterpolation(p: var Parser, tok: Token): Expr = ## Parse a string literal that contains {expr} interpolations. let text = tok.text @@ -749,7 +769,7 @@ proc parsePostfix(p: var Parser): Expr = curGroup = 0 p.skipNewlines() continue - margs.add(p.parseExpr()) + margs.add(p.parseMacroArg()) inc curGroup p.skipNewlines() if p.check(tkComma): @@ -1628,10 +1648,12 @@ proc parseMacroFragKind(p: var Parser, kindTok: Token): MacroFragKind = of "tt": mfkTt of "literal", "lit": mfkLiteral of "block": mfkBlock + of "stmt": mfkStmt + of "pat", "pattern": mfkPat else: p.emitError(kindTok.loc, "unsupported macro fragment kind '" & kindTok.text & - "' (expr|ident|tt|literal|block)") + "' (expr|ident|tt|literal|block|stmt|pat)") mfkExpr proc parseMacroFragment(p: var Parser): MacroFragment = @@ -1640,7 +1662,7 @@ proc parseMacroFragment(p: var Parser): MacroFragment = if not fragTok.text.startsWith("$"): p.emitError(fragTok.loc, "macro fragment must start with '$' (e.g. $x:expr)") discard p.expect(tkColon, "expected ':' after macro fragment name") - let kindTok = p.expect(tkIdent, "expected fragment kind (expr|ident|tt|literal|block)") + let kindTok = p.expect(tkIdent, "expected fragment kind (expr|ident|tt|literal|block|stmt|pat)") let k = p.parseMacroFragKind(kindTok) result = MacroFragment( name: fragTok.text, diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index a110b4d..63210e8 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -1902,6 +1902,10 @@ proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type = # Should have been expanded before analyze; leftover is a compiler bug sema.emitError(expr.loc, "unexpanded macro call '" & expr.exprMacroName & "!'") return makeUnknown() + of ekMacroStmt, ekMacroPat: + # Expand-only wrappers; must not reach type-checking + sema.emitError(expr.loc, "internal: unexpanded macro stmt/pat fragment") + return makeUnknown() of ekClosure: let savedRetType = sema.currentRetType let savedClosureDepth = sema.closureDepth diff --git a/docs/BuildAndTest.md b/docs/BuildAndTest.md index a4e284c..e8014f8 100644 --- a/docs/BuildAndTest.md +++ b/docs/BuildAndTest.md @@ -209,7 +209,7 @@ make test # full sequential suite (local) | `apps` | ubuntu | `test-apps` | | `selfhost` | ubuntu | `test-selfhost-smoke` | | `macos` | macos-14 | rebuild + `test-unit` + `test-examples-smoke` (subset) | -| `windows` | windows-latest | rebuild `buxc.exe` + pure Nim unit tests + CLI smoke | +| `windows` | windows-latest | `buxc.exe` + Nim unit tests + CLI + **MinGW `hello`** | | `ci-gate` | ubuntu | fails if any required job failed (branch protection) | **CI speed helpers:** @@ -217,8 +217,9 @@ make test # full sequential suite (local) Windows uses a prebuilt Nim zip) - Project-local `nimcache/` via `NIMFLAGS=--nimcache:nimcache`, cached per job by source hash - macOS skips full EXAMPLES (Linux already runs them) and skips `fmt-check` (Linux unit job) -- **Windows** does **not** run `bux run` examples yet: `rt/runtime.c` needs POSIX - (`ucontext`, `pthread`, BSD sockets). Smoke still validates bootstrap + unit tests on Win. +- **Windows** runs `tools/smoke_windows_hello.sh` with **MinGW gcc** + `rt/runtime_win.c` + (no pthread/OpenSSL/ucontext). Full POSIX runtime (`rt/runtime.c`) remains Unix-only. + Locally on Linux/macOS: `BUX_RUNTIME=win ./tools/smoke_windows_hello.sh`. Parallel Linux jobs set `BUX_SKIP_BUILD=1` after downloading the `buxc` artifact. Locally, `make test` still runs the full suite sequentially and builds once. @@ -330,7 +331,8 @@ bux/ │ ├── Task.bux │ └── Channel.bux ├── rt/ # C runtime -│ ├── runtime.c +│ ├── runtime.c # full POSIX + OpenSSL (Unix) +│ ├── runtime_win.c # MinGW minimal (Windows / BUX_RUNTIME=win) │ └── io.c ├── examples/ # Example programs ├── tests/ # Unit tests (Nim) diff --git a/docs/LanguageRef.md b/docs/LanguageRef.md index 05fda37..1892669 100644 --- a/docs/LanguageRef.md +++ b/docs/LanguageRef.md @@ -827,10 +827,59 @@ Rules: etc.). Reading `bag.tag` (`int`) does **not** mark `bag` moved. - After `let moved = bag.items`, `Bag_Drop(&bag)` is skipped; `moved` owns the array and is auto-dropped at scope end. -- Avoid using other droppable fields of the parent after a partial move (they - may be left in a moved-from state without per-field Drop). +- **Remaining fields:** if the parent has other droppable fields that were *not* + moved out, those still run their `Type_Drop` / collection Drop (session 70). + Example: move `pair.left` → skip `PairBag_Drop`, still `Tracked_Drop(&pair.right)`. -Golden smoke: `make test-drop-move` / `examples/move_field_partial.bux`. +```bux +@[Drop] +struct PairBag { + left: Array, + right: Tracked, // also @[Drop] +} +func TakeLeft() -> Array { + let pair: PairBag = …; + return pair.left; // Tracked_Drop(&pair.right) still runs +} +``` + +#### Nested path moves (`a.b.c`) + +Moving a **deep** droppable field also works. The full dotted path is recorded +so remaining fields at every level still Drop: + +```bux +@[Drop] +struct Outer { + inner: Inner, // Inner has items: Array + note: Tracked + tag: Tracked, +} +func TakeNested() -> Array { + let outer: Outer = …; + return outer.inner.items; + // skips Outer_Drop + // still: Tracked_Drop(&outer.inner.note) + Tracked_Drop(&outer.tag) +} +``` + +#### Field moves through pointers + +When a local pointer aliases a local owner (`let p = &bag`), moving a field +through the pointer marks the **owner**, not the pointer: + +```bux +let bag: Bag = …; +let p: *Bag = &bag; +return p.items; // same as (*p).items +// skips Bag_Drop; still Tracked_Drop(&bag.tag) +``` + +Nested paths work the same: `p.inner.items` resolves `p → outer` then path +`inner.items`. + +Golden smoke: `make test-drop-move` / `examples/move_field_partial.bux` / +`examples/move_field_remaining.bux` / `examples/move_field_nested.bux` / +`examples/move_field_ptr.bux`. #### Manual Drop and non-Drop types @@ -841,9 +890,11 @@ Golden smoke: `make test-drop-move` / `examples/move_field_partial.bux`. #### Limits (honest) -- Partial field moves mark the **whole parent local** as moved for Drop purposes - (not per-field Drop of remaining fields). -- Nested `a.b.c` path moves and moving through pointers are limited. +- Partial field moves skip the **parent** `Type_Drop` and drop **remaining** + droppable fields individually, including nested paths `a.b.c` and pointer + aliases `p = &owner` (sessions 70/73/74). +- Pointer aliases are tracked for **local** `p = &local` only (not parameters + that point at caller-owned data across function boundaries). - Interface Drop uses a static `TypeName_Drop` symbol (zero cost), not dynamic dispatch through a vtable. - Double-free bugs in **unchecked** code that manually free *and* auto-drop are @@ -1174,6 +1225,25 @@ macro! wrap_block { ( $b:block ) => { $b } } +// stmt — one statement (let/if/… or expression-statement) +macro! with_setup { + ( $s:stmt, $body:expr ) => { + { + $s + $body + } + } +} +// call: with_setup!(let x: int = 10, x + 1) + +// pat — match/let pattern (literals, `_`, enum variants, …) +macro! matches { + ( $p:pat, $e:expr ) => { + match $e { $p => 1, _ => 0 } + } +} +// call: matches!(1, 1) · matches!(_, 99) · matches!(Opt::Some(v), opt) + // gensym: template locals renamed per expansion macro! with_acc { ( $start:literal ) => { @@ -1195,6 +1265,8 @@ macro! with_acc { | `tt` | token-tree (MVP: same as `expr`) | | `literal` / `lit` | int/float/string/char/bool literal only | | `block` | block expression `{ … }` | + | `stmt` | one statement (`let`/`if`/… or expression-stmt) | + | `pat` / `pattern` | match pattern (`_`, literals, `Enum::Var(…)`, …) | - Fragment names start with `$` (lexer `$ident`). - **Repetition:** `$( $x:expr ),*` / `$( $x:expr )*` — one or more rep fragments per pattern. diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 525fdeb..be4b25f 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -1,7 +1,7 @@ # Bux — План към „добър“ език (v0.5 → v1.0) -> **Дата:** 2026-07-20 -> **Текущо:** v0.5.x — macros (unhygienic binders + multi-rep), partial field-move, lean CI +> **Дата:** 2026-07-21 +> **Текущо:** v0.5.x — macros, field-move via pointers, Windows hello > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -14,7 +14,7 @@ | Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ | | HIR → C | Tuples + fat `func` ABI в bootstrap **и** selfhost | ★★★★☆ | | Selfhost (`src/`) | ~12k LOC, binary-identical loop, closures+tuples | ★★★★★ | -| Gradual ownership | `@[Checked]`, move, Drop, elision, **field-move skip Drop** | ★★★★★ | +| Gradual ownership | `@[Checked]`, move, Drop, elision, **field-move + remaining-field Drop** | ★★★★★ | | Concurrency | M:N tasks + channels + async | ★★★★☆ | | Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ | | Tooling | LSP 0.5 hover/def/outline/**refs/rename** + fmt/test/doc | ★★★★★ | @@ -1117,8 +1117,98 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) --- +## Сесия 70 (per-field Drop + mono defer restore) + +1. **Bug (critical):** `lowerFunc` restored `deferStmts` / `movedOutLocals` only + when the *inner* mono function still had pending defers. Nested + `generateMethodInstance` → `lowerFunc` (e.g. `Array_Len` inside + `PeekTagAndTake`) wiped the caller's Drop stack → leaked moved Arrays. +2. **Fix bootstrap:** always restore `deferStmts` / `movedOutLocals` / + `partialMovedFields` after lowering a function body. +3. **Per-field Drop after partial move:** + - Track `partialMovedFields: local → {field names}` + - Skip parent `Type_Drop`; emit Drop for **remaining** droppable fields + - `emitDropOrPartial` at return / block exit / function tail +4. **Selfhost CBE** (`c_backend.bux`): + - partial (var, field) slots + local type registry on `hAlloca` + - `CBE_EmitRemainingFieldDrops` when skipping moved parent Drop +5. **Example + smoke:** + - `examples/move_field_remaining.bux` (PairBag left move → Tracked_Drop right) + - `tools/smoke_drop_move.sh` checks PeekTag Array_Drop + remaining Tracked_Drop +6. **LanguageRef:** remaining-field rule; limits updated +7. Verified: bootstrap + **buxc2** remaining/partial/move_field; smoke; EXAMPLES + +--- + +## Сесия 71 (Windows MinGW + `hello` smoke) + +1. **`rt/runtime_win.c`** — minimal runtime without pthread / ucontext / sockets / + OpenSSL. Real alloc, strings, files, time, env; stubs for tasks/crypto/net. +2. **Bootstrap CLI** (`bootstrap/cli.nim`): + - Windows (or `BUX_RUNTIME=win`) copies `runtime_win.c` instead of `runtime.c` + - Link: `-ffunction-sections -Wl,--gc-sections -lm` (no `-pthread` / `-lcrypto`) + - Host `gcc` on Windows; `.exe` suffix on build/run + - Fixed: `-l` libs **after** `.c` inputs (GNU ld order) +3. **CI** (`.github/workflows/ci.yml` windows job): + - MinGW via `msys2/setup-msys2` (`mingw-w64-x86_64-gcc`) + - `tools/smoke_windows_hello.sh` after unit/CLI smoke +4. **Docs:** BuildAndTest CI table + `rt/` tree +5. Verified locally: normal `hello` + `BUX_RUNTIME=win` smoke PASS + +--- + +## Сесия 72 (macro `stmt` / `pat` fragments) + +1. **Kinds:** `mfkStmt` / `mfkPat` (+ aliases `pattern`, `lit` already) +2. **AST wrappers:** `ekMacroStmt` / `ekMacroPat` (expand-only) +3. **Call-site parse:** + - stmt keywords → `parseStmt` → MacroStmt + - `_` → `parsePattern` → MacroPat + - else expr; `pat` coerces via `exprToPattern` (ident/lit/path/call/tuple/struct/range) +4. **Expand:** + - `coerceArg` at match; store normalized MacroStmt/MacroPat + - `$s` as skExpr splices MacroStmt into the statement list + - `$p` as pkIdent pattern substitutes bound MacroPat +5. **Selfhost:** same kinds, coerce, splice, pattern subst +6. **Example:** `examples/macro_stmt_pat.bux` — setup/do_twice/matches/if_let_like +7. LanguageRef kind table + docs +8. Verified: bootstrap + **buxc2** `macro_stmt_pat` PASS + +--- + +## Сесия 73 (nested `a.b.c` field-move Drop) + +1. **Bootstrap** (`hir_lower.nim`): + - `fieldPathFromAst` → base local + path `@["inner","items"]` + - `partialMovedFields` stores **dotted paths** (`"inner.items"`) + - `remainingDropsAt` recursive: exact path = skip; prefix = recurse; + other droppable fields → `Type_Drop(&(base.a.b))` + - Typed intermediate `hFieldAccess` so LIR/C keep `Inner` not `int` +2. **Selfhost CBE:** full dotted path on mark; recursive `CBE_EmitRemainingAt` +3. **Example:** `examples/move_field_nested.bux` — `outer.inner.items` → 2 Tracked drops +4. Smoke + EXAMPLES; LanguageRef nested path section +5. Selfhost: recursive remaining drops + skip Drop when `HasPartialMoved` + (struct emit multi-pass topo for Outer{Inner}) +6. Verified: bootstrap + **buxc2** `nested_drops=4` PASS; full `test-drop-move` + +--- + +## Сесия 74 (field moves through pointers) + +1. **Pointer aliases:** `let p = &bag` / `p = &bag` → `ptrAliases[p] = bag` +2. **fieldPathFromAst:** peel `(*p)` (ekUnary tkStar); resolve alias to owner +3. **`p.field`** (auto-deref) and **`(*p).field`** mark owner + path +4. Nested via ptr: `p.inner.items` → owner + `"inner.items"` +5. Selfhost CBE: alias slots + resolve in `CBE_BaseVarName`; record on store/assign +6. **Example:** `examples/move_field_ptr.bux` → `ptr_drops=5` +7. Smoke + LanguageRef; limits: local aliases only (not cross-function params) +8. Selfhost: unary C parens fix `(*p).field`; alias slots + BaseVar resolve +9. Verified: bootstrap + **buxc2** `ptr_drops=5` PASS; full `test-drop-move` + +--- + ## Следващи стъпки -1. Windows: MinGW + runtime stubs for `hello` smoke (stretch) -2. Per-field Drop after partial move (stretch) -3. Macro: true `stmt`/`pat` token-tree frags (stretch) +1. Windows: more examples (strings/ownership) on MinGW; optional Win OpenSSL +2. Macro: true token-tree `tt` / nested pattern rewrite depth +3. Cross-function pointer ownership transfer (callee `*Bag` param) diff --git a/examples/macro_stmt_pat.bux b/examples/macro_stmt_pat.bux new file mode 100644 index 0000000..d5e51f4 --- /dev/null +++ b/examples/macro_stmt_pat.bux @@ -0,0 +1,84 @@ +// Session 72 — macro fragment kinds `stmt` and `pat` +import Std::Io::{PrintLine}; +import Std::String::{String_FromInt}; +import Std::Test::{Test_AssertTrue, Test_Pass}; + +// stmt: splice a full statement (let / assign) into the template +macro! with_setup { + ( $s:stmt, $body:expr ) => { + { + $s + $body + } + } +} + +// stmt from an expression-statement (assign) +macro! do_twice { + ( $s:stmt ) => { + { + $s + $s + 0 + } + } +} + +// pat: match arm pattern from call-site +macro! matches { + ( $p:pat, $e:expr ) => { + match $e { + $p => 1, + _ => 0 + } + } +} + +// Combined: extract payload when pattern matches +macro! if_let_like { + ( $p:pat, $e:expr, $then:expr ) => { + match $e { + $p => $then, + _ => -1 + } + } +} + +enum Opt { + Some(int), + None +} + +func Main() -> int { + // with_setup: inject `let x = 10` then use x + let a: int = with_setup!(let x: int = 10, x + 1); + Test_AssertTrue(a == 11); + + // do_twice: run assign twice + var n: int = 0; + discard do_twice!(n = n + 1); + Test_AssertTrue(n == 2); + + // matches: literal / wildcard / enum (bind results — match-as-arg is fragile on buxc2) + let m1: int = matches!(1, 1); + let m2: int = matches!(2, 1); + let m3: int = matches!(_, 99); + Test_AssertTrue(m1 == 1); + Test_AssertTrue(m2 == 0); + Test_AssertTrue(m3 == 1); + + var o1: Opt = Opt { tag: Opt_Some }; + o1.data.Some_0 = 7; + let o2: Opt = Opt { tag: Opt_None }; + let e1: int = if_let_like!(Opt::Some(v), o1, v); + let e2: int = if_let_like!(Opt::Some(v), o2, v); + let e3: int = matches!(Opt::None, o2); + Test_AssertTrue(e1 == 7); + Test_AssertTrue(e2 == -1); + Test_AssertTrue(e3 == 1); + + PrintLine(String_FromInt(a)); + PrintLine(String_FromInt(n)); + Test_Pass("macro_stmt_pat"); + return 0; +} diff --git a/examples/move_field_nested.bux b/examples/move_field_nested.bux new file mode 100644 index 0000000..b1394ab --- /dev/null +++ b/examples/move_field_nested.bux @@ -0,0 +1,91 @@ +// Session 73 — nested field path moves (`outer.inner.items`) +// Moving a deep droppable field skips root Drop and still drops remaining +// fields at every level of the path. +import Std::Io::{PrintLine}; +import Std::Array::{Array, Array_New, Array_Push, Array_Len, Array_Get}; +import Std::String::{String_FromInt, String_Concat}; +import Std::Test::{Test_AssertTrue, Test_Pass}; + +@[Drop] +struct Tracked { + id: int, + counter: *int +} + +func Tracked_Drop(self: *Tracked) { + if self.counter != null as *int { + *self.counter = *self.counter + 1; + } +} + +@[Drop] +struct Inner { + items: Array, + note: Tracked +} + +func Inner_Drop(self: *Inner) { + Array_Drop(&self.items); + Tracked_Drop(&self.note); +} + +@[Drop] +struct Outer { + inner: Inner, + tag: Tracked +} + +func Outer_Drop(self: *Outer) { + Inner_Drop(&self.inner); + Tracked_Drop(&self.tag); +} + +// Move outer.inner.items — must Drop: outer.inner.note + outer.tag +// (not Outer_Drop whole, not Array double-free) +func TakeNestedItems(counter: *int) -> Array { + var items: Array = Array_New(2); + Array_Push(&items, 10); + Array_Push(&items, 20); + let outer: Outer = Outer { + inner: Inner { + items: items, + note: Tracked { id: 1, counter: counter } + }, + tag: Tracked { id: 2, counter: counter } + }; + return outer.inner.items; +} + +// let-move nested path +func PeekAfterNestedTake(counter: *int) -> int { + var items: Array = Array_New(1); + Array_Push(&items, 7); + let outer: Outer = Outer { + inner: Inner { + items: items, + note: Tracked { id: 3, counter: counter } + }, + tag: Tracked { id: 4, counter: counter } + }; + let moved: Array = outer.inner.items; + return Array_Len(&moved) as int; +} + +func Main() -> int { + var drops: int = 0; + + let taken: Array = TakeNestedItems(&drops); + Test_AssertTrue(Array_Len(&taken) == 2); + Test_AssertTrue(Array_Get(&taken, 0) == 10); + // TakeNestedItems: note(id1) + tag(id2) → 2 Tracked drops + Test_AssertTrue(drops == 2); + + let n: int = PeekAfterNestedTake(&drops); + Test_AssertTrue(n == 1); + // +2 more Tracked drops + Test_AssertTrue(drops == 4); + + PrintLine(String_Concat("nested_drops=", String_FromInt(drops as int64))); + Test_Pass("move_field_nested"); + return 0; +} diff --git a/examples/move_field_ptr.bux b/examples/move_field_ptr.bux new file mode 100644 index 0000000..e5b2ce9 --- /dev/null +++ b/examples/move_field_ptr.bux @@ -0,0 +1,131 @@ +// Session 74 — field moves through pointers (`p.field` / `(*p).field`) +// When `p = &bag`, moving `p.items` marks the owner `bag` (not the pointer). +import Std::Io::{PrintLine}; +import Std::Array::{Array, Array_New, Array_Push, Array_Len, Array_Get}; +import Std::String::{String_FromInt, String_Concat}; +import Std::Test::{Test_AssertTrue, Test_Pass}; + +@[Drop] +struct Tracked { + id: int, + counter: *int +} + +func Tracked_Drop(self: *Tracked) { + if self.counter != null as *int { + *self.counter = *self.counter + 1; + } +} + +@[Drop] +struct Bag { + items: Array, + tag: Tracked +} + +func Bag_Drop(self: *Bag) { + Array_Drop(&self.items); + Tracked_Drop(&self.tag); +} + +// p.items via auto-deref of *Bag +func TakeViaPtr(counter: *int) -> Array { + var items: Array = Array_New(2); + Array_Push(&items, 10); + Array_Push(&items, 20); + let bag: Bag = Bag { + items: items, + tag: Tracked { id: 1, counter: counter } + }; + let p: *Bag = &bag; + return p.items; +} + +// Explicit (*p).items +func TakeViaDeref(counter: *int) -> Array { + var items: Array = Array_New(1); + Array_Push(&items, 7); + let bag: Bag = Bag { + items: items, + tag: Tracked { id: 2, counter: counter } + }; + let p: *Bag = &bag; + return (*p).items; +} + +// let-move through pointer; remaining tag still Drops +func PeekAfterPtrTake(counter: *int) -> int { + var items: Array = Array_New(1); + Array_Push(&items, 3); + let bag: Bag = Bag { + items: items, + tag: Tracked { id: 3, counter: counter } + }; + var p: *Bag = &bag; + let moved: Array = p.items; + return Array_Len(&moved) as int; +} + +// Nested path through pointer: p.inner.items +@[Drop] +struct Inner { + items: Array, + note: Tracked +} + +func Inner_Drop(self: *Inner) { + Array_Drop(&self.items); + Tracked_Drop(&self.note); +} + +@[Drop] +struct Outer { + inner: Inner, + tag: Tracked +} + +func Outer_Drop(self: *Outer) { + Inner_Drop(&self.inner); + Tracked_Drop(&self.tag); +} + +func TakeNestedViaPtr(counter: *int) -> Array { + var items: Array = Array_New(1); + Array_Push(&items, 99); + let outer: Outer = Outer { + inner: Inner { + items: items, + note: Tracked { id: 4, counter: counter } + }, + tag: Tracked { id: 5, counter: counter } + }; + let p: *Outer = &outer; + return p.inner.items; +} + +func Main() -> int { + var drops: int = 0; + + let a: Array = TakeViaPtr(&drops); + Test_AssertTrue(Array_Len(&a) == 2); + Test_AssertTrue(Array_Get(&a, 0) == 10); + // tag Drop once + Test_AssertTrue(drops == 1); + + let b: Array = TakeViaDeref(&drops); + Test_AssertTrue(Array_Len(&b) == 1); + Test_AssertTrue(drops == 2); + + let n: int = PeekAfterPtrTake(&drops); + Test_AssertTrue(n == 1); + Test_AssertTrue(drops == 3); + + let c: Array = TakeNestedViaPtr(&drops); + Test_AssertTrue(Array_Get(&c, 0) == 99); + // note + tag → +2 + Test_AssertTrue(drops == 5); + + PrintLine(String_Concat("ptr_drops=", String_FromInt(drops as int64))); + Test_Pass("move_field_ptr"); + return 0; +} diff --git a/examples/move_field_remaining.bux b/examples/move_field_remaining.bux new file mode 100644 index 0000000..82e1e95 --- /dev/null +++ b/examples/move_field_remaining.bux @@ -0,0 +1,87 @@ +// Session 70 — per-field Drop after partial move +// Moving one droppable field out of a @[Drop] parent skips Type_Drop of the +// parent but still drops the *remaining* droppable fields. +import Std::Io::{PrintLine}; +import Std::Array::{Array, Array_New, Array_Push, Array_Len, Array_Get}; +import Std::String::{String_FromInt, String_Concat}; +import Std::Test::{Test_AssertTrue, Test_Pass}; + +@[Drop] +struct Tracked { + n: int, + counter: *int +} + +func Tracked_Drop(self: *Tracked) { + if self.counter != null as *int { + *self.counter = *self.counter + 1; + } +} + +@[Drop] +struct PairBag { + left: Array, + right: Tracked +} + +func PairBag_Drop(self: *PairBag) { + Array_Drop(&self.left); + Tracked_Drop(&self.right); +} + +// Move left out via return — right must still Drop (Tracked_Drop) +func TakeLeft(counter: *int) -> Array { + var left: Array = Array_New(2); + Array_Push(&left, 10); + Array_Push(&left, 20); + let pair: PairBag = PairBag { + left: left, + right: Tracked { n: 2, counter: counter } + }; + return pair.left; +} + +// Move left via let; right still drops at scope end +func PeekAfterTake(counter: *int) -> int { + var left: Array = Array_New(1); + Array_Push(&left, 7); + let pair: PairBag = PairBag { + left: left, + right: Tracked { n: 2, counter: counter } + }; + let moved: Array = pair.left; + return Array_Len(&moved) as int; +} + +// Whole-struct return: no PairBag_Drop here (caller owns it) +func MakePair() -> PairBag { + var left: Array = Array_New(1); + Array_Push(&left, 1); + let pair: PairBag = PairBag { + left: left, + right: Tracked { n: 9, counter: null as *int } + }; + return pair; +} + +func Main() -> int { + var drops: int = 0; + + let taken: Array = TakeLeft(&drops); + Test_AssertTrue(Array_Len(&taken) == 2); + Test_AssertTrue(Array_Get(&taken, 0) == 10); + // TakeLeft must have Dropped remaining right field once + Test_AssertTrue(drops == 1); + + let n: int = PeekAfterTake(&drops); + Test_AssertTrue(n == 1); + Test_AssertTrue(drops == 2); + + let p: PairBag = MakePair(); + Test_AssertTrue(Array_Len(&p.left) == 1); + Test_AssertTrue(p.right.n == 9); + + PrintLine(String_Concat("right_drops=", String_FromInt(drops as int64))); + Test_Pass("move_field_remaining"); + return 0; +} diff --git a/rt/runtime_win.c b/rt/runtime_win.c new file mode 100644 index 0000000..1de7e07 --- /dev/null +++ b/rt/runtime_win.c @@ -0,0 +1,733 @@ +/* Bux Runtime — Windows / MinGW minimal build (session 71) + * + * No pthread, ucontext, BSD sockets, or OpenSSL. Enough for hello and + * basic single-threaded programs. Advanced features return failure / no-op. + * + * Linked with -ffunction-sections -fdata-sections -Wl,--gc-sections so + * monomorphized stdlib in main.c that is never called is discarded. + * + * Unix full runtime remains rt/runtime.c (POSIX + OpenSSL). + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) || defined(_WIN64) +# include +# include +# include +# define BUX_IS_WIN 1 +# define bux_mkdir_one(p) _mkdir(p) +#else +# include +# include +# define BUX_IS_WIN 0 +# define bux_mkdir_one(p) mkdir((p), 0755) +#endif + +/* ── CLI args ─────────────────────────────────────────────────────────── */ +int g_argc = 0; +char** g_argv = NULL; + +int bux_argc(void) { return g_argc; } +char* bux_argv(int index) { + if (index < 0 || index >= g_argc) return ""; + return g_argv[index]; +} + +/* ── Memory ───────────────────────────────────────────────────────────── */ +void* bux_alloc(size_t size) { + void* ptr = calloc(1, size); + if (ptr == NULL && size > 0) { + fprintf(stderr, "bux runtime: out of memory (alloc %zu)\n", size); + abort(); + } + return ptr; +} +void* bux_realloc(void* ptr, size_t size) { + void* p = realloc(ptr, size); + if (p == NULL && size > 0) { + fprintf(stderr, "bux runtime: out of memory (realloc %zu)\n", size); + abort(); + } + return p; +} +void bux_free(void* ptr) { free(ptr); } + +/* ── Basic I/O / panic ────────────────────────────────────────────────── */ +void bux_print(const char* s) { if (s) fputs(s, stdout); fflush(stdout); } +void bux_println(const char* s) { if (s) puts(s); else puts(""); fflush(stdout); } +void bux_print_int(int64_t n) { printf("%lld", (long long)n); } +void bux_print_float(double f) { printf("%g", f); } +void bux_print_bool(bool b) { fputs(b ? "true" : "false", stdout); } +void bux_print_char(char c) { fputc(c, stdout); } +void bux_panic(const char* msg) { + fprintf(stderr, "PANIC: %s\n", msg ? msg : ""); + abort(); +} +void bux_exit(int code) { exit(code); } +void bux_assert(int cond, const char* file, int line, const char* expr) { + if (!cond) { + fprintf(stderr, "ASSERT FAILED: %s at %s:%d\n", + expr ? expr : "?", file ? file : "?", line); + exit(1); + } +} + +/* ── Checked arithmetic (same semantics as full runtime) ──────────────── */ +int64_t bux_div_i64(int64_t a, int64_t b) { + if (b == 0) bux_panic("division by zero"); + return a / b; +} +int64_t bux_mod_i64(int64_t a, int64_t b) { + if (b == 0) bux_panic("modulo by zero"); + return a % b; +} +int64_t bux_add_i64_checked(int64_t a, int64_t b) { return a + b; } +int64_t bux_sub_i64_checked(int64_t a, int64_t b) { return a - b; } +int64_t bux_mul_i64_checked(int64_t a, int64_t b) { return a * b; } +int64_t bux_neg_i64_checked(int64_t a) { return -a; } + +/* ── Strings ──────────────────────────────────────────────────────────── */ +unsigned int bux_strlen(const char* s) { return s ? (unsigned int)strlen(s) : 0; } +int bux_strlen_c(const char* s) { return s ? (int)strlen(s) : 0; } +int bux_strcmp(const char* a, const char* b) { + if (!a) a = ""; if (!b) b = ""; + return strcmp(a, b); +} +int bux_strncmp(const char* a, const char* b, unsigned int n) { + if (!a) a = ""; if (!b) b = ""; + return strncmp(a, b, (size_t)n); +} +char* bux_strcpy(char* dest, const char* src) { + if (!dest) return NULL; + if (!src) { dest[0] = 0; return dest; } + return strcpy(dest, src); +} +char* bux_strcat(char* dest, const char* src) { + if (!dest) return NULL; + if (!src) return dest; + return strcat(dest, src); +} +char* bux_strncpy(char* dest, const char* src, unsigned int n) { + if (!dest) return NULL; + if (!src) { if (n) dest[0] = 0; return dest; } + return strncpy(dest, src, (size_t)n); +} +double bux_str_to_float(const char* s) { return s ? atof(s) : 0.0; } +int64_t bux_str_to_int(const char* s) { return s ? (int64_t)atoll(s) : 0; } +const char* bux_strstr(const char* haystack, const char* needle) { + if (!haystack || !needle) return NULL; + return strstr(haystack, needle); +} +unsigned int bux_str_offset(const char* pos, const char* base) { + if (!pos || !base) return 0; + return (unsigned int)(pos - base); +} +int bux_str_contains(const char* haystack, const char* needle) { + if (!haystack || !needle) return 0; + return strstr(haystack, needle) != NULL; +} +int bux_str_is_null(const char* s) { return s == NULL; } + +char* bux_str_slice(const char* s, unsigned int start, unsigned int len) { + if (!s) s = ""; + unsigned int sl = (unsigned int)strlen(s); + if (start > sl) start = sl; + if (start + len > sl) len = sl - start; + char* out = (char*)bux_alloc(len + 1); + memcpy(out, s + start, len); + out[len] = 0; + return out; +} +static int is_ws(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} +char* bux_str_trim_left(const char* s) { + if (!s) s = ""; + while (*s && is_ws(*s)) s++; + unsigned int n = (unsigned int)strlen(s); + char* out = (char*)bux_alloc(n + 1); + memcpy(out, s, n + 1); + return out; +} +char* bux_str_trim_right(const char* s) { + if (!s) s = ""; + unsigned int n = (unsigned int)strlen(s); + while (n > 0 && is_ws(s[n - 1])) n--; + char* out = (char*)bux_alloc(n + 1); + memcpy(out, s, n); + out[n] = 0; + return out; +} +char* bux_str_trim(const char* s) { + char* a = bux_str_trim_left(s); + char* b = bux_str_trim_right(a); + bux_free(a); + return b; +} +char* bux_int_to_str(int64_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + unsigned int len = (unsigned int)strlen(buf); + char* out = (char*)bux_alloc(len + 1); + memcpy(out, buf, len + 1); + return out; +} +char* bux_float_to_string(double f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", f); + unsigned int len = (unsigned int)strlen(buf); + char* out = (char*)bux_alloc(len + 1); + memcpy(out, buf, len + 1); + return out; +} +unsigned int bux_str_split_count(const char* s, const char* delim) { + if (!s || !delim || !delim[0]) return 0; + unsigned int count = 1; + const char* p = s; + size_t dlen = strlen(delim); + while ((p = strstr(p, delim)) != NULL) { + count++; + p += dlen; + } + return count; +} +char* bux_str_split_part(const char* s, const char* delim, unsigned int index) { + if (!s || !delim) return (char*)bux_alloc(1); + size_t dlen = strlen(delim); + const char* start = s; + unsigned int i = 0; + while (i < index) { + const char* p = strstr(start, delim); + if (!p) return (char*)bux_alloc(1); + start = p + dlen; + i++; + } + const char* end = strstr(start, delim); + size_t len = end ? (size_t)(end - start) : strlen(start); + char* out = (char*)bux_alloc(len + 1); + memcpy(out, start, len); + out[len] = 0; + return out; +} +char* bux_str_join2(const char* a, const char* b, const char* sep) { + if (!a) a = ""; if (!b) b = ""; if (!sep) sep = ""; + size_t la = strlen(a), lb = strlen(b), ls = strlen(sep); + char* out = (char*)bux_alloc(la + ls + lb + 1); + memcpy(out, a, la); + memcpy(out + la, sep, ls); + memcpy(out + la + ls, b, lb + 1); + return out; +} +char* bux_str_format(const char* fmt, const char* a0, const char* a1, const char* a2, const char* a3) { + /* Minimal: return copy of fmt (full formatter is Unix runtime only) */ + (void)a0; (void)a1; (void)a2; (void)a3; + if (!fmt) fmt = ""; + size_t n = strlen(fmt); + char* out = (char*)bux_alloc(n + 1); + memcpy(out, fmt, n + 1); + return out; +} +char* bux_escape_c_string(const char* s, int len) { + if (!s || len <= 0) { + char* e = (char*)bux_alloc(1); + e[0] = 0; + return e; + } + char* buf = (char*)bux_alloc((size_t)len * 2 + 1); + int j = 0; + for (int i = 0; i < len; i++) { + char c = s[i]; + switch (c) { + case '\n': buf[j++] = '\\'; buf[j++] = 'n'; break; + case '\r': buf[j++] = '\\'; buf[j++] = 'r'; break; + case '\t': buf[j++] = '\\'; buf[j++] = 't'; break; + case '\\': buf[j++] = '\\'; buf[j++] = '\\'; break; + case '"': buf[j++] = '\\'; buf[j++] = '"'; break; + default: buf[j++] = c; break; + } + } + buf[j] = 0; + return buf; +} + +/* ── String builder ───────────────────────────────────────────────────── */ +typedef struct { + char* data; + unsigned int len; + unsigned int cap; +} BuxStringBuilder; + +BuxStringBuilder* bux_sb_new(unsigned int initial_cap) { + if (initial_cap < 16) initial_cap = 16; + BuxStringBuilder* sb = (BuxStringBuilder*)bux_alloc(sizeof(BuxStringBuilder)); + sb->data = (char*)bux_alloc(initial_cap); + sb->data[0] = 0; + sb->len = 0; + sb->cap = initial_cap; + return sb; +} +static void sb_ensure(BuxStringBuilder* sb, unsigned int need) { + if (sb->len + need + 1 <= sb->cap) return; + unsigned int ncap = sb->cap * 2; + while (ncap < sb->len + need + 1) ncap *= 2; + sb->data = (char*)bux_realloc(sb->data, ncap); + sb->cap = ncap; +} +void bux_sb_append(BuxStringBuilder* sb, const char* s) { + if (!sb || !s) return; + unsigned int n = (unsigned int)strlen(s); + sb_ensure(sb, n); + memcpy(sb->data + sb->len, s, n + 1); + sb->len += n; +} +void bux_sb_append_int(BuxStringBuilder* sb, int64_t n) { + char* t = bux_int_to_str(n); + bux_sb_append(sb, t); + bux_free(t); +} +void bux_sb_append_float(BuxStringBuilder* sb, double f) { + char* t = bux_float_to_string(f); + bux_sb_append(sb, t); + bux_free(t); +} +void bux_sb_append_char(BuxStringBuilder* sb, char c) { + if (!sb) return; + sb_ensure(sb, 1); + sb->data[sb->len++] = c; + sb->data[sb->len] = 0; +} +const char* bux_sb_build(BuxStringBuilder* sb) { return sb ? sb->data : ""; } +void bux_sb_free(BuxStringBuilder* sb) { + if (!sb) return; + bux_free(sb->data); + bux_free(sb); +} + +/* ── Files / paths ────────────────────────────────────────────────────── */ +char* bux_read_file(const char* path) { + if (!path) return NULL; + FILE* f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + if (sz < 0) { fclose(f); return NULL; } + char* buf = (char*)bux_alloc((size_t)sz + 1); + size_t n = fread(buf, 1, (size_t)sz, f); + buf[n] = 0; + fclose(f); + return buf; +} +int bux_write_file(const char* path, const char* content) { + if (!path) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + if (content) fputs(content, f); + fclose(f); + return 1; +} +int bux_file_exists(const char* path) { + if (!path) return 0; + FILE* f = fopen(path, "rb"); + if (!f) return 0; + fclose(f); + return 1; +} +char* bux_path_join(const char* a, const char* b) { + if (!a && !b) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + if (!a) { + size_t n = strlen(b); + char* r = (char*)bux_alloc(n + 1); + memcpy(r, b, n + 1); + return r; + } + if (!b) { + size_t n = strlen(a); + char* r = (char*)bux_alloc(n + 1); + memcpy(r, a, n + 1); + return r; + } + size_t la = strlen(a), lb = strlen(b); + int need = (la > 0 && a[la-1] != '/' && a[la-1] != '\\') ? 1 : 0; + char* r = (char*)bux_alloc(la + need + lb + 1); + memcpy(r, a, la); + if (need) r[la] = '/'; + memcpy(r + la + need, b, lb + 1); + return r; +} +char* bux_path_parent(const char* path) { + if (!path) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + int len = (int)strlen(path); + while (len > 0 && (path[len-1] == '/' || path[len-1] == '\\')) len--; + while (len > 0 && path[len-1] != '/' && path[len-1] != '\\') len--; + while (len > 0 && (path[len-1] == '/' || path[len-1] == '\\')) len--; + if (len == 0) { + char* d = (char*)bux_alloc(2); + d[0] = '.'; d[1] = 0; + return d; + } + char* r = (char*)bux_alloc((size_t)len + 1); + memcpy(r, path, (size_t)len); + r[len] = 0; + return r; +} +char* bux_path_ext(const char* path) { + if (!path) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + const char* dot = strrchr(path, '.'); + if (!dot) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + const char* slash = strrchr(path, '/'); + const char* bslash = strrchr(path, '\\'); + if (slash && slash > dot) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + if (bslash && bslash > dot) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + size_t n = strlen(dot); + char* r = (char*)bux_alloc(n + 1); + memcpy(r, dot, n + 1); + return r; +} +int bux_mkdir_if_needed(const char* path) { + if (!path) return -1; + return bux_mkdir_one(path); +} +int bux_dir_exists(const char* path) { + if (!path) return 0; +#if BUX_IS_WIN + DWORD attr = GetFileAttributesA(path); + return (attr != INVALID_FILE_ATTRIBUTES) && (attr & FILE_ATTRIBUTE_DIRECTORY); +#else + struct stat st; + return (stat(path, &st) == 0 && S_ISDIR(st.st_mode)); +#endif +} +char** bux_list_dir(const char* dir, const char* ext, int* out_count) { + (void)dir; (void)ext; + if (out_count) *out_count = 0; + return NULL; /* stub: recursive listing not ported */ +} + +/* ── Math / hash ──────────────────────────────────────────────────────── */ +double bux_sqrt(double x) { return sqrt(x); } +double bux_pow(double x, double y) { return pow(x, y); } +int64_t bux_abs_i64(int64_t x) { return x < 0 ? -x : x; } +double bux_abs_f64(double x) { return x < 0 ? -x : x; } +int64_t bux_min_i64(int64_t a, int64_t b) { return a < b ? a : b; } +int64_t bux_max_i64(int64_t a, int64_t b) { return a > b ? a : b; } +double bux_min_f64(double a, double b) { return a < b ? a : b; } +double bux_max_f64(double a, double b) { return a > b ? a : b; } +unsigned int bux_hash_bytes(const void* ptr, size_t size) { + if (!ptr) return 0; + unsigned int hash = 5381; + const unsigned char* b = (const unsigned char*)ptr; + for (size_t i = 0; i < size; i++) hash = ((hash << 5) + hash) + b[i]; + return hash; +} +int bux_mem_eq(const void* a, const void* b, size_t size) { + if (a == b) return 1; + if (!a || !b) return 0; + return memcmp(a, b, size) == 0; +} +unsigned int bux_hash_string(const char* s) { + return bux_hash_bytes(s, s ? strlen(s) : 0); +} + +/* ── OS env / cwd ─────────────────────────────────────────────────────── */ +const char* bux_getenv(const char* name) { + if (!name) return ""; + const char* v = getenv(name); + return v ? v : ""; +} +const char* bux_cc_ld_stable(void) { return ""; } +int bux_setenv(const char* name, const char* value) { + if (!name || !value) return -1; +#if BUX_IS_WIN + return _putenv_s(name, value) == 0 ? 0 : -1; +#else + return setenv(name, value, 1); +#endif +} +const char* bux_getcwd(void) { + static char buf[4096]; +#if BUX_IS_WIN + if (_getcwd(buf, (int)sizeof(buf))) return buf; +#else + if (getcwd(buf, sizeof(buf))) return buf; +#endif + return ""; +} +int bux_chdir(const char* path) { + if (!path) return -1; +#if BUX_IS_WIN + return _chdir(path); +#else + return chdir(path); +#endif +} + +/* ── Time ─────────────────────────────────────────────────────────────── */ +int64_t bux_time_ms(void) { +#if BUX_IS_WIN + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + ULARGE_INTEGER u; + u.LowPart = ft.dwLowDateTime; + u.HighPart = ft.dwHighDateTime; + /* 100-ns intervals since 1601 → ms since Unix epoch */ + return (int64_t)((u.QuadPart / 10000ULL) - 11644473600000ULL); +#else + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + return (int64_t)time(NULL) * 1000; +#endif +} +int64_t bux_time_us(void) { return bux_time_ms() * 1000; } +void bux_sleep_ms(int64_t ms) { + if (ms <= 0) return; +#if BUX_IS_WIN + Sleep((DWORD)ms); +#else + struct timespec ts; + ts.tv_sec = (time_t)(ms / 1000); + ts.tv_nsec = (long)((ms % 1000) * 1000000); + nanosleep(&ts, NULL); +#endif +} + +/* ── Process ──────────────────────────────────────────────────────────── */ +int bux_system(const char* cmd) { return cmd ? system(cmd) : -1; } +int bux_run_nim(const char* nim_file, const char* out_bin) { + char cmd[4096]; + snprintf(cmd, sizeof(cmd), "nim c -o:%s -d:release --gc:orc %s 2>&1", + out_bin ? out_bin : "a.out", nim_file ? nim_file : ""); + return system(cmd); +} +int bux_process_run(const char* cmd) { return bux_system(cmd); } +char* bux_process_output(const char* cmd) { + (void)cmd; + return NULL; /* popen portability varies; stub on minimal runtime */ +} + +/* ── Tasks / channels / mutex / async — stubs ─────────────────────────── */ +void bux_task_init(int num_workers) { (void)num_workers; } +void bux_task_shutdown(void) {} +void* bux_task_spawn(void* (*func)(void*), void* arg) { + (void)func; (void)arg; + return NULL; +} +void bux_task_join(void* handle) { (void)handle; } +void bux_task_sleep(int64_t ms) { bux_sleep_ms(ms); } +void bux_task_yield(void) {} +int bux_task_current_id(void) { return 0; } + +void* bux_channel_new(int64_t capacity, int64_t elem_size) { + (void)capacity; (void)elem_size; + return NULL; +} +void bux_channel_send(void* handle, void* elem) { (void)handle; (void)elem; } +int bux_channel_recv(void* handle, void* out) { (void)handle; (void)out; return 0; } +void bux_channel_close(void* handle) { (void)handle; } +void bux_channel_free(void* handle) { (void)handle; } + +void* bux_mutex_new(void) { return bux_alloc(1); } +void bux_mutex_lock(void* handle) { (void)handle; } +void bux_mutex_unlock(void* handle) { (void)handle; } +void bux_mutex_free(void* handle) { bux_free(handle); } +void* bux_rwlock_new(void) { return bux_alloc(1); } +void bux_rwlock_rdlock(void* handle) { (void)handle; } +void bux_rwlock_wrlock(void* handle) { (void)handle; } +void bux_rwlock_unlock(void* handle) { (void)handle; } +void bux_rwlock_free(void* handle) { bux_free(handle); } + +void* bux_async_spawn(void (*func)(void)) { (void)func; return NULL; } +void bux_async_yield(void) {} +void bux_async_run(void) {} +void* bux_async_await(void* handle) { (void)handle; return NULL; } +void bux_async_sleep(int64_t ms) { bux_sleep_ms(ms); } +void bux_async_return(void* value, size_t size) { (void)value; (void)size; } +void* bux_async_result(void* handle) { (void)handle; return NULL; } + +/* ── Sockets — stubs ──────────────────────────────────────────────────── */ +int bux_socket_create(void) { return -1; } +int bux_socket_reuse(int fd) { (void)fd; return -1; } +int bux_socket_bind(int fd, const char* addr, int port) { + (void)fd; (void)addr; (void)port; return -1; +} +int bux_socket_listen(int fd, int backlog) { (void)fd; (void)backlog; return -1; } +int bux_socket_accept(int fd) { (void)fd; return -1; } +int bux_socket_connect(int fd, const char* addr, int port) { + (void)fd; (void)addr; (void)port; return -1; +} +int bux_socket_send(int fd, const char* data, int len) { + (void)fd; (void)data; (void)len; return -1; +} +/* BuxString used by full runtime; provide a simple struct-compatible layout */ +typedef struct { char* data; int len; } BuxString; +BuxString bux_socket_recv(int fd, int max_len) { + (void)fd; (void)max_len; + BuxString s; s.data = NULL; s.len = 0; return s; +} +int bux_socket_close(int fd) { (void)fd; return -1; } +const char* bux_socket_error(void) { return "sockets not available on this platform"; } + +/* ── Crypto — stubs (no OpenSSL) ──────────────────────────────────────── */ +static void zero_out(unsigned char* out, int n) { + if (out && n > 0) memset(out, 0, (size_t)n); +} +void bux_sha1(const char* data, int len, unsigned char* out) { + (void)data; (void)len; zero_out(out, 20); +} +void bux_sha256(const char* data, int len, unsigned char* out) { + (void)data; (void)len; zero_out(out, 32); +} +void bux_sha384(const char* data, int len, unsigned char* out) { + (void)data; (void)len; zero_out(out, 48); +} +void bux_sha512(const char* data, int len, unsigned char* out) { + (void)data; (void)len; zero_out(out, 64); +} +void bux_hmac_sha256(const char* key, int keylen, const char* msg, int msglen, unsigned char* out) { + (void)key; (void)keylen; (void)msg; (void)msglen; zero_out(out, 32); +} +void bux_hmac_sha384(const char* key, int keylen, const char* msg, int msglen, unsigned char* out) { + (void)key; (void)keylen; (void)msg; (void)msglen; zero_out(out, 48); +} +void bux_hmac_sha512(const char* key, int keylen, const char* msg, int msglen, unsigned char* out) { + (void)key; (void)keylen; (void)msg; (void)msglen; zero_out(out, 64); +} +int bux_random_bytes(unsigned char* buf, int len) { + if (!buf || len <= 0) return 0; +#if BUX_IS_WIN + /* Best-effort: not cryptographically strong */ + for (int i = 0; i < len; i++) buf[i] = (unsigned char)(rand() & 0xFF); + return 1; +#else + for (int i = 0; i < len; i++) buf[i] = (unsigned char)(rand() & 0xFF); + return 1; +#endif +} +char* bux_base64_encode(const unsigned char* in, int inlen) { + (void)in; (void)inlen; + char* o = (char*)bux_alloc(1); o[0] = 0; return o; +} +char* bux_base64_decode(const char* in, int inlen, int* outlen) { + (void)in; (void)inlen; + if (outlen) *outlen = 0; + return (char*)bux_alloc(1); +} +char* bux_base64url_encode(const unsigned char* in, int inlen) { + return bux_base64_encode(in, inlen); +} +char* bux_base64url_decode(const char* in, int inlen, int* outlen) { + return bux_base64_decode(in, inlen, outlen); +} +char* bux_bytes_to_hex(const unsigned char* data, int len) { + if (!data || len <= 0) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + char* out = (char*)bux_alloc((size_t)len * 2 + 1); + static const char* hex = "0123456789abcdef"; + for (int i = 0; i < len; i++) { + out[i*2] = hex[(data[i] >> 4) & 0xF]; + out[i*2+1] = hex[data[i] & 0xF]; + } + out[len*2] = 0; + return out; +} +int bux_aes_256_cbc_encrypt(const unsigned char* key, const unsigned char* iv, + const char* in, int inlen, unsigned char* out, int* outlen) { + (void)key; (void)iv; (void)in; (void)inlen; (void)out; + if (outlen) *outlen = 0; + return 0; +} +int bux_aes_256_cbc_decrypt(const unsigned char* key, const unsigned char* iv, + const char* in, int inlen, unsigned char* out, int* outlen) { + (void)key; (void)iv; (void)in; (void)inlen; (void)out; + if (outlen) *outlen = 0; + return 0; +} +int bux_aes_256_gcm_encrypt(const unsigned char* key, const unsigned char* iv, int ivlen, + const char* in, int inlen, unsigned char* out, int* outlen, + unsigned char* tag) { + (void)key; (void)iv; (void)ivlen; (void)in; (void)inlen; (void)out; (void)tag; + if (outlen) *outlen = 0; + return 0; +} +int bux_aes_256_gcm_decrypt(const unsigned char* key, const unsigned char* iv, int ivlen, + const char* in, int inlen, const unsigned char* tag, + unsigned char* out, int* outlen) { + (void)key; (void)iv; (void)ivlen; (void)in; (void)inlen; (void)tag; (void)out; + if (outlen) *outlen = 0; + return 0; +} +char* bux_rsa_sign_sha256(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; + if (siglen) *siglen = 0; return NULL; +} +char* bux_rsa_sign_sha384(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; + if (siglen) *siglen = 0; return NULL; +} +char* bux_rsa_sign_sha512(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; + if (siglen) *siglen = 0; return NULL; +} +int bux_rsa_verify_sha256(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; (void)sig; (void)siglen; + return 0; +} +int bux_rsa_verify_sha384(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; (void)sig; (void)siglen; + return 0; +} +int bux_rsa_verify_sha512(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; (void)sig; (void)siglen; + return 0; +} +char* bux_ecdsa_sign_p256(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; + if (siglen) *siglen = 0; return NULL; +} +char* bux_ecdsa_sign_p384(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; + if (siglen) *siglen = 0; return NULL; +} +int bux_ecdsa_verify_p256(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; (void)sig; (void)siglen; + return 0; +} +int bux_ecdsa_verify_p384(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; (void)sig; (void)siglen; + return 0; +} +int bux_ed25519_keypair(unsigned char* pub, unsigned char* priv) { + zero_out(pub, 32); zero_out(priv, 32); return 0; +} +int bux_ed25519_sign(const char* priv, const char* data, int datalen, unsigned char* sig) { + (void)priv; (void)data; (void)datalen; zero_out(sig, 64); return 0; +} +int bux_ed25519_verify(const char* pub, const char* sig, const char* data, int datalen) { + (void)pub; (void)sig; (void)data; (void)datalen; return 0; +} + +/* Legacy string helpers used by some mono paths */ +typedef struct { char* data; int len; } BuxStringLegacy; +BuxStringLegacy bux_string_from_cstr(const char* s) { + BuxStringLegacy r; + r.data = (char*)(s ? s : ""); + r.len = s ? (int)strlen(s) : 0; + return r; +} +BuxStringLegacy bux_string_concat(BuxStringLegacy a, BuxStringLegacy b) { + (void)a; (void)b; + BuxStringLegacy r; r.data = ""; r.len = 0; return r; +} diff --git a/src/ast.bux b/src/ast.bux index 5ccd19e..98256ae 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -128,6 +128,8 @@ module Ast { const ekStringInterp: int = 26; const ekClosure: int = 27; const ekMacroCall: int = 28; // name!(args) — expanded before sema + const ekMacroStmt: int = 29; // `$s:stmt` arg wrapper (expand only) + const ekMacroPat: int = 30; // `$p:pat` arg wrapper (expand only) struct ExprList { expr: *Expr, @@ -188,6 +190,9 @@ module Ast { // Match arms (for ekMatch) matchArms: *MatchArm, matchArmCount: int, + // Macro fragment wrappers (session 72) + macroStmt: *Stmt, // ekMacroStmt + macroPat: *Pattern, // ekMacroPat } // --------------------------------------------------------------------------- @@ -387,7 +392,8 @@ module Ast { refType: null as *TypeExpr, refBlock: null as *Block, genericCallee: "", genericTypeArg0: "", genericTypeArg1: "", genericTypeArgCount: 0, structName: "", structFieldCount: 0, - callArgs: null as *ExprList, callArgCount: 0 }; + callArgs: null as *ExprList, callArgCount: 0, + macroStmt: null as *Stmt, macroPat: null as *Pattern }; } func Ast_MakeIdent(name: String, line: uint32, col: uint32) -> Expr { @@ -706,6 +712,8 @@ module Ast { n.callArgCount = e.callArgCount; n.matchArms = Ast_CloneMatchArm(e.matchArms); n.matchArmCount = e.matchArmCount; + n.macroStmt = Ast_CloneStmt(e.macroStmt); + n.macroPat = Ast_ClonePattern(e.macroPat); return n; } diff --git a/src/c_backend.bux b/src/c_backend.bux index 666fdaa..0988327 100644 --- a/src/c_backend.bux +++ b/src/c_backend.bux @@ -76,6 +76,60 @@ module CBackend { movedName5: String, movedName6: String, movedName7: String, + // Partial field moves: (var, field) pairs (session 70) + partialCount: int, + partialVar0: String, + partialField0: String, + partialVar1: String, + partialField1: String, + partialVar2: String, + partialField2: String, + partialVar3: String, + partialField3: String, + partialVar4: String, + partialField4: String, + partialVar5: String, + partialField5: String, + partialVar6: String, + partialField6: String, + partialVar7: String, + partialField7: String, + // Local name → typeName for remaining-field Drop lookup + localCount: int, + localName0: String, + localType0: String, + localName1: String, + localType1: String, + localName2: String, + localType2: String, + localName3: String, + localType3: String, + localName4: String, + localType4: String, + localName5: String, + localType5: String, + localName6: String, + localType6: String, + localName7: String, + localType7: String, + // Pointer aliases: p → bag when `p = &bag` (session 74) + aliasCount: int, + aliasPtr0: String, + aliasOwner0: String, + aliasPtr1: String, + aliasOwner1: String, + aliasPtr2: String, + aliasOwner2: String, + aliasPtr3: String, + aliasOwner3: String, + aliasPtr4: String, + aliasOwner4: String, + aliasPtr5: String, + aliasOwner5: String, + aliasPtr6: String, + aliasOwner6: String, + aliasPtr7: String, + aliasOwner7: String, tmpCounter: int, currentRetType: String, // #line debug maps (E.4 selfhost parity) @@ -130,6 +184,329 @@ module CBackend { return true; } + func CBE_AddPtrAlias(cbe: *CEmitter, ptrName: String, ownerName: String) { + if ptrName == null as String || String_Eq(ptrName, "") { return; } + if ownerName == null as String || String_Eq(ownerName, "") { return; } + // Update existing alias for same pointer + var i: int = 0; + while i < cbe.aliasCount { + var pn: String = ""; + if i == 0 { pn = cbe.aliasPtr0; } + else if i == 1 { pn = cbe.aliasPtr1; } + else if i == 2 { pn = cbe.aliasPtr2; } + else if i == 3 { pn = cbe.aliasPtr3; } + else if i == 4 { pn = cbe.aliasPtr4; } + else if i == 5 { pn = cbe.aliasPtr5; } + else if i == 6 { pn = cbe.aliasPtr6; } + else if i == 7 { pn = cbe.aliasPtr7; } + if String_Eq(pn, ptrName) { + if i == 0 { cbe.aliasOwner0 = ownerName; } + else if i == 1 { cbe.aliasOwner1 = ownerName; } + else if i == 2 { cbe.aliasOwner2 = ownerName; } + else if i == 3 { cbe.aliasOwner3 = ownerName; } + else if i == 4 { cbe.aliasOwner4 = ownerName; } + else if i == 5 { cbe.aliasOwner5 = ownerName; } + else if i == 6 { cbe.aliasOwner6 = ownerName; } + else if i == 7 { cbe.aliasOwner7 = ownerName; } + return; + } + i = i + 1; + } + if cbe.aliasCount >= 8 { return; } + if cbe.aliasCount == 0 { cbe.aliasPtr0 = ptrName; cbe.aliasOwner0 = ownerName; } + else if cbe.aliasCount == 1 { cbe.aliasPtr1 = ptrName; cbe.aliasOwner1 = ownerName; } + else if cbe.aliasCount == 2 { cbe.aliasPtr2 = ptrName; cbe.aliasOwner2 = ownerName; } + else if cbe.aliasCount == 3 { cbe.aliasPtr3 = ptrName; cbe.aliasOwner3 = ownerName; } + else if cbe.aliasCount == 4 { cbe.aliasPtr4 = ptrName; cbe.aliasOwner4 = ownerName; } + else if cbe.aliasCount == 5 { cbe.aliasPtr5 = ptrName; cbe.aliasOwner5 = ownerName; } + else if cbe.aliasCount == 6 { cbe.aliasPtr6 = ptrName; cbe.aliasOwner6 = ownerName; } + else if cbe.aliasCount == 7 { cbe.aliasPtr7 = ptrName; cbe.aliasOwner7 = ownerName; } + cbe.aliasCount = cbe.aliasCount + 1; + } + + func CBE_ResolvePtrAlias(cbe: *CEmitter, name: String) -> String { + var cur: String = name; + var guard: int = 0; + while guard < 8 { + var found: bool = false; + var owner: String = ""; + var i: int = 0; + while i < cbe.aliasCount { + var pn: String = ""; + var on: String = ""; + if i == 0 { pn = cbe.aliasPtr0; on = cbe.aliasOwner0; } + else if i == 1 { pn = cbe.aliasPtr1; on = cbe.aliasOwner1; } + else if i == 2 { pn = cbe.aliasPtr2; on = cbe.aliasOwner2; } + else if i == 3 { pn = cbe.aliasPtr3; on = cbe.aliasOwner3; } + else if i == 4 { pn = cbe.aliasPtr4; on = cbe.aliasOwner4; } + else if i == 5 { pn = cbe.aliasPtr5; on = cbe.aliasOwner5; } + else if i == 6 { pn = cbe.aliasPtr6; on = cbe.aliasOwner6; } + else if i == 7 { pn = cbe.aliasPtr7; on = cbe.aliasOwner7; } + if String_Eq(pn, cur) { + owner = on; + found = true; + break; + } + i = i + 1; + } + if !found { break; } + cur = owner; + guard = guard + 1; + } + return cur; + } + + /// Record alias if init is `&local` (hUnary Amp of hVar). + func CBE_TryRecordPtrAlias(cbe: *CEmitter, ptrName: String, init: *HirNode) { + if init == null as *HirNode { return; } + var n: *HirNode = init; + if n.kind == hLoad { n = n.child1; } + if n == null as *HirNode { return; } + if n.kind == hUnary && n.intValue == tkAmp { + let op: *HirNode = n.child1; + if op != null as *HirNode && op.kind == hVar { + CBE_AddPtrAlias(cbe, ptrName, op.strValue); + } + } + } + + /// Base local name under load/field/deref chains (for partial field moves). + /// Resolves pointer aliases: p → bag when `p = &bag`. + func CBE_BaseVarName(cbe: *CEmitter, node: *HirNode) -> String { + if node == null as *HirNode { return ""; } + if node.kind == hVar { + return CBE_ResolvePtrAlias(cbe, node.strValue); + } + // Deref: *p → follow p's alias + if node.kind == hUnary && node.intValue == tkStar { + return CBE_BaseVarName(cbe, node.child1); + } + if node.kind == hLoad || node.kind == hFieldAccess || node.kind == hFieldPtr { + return CBE_BaseVarName(cbe, node.child1); + } + return ""; + } + + /// Back-compat: BaseVarName without emitter (no alias resolve). + func CBE_BaseVarNameRaw(node: *HirNode) -> String { + if node == null as *HirNode { return ""; } + if node.kind == hVar { return node.strValue; } + if node.kind == hUnary && node.intValue == tkStar { + return CBE_BaseVarNameRaw(node.child1); + } + if node.kind == hLoad || node.kind == hFieldAccess || node.kind == hFieldPtr { + return CBE_BaseVarNameRaw(node.child1); + } + return ""; + } + + /// Dotted field path from nested hFieldAccess/hFieldPtr chain: "inner.items". + /// Peels explicit derefs (`*p`) so path is field-only. + func CBE_FieldPathFromNode(node: *HirNode) -> String { + if node == null as *HirNode { return ""; } + if node.kind == hLoad { + return CBE_FieldPathFromNode(node.child1); + } + if node.kind == hUnary && node.intValue == tkStar { + return CBE_FieldPathFromNode(node.child1); + } + if node.kind == hFieldAccess || node.kind == hFieldPtr { + let rest: String = CBE_FieldPathFromNode(node.child1); + let seg: String = node.strValue; + if String_Eq(seg, "") { return rest; } + if String_Eq(rest, "") { return seg; } + return String_Concat(rest, String_Concat(".", seg)); + } + return ""; + } + + /// True if dotted path `key` is exactly moved, or is a prefix of a moved path. + func CBE_PathIsFullyMoved(cbe: *CEmitter, varName: String, key: String) -> bool { + return CBE_IsPartialMovedField(cbe, varName, key); + } + + func CBE_PathHasNestedMove(cbe: *CEmitter, varName: String, key: String) -> bool { + let prefix: String = String_Concat(key, "."); + var i: int = 0; + while i < cbe.partialCount { + var pvar: String = ""; + var pfield: String = ""; + if i == 0 { pvar = cbe.partialVar0; pfield = cbe.partialField0; } + else if i == 1 { pvar = cbe.partialVar1; pfield = cbe.partialField1; } + else if i == 2 { pvar = cbe.partialVar2; pfield = cbe.partialField2; } + else if i == 3 { pvar = cbe.partialVar3; pfield = cbe.partialField3; } + else if i == 4 { pvar = cbe.partialVar4; pfield = cbe.partialField4; } + else if i == 5 { pvar = cbe.partialVar5; pfield = cbe.partialField5; } + else if i == 6 { pvar = cbe.partialVar6; pfield = cbe.partialField6; } + else if i == 7 { pvar = cbe.partialVar7; pfield = cbe.partialField7; } + if String_Eq(pvar, varName) && String_StartsWith(pfield, prefix) { + return true; + } + i = i + 1; + } + return false; + } + + func CBE_RegisterLocalType(cbe: *CEmitter, name: String, typeName: String) { + if name == null as String || String_Eq(name, "") { return; } + if typeName == null as String || String_Eq(typeName, "") { return; } + if cbe.localCount >= 8 { return; } + let tn: String = CBE_NormalizeTypeName(typeName); + if cbe.localCount == 0 { cbe.localName0 = name; cbe.localType0 = tn; } + else if cbe.localCount == 1 { cbe.localName1 = name; cbe.localType1 = tn; } + else if cbe.localCount == 2 { cbe.localName2 = name; cbe.localType2 = tn; } + else if cbe.localCount == 3 { cbe.localName3 = name; cbe.localType3 = tn; } + else if cbe.localCount == 4 { cbe.localName4 = name; cbe.localType4 = tn; } + else if cbe.localCount == 5 { cbe.localName5 = name; cbe.localType5 = tn; } + else if cbe.localCount == 6 { cbe.localName6 = name; cbe.localType6 = tn; } + else if cbe.localCount == 7 { cbe.localName7 = name; cbe.localType7 = tn; } + cbe.localCount = cbe.localCount + 1; + } + + func CBE_LookupLocalType(cbe: *CEmitter, name: String) -> String { + if cbe.localCount > 0 && String_Eq(cbe.localName0, name) { return cbe.localType0; } + if cbe.localCount > 1 && String_Eq(cbe.localName1, name) { return cbe.localType1; } + if cbe.localCount > 2 && String_Eq(cbe.localName2, name) { return cbe.localType2; } + if cbe.localCount > 3 && String_Eq(cbe.localName3, name) { return cbe.localType3; } + if cbe.localCount > 4 && String_Eq(cbe.localName4, name) { return cbe.localType4; } + if cbe.localCount > 5 && String_Eq(cbe.localName5, name) { return cbe.localType5; } + if cbe.localCount > 6 && String_Eq(cbe.localName6, name) { return cbe.localType6; } + if cbe.localCount > 7 && String_Eq(cbe.localName7, name) { return cbe.localType7; } + return ""; + } + + func CBE_AddPartialMoved(cbe: *CEmitter, varName: String, fieldName: String) { + if varName == null as String || String_Eq(varName, "") { return; } + if fieldName == null as String || String_Eq(fieldName, "") { return; } + if cbe.partialCount >= 8 { return; } + if cbe.partialCount == 0 { cbe.partialVar0 = varName; cbe.partialField0 = fieldName; } + else if cbe.partialCount == 1 { cbe.partialVar1 = varName; cbe.partialField1 = fieldName; } + else if cbe.partialCount == 2 { cbe.partialVar2 = varName; cbe.partialField2 = fieldName; } + else if cbe.partialCount == 3 { cbe.partialVar3 = varName; cbe.partialField3 = fieldName; } + else if cbe.partialCount == 4 { cbe.partialVar4 = varName; cbe.partialField4 = fieldName; } + else if cbe.partialCount == 5 { cbe.partialVar5 = varName; cbe.partialField5 = fieldName; } + else if cbe.partialCount == 6 { cbe.partialVar6 = varName; cbe.partialField6 = fieldName; } + else if cbe.partialCount == 7 { cbe.partialVar7 = varName; cbe.partialField7 = fieldName; } + cbe.partialCount = cbe.partialCount + 1; + } + + func CBE_IsPartialMovedField(cbe: *CEmitter, varName: String, fieldName: String) -> bool { + if cbe.partialCount > 0 && String_Eq(cbe.partialVar0, varName) && String_Eq(cbe.partialField0, fieldName) { return true; } + if cbe.partialCount > 1 && String_Eq(cbe.partialVar1, varName) && String_Eq(cbe.partialField1, fieldName) { return true; } + if cbe.partialCount > 2 && String_Eq(cbe.partialVar2, varName) && String_Eq(cbe.partialField2, fieldName) { return true; } + if cbe.partialCount > 3 && String_Eq(cbe.partialVar3, varName) && String_Eq(cbe.partialField3, fieldName) { return true; } + if cbe.partialCount > 4 && String_Eq(cbe.partialVar4, varName) && String_Eq(cbe.partialField4, fieldName) { return true; } + if cbe.partialCount > 5 && String_Eq(cbe.partialVar5, varName) && String_Eq(cbe.partialField5, fieldName) { return true; } + if cbe.partialCount > 6 && String_Eq(cbe.partialVar6, varName) && String_Eq(cbe.partialField6, fieldName) { return true; } + if cbe.partialCount > 7 && String_Eq(cbe.partialVar7, varName) && String_Eq(cbe.partialField7, fieldName) { return true; } + return false; + } + + func CBE_HasPartialMoved(cbe: *CEmitter, varName: String) -> bool { + if cbe.partialCount > 0 && String_Eq(cbe.partialVar0, varName) { return true; } + if cbe.partialCount > 1 && String_Eq(cbe.partialVar1, varName) { return true; } + if cbe.partialCount > 2 && String_Eq(cbe.partialVar2, varName) { return true; } + if cbe.partialCount > 3 && String_Eq(cbe.partialVar3, varName) { return true; } + if cbe.partialCount > 4 && String_Eq(cbe.partialVar4, varName) { return true; } + if cbe.partialCount > 5 && String_Eq(cbe.partialVar5, varName) { return true; } + if cbe.partialCount > 6 && String_Eq(cbe.partialVar6, varName) { return true; } + if cbe.partialCount > 7 && String_Eq(cbe.partialVar7, varName) { return true; } + return false; + } + + /// Drop function name for a C type (`Array_int` → `Array_Drop_int`, `Bag` → `Bag_Drop`). + func CBE_DropFuncName(tn: String) -> String { + if tn == null as String || String_Eq(tn, "") { return ""; } + if !CBE_IsDroppableTypeName(tn) { return ""; } + if String_StartsWith(tn, "Array_") { + let n: uint = String_Len(tn); + let elem: String = String_Slice(tn, 6, n - 6); + return String_Concat("Array_Drop_", elem); + } + if String_StartsWith(tn, "Map_") { + let n: uint = String_Len(tn); + let rest: String = String_Slice(tn, 4, n - 4); + return String_Concat("Map_Drop_", rest); + } + if String_StartsWith(tn, "Set_") { + let n: uint = String_Len(tn); + let elem: String = String_Slice(tn, 4, n - 4); + return String_Concat("Set_Drop_", elem); + } + if String_StartsWith(tn, "Channel_") { + let n: uint = String_Len(tn); + let elem: String = String_Slice(tn, 8, n - 8); + return String_Concat("Channel_Drop_", elem); + } + return String_Concat(tn, "_Drop"); + } + + /// Lookup struct index by name; -1 if missing. + func CBE_FindStruct(cbe: *CEmitter, typeName: String) -> int { + if cbe.mod == null as *HirModule { return -1; } + var si: int = 0; + while si < cbe.mod.structCount { + if String_Eq(cbe.mod.structs[si].name, typeName) { return si; } + si = si + 1; + } + return -1; + } + + /// Emit one Drop call: `Type_Drop(&(accessPath));` + func CBE_EmitOneFieldDrop(cbe: *CEmitter, dropFn: String, accessPath: String) { + if String_Eq(dropFn, "") { return; } + StringBuilder_Append(&cbe.sb, "\n"); + var sp: int = 0; + while sp < cbe.indent { + StringBuilder_Append(&cbe.sb, " "); + sp = sp + 1; + } + StringBuilder_Append(&cbe.sb, dropFn); + StringBuilder_Append(&cbe.sb, "(&("); + StringBuilder_Append(&cbe.sb, accessPath); + StringBuilder_Append(&cbe.sb, "));"); + } + + /// Recursive remaining-field drops for nested paths (session 73). + /// `prefix` is dotted path under varName ("" at root); `typeName` is type at that prefix. + func CBE_EmitRemainingAt(cbe: *CEmitter, varName: String, typeName: String, prefix: String) { + let si: int = CBE_FindStruct(cbe, typeName); + if si < 0 { return; } + var fi: int = 0; + while fi < cbe.mod.structs[si].fieldCount { + let fname: String = cbe.mod.structs[si].fields[fi].name; + let fty: String = CBE_NormalizeTypeName(cbe.mod.structs[si].fields[fi].typeName); + var key: String = fname; + if !String_Eq(prefix, "") { + key = String_Concat(prefix, String_Concat(".", fname)); + } + var access: String = String_Concat(varName, String_Concat(".", key)); + // path may use dots in key already: var.inner.note + if String_Eq(prefix, "") { + access = String_Concat(varName, String_Concat(".", fname)); + } else { + access = String_Concat(varName, String_Concat(".", String_Concat(prefix, String_Concat(".", fname)))); + } + if CBE_PathIsFullyMoved(cbe, varName, key) { + // fully moved — skip + } else if CBE_PathHasNestedMove(cbe, varName, key) { + // recurse into nested type + CBE_EmitRemainingAt(cbe, varName, fty, key); + } else { + let dropFn: String = CBE_DropFuncName(fty); + CBE_EmitOneFieldDrop(cbe, dropFn, access); + } + fi = fi + 1; + } + } + + /// After partial field move, Drop remaining droppable fields (incl. nested). + func CBE_EmitRemainingFieldDrops(cbe: *CEmitter, varName: String) { + let typeName: String = CBE_LookupLocalType(cbe, varName); + if String_Eq(typeName, "") { return; } + CBE_EmitRemainingAt(cbe, varName, typeName, ""); + } + /// Mark droppable locals moved by-value (struct fields / nested / partial field). /// `valueTypeHint`: when non-empty (e.g. function return type), used to decide /// whether a field access is an ownership move (`return bag.items` vs `return bag.tag`). @@ -145,14 +522,43 @@ module CBackend { } // Partial field move: only when the *value* type is droppable if node.kind == hFieldPtr || node.kind == hFieldAccess { + // Prefer return-type hint, then resolved field type, then node.typeName + // (selfhost stores *base* struct on field HIR, not the field type). var vty: String = valueTypeHint; + if String_Eq(vty, "") && cbe.mod != null as *HirModule { + vty = CBE_GetExprTypeName(cbe.mod, node); + } if String_Eq(vty, "") { vty = node.typeName; } - // Field HIR often stores the *base* struct typeName — prefer hint - if CBE_IsDroppableTypeName(vty) { - // Walk to base local (hVar / load / nested) - CBE_MarkMovedFromNodeHint(cbe, node.child1, ""); + vty = CBE_NormalizeTypeName(vty); + let baseName: String = CBE_BaseVarName(cbe, node); + var path: String = CBE_FieldPathFromNode(node); + // Fallback path: just the leaf field name if chain walk failed + if String_Eq(path, "") && !String_Eq(node.strValue, "") { + path = node.strValue; + } + // Single-level: mark when value type is droppable. + // Nested path (`a.b.c`): always mark — leaf is ownership transfer. + if !String_Eq(baseName, "") && !String_Eq(path, "") { + var shouldMark: bool = CBE_IsDroppableTypeName(vty); + if !shouldMark && cbe.mod != null as *HirModule { + let ft: String = CBE_NormalizeTypeName(CBE_GetExprTypeName(cbe.mod, node)); + shouldMark = CBE_IsDroppableTypeName(ft); + } + var pi: int = 0; + let plen: int = bux_strlen(path) as int; + while pi < plen { + if path[pi] == 46 as char8 { // '.' + shouldMark = true; + break; + } + pi = pi + 1; + } + if shouldMark { + CBE_AddPartialMoved(cbe, baseName, path); + CBE_AddMoved(cbe, baseName); + } } return; } @@ -292,10 +698,16 @@ module CBackend { if i == 5 { dn = cbe.defer5; } if i == 6 { dn = cbe.defer6; } if i == 7 { dn = cbe.defer7; } - // Skip auto-drop for moved variables + // Skip auto-drop for moved variables; after partial field move still + // Drop remaining droppable fields (session 70/73). let deferVarName: String = CBE_GetAutoDropVarName(dn); - if !String_Eq(deferVarName, "") && CBE_IsMoved(cbe, deferVarName) { - return; + if !String_Eq(deferVarName, "") { + if CBE_IsMoved(cbe, deferVarName) || CBE_HasPartialMoved(cbe, deferVarName) { + if CBE_HasPartialMoved(cbe, deferVarName) { + CBE_EmitRemainingFieldDrops(cbe, deferVarName); + } + return; + } } StringBuilder_Append(&cbe.sb, "\n"); var sp: int = 0; @@ -427,12 +839,14 @@ module CBackend { return; } - // Unary — always parenthesize operand so `!(a && b)` is not `!a && b` + // Unary — wrap whole op so `(*p).field` is not parsed as `*(p.field)` + // and `!(a && b)` is not `!a && b`. if kind == hUnary { + StringBuilder_Append(&cbe.sb, "("); StringBuilder_Append(&cbe.sb, CBackend_OpToC(node.intValue)); StringBuilder_Append(&cbe.sb, "("); CBE_EmitExpr(cbe, node.child1); - StringBuilder_Append(&cbe.sb, ")"); + StringBuilder_Append(&cbe.sb, "))"); return; } @@ -596,6 +1010,7 @@ module CBackend { if !String_Eq(node.typeName, "") { ct = node.typeName; } + CBE_RegisterLocalType(cbe, node.strValue, ct); StringBuilder_Append(&cbe.sb, CBE_CParamDecl(ct, node.strValue)); return; } @@ -610,6 +1025,15 @@ module CBackend { CBE_MarkMovedFromNode(cbe, node.child2); } } + // Pointer alias: `p = &bag` / `let p = &bag` + if node.child1 != null as *HirNode && node.child2 != null as *HirNode { + var ptrN: String = ""; + if node.child1.kind == hAlloca { ptrN = node.child1.strValue; } + if node.child1.kind == hVar { ptrN = node.child1.strValue; } + if !String_Eq(ptrN, "") { + CBE_TryRecordPtrAlias(cbe, ptrN, node.child2); + } + } // Reinitialization removes moved status if node.child1 != null as *HirNode && node.child1.kind == hVar { CBE_RemoveMoved(cbe, node.child1.strValue); @@ -620,6 +1044,8 @@ module CBackend { if !String_Eq(node.child1.typeName, "") { ct = node.child1.typeName; } + // Register for remaining-field Drop after partial move (session 70) + CBE_RegisterLocalType(cbe, node.child1.strValue, ct); StringBuilder_Append(&cbe.sb, CBE_CParamDecl(ct, node.child1.strValue)); if node.child2 != null as *HirNode { StringBuilder_Append(&cbe.sb, " = "); @@ -880,6 +1306,10 @@ module CBackend { // Assign: target = value if kind == hAssign { + // Pointer alias: `p = &bag` + if node.child1 != null as *HirNode && node.child1.kind == hVar && node.child2 != null as *HirNode { + CBE_TryRecordPtrAlias(cbe, node.child1.strValue, node.child2); + } CBE_EmitExpr(cbe, node.child1); StringBuilder_Append(&cbe.sb, " = "); CBE_EmitExpr(cbe, node.child2); @@ -1478,6 +1908,9 @@ module CBackend { cbe.mod = mod; cbe.deferCount = 0; cbe.movedCount = 0; + cbe.partialCount = 0; + cbe.aliasCount = 0; + cbe.localCount = 0; cbe.tmpCounter = 0; cbe.lastDebugLine = 0; cbe.lastDebugFile = ""; @@ -1542,34 +1975,59 @@ module CBackend { StringBuilder_Append(&cbe.sb, "typedef struct Tuple_int_int_int {\n int _0;\n int _1;\n int _2;\n} Tuple_int_int_int;\n"); StringBuilder_Append(&cbe.sb, "typedef struct Tuple_Empty {\n char _pad;\n} Tuple_Empty;\n\n"); - // Struct definitions before enums (enums may embed structs by value, e.g. Shape::Dot(Point)) - // Pass 1: emit structs with no value-typed struct fields (leaf structs) - si = 0; - while si < mod.structCount { - if String_Eq(mod.structs[si].name, "") || CBE_StructHasGeneric(&mod.structs[si]) { + // Struct definitions before enums (enums may embed structs by value). + // Multi-pass topo: emit only when every value-typed field type is already + // emitted (handles Outer { inner: Inner } nesting — session 73). + var emittedNames: String = ";"; + var progress: bool = true; + var pass: int = 0; + while progress && pass < 64 { + progress = false; + pass = pass + 1; + si = 0; + while si < mod.structCount { + let sn: String = mod.structs[si].name; + if String_Eq(sn, "") || CBE_StructHasGeneric(&mod.structs[si]) { + si = si + 1; + continue; + } + let mark: String = String_Concat(";", String_Concat(sn, ";")); + if String_Contains(emittedNames, mark) { + si = si + 1; + continue; + } + var ready: bool = true; + var fi2: int = 0; + while fi2 < mod.structs[si].fieldCount { + let ft: String = CBE_NormalizeTypeName(mod.structs[si].fields[fi2].typeName); + if !CBE_IsPrimitiveTypeName(ft) && !String_EndsWith(ft, "*") { + let fmark: String = String_Concat(";", String_Concat(ft, ";")); + if !String_Contains(emittedNames, fmark) { + // Only block if ft is a pending user struct we define + var isOur: bool = false; + var oj: int = 0; + while oj < mod.structCount { + if String_Eq(mod.structs[oj].name, ft) { + isOur = true; + break; + } + oj = oj + 1; + } + if isOur { + ready = false; + break; + } + } + } + fi2 = fi2 + 1; + } + if ready { + CBE_EmitStructDef(cbe, &mod.structs[si]); + emittedNames = String_Concat(emittedNames, String_Concat(sn, ";")); + progress = true; + } si = si + 1; - continue; } - if CBE_StructHasValueStructField(&mod.structs[si]) { - si = si + 1; - continue; - } - CBE_EmitStructDef(cbe, &mod.structs[si]); - si = si + 1; - } - // Pass 2: emit structs that contain value-typed struct fields - si = 0; - while si < mod.structCount { - if String_Eq(mod.structs[si].name, "") || CBE_StructHasGeneric(&mod.structs[si]) { - si = si + 1; - continue; - } - if !CBE_StructHasValueStructField(&mod.structs[si]) { - si = si + 1; - continue; - } - CBE_EmitStructDef(cbe, &mod.structs[si]); - si = si + 1; } // Enum definitions @@ -1810,6 +2268,8 @@ module CBackend { cbe.checkedFunc = mod.funcs[i].checkedFunc; cbe.deferCount = 0; cbe.movedCount = 0; + cbe.partialCount = 0; + cbe.localCount = 0; cbe.tmpCounter = 0; cbe.currentRetType = mod.funcs[i].retTypeName; var hasReturn: bool = false; diff --git a/src/macroexpand.bux b/src/macroexpand.bux index 85bcd0b..9bdde5d 100644 --- a/src/macroexpand.bux +++ b/src/macroexpand.bux @@ -329,20 +329,134 @@ module MacroExpand { } } - // Fragment kind check: "ident" | "literal" | "block" | expr|tt (any) - func Macro_FragMatches(kindStr: String, aexp: *Expr) -> bool { - if aexp == null as *Expr { return false; } + // Flatten ekIdent / ekField(::) chain to "A::B::C" (selfhost path style) + func Macro_PathFromExpr(e: *Expr) -> String { + if e == null as *Expr { return ""; } + if e.kind == ekIdent { return e.strValue; } + if e.kind == ekPath { return e.strValue; } + if e.kind == ekField { + let base: String = Macro_PathFromExpr(e.child1); + if String_Eq(base, "") { return e.strValue; } + return String_Concat(base, String_Concat("::", e.strValue)); + } + return ""; + } + + // Convert call-site expr → pattern for `$p:pat` (ident/lit/path/call/field) + func Macro_ExprToPattern(aexp: *Expr) -> *Pattern { + if aexp == null as *Expr { return null as *Pattern; } + if aexp.kind == ekMacroPat { return Ast_ClonePattern(aexp.macroPat); } + if aexp.kind == ekIdent { + if String_Eq(aexp.strValue, "_") { + let p: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern; + p.kind = pkWildcard; + p.line = aexp.line; + p.column = aexp.column; + return p; + } + let p: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern; + p.kind = pkIdent; + p.line = aexp.line; + p.column = aexp.column; + p.patIdent = aexp.strValue; + return p; + } + if aexp.kind == ekLiteral { + let p: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern; + p.kind = pkLiteral; + p.line = aexp.line; + p.column = aexp.column; + p.patLitKind = aexp.tokKind; + p.patLitText = aexp.tokText; + return p; + } + if aexp.kind == ekPath || aexp.kind == ekField { + let path: String = Macro_PathFromExpr(aexp); + if String_Eq(path, "") { return null as *Pattern; } + let p: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern; + p.kind = pkEnum; + p.line = aexp.line; + p.column = aexp.column; + p.patEnumPath = path; + return p; + } + if aexp.kind == ekCall { + // Opt::Some(v) — callee is ekField chain + var path: String = Macro_PathFromExpr(aexp.child1); + if String_Eq(path, "") { return null as *Pattern; } + let p: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern; + p.kind = pkEnum; + p.line = aexp.line; + p.column = aexp.column; + p.patEnumPath = path; + var last: *Pattern = null as *Pattern; + var arg: *ExprList = aexp.callArgs; + while arg != null as *ExprList { + let ap: *Pattern = Macro_ExprToPattern(arg.expr); + if ap == null as *Pattern { return null as *Pattern; } + if last == null as *Pattern { + p.patArgs = ap; + last = ap; + } else { + last.patNext = ap; + last = ap; + } + arg = arg.next; + } + return p; + } + return null as *Pattern; + } + + // Coerce arg for kind; returns normalized expr or null on mismatch + func Macro_CoerceArg(kindStr: String, aexp: *Expr) -> *Expr { + if aexp == null as *Expr { return null as *Expr; } if String_Eq(kindStr, "ident") { - return aexp.kind == ekIdent; + if aexp.kind != ekIdent { return null as *Expr; } + return aexp; } if String_Eq(kindStr, "literal") { - return aexp.kind == ekLiteral; + if aexp.kind != ekLiteral { return null as *Expr; } + return aexp; } if String_Eq(kindStr, "block") { - return aexp.kind == ekBlock; + if aexp.kind != ekBlock { return null as *Expr; } + return aexp; } - // expr / tt / unknown → accept - return true; + if String_Eq(kindStr, "stmt") { + if aexp.kind == ekMacroStmt { return aexp; } + if aexp.kind == ekMacroPat { return null as *Expr; } + // Wrap expression as expression-statement + let st: *Stmt = bux_alloc(sizeof(Stmt)) as *Stmt; + st.kind = skExpr; + st.line = aexp.line; + st.column = aexp.column; + st.child1 = aexp; + let e: *Expr = bux_alloc(sizeof(Expr)) as *Expr; + e.kind = ekMacroStmt; + e.line = aexp.line; + e.column = aexp.column; + e.macroStmt = st; + return e; + } + if String_Eq(kindStr, "pat") { + let pat: *Pattern = Macro_ExprToPattern(aexp); + if pat == null as *Pattern { return null as *Expr; } + let e: *Expr = bux_alloc(sizeof(Expr)) as *Expr; + e.kind = ekMacroPat; + e.line = aexp.line; + e.column = aexp.column; + e.macroPat = pat; + return e; + } + // expr / tt + if aexp.kind == ekMacroStmt || aexp.kind == ekMacroPat { return null as *Expr; } + return aexp; + } + + // Fragment kind check: "ident" | "literal" | "block" | "stmt" | "pat" | expr|tt + func Macro_FragMatches(kindStr: String, aexp: *Expr) -> bool { + return Macro_CoerceArg(kindStr, aexp) != null as *Expr; } // kinds encoded as "expr;ident;rep:expr," — return fi-th segment @@ -479,6 +593,36 @@ module MacroExpand { li = li + 1; } } + } else if s.kind == skExpr && s.child1 != null as *Expr && s.child1.kind == ekIdent { + // Splice `$s:stmt` bound to ekMacroStmt as a real statement + let bound: *Expr = Env_Lookup(env, s.child1.strValue); + if bound != null as *Expr && bound.kind == ekMacroStmt && bound.macroStmt != null as *Stmt { + let one: *Stmt = Subst_Stmt(bound.macroStmt, env, file, line, col); + if one != null as *Stmt { + one.nextStmt = null as *Stmt; + if n.firstStmt == null as *Stmt { + n.firstStmt = one; + n.lastStmt = one; + } else { + n.lastStmt.nextStmt = one; + n.lastStmt = one; + } + n.stmtCount = n.stmtCount + 1; + } + } else { + let one2: *Stmt = Subst_Stmt(s, env, file, line, col); + if one2 != null as *Stmt { + one2.nextStmt = null as *Stmt; + if n.firstStmt == null as *Stmt { + n.firstStmt = one2; + n.lastStmt = one2; + } else { + n.lastStmt.nextStmt = one2; + n.lastStmt = one2; + } + n.stmtCount = n.stmtCount + 1; + } + } } else { let one: *Stmt = Subst_Stmt(s, env, file, line, col); if one != null as *Stmt { @@ -647,6 +791,7 @@ module MacroExpand { } var arm: *MatchArm = c.matchArms; while arm != null as *MatchArm { + arm.pattern = Subst_Pattern(arm.pattern, env, file, line, col); arm.body = Subst_Expr(arm.body, env, file, line, col); arm = arm.next; } @@ -658,6 +803,29 @@ module MacroExpand { return c; } + func Subst_Pattern(p: *Pattern, env: *MacroEnv, file: String, line: uint32, col: uint32) -> *Pattern { + if p == null as *Pattern { return null as *Pattern; } + // `$p:pat` as whole pattern (pkIdent name `$p`) + if p.kind == pkIdent && !String_Eq(p.patIdent, "") { + let bound: *Expr = Env_Lookup(env, p.patIdent); + if bound != null as *Expr && bound.kind == ekMacroPat && bound.macroPat != null as *Pattern { + let np: *Pattern = Ast_ClonePattern(bound.macroPat); + if np != null as *Pattern { + np.line = line; + np.column = col; + } + return np; + } + } + // MVP: other patterns kept as cloned (no nested `$p` rewrite) + let c: *Pattern = Ast_ClonePattern(p); + if c != null as *Pattern { + c.line = line; + c.column = col; + } + return c; + } + func Subst_Stmt(s: *Stmt, env: *MacroEnv, file: String, line: uint32, col: uint32) -> *Stmt { if s == null as *Stmt { return null as *Stmt; } let c: *Stmt = Ast_CloneStmt(s); @@ -944,8 +1112,9 @@ module MacroExpand { if argList == null as *ExprList { ok = false; break; } } let aexp: *Expr = argList.expr; - if !Macro_FragMatches(kindStr, aexp) { ok = false; break; } - Env_Set(&env, paramIdx, Rule_FragName(rule, paramIdx), aexp); + let coerced: *Expr = Macro_CoerceArg(kindStr, aexp); + if coerced == null as *Expr { ok = false; break; } + Env_Set(&env, paramIdx, Rule_FragName(rule, paramIdx), coerced); argList = argList.next; flatLeft = flatLeft - 1; paramIdx = paramIdx + 1; diff --git a/src/parser.bux b/src/parser.bux index 1640f5a..ff499fa 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -395,9 +395,38 @@ module Parser { e.callArgCount = 0; e.matchArms = null as *MatchArm; e.matchArmCount = 0; + e.macroStmt = null as *Stmt; + e.macroPat = null as *Pattern; return e; } + func parserIsMacroStmtStart(p: *Parser) -> bool { + let k: int = parserPeek(p, 0); + if k == tkLet || k == tkVar { return true; } + if k == tkIf || k == tkWhile || k == tkFor || k == tkLoop { return true; } + if k == tkMatch || k == tkReturn || k == tkBreak || k == tkContinue { return true; } + if k == tkDefer || k == tkSwitch || k == tkDo { return true; } + return false; + } + + func parserParseMacroArg(p: *Parser) -> *Expr { + let line: uint32 = parserCurToken(p).line; + let col: uint32 = parserCurToken(p).column; + if parserIsMacroStmtStart(p) { + let st: *Stmt = parserParseStmt(p); + let e: *Expr = parserMakeExpr(ekMacroStmt, line, col); + e.macroStmt = st; + return e; + } + if parserCheck(p, tkUnderscore) { + let pat: *Pattern = parserParsePattern(p); + let e: *Expr = parserMakeExpr(ekMacroPat, line, col); + e.macroPat = pat; + return e; + } + return parserParseExpr(p); + } + func parserMakeStringLitExpr(text: String, line: uint32, col: uint32) -> *Expr { let quoted: String = String_Concat(String_Concat("\"", text), "\""); @@ -1284,7 +1313,7 @@ module Parser { while parserCheck(p, tkNewLine) { discard parserAdvance(p); } continue; } - let argExpr: *Expr = parserParseExpr(p); + let argExpr: *Expr = parserParseMacroArg(p); let argNode: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList; argNode.expr = argExpr; argNode.next = null as *ExprList; @@ -2387,8 +2416,10 @@ module Parser { let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind"); var kname: String = kindTok.text; if String_Eq(kname, "lit") { kname = "literal"; } + if String_Eq(kname, "pattern") { kname = "pat"; } if !(String_Eq(kname, "expr") || String_Eq(kname, "ident") || String_Eq(kname, "tt") - || String_Eq(kname, "literal") || String_Eq(kname, "block")) { + || String_Eq(kname, "literal") || String_Eq(kname, "block") + || String_Eq(kname, "stmt") || String_Eq(kname, "pat")) { kname = "expr"; } if nIn == 0 { @@ -2432,8 +2463,10 @@ module Parser { let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind"); var kname: String = kindTok.text; if String_Eq(kname, "lit") { kname = "literal"; } + if String_Eq(kname, "pattern") { kname = "pat"; } if !(String_Eq(kname, "expr") || String_Eq(kname, "ident") || String_Eq(kname, "tt") - || String_Eq(kname, "literal") || String_Eq(kname, "block")) { + || String_Eq(kname, "literal") || String_Eq(kname, "block") + || String_Eq(kname, "stmt") || String_Eq(kname, "pat")) { kname = "expr"; } if rule.paramCount < 9 { diff --git a/tools/smoke_drop_move.sh b/tools/smoke_drop_move.sh index a14e8cc..de3bf6c 100755 --- a/tools/smoke_drop_move.sh +++ b/tools/smoke_drop_move.sh @@ -58,7 +58,97 @@ if sed -n '/^Array_int TakeItems/,/^}/p' "$TMP/mp/build/main.c" | grep -q 'Bag_D sed -n '/^Array_int TakeItems/,/^}/p' "$TMP/mp/build/main.c" exit 1 fi -echo " move_field_partial: PASS (run + TakeItems has no Bag_Drop)" +# PeekTagAndTake must Drop the moved-out Array after Array_Len mono (defer restore) +if ! sed -n '/^int PeekTagAndTake/,/^}/p' "$TMP/mp/build/main.c" | grep -q 'Array_Drop'; then + echo "error: PeekTagAndTake missing Array_Drop for moved local (defer restore?)" >&2 + sed -n '/^int PeekTagAndTake/,/^}/p' "$TMP/mp/build/main.c" + exit 1 +fi +echo " move_field_partial: PASS (run + TakeItems has no Bag_Drop + PeekTag drops moved)" + +# --- remaining field Drop after partial move --- +echo "=== smoke: move_field_remaining ===" +mkdir -p "$TMP/mr/src" +cp -a "$ROOT/rt" "$TMP/mr/" +cat > "$TMP/mr/bux.toml" <<'EOF' +[Package] +Name = "move_field_remaining" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" +EOF +cp "$ROOT/examples/move_field_remaining.bux" "$TMP/mr/src/Main.bux" +(cd "$TMP/mr" && "$BUXC" run .) +# TakeLeft must not PairBag_Drop, but must Tracked_Drop remaining right field +if sed -n '/^Array_int TakeLeft/,/^}/p' "$TMP/mr/build/main.c" | grep -q 'PairBag_Drop'; then + echo "error: TakeLeft still PairBag_Drops after partial field move" >&2 + sed -n '/^Array_int TakeLeft/,/^}/p' "$TMP/mr/build/main.c" + exit 1 +fi +if ! sed -n '/^Array_int TakeLeft/,/^}/p' "$TMP/mr/build/main.c" | grep -q 'Tracked_Drop'; then + echo "error: TakeLeft missing Tracked_Drop for remaining field" >&2 + sed -n '/^Array_int TakeLeft/,/^}/p' "$TMP/mr/build/main.c" + exit 1 +fi +echo " move_field_remaining: PASS (run + no PairBag_Drop + Tracked_Drop on right)" + +# --- nested path move outer.inner.items --- +echo "=== smoke: move_field_nested ===" +mkdir -p "$TMP/mn/src" +cp -a "$ROOT/rt" "$TMP/mn/" +cat > "$TMP/mn/bux.toml" <<'EOF' +[Package] +Name = "move_field_nested" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" +EOF +cp "$ROOT/examples/move_field_nested.bux" "$TMP/mn/src/Main.bux" +(cd "$TMP/mn" && "$BUXC" run .) +# TakeNestedItems: no Outer_Drop; must Tracked_Drop remaining note + tag +if sed -n '/^Array_int TakeNestedItems/,/^}/p' "$TMP/mn/build/main.c" | grep -q 'Outer_Drop'; then + echo "error: TakeNestedItems still Outer_Drops after nested path move" >&2 + sed -n '/^Array_int TakeNestedItems/,/^}/p' "$TMP/mn/build/main.c" + exit 1 +fi +tdrops=$(sed -n '/^Array_int TakeNestedItems/,/^}/p' "$TMP/mn/build/main.c" | grep -c 'Tracked_Drop' || true) +if [[ "${tdrops:-0}" -lt 2 ]]; then + echo "error: TakeNestedItems expected ≥2 Tracked_Drop (inner.note + tag), got $tdrops" >&2 + sed -n '/^Array_int TakeNestedItems/,/^}/p' "$TMP/mn/build/main.c" + exit 1 +fi +echo " move_field_nested: PASS (run + no Outer_Drop + Tracked_Drop remaining)" + +# --- pointer field move p.items / (*p).items --- +echo "=== smoke: move_field_ptr ===" +mkdir -p "$TMP/mptr/src" +cp -a "$ROOT/rt" "$TMP/mptr/" +cat > "$TMP/mptr/bux.toml" <<'EOF' +[Package] +Name = "move_field_ptr" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" +EOF +cp "$ROOT/examples/move_field_ptr.bux" "$TMP/mptr/src/Main.bux" +(cd "$TMP/mptr" && "$BUXC" run .) +if sed -n '/^Array_int TakeViaPtr/,/^}/p' "$TMP/mptr/build/main.c" | grep -q 'Bag_Drop'; then + echo "error: TakeViaPtr still Bag_Drops after p.items move" >&2 + sed -n '/^Array_int TakeViaPtr/,/^}/p' "$TMP/mptr/build/main.c" + exit 1 +fi +if ! sed -n '/^Array_int TakeViaPtr/,/^}/p' "$TMP/mptr/build/main.c" | grep -q 'Tracked_Drop'; then + echo "error: TakeViaPtr missing Tracked_Drop for remaining tag" >&2 + sed -n '/^Array_int TakeViaPtr/,/^}/p' "$TMP/mptr/build/main.c" + exit 1 +fi +echo " move_field_ptr: PASS (run + no Bag_Drop + Tracked_Drop remaining)" # --- early return Drop counts --- echo "=== smoke: drop_early_return ===" @@ -78,4 +168,4 @@ out=$(cd "$TMP/de" && "$BUXC" run .) echo "$out" | grep -q 'PASS' echo " drop_early_return: PASS" -echo "PASS: smoke_drop_move (field-move + partial + early-return)" +echo "PASS: smoke_drop_move (field-move + partial + remaining + nested + ptr + early-return)" diff --git a/tools/smoke_windows_hello.sh b/tools/smoke_windows_hello.sh new file mode 100755 index 0000000..19973d0 --- /dev/null +++ b/tools/smoke_windows_hello.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Session 71 — Windows / MinGW hello smoke (also testable on Unix via BUX_RUNTIME=win). +# Builds examples/hello.bux with the minimal runtime and checks stdout. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}" +unset BUX_DEBUG_FILE || true + +# Prefer buxc.exe on Windows, else buxc +if [[ -x "$ROOT/buxc.exe" ]]; then + BUXC="$ROOT/buxc.exe" +elif [[ -x "$ROOT/buxc" ]]; then + BUXC="$ROOT/buxc" +else + (cd "$ROOT" && make build) + if [[ -x "$ROOT/buxc.exe" ]]; then BUXC="$ROOT/buxc.exe" + else BUXC="$ROOT/buxc" + fi +fi + +# Force minimal runtime when not already on Windows (Linux/macOS local check) +case "$(uname -s 2>/dev/null || echo unknown)" in + MINGW*|MSYS*|CYGWIN*|Windows_NT) ;; + *) + export BUX_RUNTIME="${BUX_RUNTIME:-win}" + ;; +esac + +if ! command -v gcc >/dev/null 2>&1 && ! command -v cc >/dev/null 2>&1; then + echo "error: need gcc/cc on PATH (MinGW on Windows)" >&2 + exit 1 +fi + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +mkdir -p "$TMP/src" +cp -a "$ROOT/rt" "$TMP/" +cat > "$TMP/bux.toml" <<'EOF' +[Package] +Name = "hello" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" +EOF +cp "$ROOT/examples/hello.bux" "$TMP/src/Main.bux" + +echo "=== smoke_windows_hello: build+run ===" +out=$("$BUXC" run "$TMP" 2>&1) || { + echo "$out" >&2 + exit 1 +} +echo "$out" +echo "$out" | grep -q 'Hello, Bux!' + +# Confirm minimal runtime was used when forced / on Windows +if [[ -f "$TMP/build/runtime.c" ]]; then + if ! grep -q 'Windows / MinGW minimal' "$TMP/build/runtime.c"; then + # On native Windows the CLI always copies runtime_win.c content into runtime.c + if grep -q 'pthread\|openssl/evp' "$TMP/build/runtime.c"; then + echo "error: expected runtime_win.c content, found full POSIX runtime" >&2 + exit 1 + fi + fi +fi + +echo "PASS: smoke_windows_hello"