feat: field-move Drop (partial/nested/ptr), stmt/pat macros, Windows runtime
ci / build (ubuntu) (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled

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.
This commit is contained in:
2026-07-21 12:29:53 +03:00
parent fe3b1e8b6a
commit a939f74b1b
22 changed files with 2672 additions and 117 deletions
+36 -6
View File
@@ -310,14 +310,12 @@ jobs:
make test-unit make test-unit
make test-examples-smoke make test-examples-smoke
# ── Windows smoke (bootstrap + pure Nim unit tests) ───────────────────── # ── Windows smoke (bootstrap + unit + MinGW hello) ─────────────────────
# Full bux→C examples need POSIX runtime (ucontext / pthread sockets in # Uses rt/runtime_win.c (no pthread/OpenSSL). Full POSIX runtime is Unix-only.
# rt/runtime.c) — not ported yet. This job still catches Nim/bootstrap
# regressions on Windows (prebuilt Nim zip = fast install).
windows: windows:
name: windows smoke name: windows smoke
runs-on: windows-latest runs-on: windows-latest
timeout-minutes: 30 timeout-minutes: 35
defaults: defaults:
run: run:
shell: bash shell: bash
@@ -345,6 +343,26 @@ jobs:
echo "$PWD/${{ env.NIM_INSTALL_DIR }}/bin" >> "$GITHUB_PATH" echo "$PWD/${{ env.NIM_INSTALL_DIR }}/bin" >> "$GITHUB_PATH"
echo "$HOME/.nimble/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) - name: Cache nimcache (bootstrap + unit)
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -357,6 +375,8 @@ jobs:
run: | run: |
set -e set -e
nim -v nim -v
# MinGW gcc on PATH (from setup-msys2)
gcc --version | head -1
# Windows produces buxc.exe; keep name predictable for the smoke steps # 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 nim c --nimcache:nimcache -o:buxc.exe -d:release --opt:size bootstrap/main.nim
./buxc.exe --version ./buxc.exe --version
@@ -380,7 +400,17 @@ jobs:
rm -rf _test_tmp_pkg rm -rf _test_tmp_pkg
./buxc.exe new _test_tmp_pkg ./buxc.exe new _test_tmp_pkg
./buxc.exe --version ./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 ──────────────────────── # ── Single required status for branch protection ────────────────────────
ci-gate: ci-gate:
+2 -2
View File
@@ -5,10 +5,10 @@ BUILD_DIR := build
# Project-local nimcache so CI can cache compiles (default is ~/.cache/nim). # Project-local nimcache so CI can cache compiles (default is ~/.cache/nim).
NIMFLAGS ?= --nimcache:nimcache 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. # 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 .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
+10
View File
@@ -134,6 +134,8 @@ type
ekStringInterp ekStringInterp
ekClosure ekClosure
ekMacroCall ## name!(args) — expanded before sema ekMacroCall ## name!(args) — expanded before sema
ekMacroStmt ## `$s:stmt` arg wrapper (expand only)
ekMacroPat ## `$p:pat` arg wrapper (expand only)
MatchArm* = object MatchArm* = object
loc*: SourceLocation loc*: SourceLocation
@@ -243,6 +245,12 @@ type
## Group lengths for multi-rep: `m!(1,2; 3,4)` → @[2, 2]. ## Group lengths for multi-rep: `m!(1,2; 3,4)` → @[2, 2].
## Empty means a single group of all args. ## Empty means a single group of all args.
exprMacroGroupLens*: seq[int] 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 # Statements
@@ -375,6 +383,8 @@ type
mfkTt ## token-tree (MVP: same as expr) mfkTt ## token-tree (MVP: same as expr)
mfkLiteral ## int/float/string/char/bool literal only mfkLiteral ## int/float/string/char/bool literal only
mfkBlock ## block expression `{ … }` mfkBlock ## block expression `{ … }`
mfkStmt ## one statement (let/if/… or expression-stmt)
mfkPat ## match/let pattern
MacroFragment* = object MacroFragment* = object
name*: string ## primary / first name (compat) name*: string ## primary / first name (compat)
+34 -7
View File
@@ -805,11 +805,16 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
return 1 return 1
let baseDir = stdlibDir.parentDir() 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): if fileExists(runtimeSrc):
copyFile(runtimeSrc, runtimeDst) copyFile(runtimeSrc, runtimeDst)
else: else:
printError("runtime.c not found in rt/", useColor) printError(&"{runtimeName} not found in rt/", useColor)
return 1 return 1
let ioSrc = baseDir / "rt" / "io.c" 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) # Compile with cc — debug default (-O0 -g) or --release (-O2)
let outputName = if pctx.man.name != "": pctx.man.name else: "bux_out" let outputName = if pctx.man.name != "": pctx.man.name else: "bux_out"
let outputFile = buildDir / outputName let exeSuffix = when defined(windows): ".exe" else: ""
let outputFile = buildDir / (outputName & exeSuffix)
let optFlags = if opts.release: "-O2 -DNDEBUG" else: "-O0 -g" let optFlags = if opts.release: "-O2 -DNDEBUG" else: "-O0 -g"
let extraCflags = getEnv("BUX_CFLAGS") let extraCflags = getEnv("BUX_CFLAGS")
let cflags = if extraCflags.len > 0: optFlags & " " & extraCflags else: optFlags 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. # Host C toolchain + link flags
let ldStable = when defined(linux): " -Wl,--build-id=none" else: "" let envCc = getEnv("BUX_CC")
let ccCmd = &"cc {cflags} -pthread{ldStable} -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm -lcrypto 2>&1" 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: if opts.verbose:
printInfo(&"running: {ccCmd}", useColor) printInfo(&"running: {ccCmd}", useColor)
let (output, exitCode) = execCmdEx(ccCmd) let (output, exitCode) = execCmdEx(ccCmd)
@@ -848,7 +871,11 @@ proc cmdRun*(args: seq[string], opts: GlobalOptions): int =
return buildRes return buildRes
let man = loadManifest(root / "bux.toml") let man = loadManifest(root / "bux.toml")
let outputName = if man.name != "": man.name else: "bux_out" 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): if not fileExists(outputFile):
printError("executable not found after build", useColor) printError("executable not found after build", useColor)
return 1 return 1
+223 -16
View File
@@ -38,6 +38,14 @@ type
## Locals whose value was moved into another owner (struct field, let, return). ## Locals whose value was moved into another owner (struct field, let, return).
## Auto-Drop is skipped for these (session 37 — field-move ownership). ## Auto-Drop is skipped for these (session 37 — field-move ownership).
movedOutLocals*: HashSet[string] 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 = proc freshName(ctx: var LowerCtx): string =
inc ctx.varCounter inc ctx.varCounter
@@ -85,23 +93,80 @@ proc markMovedOutLocal(ctx: var LowerCtx, name: string) =
if name.len > 0 and ctx.hasPendingDrop(name): if name.len > 0 and ctx.hasPendingDrop(name):
ctx.movedOutLocals.incl(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 resolveExprType(ctx: var LowerCtx, expr: Expr): Type
proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string 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) = proc markMovedOutFromAst(ctx: var LowerCtx, expr: Expr) =
## Mark droppable locals used by-value in ownership-taking contexts. ## Mark droppable locals used by-value in ownership-taking contexts.
## Partial field moves: `return bag.items` / `let x = bag.items` mark `bag` ## Partial field moves: `return bag.items` / `return outer.inner.items` /
## so auto-Drop of the parent is skipped — **only when the field type itself ## `return p.items` (p = &bag) mark the **owner** local so auto-Drop of the
## is droppable** (not `return bag.tag` for an int field). ## parent is skipped. Records dotted path so remaining fields still Drop
## (sessions 70/73/74).
if expr == nil: return if expr == nil: return
case expr.kind case expr.kind
of ekIdent: of ekIdent:
ctx.markMovedOutLocal(expr.exprIdent) ctx.markMovedOutLocal(ctx.resolvePtrAlias(expr.exprIdent))
of ekField: of ekField:
let fieldTy = ctx.resolveExprType(expr) let fieldTy = ctx.resolveExprType(expr)
if ctx.autoDropFuncName(fieldTy).len > 0: 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: of ekStructInit:
for f in expr.exprStructInitFields: for f in expr.exprStructInitFields:
ctx.markMovedOutFromAst(f.value) ctx.markMovedOutFromAst(f.value)
@@ -119,6 +184,136 @@ proc shouldSkipDrop(ctx: LowerCtx, dropNode: HirNode, skipName: string): bool =
if target in ctx.movedOutLocals: return true if target in ctx.movedOutLocals: return true
false 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 = proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string =
## Return `Type_Drop` if this type should be auto-dropped, else "". ## Return `Type_Drop` if this type should be auto-dropped, else "".
## Also monomorphizes generic Drop/Free helpers for stdlib collections. ## Also monomorphizes generic Drop/Free helpers for stdlib collections.
@@ -195,8 +390,6 @@ proc patternLiteralNode(pat: Pattern, loc: SourceLocation): HirNode =
return nil return nil
return hirLit(pat.patLit, litTokenType(pat.patLit), loc) 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, proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
subjectEnumName: string, subjectHasData: bool, subjectEnumName: string, subjectHasData: bool,
loc: SourceLocation): HirNode = loc: SourceLocation): HirNode =
@@ -527,6 +720,8 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx =
result.patternBoundNames = initHashSet[string]() result.patternBoundNames = initHashSet[string]()
result.patternRenames = initTable[string, string]() result.patternRenames = initTable[string, string]()
result.movedOutLocals = initHashSet[string]() result.movedOutLocals = initHashSet[string]()
result.partialMovedFields = initTable[string, HashSet[string]]()
result.ptrAliases = initTable[string, string]()
proc sanitizeFatPart(s: string): string = proc sanitizeFatPart(s: string): string =
result = s.replace("const char*", "cstr").replace("unsigned int", "uint") 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, return HirNode(kind: hAssign, assignOp: tkAssign,
assignTarget: loadTarget, assignValue: value, assignTarget: loadTarget, assignValue: value,
typ: makeVoid(), loc: loc) 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 target = ctx.lowerExpr(expr.exprAssignTarget)
let value = ctx.lowerExpr(expr.exprAssignValue) let value = ctx.lowerExpr(expr.exprAssignValue)
return HirNode(kind: hAssign, assignOp: expr.exprAssignOp, return HirNode(kind: hAssign, assignOp: expr.exprAssignOp,
@@ -1842,6 +2040,9 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
if initHir != nil: if initHir != nil:
let store = hirStore(varNode, initHir, loc) let store = hirStore(varNode, initHir, loc)
stmts.add(store) 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` # Move: `let a = b` takes ownership of droppable local `b`
if stmt.stmtLetInit != nil: if stmt.stmtLetInit != nil:
ctx.markMovedOutFromAst(stmt.stmtLetInit) ctx.markMovedOutFromAst(stmt.stmtLetInit)
@@ -1879,8 +2080,7 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
retVal = hirVar(tmp, retTy, loc) retVal = hirVar(tmp, retTy, loc)
# Add defers in reverse order (LIFO); snapshot full stack for every return path # Add defers in reverse order (LIFO); snapshot full stack for every return path
for i in countdown(ctx.deferStmts.len - 1, 0): for i in countdown(ctx.deferStmts.len - 1, 0):
if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop): ctx.emitDropOrPartial(stmts, ctx.deferStmts[i], skipDrop)
stmts.add(ctx.deferStmts[i])
stmts.add(hirReturn(retVal, loc)) stmts.add(hirReturn(retVal, loc))
return hirBlock(stmts, nil, makeVoid(), 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]) let lastAlwaysReturns = stmts.len > 0 and blockAlwaysReturns(stmts[^1])
if ctx.deferStmts.len > deferBase and not lastAlwaysReturns: if ctx.deferStmts.len > deferBase and not lastAlwaysReturns:
for i in countdown(ctx.deferStmts.len - 1, deferBase): for i in countdown(ctx.deferStmts.len - 1, deferBase):
if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop): ctx.emitDropOrPartial(stmts, ctx.deferStmts[i], skipDrop)
stmts.add(ctx.deferStmts[i])
ctx.deferStmts.setLen(deferBase) ctx.deferStmts.setLen(deferBase)
elif ctx.deferStmts.len > deferBase and lastAlwaysReturns: elif ctx.deferStmts.len > deferBase and lastAlwaysReturns:
# Return path already owns these drops; pop so outer scopes don't re-run them # 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]() ctx.patternRenames = initTable[string, string]()
let oldDefers = ctx.deferStmts let oldDefers = ctx.deferStmts
let oldMovedOut = ctx.movedOutLocals let oldMovedOut = ctx.movedOutLocals
let oldPartialMoved = ctx.partialMovedFields
let oldPtrAliases = ctx.ptrAliases
ctx.deferStmts = @[] ctx.deferStmts = @[]
ctx.movedOutLocals = initHashSet[string]() 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. # Add parameters to varTypeExprs after clearing so they are visible in the body.
for p in funcParams: for p in funcParams:
if p.ptype != nil: if p.ptype != nil:
@@ -2279,10 +2482,14 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
hasReturn = true hasReturn = true
if not hasReturn: if not hasReturn:
for i in countdown(ctx.deferStmts.len - 1, 0): for i in countdown(ctx.deferStmts.len - 1, 0):
if not ctx.shouldSkipDrop(ctx.deferStmts[i], ""): ctx.emitDropOrPartial(body.blockStmts, ctx.deferStmts[i], "")
body.blockStmts.add(ctx.deferStmts[i]) # Always restore — mono of generics (generateMethodInstance → lowerFunc) nests
ctx.deferStmts = oldDefers # inside an outer function. Restoring only when deferStmts.len > 0 wiped the
ctx.movedOutLocals = oldMovedOut # 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.currentFuncDecl = oldFuncDecl
ctx.currentFuncRetType = oldFuncRetType ctx.currentFuncRetType = oldFuncRetType
+153 -16
View File
@@ -3,7 +3,7 @@
## Hygiene: substitute clones args at call-site, graft call-site SourceLocation ## Hygiene: substitute clones args at call-site, graft call-site SourceLocation
## onto expanded template nodes (Ast_QuoteCallSite policy from QUALITY_PLAN). ## 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 import ast, token, source_location
type type
@@ -193,9 +193,16 @@ proc cloneExpr*(e: Expr): Expr =
captureCount: 0, captureNames: @[], captureTypeKinds: @[]) captureCount: 0, captureNames: @[], captureTypeKinds: @[])
of ekMacroCall: of ekMacroCall:
result = Expr(kind: ekMacroCall, loc: e.loc, result = Expr(kind: ekMacroCall, loc: e.loc,
exprMacroName: e.exprMacroName, exprMacroArgs: @[]) exprMacroName: e.exprMacroName, exprMacroArgs: @[],
exprMacroGroupLens: e.exprMacroGroupLens)
for a in e.exprMacroArgs: for a in e.exprMacroArgs:
result.exprMacroArgs.add(cloneExpr(a)) 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 = proc cloneStmt*(s: Stmt): Stmt =
if s == nil: return nil if s == nil: return nil
@@ -329,6 +336,10 @@ proc graftExprLoc(e: Expr, loc: SourceLocation) =
of ekClosure: graftBlockLoc(e.exprClosureBody, loc) of ekClosure: graftBlockLoc(e.exprClosureBody, loc)
of ekMacroCall: of ekMacroCall:
for a in e.exprMacroArgs: graftExprLoc(a, loc) for a in e.exprMacroArgs: graftExprLoc(a, loc)
of ekMacroStmt:
graftStmtLoc(e.exprMacroStmt, loc)
of ekMacroPat:
discard
else: discard else: discard
proc graftStmtLoc(s: Stmt, loc: SourceLocation) = 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 substStmt(s: Stmt, env: MacroEnv, callLoc: SourceLocation): Stmt
proc substBlock(b: Block, env: MacroEnv, callLoc: SourceLocation): Block proc substBlock(b: Block, env: MacroEnv, callLoc: SourceLocation): Block
proc substStmtsFlat(stmts: seq[Stmt], env: MacroEnv, callLoc: SourceLocation): seq[Stmt] 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 = proc substBlock(b: Block, env: MacroEnv, callLoc: SourceLocation): Block =
if b == nil: return nil if b == nil: return nil
@@ -611,6 +662,14 @@ proc substStmtsFlat(stmts: seq[Stmt], env: MacroEnv, callLoc: SourceLocation): s
if body != nil: if body != nil:
for st in body.stmts: for st in body.stmts:
result.add(st) 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: else:
result.add(substStmt(s, env, callLoc)) 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) c.stmtMatchSubject = substExpr(c.stmtMatchSubject, env, callLoc)
var arms: seq[MatchArm] = @[] var arms: seq[MatchArm] = @[]
for arm in c.stmtMatchArms: 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))) body: substExpr(arm.body, env, callLoc)))
c.stmtMatchArms = arms c.stmtMatchArms = arms
of skReturn: of skReturn:
@@ -775,7 +834,7 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
c.exprMatchSubject = substExpr(c.exprMatchSubject, env, callLoc) c.exprMatchSubject = substExpr(c.exprMatchSubject, env, callLoc)
var arms: seq[MatchArm] = @[] var arms: seq[MatchArm] = @[]
for arm in c.exprMatchArms: 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))) body: substExpr(arm.body, env, callLoc)))
c.exprMatchArms = arms c.exprMatchArms = arms
of ekStringInterp: of ekStringInterp:
@@ -871,14 +930,89 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
if f.kinds.len > 0: return f.kinds if f.kinds.len > 0: return f.kinds
@[f.kind] @[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 = proc fragMatches(k: MacroFragKind, arg: Expr): bool =
## Kind constraint at match time (after arg expand). ## Kind constraint at match time (after arg expand).
if arg == nil: return false coerceArg(k, arg) != nil
case k
of mfkIdent: arg.kind == ekIdent
of mfkLiteral: arg.kind == ekLiteral
of mfkBlock: arg.kind == ekBlock
of mfkExpr, mfkTt: true
var matched: MacroRule var matched: MacroRule
var env: MacroEnv var env: MacroEnv
@@ -916,10 +1050,11 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
for c in 0 ..< chunk: for c in 0 ..< chunk:
let arg = g[i + c] let arg = g[i + c]
let k = if c < ks.len: ks[c] else: mfkExpr 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 failed = true
break break
e.lists[ns[c]].add(arg) e.lists[ns[c]].add(coerced)
if failed: break if failed: break
i += chunk i += chunk
else: else:
@@ -930,10 +1065,11 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
for c in 0 ..< chunk: for c in 0 ..< chunk:
let arg = flat[ai] let arg = flat[ai]
let k = if c < ks.len: ks[c] else: mfkExpr 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 failed = true
break break
e.lists[ns[c]].add(arg) e.lists[ns[c]].add(coerced)
inc ai inc ai
if failed: break if failed: break
else: else:
@@ -954,11 +1090,12 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
arg = flat[ai] arg = flat[ai]
inc ai inc ai
let k = if ks.len > 0: ks[0] else: frag.kind 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 failed = true
break break
let n = if ns.len > 0: ns[0] else: frag.name let n = if ns.len > 0: ns[0] else: frag.name
e.singles[n] = arg e.singles[n] = coerced
if not failed: if not failed:
if useGroups: if useGroups:
+25 -3
View File
@@ -429,6 +429,26 @@ proc parseAssign(p: var Parser): Expr
proc parseExpr(p: var Parser): Expr = proc parseExpr(p: var Parser): Expr =
p.parseAssign() 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 = proc parseStringInterpolation(p: var Parser, tok: Token): Expr =
## Parse a string literal that contains {expr} interpolations. ## Parse a string literal that contains {expr} interpolations.
let text = tok.text let text = tok.text
@@ -749,7 +769,7 @@ proc parsePostfix(p: var Parser): Expr =
curGroup = 0 curGroup = 0
p.skipNewlines() p.skipNewlines()
continue continue
margs.add(p.parseExpr()) margs.add(p.parseMacroArg())
inc curGroup inc curGroup
p.skipNewlines() p.skipNewlines()
if p.check(tkComma): if p.check(tkComma):
@@ -1628,10 +1648,12 @@ proc parseMacroFragKind(p: var Parser, kindTok: Token): MacroFragKind =
of "tt": mfkTt of "tt": mfkTt
of "literal", "lit": mfkLiteral of "literal", "lit": mfkLiteral
of "block": mfkBlock of "block": mfkBlock
of "stmt": mfkStmt
of "pat", "pattern": mfkPat
else: else:
p.emitError(kindTok.loc, p.emitError(kindTok.loc,
"unsupported macro fragment kind '" & kindTok.text & "unsupported macro fragment kind '" & kindTok.text &
"' (expr|ident|tt|literal|block)") "' (expr|ident|tt|literal|block|stmt|pat)")
mfkExpr mfkExpr
proc parseMacroFragment(p: var Parser): MacroFragment = proc parseMacroFragment(p: var Parser): MacroFragment =
@@ -1640,7 +1662,7 @@ proc parseMacroFragment(p: var Parser): MacroFragment =
if not fragTok.text.startsWith("$"): if not fragTok.text.startsWith("$"):
p.emitError(fragTok.loc, "macro fragment must start with '$' (e.g. $x:expr)") p.emitError(fragTok.loc, "macro fragment must start with '$' (e.g. $x:expr)")
discard p.expect(tkColon, "expected ':' after macro fragment name") 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) let k = p.parseMacroFragKind(kindTok)
result = MacroFragment( result = MacroFragment(
name: fragTok.text, name: fragTok.text,
+4
View File
@@ -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 # Should have been expanded before analyze; leftover is a compiler bug
sema.emitError(expr.loc, "unexpanded macro call '" & expr.exprMacroName & "!'") sema.emitError(expr.loc, "unexpanded macro call '" & expr.exprMacroName & "!'")
return makeUnknown() 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: of ekClosure:
let savedRetType = sema.currentRetType let savedRetType = sema.currentRetType
let savedClosureDepth = sema.closureDepth let savedClosureDepth = sema.closureDepth
+6 -4
View File
@@ -209,7 +209,7 @@ make test # full sequential suite (local)
| `apps` | ubuntu | `test-apps` | | `apps` | ubuntu | `test-apps` |
| `selfhost` | ubuntu | `test-selfhost-smoke` | | `selfhost` | ubuntu | `test-selfhost-smoke` |
| `macos` | macos-14 | rebuild + `test-unit` + `test-examples-smoke` (subset) | | `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-gate` | ubuntu | fails if any required job failed (branch protection) |
**CI speed helpers:** **CI speed helpers:**
@@ -217,8 +217,9 @@ make test # full sequential suite (local)
Windows uses a prebuilt Nim zip) Windows uses a prebuilt Nim zip)
- Project-local `nimcache/` via `NIMFLAGS=--nimcache:nimcache`, cached per job by source hash - 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) - 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 - **Windows** runs `tools/smoke_windows_hello.sh` with **MinGW gcc** + `rt/runtime_win.c`
(`ucontext`, `pthread`, BSD sockets). Smoke still validates bootstrap + unit tests on Win. (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. 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. Locally, `make test` still runs the full suite sequentially and builds once.
@@ -330,7 +331,8 @@ bux/
│ ├── Task.bux │ ├── Task.bux
│ └── Channel.bux │ └── Channel.bux
├── rt/ # C runtime ├── rt/ # C runtime
│ ├── runtime.c │ ├── runtime.c # full POSIX + OpenSSL (Unix)
│ ├── runtime_win.c # MinGW minimal (Windows / BUX_RUNTIME=win)
│ └── io.c │ └── io.c
├── examples/ # Example programs ├── examples/ # Example programs
├── tests/ # Unit tests (Nim) ├── tests/ # Unit tests (Nim)
+78 -6
View File
@@ -827,10 +827,59 @@ Rules:
etc.). Reading `bag.tag` (`int`) does **not** mark `bag` moved. etc.). Reading `bag.tag` (`int`) does **not** mark `bag` moved.
- After `let moved = bag.items`, `Bag_Drop(&bag)` is skipped; `moved` owns the - After `let moved = bag.items`, `Bag_Drop(&bag)` is skipped; `moved` owns the
array and is auto-dropped at scope end. array and is auto-dropped at scope end.
- Avoid using other droppable fields of the parent after a partial move (they - **Remaining fields:** if the parent has other droppable fields that were *not*
may be left in a moved-from state without per-field Drop). 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<int>,
right: Tracked, // also @[Drop]
}
func TakeLeft() -> Array<int> {
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<int> {
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 #### Manual Drop and non-Drop types
@@ -841,9 +890,11 @@ Golden smoke: `make test-drop-move` / `examples/move_field_partial.bux`.
#### Limits (honest) #### Limits (honest)
- Partial field moves mark the **whole parent local** as moved for Drop purposes - Partial field moves skip the **parent** `Type_Drop` and drop **remaining**
(not per-field Drop of remaining fields). droppable fields individually, including nested paths `a.b.c` and pointer
- Nested `a.b.c` path moves and moving through pointers are limited. 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 - Interface Drop uses a static `TypeName_Drop` symbol (zero cost), not dynamic
dispatch through a vtable. dispatch through a vtable.
- Double-free bugs in **unchecked** code that manually free *and* auto-drop are - Double-free bugs in **unchecked** code that manually free *and* auto-drop are
@@ -1174,6 +1225,25 @@ macro! wrap_block {
( $b:block ) => { $b } ( $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 // gensym: template locals renamed per expansion
macro! with_acc { macro! with_acc {
( $start:literal ) => { ( $start:literal ) => {
@@ -1195,6 +1265,8 @@ macro! with_acc {
| `tt` | token-tree (MVP: same as `expr`) | | `tt` | token-tree (MVP: same as `expr`) |
| `literal` / `lit` | int/float/string/char/bool literal only | | `literal` / `lit` | int/float/string/char/bool literal only |
| `block` | block expression `{ … }` | | `block` | block expression `{ … }` |
| `stmt` | one statement (`let`/`if`/… or expression-stmt) |
| `pat` / `pattern` | match pattern (`_`, literals, `Enum::Var(…)`, …) |
- Fragment names start with `$` (lexer `$ident`). - Fragment names start with `$` (lexer `$ident`).
- **Repetition:** `$( $x:expr ),*` / `$( $x:expr )*` — one or more rep fragments per pattern. - **Repetition:** `$( $x:expr ),*` / `$( $x:expr )*` — one or more rep fragments per pattern.
+96 -6
View File
@@ -1,7 +1,7 @@
# Bux — План към „добър“ език (v0.5 → v1.0) # Bux — План към „добър“ език (v0.5 → v1.0)
> **Дата:** 2026-07-20 > **Дата:** 2026-07-21
> **Текущо:** v0.5.x — macros (unhygienic binders + multi-rep), partial field-move, lean CI > **Текущо:** v0.5.x — macros, field-move via pointers, Windows hello
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
--- ---
@@ -14,7 +14,7 @@
| Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ | | Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ |
| HIR → C | Tuples + fat `func` ABI в bootstrap **и** selfhost | ★★★★☆ | | HIR → C | Tuples + fat `func` ABI в bootstrap **и** selfhost | ★★★★☆ |
| Selfhost (`src/`) | ~12k LOC, binary-identical loop, closures+tuples | ★★★★★ | | 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 | ★★★★☆ | | Concurrency | M:N tasks + channels + async | ★★★★☆ |
| Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ | | Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ |
| Tooling | LSP 0.5 hover/def/outline/**refs/rename** + fmt/test/doc | ★★★★★ | | 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) 1. Windows: more examples (strings/ownership) on MinGW; optional Win OpenSSL
2. Per-field Drop after partial move (stretch) 2. Macro: true token-tree `tt` / nested pattern rewrite depth
3. Macro: true `stmt`/`pat` token-tree frags (stretch) 3. Cross-function pointer ownership transfer (callee `*Bag` param)
+84
View File
@@ -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;
}
+91
View File
@@ -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<int>,
note: Tracked
}
func Inner_Drop(self: *Inner) {
Array_Drop<int>(&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<int> {
var items: Array<int> = Array_New<int>(2);
Array_Push<int>(&items, 10);
Array_Push<int>(&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<int> = Array_New<int>(1);
Array_Push<int>(&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<int> = outer.inner.items;
return Array_Len<int>(&moved) as int;
}
func Main() -> int {
var drops: int = 0;
let taken: Array<int> = TakeNestedItems(&drops);
Test_AssertTrue(Array_Len<int>(&taken) == 2);
Test_AssertTrue(Array_Get<int>(&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;
}
+131
View File
@@ -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<int>,
tag: Tracked
}
func Bag_Drop(self: *Bag) {
Array_Drop<int>(&self.items);
Tracked_Drop(&self.tag);
}
// p.items via auto-deref of *Bag
func TakeViaPtr(counter: *int) -> Array<int> {
var items: Array<int> = Array_New<int>(2);
Array_Push<int>(&items, 10);
Array_Push<int>(&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<int> {
var items: Array<int> = Array_New<int>(1);
Array_Push<int>(&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<int> = Array_New<int>(1);
Array_Push<int>(&items, 3);
let bag: Bag = Bag {
items: items,
tag: Tracked { id: 3, counter: counter }
};
var p: *Bag = &bag;
let moved: Array<int> = p.items;
return Array_Len<int>(&moved) as int;
}
// Nested path through pointer: p.inner.items
@[Drop]
struct Inner {
items: Array<int>,
note: Tracked
}
func Inner_Drop(self: *Inner) {
Array_Drop<int>(&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<int> {
var items: Array<int> = Array_New<int>(1);
Array_Push<int>(&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<int> = TakeViaPtr(&drops);
Test_AssertTrue(Array_Len<int>(&a) == 2);
Test_AssertTrue(Array_Get<int>(&a, 0) == 10);
// tag Drop once
Test_AssertTrue(drops == 1);
let b: Array<int> = TakeViaDeref(&drops);
Test_AssertTrue(Array_Len<int>(&b) == 1);
Test_AssertTrue(drops == 2);
let n: int = PeekAfterPtrTake(&drops);
Test_AssertTrue(n == 1);
Test_AssertTrue(drops == 3);
let c: Array<int> = TakeNestedViaPtr(&drops);
Test_AssertTrue(Array_Get<int>(&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;
}
+87
View File
@@ -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<int>,
right: Tracked
}
func PairBag_Drop(self: *PairBag) {
Array_Drop<int>(&self.left);
Tracked_Drop(&self.right);
}
// Move left out via return — right must still Drop (Tracked_Drop)
func TakeLeft(counter: *int) -> Array<int> {
var left: Array<int> = Array_New<int>(2);
Array_Push<int>(&left, 10);
Array_Push<int>(&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<int> = Array_New<int>(1);
Array_Push<int>(&left, 7);
let pair: PairBag = PairBag {
left: left,
right: Tracked { n: 2, counter: counter }
};
let moved: Array<int> = pair.left;
return Array_Len<int>(&moved) as int;
}
// Whole-struct return: no PairBag_Drop here (caller owns it)
func MakePair() -> PairBag {
var left: Array<int> = Array_New<int>(1);
Array_Push<int>(&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<int> = TakeLeft(&drops);
Test_AssertTrue(Array_Len<int>(&taken) == 2);
Test_AssertTrue(Array_Get<int>(&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<int>(&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;
}
+733
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <errno.h>
#if defined(_WIN32) || defined(_WIN64)
# include <windows.h>
# include <direct.h>
# include <io.h>
# define BUX_IS_WIN 1
# define bux_mkdir_one(p) _mkdir(p)
#else
# include <unistd.h>
# include <sys/stat.h>
# 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;
}
+9 -1
View File
@@ -128,6 +128,8 @@ module Ast {
const ekStringInterp: int = 26; const ekStringInterp: int = 26;
const ekClosure: int = 27; const ekClosure: int = 27;
const ekMacroCall: int = 28; // name!(args) — expanded before sema 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 { struct ExprList {
expr: *Expr, expr: *Expr,
@@ -188,6 +190,9 @@ module Ast {
// Match arms (for ekMatch) // Match arms (for ekMatch)
matchArms: *MatchArm, matchArms: *MatchArm,
matchArmCount: int, 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, refType: null as *TypeExpr, refBlock: null as *Block,
genericCallee: "", genericTypeArg0: "", genericTypeArg1: "", genericTypeArgCount: 0, genericCallee: "", genericTypeArg0: "", genericTypeArg1: "", genericTypeArgCount: 0,
structName: "", structFieldCount: 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 { func Ast_MakeIdent(name: String, line: uint32, col: uint32) -> Expr {
@@ -706,6 +712,8 @@ module Ast {
n.callArgCount = e.callArgCount; n.callArgCount = e.callArgCount;
n.matchArms = Ast_CloneMatchArm(e.matchArms); n.matchArms = Ast_CloneMatchArm(e.matchArms);
n.matchArmCount = e.matchArmCount; n.matchArmCount = e.matchArmCount;
n.macroStmt = Ast_CloneStmt(e.macroStmt);
n.macroPat = Ast_ClonePattern(e.macroPat);
return n; return n;
} }
+495 -35
View File
@@ -76,6 +76,60 @@ module CBackend {
movedName5: String, movedName5: String,
movedName6: String, movedName6: String,
movedName7: 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, tmpCounter: int,
currentRetType: String, currentRetType: String,
// #line debug maps (E.4 selfhost parity) // #line debug maps (E.4 selfhost parity)
@@ -130,6 +184,329 @@ module CBackend {
return true; 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). /// Mark droppable locals moved by-value (struct fields / nested / partial field).
/// `valueTypeHint`: when non-empty (e.g. function return type), used to decide /// `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`). /// 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 // Partial field move: only when the *value* type is droppable
if node.kind == hFieldPtr || node.kind == hFieldAccess { 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; var vty: String = valueTypeHint;
if String_Eq(vty, "") && cbe.mod != null as *HirModule {
vty = CBE_GetExprTypeName(cbe.mod, node);
}
if String_Eq(vty, "") { if String_Eq(vty, "") {
vty = node.typeName; vty = node.typeName;
} }
// Field HIR often stores the *base* struct typeName — prefer hint vty = CBE_NormalizeTypeName(vty);
if CBE_IsDroppableTypeName(vty) { let baseName: String = CBE_BaseVarName(cbe, node);
// Walk to base local (hVar / load / nested) var path: String = CBE_FieldPathFromNode(node);
CBE_MarkMovedFromNodeHint(cbe, node.child1, ""); // 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; return;
} }
@@ -292,10 +698,16 @@ module CBackend {
if i == 5 { dn = cbe.defer5; } if i == 5 { dn = cbe.defer5; }
if i == 6 { dn = cbe.defer6; } if i == 6 { dn = cbe.defer6; }
if i == 7 { dn = cbe.defer7; } 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); let deferVarName: String = CBE_GetAutoDropVarName(dn);
if !String_Eq(deferVarName, "") && CBE_IsMoved(cbe, deferVarName) { if !String_Eq(deferVarName, "") {
return; if CBE_IsMoved(cbe, deferVarName) || CBE_HasPartialMoved(cbe, deferVarName) {
if CBE_HasPartialMoved(cbe, deferVarName) {
CBE_EmitRemainingFieldDrops(cbe, deferVarName);
}
return;
}
} }
StringBuilder_Append(&cbe.sb, "\n"); StringBuilder_Append(&cbe.sb, "\n");
var sp: int = 0; var sp: int = 0;
@@ -427,12 +839,14 @@ module CBackend {
return; 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 { if kind == hUnary {
StringBuilder_Append(&cbe.sb, "(");
StringBuilder_Append(&cbe.sb, CBackend_OpToC(node.intValue)); StringBuilder_Append(&cbe.sb, CBackend_OpToC(node.intValue));
StringBuilder_Append(&cbe.sb, "("); StringBuilder_Append(&cbe.sb, "(");
CBE_EmitExpr(cbe, node.child1); CBE_EmitExpr(cbe, node.child1);
StringBuilder_Append(&cbe.sb, ")"); StringBuilder_Append(&cbe.sb, "))");
return; return;
} }
@@ -596,6 +1010,7 @@ module CBackend {
if !String_Eq(node.typeName, "") { if !String_Eq(node.typeName, "") {
ct = node.typeName; ct = node.typeName;
} }
CBE_RegisterLocalType(cbe, node.strValue, ct);
StringBuilder_Append(&cbe.sb, CBE_CParamDecl(ct, node.strValue)); StringBuilder_Append(&cbe.sb, CBE_CParamDecl(ct, node.strValue));
return; return;
} }
@@ -610,6 +1025,15 @@ module CBackend {
CBE_MarkMovedFromNode(cbe, node.child2); 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 // Reinitialization removes moved status
if node.child1 != null as *HirNode && node.child1.kind == hVar { if node.child1 != null as *HirNode && node.child1.kind == hVar {
CBE_RemoveMoved(cbe, node.child1.strValue); CBE_RemoveMoved(cbe, node.child1.strValue);
@@ -620,6 +1044,8 @@ module CBackend {
if !String_Eq(node.child1.typeName, "") { if !String_Eq(node.child1.typeName, "") {
ct = 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)); StringBuilder_Append(&cbe.sb, CBE_CParamDecl(ct, node.child1.strValue));
if node.child2 != null as *HirNode { if node.child2 != null as *HirNode {
StringBuilder_Append(&cbe.sb, " = "); StringBuilder_Append(&cbe.sb, " = ");
@@ -880,6 +1306,10 @@ module CBackend {
// Assign: target = value // Assign: target = value
if kind == hAssign { 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); CBE_EmitExpr(cbe, node.child1);
StringBuilder_Append(&cbe.sb, " = "); StringBuilder_Append(&cbe.sb, " = ");
CBE_EmitExpr(cbe, node.child2); CBE_EmitExpr(cbe, node.child2);
@@ -1478,6 +1908,9 @@ module CBackend {
cbe.mod = mod; cbe.mod = mod;
cbe.deferCount = 0; cbe.deferCount = 0;
cbe.movedCount = 0; cbe.movedCount = 0;
cbe.partialCount = 0;
cbe.aliasCount = 0;
cbe.localCount = 0;
cbe.tmpCounter = 0; cbe.tmpCounter = 0;
cbe.lastDebugLine = 0; cbe.lastDebugLine = 0;
cbe.lastDebugFile = ""; 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_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"); 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)) // Struct definitions before enums (enums may embed structs by value).
// Pass 1: emit structs with no value-typed struct fields (leaf structs) // Multi-pass topo: emit only when every value-typed field type is already
si = 0; // emitted (handles Outer { inner: Inner } nesting — session 73).
while si < mod.structCount { var emittedNames: String = ";";
if String_Eq(mod.structs[si].name, "") || CBE_StructHasGeneric(&mod.structs[si]) { 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; 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 // Enum definitions
@@ -1810,6 +2268,8 @@ module CBackend {
cbe.checkedFunc = mod.funcs[i].checkedFunc; cbe.checkedFunc = mod.funcs[i].checkedFunc;
cbe.deferCount = 0; cbe.deferCount = 0;
cbe.movedCount = 0; cbe.movedCount = 0;
cbe.partialCount = 0;
cbe.localCount = 0;
cbe.tmpCounter = 0; cbe.tmpCounter = 0;
cbe.currentRetType = mod.funcs[i].retTypeName; cbe.currentRetType = mod.funcs[i].retTypeName;
var hasReturn: bool = false; var hasReturn: bool = false;
+179 -10
View File
@@ -329,20 +329,134 @@ module MacroExpand {
} }
} }
// Fragment kind check: "ident" | "literal" | "block" | expr|tt (any) // Flatten ekIdent / ekField(::) chain to "A::B::C" (selfhost path style)
func Macro_FragMatches(kindStr: String, aexp: *Expr) -> bool { func Macro_PathFromExpr(e: *Expr) -> String {
if aexp == null as *Expr { return false; } 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") { if String_Eq(kindStr, "ident") {
return aexp.kind == ekIdent; if aexp.kind != ekIdent { return null as *Expr; }
return aexp;
} }
if String_Eq(kindStr, "literal") { if String_Eq(kindStr, "literal") {
return aexp.kind == ekLiteral; if aexp.kind != ekLiteral { return null as *Expr; }
return aexp;
} }
if String_Eq(kindStr, "block") { if String_Eq(kindStr, "block") {
return aexp.kind == ekBlock; if aexp.kind != ekBlock { return null as *Expr; }
return aexp;
} }
// expr / tt / unknown → accept if String_Eq(kindStr, "stmt") {
return true; 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 // kinds encoded as "expr;ident;rep:expr," — return fi-th segment
@@ -479,6 +593,36 @@ module MacroExpand {
li = li + 1; 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 { } else {
let one: *Stmt = Subst_Stmt(s, env, file, line, col); let one: *Stmt = Subst_Stmt(s, env, file, line, col);
if one != null as *Stmt { if one != null as *Stmt {
@@ -647,6 +791,7 @@ module MacroExpand {
} }
var arm: *MatchArm = c.matchArms; var arm: *MatchArm = c.matchArms;
while arm != null as *MatchArm { 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.body = Subst_Expr(arm.body, env, file, line, col);
arm = arm.next; arm = arm.next;
} }
@@ -658,6 +803,29 @@ module MacroExpand {
return c; 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 { func Subst_Stmt(s: *Stmt, env: *MacroEnv, file: String, line: uint32, col: uint32) -> *Stmt {
if s == null as *Stmt { return null as *Stmt; } if s == null as *Stmt { return null as *Stmt; }
let c: *Stmt = Ast_CloneStmt(s); let c: *Stmt = Ast_CloneStmt(s);
@@ -944,8 +1112,9 @@ module MacroExpand {
if argList == null as *ExprList { ok = false; break; } if argList == null as *ExprList { ok = false; break; }
} }
let aexp: *Expr = argList.expr; let aexp: *Expr = argList.expr;
if !Macro_FragMatches(kindStr, aexp) { ok = false; break; } let coerced: *Expr = Macro_CoerceArg(kindStr, aexp);
Env_Set(&env, paramIdx, Rule_FragName(rule, paramIdx), aexp); if coerced == null as *Expr { ok = false; break; }
Env_Set(&env, paramIdx, Rule_FragName(rule, paramIdx), coerced);
argList = argList.next; argList = argList.next;
flatLeft = flatLeft - 1; flatLeft = flatLeft - 1;
paramIdx = paramIdx + 1; paramIdx = paramIdx + 1;
+36 -3
View File
@@ -395,9 +395,38 @@ module Parser {
e.callArgCount = 0; e.callArgCount = 0;
e.matchArms = null as *MatchArm; e.matchArms = null as *MatchArm;
e.matchArmCount = 0; e.matchArmCount = 0;
e.macroStmt = null as *Stmt;
e.macroPat = null as *Pattern;
return e; 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 { func parserMakeStringLitExpr(text: String, line: uint32, col: uint32) -> *Expr {
let quoted: String = String_Concat(String_Concat("\"", text), "\""); let quoted: String = String_Concat(String_Concat("\"", text), "\"");
@@ -1284,7 +1313,7 @@ module Parser {
while parserCheck(p, tkNewLine) { discard parserAdvance(p); } while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
continue; continue;
} }
let argExpr: *Expr = parserParseExpr(p); let argExpr: *Expr = parserParseMacroArg(p);
let argNode: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList; let argNode: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
argNode.expr = argExpr; argNode.expr = argExpr;
argNode.next = null as *ExprList; argNode.next = null as *ExprList;
@@ -2387,8 +2416,10 @@ module Parser {
let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind"); let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind");
var kname: String = kindTok.text; var kname: String = kindTok.text;
if String_Eq(kname, "lit") { kname = "literal"; } 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") 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"; kname = "expr";
} }
if nIn == 0 { if nIn == 0 {
@@ -2432,8 +2463,10 @@ module Parser {
let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind"); let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind");
var kname: String = kindTok.text; var kname: String = kindTok.text;
if String_Eq(kname, "lit") { kname = "literal"; } 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") 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"; kname = "expr";
} }
if rule.paramCount < 9 { if rule.paramCount < 9 {
+92 -2
View File
@@ -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" sed -n '/^Array_int TakeItems/,/^}/p' "$TMP/mp/build/main.c"
exit 1 exit 1
fi 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 --- # --- early return Drop counts ---
echo "=== smoke: drop_early_return ===" echo "=== smoke: drop_early_return ==="
@@ -78,4 +168,4 @@ out=$(cd "$TMP/de" && "$BUXC" run .)
echo "$out" | grep -q 'PASS' echo "$out" | grep -q 'PASS'
echo " drop_early_return: 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)"
+68
View File
@@ -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"