feat: stdlib daily APIs, macro tt/type paste, riscv64 cross smoke
ci / build (ubuntu) (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 / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled

Sessions 83–87: grow Array/Map/String/Test ergonomics; delimiter-balanced
and juxta :tt macros plus $t:type fragments; expression-level $(…),* in
templates; selfhost slice lits; riscv64/aarch64 cross smoke helper and
freestanding docs. Null-safe CBE type names and String_StartsWith.
This commit is contained in:
2026-07-27 21:40:11 +03:00
parent a785747c37
commit d60ce2bc3f
23 changed files with 1263 additions and 86 deletions
+4 -4
View File
@@ -5,10 +5,10 @@ BUILD_DIR := build
# Project-local nimcache so CI can cache compiles (default is ~/.cache/nim).
NIMFLAGS ?= --nimcache:nimcache
EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked ownership_release drop_early_return lifetime_elision ctfe ctfe_crc 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 move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt
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 ctfe_crc 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 move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt macro_tt_raw macro_type collections_extra
# Platform smoke (macOS CI): full EXAMPLES still runs on Linux.
EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt ctfe_crc
EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt macro_tt_raw ctfe_crc
.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 test-linux-targets ensure-buxc
@@ -265,10 +265,10 @@ test-dwarf: ensure-buxc
@chmod +x tools/smoke_dwarf.sh
@tools/smoke_dwarf.sh
# Session 75 — Linux / cloud / embedded: minimal runtime, static, aarch64 cross, CTFE CRC
# Session 75/85 — Linux / cloud / embedded: minimal, static, aarch64+riscv64 cross, CTFE CRC
.PHONY: test-linux-targets
test-linux-targets: ensure-buxc
@echo "=== Linux targets smoke (minimal / static / cross) ==="
@echo "=== Linux targets smoke (minimal / static / aarch64+riscv64 cross) ==="
@chmod +x tools/smoke_linux_targets.sh
@tools/smoke_linux_targets.sh
+16
View File
@@ -136,6 +136,9 @@ type
ekMacroCall ## name!(args) — expanded before sema
ekMacroStmt ## `$s:stmt` arg wrapper (expand only)
ekMacroPat ## `$p:pat` arg wrapper (expand only)
ekMacroTt ## `$x:tt` bound fragment (expand only; may flatten groups)
ekMacroRep ## `$( expr ),*` expression-level rep in templates (expand only)
ekMacroType ## `$t:type` bound type (expand only)
MatchArm* = object
loc*: SourceLocation
@@ -251,6 +254,18 @@ type
of ekMacroPat:
## Pattern fragment argument (`$p:pat`) — only during expand
exprMacroPat*: Pattern
of ekMacroTt:
## Bound `:tt` fragment. `exprMacroTtGroup` is true for parenthesized
## multi-element groups (parsed as tuples) — flattened when spliced as
## the sole argument of a call (`$f($args)` → `f(a, b)`).
exprMacroTtInner*: Expr
exprMacroTtGroup*: bool
of ekMacroRep:
## Expression-level `$( body ),*` / `$( body )*` in macro templates.
exprMacroRepBody*: Expr
of ekMacroType:
## Bound `:type` fragment.
exprMacroType*: TypeExpr
# ---------------------------------------------------------------------------
# Statements
@@ -385,6 +400,7 @@ type
mfkBlock ## block expression `{ … }`
mfkStmt ## one statement (let/if/… or expression-stmt)
mfkPat ## match/let pattern
mfkType ## type expression (`int`, `*int`, `String`, …)
MacroFragment* = object
name*: string ## primary / first name (compat)
+232 -6
View File
@@ -24,6 +24,45 @@ proc emitErr(res: var MacroExpandResult, loc: SourceLocation, msg: string) =
proc cloneExpr*(e: Expr): Expr
proc cloneStmt*(s: Stmt): Stmt
proc cloneBlock*(b: Block): Block
proc cloneTypeExpr*(t: TypeExpr): TypeExpr
proc cloneTypeExpr*(t: TypeExpr): TypeExpr =
if t == nil: return nil
case t.kind
of tekNamed:
result = TypeExpr(kind: tekNamed, loc: t.loc, typeName: t.typeName, typeArgs: @[])
for a in t.typeArgs:
result.typeArgs.add(cloneTypeExpr(a))
of tekPath:
result = TypeExpr(kind: tekPath, loc: t.loc, pathSegments: t.pathSegments)
of tekSlice:
result = TypeExpr(kind: tekSlice, loc: t.loc,
sliceElement: cloneTypeExpr(t.sliceElement), sliceSize: cloneExpr(t.sliceSize))
of tekOwn:
result = TypeExpr(kind: tekOwn, loc: t.loc,
pointerPointee: cloneTypeExpr(t.pointerPointee), refLifetime: t.refLifetime)
of tekPointer:
result = TypeExpr(kind: tekPointer, loc: t.loc,
pointerPointee: cloneTypeExpr(t.pointerPointee), refLifetime: t.refLifetime)
of tekRef:
result = TypeExpr(kind: tekRef, loc: t.loc,
pointerPointee: cloneTypeExpr(t.pointerPointee), refLifetime: t.refLifetime)
of tekMutRef:
result = TypeExpr(kind: tekMutRef, loc: t.loc,
pointerPointee: cloneTypeExpr(t.pointerPointee), refLifetime: t.refLifetime)
of tekDynRef:
result = TypeExpr(kind: tekDynRef, loc: t.loc, dynInterface: t.dynInterface)
of tekTuple:
result = TypeExpr(kind: tekTuple, loc: t.loc, tupleElements: @[])
for el in t.tupleElements:
result.tupleElements.add(cloneTypeExpr(el))
of tekSelf:
result = TypeExpr(kind: tekSelf, loc: t.loc)
of tekFunc:
result = TypeExpr(kind: tekFunc, loc: t.loc, funcParams: @[],
funcRet: cloneTypeExpr(t.funcRet))
for p in t.funcParams:
result.funcParams.add(cloneTypeExpr(p))
proc cloneBlock*(b: Block): Block =
if b == nil: return nil
@@ -203,6 +242,16 @@ proc cloneExpr*(e: Expr): Expr =
of ekMacroPat:
result = Expr(kind: ekMacroPat, loc: e.loc,
exprMacroPat: clonePattern(e.exprMacroPat))
of ekMacroTt:
result = Expr(kind: ekMacroTt, loc: e.loc,
exprMacroTtInner: cloneExpr(e.exprMacroTtInner),
exprMacroTtGroup: e.exprMacroTtGroup)
of ekMacroRep:
result = Expr(kind: ekMacroRep, loc: e.loc,
exprMacroRepBody: cloneExpr(e.exprMacroRepBody))
of ekMacroType:
result = Expr(kind: ekMacroType, loc: e.loc,
exprMacroType: cloneTypeExpr(e.exprMacroType))
proc cloneStmt*(s: Stmt): Stmt =
if s == nil: return nil
@@ -340,6 +389,12 @@ proc graftExprLoc(e: Expr, loc: SourceLocation) =
graftStmtLoc(e.exprMacroStmt, loc)
of ekMacroPat:
discard
of ekMacroTt:
graftExprLoc(e.exprMacroTtInner, loc)
of ekMacroRep:
graftExprLoc(e.exprMacroRepBody, loc)
of ekMacroType:
discard
else: discard
proc graftStmtLoc(s: Stmt, loc: SourceLocation) =
@@ -440,6 +495,10 @@ proc renameIdents(e: Expr, map: Table[string, string]): Expr =
c.exprTernaryCond = renameIdents(c.exprTernaryCond, map)
c.exprTernaryThen = renameIdents(c.exprTernaryThen, map)
c.exprTernaryElse = renameIdents(c.exprTernaryElse, map)
of ekMacroRep:
c.exprMacroRepBody = renameIdents(c.exprMacroRepBody, map)
of ekMacroTt:
c.exprMacroTtInner = renameIdents(c.exprMacroTtInner, map)
else: discard
result = c
@@ -539,6 +598,7 @@ proc gensymLocals(b: Block, callLoc: SourceLocation): Block =
proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr
proc substStmt(s: Stmt, env: MacroEnv, callLoc: SourceLocation): Stmt
proc substType(t: TypeExpr, env: MacroEnv, callLoc: SourceLocation): TypeExpr
proc substBlock(b: Block, env: MacroEnv, callLoc: SourceLocation): Block
proc substStmtsFlat(stmts: seq[Stmt], env: MacroEnv, callLoc: SourceLocation): seq[Stmt]
proc substPattern(p: Pattern, env: MacroEnv, callLoc: SourceLocation): Pattern
@@ -608,6 +668,10 @@ proc collectListNames(e: Expr, env: MacroEnv, into: var seq[string]) =
if st == nil: continue
if st.kind == skExpr: collectListNames(st.stmtExpr, env, into)
elif st.kind == skLet: collectListNames(st.stmtLetInit, env, into)
of ekMacroRep:
collectListNames(e.exprMacroRepBody, env, into)
of ekMacroTt:
collectListNames(e.exprMacroTtInner, env, into)
else: discard
proc collectListNamesStmt(st: Stmt, env: MacroEnv, into: var seq[string]) =
@@ -689,6 +753,7 @@ proc substStmt(s: Stmt, env: MacroEnv, callLoc: SourceLocation): Stmt =
if letBn.len > 0:
c.stmtLetName = letBn
expandUnhygienic.incl(letBn)
c.stmtLetType = substType(c.stmtLetType, env, callLoc)
c.stmtLetInit = substExpr(c.stmtLetInit, env, callLoc)
of skIf:
c.stmtIfCond = substExpr(c.stmtIfCond, env, callLoc)
@@ -748,11 +813,55 @@ proc substStmt(s: Stmt, env: MacroEnv, callLoc: SourceLocation): Stmt =
c.loc = callLoc
result = c
proc substType(t: TypeExpr, env: MacroEnv, callLoc: SourceLocation): TypeExpr =
## Substitute `$t:type` in type positions (sizeof, cast, let annotation, …).
if t == nil: return nil
if t.kind == tekNamed and t.typeName.startsWith("$") and env.singles.hasKey(t.typeName):
let bound = env.singles[t.typeName]
if bound != nil and bound.kind == ekMacroType and bound.exprMacroType != nil:
result = cloneTypeExpr(bound.exprMacroType)
if result != nil: result.loc = callLoc
return
result = cloneTypeExpr(t)
if result == nil: return
result.loc = callLoc
case result.kind
of tekNamed:
var args: seq[TypeExpr] = @[]
for a in result.typeArgs:
args.add(substType(a, env, callLoc))
result.typeArgs = args
of tekSlice:
result.sliceElement = substType(result.sliceElement, env, callLoc)
result.sliceSize = substExpr(result.sliceSize, env, callLoc)
of tekOwn, tekPointer, tekRef, tekMutRef:
result.pointerPointee = substType(result.pointerPointee, env, callLoc)
of tekTuple:
var els: seq[TypeExpr] = @[]
for el in result.tupleElements:
els.add(substType(el, env, callLoc))
result.tupleElements = els
of tekFunc:
var ps: seq[TypeExpr] = @[]
for p in result.funcParams:
ps.add(substType(p, env, callLoc))
result.funcParams = ps
result.funcRet = substType(result.funcRet, env, callLoc)
else:
discard
proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
if e == nil: return nil
# Fragment splice: $x → clone of bound argument (already call-site loc)
if e.kind == ekIdent and env.singles.hasKey(e.exprIdent):
result = cloneExpr(env.singles[e.exprIdent])
# Value position: unwrap MacroTt wrapper (groups become tuples as values)
if result != nil and result.kind == ekMacroTt:
result = cloneExpr(result.exprMacroTtInner)
# Type fragments are not values — leave as-is only if wrongly spliced as expr
if result != nil and result.kind == ekMacroType:
# Recover as sizeof? no — invalid value splice
return newLiteralExpr(Token(kind: tkIntLiteral, text: "0", loc: callLoc))
graftExprLoc(result, callLoc)
return
# Bare use of list frag outside $(…)* → first element if any, else 0
@@ -760,6 +869,10 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
let items = env.lists[e.exprIdent]
if items.len > 0:
result = cloneExpr(items[0])
if result != nil and result.kind == ekMacroTt:
result = cloneExpr(result.exprMacroTtInner)
if result != nil and result.kind == ekMacroType:
return newLiteralExpr(Token(kind: tkIntLiteral, text: "0", loc: callLoc))
graftExprLoc(result, callLoc)
return
return newLiteralExpr(Token(kind: tkIntLiteral, text: "0", loc: callLoc))
@@ -785,9 +898,58 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
of ekCall:
c.exprCallCallee = substExpr(c.exprCallCallee, env, callLoc)
var args: seq[Expr] = @[]
var argNames: seq[string] = @[]
for a in c.exprCallArgs:
# Expression-level `$( body ),*` → flatten into N call arguments
if a != nil and a.kind == ekMacroRep:
var listNames: seq[string] = @[]
collectListNames(a.exprMacroRepBody, env, listNames)
if listNames.len == 0:
args.add(substExpr(a.exprMacroRepBody, env, callLoc))
argNames.add("")
else:
var n = 0
for ln in listNames:
if env.lists.hasKey(ln):
n = max(n, env.lists[ln].len)
for i in 0 ..< n:
var singles = initTable[string, Expr]()
for k, v in env.singles.pairs: singles[k] = v
var lists = initTable[string, seq[Expr]]()
for k, v in env.lists.pairs:
if k notin listNames:
lists[k] = v
for ln in listNames:
if env.lists.hasKey(ln) and i < env.lists[ln].len:
singles[ln] = env.lists[ln][i]
let subEnv = MacroEnv(singles: singles, lists: lists)
args.add(substExpr(a.exprMacroRepBody, subEnv, callLoc))
argNames.add("")
continue
# Bare `$args:tt` that is a delimiter-balanced group → flatten elems
if a != nil and a.kind == ekIdent and env.singles.hasKey(a.exprIdent):
let bound = env.singles[a.exprIdent]
if bound != nil and bound.kind == ekMacroTt and bound.exprMacroTtGroup and
bound.exprMacroTtInner != nil:
let inner = bound.exprMacroTtInner
if inner.kind == ekTuple:
for el in inner.exprTupleElements:
let ce = cloneExpr(el)
graftExprLoc(ce, callLoc)
args.add(ce)
argNames.add("")
continue
if inner.kind == ekSlice:
for el in inner.exprSliceElements:
let ce = cloneExpr(el)
graftExprLoc(ce, callLoc)
args.add(ce)
argNames.add("")
continue
args.add(substExpr(a, env, callLoc))
argNames.add("")
c.exprCallArgs = args
c.exprCallArgNames = argNames
of ekIndex:
c.exprIndexObj = substExpr(c.exprIndexObj, env, callLoc)
c.exprIndexIdx = substExpr(c.exprIndexIdx, env, callLoc)
@@ -812,8 +974,12 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
c.exprTupleElements = els
of ekCast:
c.exprCastOperand = substExpr(c.exprCastOperand, env, callLoc)
c.exprCastType = substType(c.exprCastType, env, callLoc)
of ekIs:
c.exprIsOperand = substExpr(c.exprIsOperand, env, callLoc)
c.exprIsType = substType(c.exprIsType, env, callLoc)
of ekSizeOf:
c.exprSizeOfType = substType(c.exprSizeOfType, env, callLoc)
of ekTry:
c.exprTryOperand = substExpr(c.exprTryOperand, env, callLoc)
of ekUnwrap:
@@ -1010,17 +1176,77 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
if arg.kind in {ekMacroStmt, ekMacroPat}: return nil
return arg
of mfkTt:
# Session 76: token-tree is a *superset* of expr — any single
# well-formed AST fragment the call parser already produced:
# expr, block, ident, literal, path, call, stmt, or pat wrapper.
# (True delimiter-balanced raw tokens remain future work.)
# Session 76/84/85: token-tree is a *superset* of expr — any single
# well-formed AST fragment. Delimiter-balanced multi-element groups
# `(a, b)` (tuple) and `[a, b]` (slice lit) flatten when spliced as
# the sole call argument: `$f($args)` → `f(a, b)`.
if arg == nil: return nil
return arg
if arg.kind == ekMacroTt: return arg
let isGroup =
arg.kind == ekTuple or
(arg.kind == ekSlice and arg.exprSliceElements.len > 0)
return Expr(kind: ekMacroTt, loc: arg.loc,
exprMacroTtInner: arg, exprMacroTtGroup: isGroup)
of mfkType:
# Session 87 — type fragment from call-site expr shape
if arg == nil: return nil
if arg.kind == ekMacroType: return arg
proc exprToType(x: Expr): TypeExpr =
if x == nil: return nil
case x.kind
of ekIdent:
return TypeExpr(kind: tekNamed, loc: x.loc, typeName: x.exprIdent, typeArgs: @[])
of ekPath:
return TypeExpr(kind: tekPath, loc: x.loc, pathSegments: x.exprPath)
of ekUnary:
if x.exprUnaryOp == tkStar:
let inner = exprToType(x.exprUnaryOperand)
if inner == nil: return nil
return TypeExpr(kind: tekPointer, loc: x.loc, pointerPointee: inner, refLifetime: "")
if x.exprUnaryOp == tkAmp:
let inner = exprToType(x.exprUnaryOperand)
if inner == nil: return nil
return TypeExpr(kind: tekRef, loc: x.loc, pointerPointee: inner, refLifetime: "")
return nil
of ekGenericCall:
# Foo<Bar> written as generic-call shape at call site (limited)
var targs: seq[TypeExpr] = @[]
for ta in x.exprGenericTypeArgs:
targs.add(cloneTypeExpr(ta))
return TypeExpr(kind: tekNamed, loc: x.loc, typeName: x.exprGenericCallee, typeArgs: targs)
else:
return nil
let ty = exprToType(arg)
if ty == nil: return nil
return Expr(kind: ekMacroType, loc: arg.loc, exprMacroType: ty)
proc fragMatches(k: MacroFragKind, arg: Expr): bool =
## Kind constraint at match time (after arg expand).
coerceArg(k, arg) != nil
## Session 86 — free-form juxta: single call arg `F(a, b)` matches
## pattern `$f:ident $args:tt` (or `$f:ident, $args:tt`) as two fragments.
proc juxtaCallSplit(rule: MacroRule, inArgs: seq[Expr]): seq[Expr] =
result = inArgs
if inArgs.len != 1 or inArgs[0] == nil: return
if inArgs[0].kind != ekCall: return
if inArgs[0].exprCallCallee == nil or inArgs[0].exprCallCallee.kind != ekIdent:
return
# Exactly two fixed frags: ident + tt (no reps)
if rule.frags.len != 2: return
if rule.frags[0].isRep or rule.frags[1].isRep: return
let k0 = if rule.frags[0].kinds.len > 0: rule.frags[0].kinds[0] else: rule.frags[0].kind
let k1 = if rule.frags[1].kinds.len > 0: rule.frags[1].kinds[0] else: rule.frags[1].kind
if k0 != mfkIdent or k1 != mfkTt: return
let callee = inArgs[0].exprCallCallee
var elems: seq[Expr] = @[]
for a in inArgs[0].exprCallArgs:
elems.add(a)
let inner = Expr(kind: ekTuple, loc: inArgs[0].loc, exprTupleElements: elems)
let group = Expr(kind: ekMacroTt, loc: inArgs[0].loc,
exprMacroTtInner: inner, exprMacroTtGroup: true)
result = @[callee, group]
var matched: MacroRule
var env: MacroEnv
var found = false
@@ -1031,7 +1257,7 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
var gi = 0
var ai = 0
let useGroups = nReps > 1 and groups.len > 1
let flat = args
let flat = juxtaCallSplit(rule, args)
for frag in rule.frags:
if failed: break
+37 -3
View File
@@ -449,6 +449,19 @@ proc parseMacroArg(p: var Parser): Expr =
return Expr(kind: ekMacroPat, loc: loc, exprMacroPat: pat)
p.parseExpr()
proc parseMacroRepExpr(p: var Parser): Expr =
## Expression-level `$( body ),*` or `$( body )*` inside call args (templates only).
let loc = p.currentLoc
discard p.expect(tkDollar, "expected '$' to start expression macro rep")
discard p.expect(tkLParen, "expected '(' after '$' in expression macro rep")
let body = p.parseExpr()
discard p.expect(tkRParen, "expected ')' after expression macro rep body")
# Optional separator token before Kleene star: `),*` vs `)*`
if p.check(tkComma):
discard p.advance()
discard p.expect(tkStar, "expected '*' after expression macro rep")
return Expr(kind: ekMacroRep, loc: loc, exprMacroRepBody: body)
proc parseStringInterpolation(p: var Parser, tok: Token): Expr =
## Parse a string literal that contains {expr} interpolations.
let text = tok.text
@@ -689,6 +702,10 @@ proc parsePostfix(p: var Parser): Expr =
let operand = p.parseExpr()
args.add(Expr(kind: ekSpread, loc: operand.loc, exprSpreadOperand: operand))
argNames.add("")
elif p.macroTemplateMode and p.check(tkDollar) and p.peek(1) == tkLParen:
# Expression-level `$( expr ),*` / `$( expr )*` (session 84)
args.add(p.parseMacroRepExpr())
argNames.add("")
elif p.peek() == tkIdent and p.peek(1) == tkColon:
# Named argument: name: value
let nameTok = p.advance()
@@ -1650,10 +1667,11 @@ proc parseMacroFragKind(p: var Parser, kindTok: Token): MacroFragKind =
of "block": mfkBlock
of "stmt": mfkStmt
of "pat", "pattern": mfkPat
of "type": mfkType
else:
p.emitError(kindTok.loc,
"unsupported macro fragment kind '" & kindTok.text &
"' (expr|ident|tt|literal|block|stmt|pat)")
"' (expr|ident|tt|literal|block|stmt|pat|type)")
mfkExpr
proc parseMacroFragment(p: var Parser): MacroFragment =
@@ -1662,7 +1680,13 @@ proc parseMacroFragment(p: var Parser): MacroFragment =
if not fragTok.text.startsWith("$"):
p.emitError(fragTok.loc, "macro fragment must start with '$' (e.g. $x:expr)")
discard p.expect(tkColon, "expected ':' after macro fragment name")
let kindTok = p.expect(tkIdent, "expected fragment kind (expr|ident|tt|literal|block|stmt|pat)")
# `type` is a keyword (tkType); other kinds are bare idents
var kindTok: Token
if p.check(tkType):
kindTok = p.advance()
kindTok.text = "type"
else:
kindTok = p.expect(tkIdent, "expected fragment kind (expr|ident|tt|literal|block|stmt|pat|type)")
let k = p.parseMacroFragKind(kindTok)
result = MacroFragment(
name: fragTok.text,
@@ -1685,7 +1709,12 @@ proc parseMacroRepGroup(p: var Parser): MacroFragment =
if not fragTok.text.startsWith("$"):
p.emitError(fragTok.loc, "macro fragment must start with '$'")
discard p.expect(tkColon, "expected ':' after fragment name")
let kindTok = p.expect(tkIdent, "expected fragment kind")
var kindTok: Token
if p.check(tkType):
kindTok = p.advance()
kindTok.text = "type"
else:
kindTok = p.expect(tkIdent, "expected fragment kind")
names.add(fragTok.text)
kinds.add(p.parseMacroFragKind(kindTok))
p.skipNewlines()
@@ -1762,6 +1791,11 @@ proc parseMacroDecl(p: var Parser, isPublic: bool): Decl =
discard p.advance()
elif p.check(tkSemicolon):
discard p.advance()
elif p.check(tkDollar):
# Juxtaposition (session 86): `$f:ident $args:tt` without comma
continue
elif p.check(tkIdent) and p.at.text.startsWith("$"):
continue
else:
break
discard p.expect(tkRParen, "expected ')' to close macro pattern")
+2 -2
View File
@@ -1921,9 +1921,9 @@ proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type =
# Should have been expanded before analyze; leftover is a compiler bug
sema.emitError(expr.loc, "unexpanded macro call '" & expr.exprMacroName & "!'")
return makeUnknown()
of ekMacroStmt, ekMacroPat:
of ekMacroStmt, ekMacroPat, ekMacroTt, ekMacroRep, ekMacroType:
# Expand-only wrappers; must not reach type-checking
sema.emitError(expr.loc, "internal: unexpanded macro stmt/pat fragment")
sema.emitError(expr.loc, "internal: unexpanded macro fragment")
return makeUnknown()
of ekClosure:
let savedRetType = sema.currentRetType
+35 -2
View File
@@ -122,6 +122,9 @@ BUX_RUNTIME=minimal ./buxc build
# Cross-compile for ARM64 Linux (prefers aarch64-linux-gnu-gcc, else clang -target)
./buxc --static --release --target aarch64-linux-gnu build
# Cross-compile for RISC-V 64 (session 85; needs riscv64-linux-gnu-gcc)
./buxc --static --release --target riscv64-linux-gnu build
# Override C compiler
BUX_CC=aarch64-linux-gnu-gcc ./buxc --static --target aarch64-linux-gnu build
@@ -142,7 +145,7 @@ make test-musl-static # SKIP if no musl-gcc/zig
| `BUX_CFLAGS` | Extra flags appended to the C line |
```bash
# Smoke all of the above (+ CTFE CRC example)
# Smoke all of the above (+ CTFE CRC + optional aarch64/riscv64 cross)
make test-linux-targets
# Build static hello for Docker scratch/distroless
@@ -153,6 +156,36 @@ docker build -f examples/docker/Dockerfile.static \
> **Note:** Full runtime + fully-static OpenSSL is intentionally not the default (painful). Use minimal for static containers; keep full runtime for servers that need net/crypto (`nexus`).
### Cross toolchains
| Triple | Typical package | Smoke |
|--------|-----------------|-------|
| `aarch64-linux-gnu` | `gcc-aarch64-linux-gnu` | `make test-linux-targets` (SKIP if missing) |
| `riscv64-linux-gnu` | `gcc-riscv64-linux-gnu` | same (session 85) |
`clang -target <triple>` alone is **not** enough: you still need target headers and libc
(sysroot). Prefer `*-gcc` from a cross package, or set `BUX_CC` to a wrapper that
already knows the sysroot (e.g. Zig `zig cc -target …`).
### Freestanding / bare-metal (research spike, not v1.0)
`BUX_RUNTIME=minimal` / `--static` is the **Linux userspace / container / CTFE** path:
it still links against a libc (`malloc`, `printf`, `strlen`, …). It is *not* true
no-libc freestanding firmware.
| Layer | Status | Notes |
|-------|--------|-------|
| Thin runtime (no pthread/OpenSSL) | ✅ | `rt/runtime_minimal.c` |
| Static musl / distroless | ✅ | `make test-musl-static`, Dockerfiles |
| Linux multi-arch cross | ✅ | aarch64 + riscv64 smokes (SKIP without gcc) |
| True freestanding (`-ffreestanding`, no libc) | 🔬 spike | Needs custom alloc, panic, and I/O stubs |
| Cortex-M / qemu-system | 🔬 spike | Same; plus linker scripts and startup |
**Practical path today:** build with `BUX_RUNTIME=minimal --static --target …` for
Linux userspace on foreign ISAs; treat bare-metal as a research project that
starts from a custom `runtime_freestanding.c` (not shipped) and does **not**
import `Std::Net` / `Std::Task` / OpenSSL.
---
## Running Tests
@@ -165,7 +198,7 @@ make test-stdlib # stdlib golden packages
make test-registry # package registry (local + HTTP index)
make test-apps # showcase apps build + simpledb/jwt CLI smoke (in `make test`)
make test-dwarf # #line maps + .debug_info + --release (in `make test`)
make test-linux-targets # minimal runtime + static + aarch64 cross + CTFE CRC
make test-linux-targets # minimal + static + aarch64/riscv64 cross (SKIP) + CTFE CRC
make test-registry # package registry local + HTTP (in `make test`)
make test-selfhost-smoke # buxc2: move_field + multi-file #line (in `make test`)
make test-lsp # hover + references/rename + call hierarchy
+28 -1
View File
@@ -1284,11 +1284,12 @@ macro! with_acc {
|------|---------|
| `expr` | any expression |
| `ident` | bare identifier (`ekIdent`) |
| `tt` | token-tree: any single call-site AST fragment (expr/ident/lit/block/stmt/pat); broader than `expr` |
| `tt` | token-tree: any single call-site AST fragment; **delimiter-balanced multi-element groups** `(a, b)` and `[a, b]` flatten when spliced as the sole call argument (`$f($args)``f(a, b)`, not `f((a, b))`). Non-group `tt` unwraps to the value. Broader than `expr`. |
| `literal` / `lit` | int/float/string/char/bool literal only |
| `block` | block expression `{ … }` |
| `stmt` | one statement (`let`/`if`/… or expression-stmt) |
| `pat` / `pattern` | match pattern (`_`, literals, `Enum::Var(…)`, …) |
| `type` | type expression from call-site shape: named (`int`), pointer (`*int`); spliced into `sizeof($t)`, `as $t`, `let x: $t` |
- Fragment names start with `$` (lexer `$ident`).
- **Repetition:** `$( $x:expr ),*` / `$( $x:expr )*` — one or more rep fragments per pattern.
@@ -1297,6 +1298,23 @@ macro! with_acc {
`sum_groups!(1, 2; 10, 20, 30)`.
- Template `$( stmt; … )*` expands once per list item (zip when multiple lists used).
- Nested `$( $(…)* )*`: after outer binds list items as singles, inner expands once.
- **Expression-level rep in templates:** `$f( $($a),* )``$( expr ),*` / `$( expr )*`
inside call arguments expands to N positional args (session 84).
- **Delimiter-balanced `:tt` groups:** `apply_tt!(Add, (3, 4))` or
`apply_tt!(Add, [3, 4])` with `($f:ident, $args:tt) => { $f($args) }`
expands to `Add(3, 4)`. Contrast `:expr`, which keeps the group as one value.
- **Free-form juxta (session 86):** pattern `$f:ident $args:tt` (comma optional
between fragments) matches a **single call-site argument** that is a call
expression: `apply_juxta!(Add(2, 5))` → binds `$f=Add`, `$args` = arg-list
group, then `$f($args)` flattens to `Add(2, 5)`.
- **Type fragments (session 87):**
```bux
macro! size_of {
( $t:type ) => { sizeof($t) as int }
}
let n: int = size_of!(int);
let p: int = size_of!(*int);
```
### Invocation
@@ -1388,3 +1406,12 @@ Examples: `examples/macro_hygiene.bux`, `examples/macro_unhygienic.bux`.
Scheme/Rust colored identifiers or `stmt`/`pat` token trees.
- Macro expansion still yields a **block expression**; unhygienic names are
scoped to that block (not automatically injected into the caller scope).
- Expression-level `$(…)*` is only parsed inside **call argument lists** in
templates (not as a free-standing primary expression).
- Raw delimiter-balanced `tt` covers **tuple** `(a, b)` and **slice lit**
`[a, b]` groups, plus **juxta call-split** for `$f:ident $args:tt` matching
`F(a, b)`. Arbitrary free-form token pastes (operators-only, type-only
without AST) remain out of scope.
Examples: `examples/macro_tt.bux`, `examples/macro_tt_raw.bux`,
`examples/macro_repeat.bux`, `examples/macro_nested.bux`.
+101 -22
View File
@@ -1,7 +1,7 @@
# Bux — План към „добър“ език (v0.5 → v1.0)
> **Дата:** 2026-07-23
> **Текущо:** v0.5.x — CI cloud smokes + registry path cleanup (session 82)
> **Текущо:** v0.5.x — `:type` macros + Array_Reverse (session 87)
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
> **Платформен фокус:** **Linux** (primary) · **cloud-native** (servers, containers, HTTP) · **embedded** (cross, freestanding-ish, CTFE).
> **Не-цел:** MS Windows като product platform (исторически CI/hello smoke остават; няма roadmap investment).
@@ -116,7 +116,7 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
## Acceptance criteria за „добър v1.0“
- [x] Всички examples + apps + selfhost smoke на CI (`make test` via `.github/workflows/ci.yml`); selfhost-loop optional
- [ ] Array/Map/String/Test API покрива 90% от ежедневните нужди
- [x] Array/Map/String/Test API покрива 90% от ежедневните нужди (+ Insert/Remove/Clone/case/GetOr)
- [x] `@[Checked]` хваща use-after-move + double `&mut` + dangling return / elision fail
- [x] `bux test` + `bux fmt` + `bux check` са default developer loop (`--filter` / `--check` shipped)
- [x] LanguageRef синхронизиран с компилатора (incl. C.1 elision)
@@ -1216,7 +1216,7 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
|------|--------------------|-----------------|
| **Linux** | Host + CI + full `rt/runtime.c` (pthread, ucontext, sockets, OpenSSL) | ✅ primary; macOS secondary smoke only |
| **Cloud-native** | HTTP/HTTPS, registry (lock+HTTPS), containers, musl docs | ✅ sessions 7579 |
| **Embedded** | Cross (`--target`), CTFE tables, **thin runtime**, no-GC story | ✅ minimal + aarch64 + ctfe_crc (75); 🔧 riscv / bare-metal spike |
| **Embedded** | Cross (`--target`), CTFE tables, **thin runtime**, no-GC story | ✅ minimal + aarch64 + riscv64 smoke (85) + freestanding notes; bare-metal still spike |
| **Windows** | Не е product target | ⛔ no further investment (existing MinGW hello = historical) |
**Правило:** нов runtime / stdlib / CI effort отива към Linux + cloud + embedded. Windows-only work не влиза в следващи сесии.
@@ -1369,35 +1369,114 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
---
## Сесия 83 (stdlib daily API — collections_extra)
1. **Array** (`lib/Array.bux`):
- `Array_RemoveAt` — shift-left remove, returns value
- `Array_Insert` — insert at `0..=len` (append at end)
- `Array_SwapRemove` — O(1) unordered remove
- `Array_Clone` — shallow value clone into new buffer
2. **String** (`lib/String.bux`):
- `String_Cmp``strcmp` wrapper
- `String_IndexOf` — byte index or `-1`
- `String_ToUpper` / `String_ToLower` — ASCII only via StringBuilder
3. **Map** (`lib/Map.bux`):
- `Map_GetOr` / `StringMap_GetOr` — default when key missing
4. **Example** `examples/collections_extra.bux` + Makefile `EXAMPLES`
5. **Goldens** `tests/stdlib_golden/{array,string}` extended
6. **Docs** `docs/Stdlib.md` tables updated
**Verified:** `collections_extra` PASS; `make test-stdlib` 3/3 PASS.
---
## Сесия 84 (macro raw `tt` groups + expression-level `$(…),*`)
1. **Delimiter-balanced `:tt` groups** (bootstrap + selfhost):
- `$args:tt` that is a multi-element tuple `(a, b)` is wrapped as `ekMacroTt` with group flag
- Splice `$f($args)` flattens to `f(a, b)` (not `f((a, b))`)
- Value-position splice unwraps to the inner tuple / fragment
- Contrast `:expr` keeps the tuple as one argument
2. **Expression-level rep in templates:**
- Parse `$( expr ),*` / `$( expr )*` inside **call argument lists** when `macroTemplateMode`
- `ekMacroRep` expands to N call args (zip list fragments)
- Example: `apply_rep!(Add3, 1, 2, 3)``Add3(1, 2, 3)`
3. **Example** `examples/macro_tt_raw.bux` + Makefile EXAMPLES / EXAMPLES_SMOKE
4. **Docs** LanguageRef tt + expression-level rep + limits
5. **Hardening:** `CBE_NormalizeTypeName` / `String_StartsWith` null-safe
(fixed selfhost segfault on `if_let_like` + enum field-move path)
**Verified:** bootstrap + **buxc2** `macro_tt_raw` PASS; `macro_stmt_pat` PASS; macro regressions OK.
---
## Сесия 85 (riscv64 cross smoke + freestanding notes + slice `:tt`)
1. **riscv64 cross smoke** (`tools/smoke_linux_targets.sh`):
- Shared `try_cross` helper for aarch64 + riscv64
- `PASS` when `${triple}-gcc` present; **SKIP** otherwise (no false fail)
- Documented: clang `-target` alone needs a sysroot
2. **Freestanding / bare-metal research** (`docs/BuildAndTest.md`):
- Table: thin/static/cross ✅ vs true freestanding / Cortex-M 🔬
- Clarifies `runtime_minimal` still uses libc (not no-libc)
3. **Delimiter-balanced `:tt` + slice lit** (session 84 extension):
- `[a, b]` slice groups flatten like tuples in `$f($args)`
- Selfhost parser: primary `[a, b, …]``ekSlice` (parity with bootstrap)
- `examples/macro_tt_raw.bux` case `apply_tt!(Add, [8, 9])` → 17
4. Docs: LanguageRef, BuildAndTest, Makefile target blurb
**Verified:** `make test-linux-targets` — minimal/static/aarch64/ctfe PASS; riscv64 SKIP;
bootstrap + **buxc2** `macro_tt_raw` (incl. slice) PASS.
---
## Сесия 86 (free-form juxta `:tt` paste)
1. **Pattern juxtaposition:** `$f:ident $args:tt` without comma between fragments
(bootstrap + selfhost parser continue on next `$…`)
2. **Expand-time call split:** when rule is exactly two fixed frags `ident` + `tt`
and the call site has **one** arg that is `ekCall` with ident callee:
- bind `$f` → callee
- bind `$args` → MacroTt group of the calls arguments
- `$f($args)` flattens as before → `f(a, b)`
3. **Example** `apply_juxta!(Add(2, 5))` / `apply_juxta!(Add3(1, 2, 4))` in
`examples/macro_tt_raw.bux`
4. LanguageRef juxta section; comma form still works
**Verified:** bootstrap + **buxc2** `macro_tt_raw` (h=7, i=7) PASS; macro regressions OK.
---
## Сесия 87 (`:type` fragments + Array_Reverse / Test_AssertNeqString)
1. **Macro `$t:type`** (bootstrap + selfhost):
- Fragment kind `type` (`type` is a keyword — special-cased in parsers)
- Call-site coerce: `int` → named; `*int` → pointer
- Subst into `sizeof($t)`, `as $t`, `let x: $t`
2. **Example** `examples/macro_type.bux` — size_of / cast_zero + reverse/neq
3. **Stdlib:** `Array_Reverse`, `Test_AssertNeqString`
4. Docs: LanguageRef type table; Stdlib Array_Reverse
**Verified:** bootstrap + **buxc2** `macro_type` PASS (sizeof int=4, *int=8).
---
## Следващи стъпки
### P0 — Compiler / language
1. ~~Cross-function pointer ownership~~ session 76
2. ~~Selfhost `--static` / `BUX_RUNTIME` / `--target`~~ ✅ session 76
3. ~~Macro `tt` broader than expr~~ ✅ session 76 (raw delimiter-balanced tokens still open)
4. Macro: raw token-tree delimiter balancing / deeper nested rewrite edge cases
5. ~~Selfhost cross-fn moves~~ ✅ session 77
1. ~~… through session 86~~
2. ~~**`:type` macro fragments**~~ ✅ session 87
3. Optional: richer free-form (operators-only tt); generics in `:type` (`Array<int>`)
### P1 — Linux / cloud-native
6. ~~**Static path**~~session 7576
7. ~~**Multi-arch Linux smoke**~~ ✅ session 75
8. ~~**Nexus production polish**~~ ✅ session 77
9. ~~**Nexus TLS**~~ ✅ session 78
10. ~~**Container story**~~ ✅ session 78
11. ~~**Registry + deploy**~~ ✅ session 79 (HTTPS + lock checksum + `--locked`)
12. ~~**musl path**~~ ✅ session 79 (smoke + docs; SKIP without toolchain)
13. ~~**mTLS / client certs**~~ ✅ session 80 (`NEXUS_TLS_CLIENT_CA`)
14. ~~**Selfhost install --locked**~~ ✅ session 80
15. ~~**Selfhost full registry**~~ ✅ session 81 (search / add / HTTP / path-dep build)
16. **Language P0 leftovers** — raw macro `tt` delimiter balancing (optional)
4. ~~(sessions 7581)~~
### P2 — Embedded / cross
12. ~~**Cross / thin / CTFE**~~ ✅ session 75
13. **riscv64 cross smoke** (when toolchain available)
14. **no-libc / bare-metal research** (spike only) — Cortex-M / qemu-system; not v1.0 blocker
5. ~~riscv64 smoke + freestanding notes~~ ✅ session 85
6. Optional: real `runtime_freestanding.c` + Cortex-M qemu — not v1.0
### Изрично **не** правим
+12
View File
@@ -85,6 +85,11 @@ struct Array<T> {
| `Array_Contains<T>` | `func Array_Contains<T>(arr: *Array<T>, value: T) -> bool` | Linear search for value |
| `Array_IndexOf<T>` | `func Array_IndexOf<T>(arr: *Array<T>, value: T) -> int` | First index or -1 |
| `Array_Extend<T>` | `func Array_Extend<T>(arr: *Array<T>, other: *Array<T>)` | Append all from other |
| `Array_RemoveAt<T>` | `func Array_RemoveAt<T>(arr: *Array<T>, index: uint) -> T` | Remove at index (shift left) |
| `Array_Insert<T>` | `func Array_Insert<T>(arr: *Array<T>, index: uint, value: T)` | Insert at index (`0..=len`) |
| `Array_SwapRemove<T>` | `func Array_SwapRemove<T>(arr: *Array<T>, index: uint) -> T` | O(1) remove (swap with last) |
| `Array_Clone<T>` | `func Array_Clone<T>(arr: *Array<T>) -> Array<T>` | Shallow clone (value copy) |
| `Array_Reverse<T>` | `func Array_Reverse<T>(arr: *Array<T>)` | Reverse elements in place |
| `Array_Get<T>` | `func Array_Get<T>(arr: *Array<T>, index: uint) -> T` | Get element at index |
| `Array_Set<T>` | `func Array_Set<T>(arr: *Array<T>, index: uint, value: T)` | Set element at index |
| `Array_First<T>` | `func Array_First<T>(arr: *Array<T>) -> T` | First element (bounds-checked) |
@@ -257,6 +262,10 @@ String manipulation utilities.
| `String_IsBlank` | `func String_IsBlank(s: String) -> bool` | True if empty or only whitespace |
| `String_Repeat` | `func String_Repeat(s: String, count: uint) -> String` | Repeat string N times |
| `String_Find` | `func String_Find(haystack: String, needle: String) -> String` | Find substring (returns pointer; 0 = not found) |
| `String_IndexOf` | `func String_IndexOf(s: String, needle: String) -> int` | Byte index of first match, or `-1` |
| `String_Cmp` | `func String_Cmp(a: String, b: String) -> int` | Lexicographic compare (`strcmp`) |
| `String_ToUpper` | `func String_ToUpper(s: String) -> String` | ASCII `a``z` → upper (allocates) |
| `String_ToLower` | `func String_ToLower(s: String) -> String` | ASCII `A``Z` → lower (allocates) |
| `String_Replace` | `func String_Replace(s: String, old: String, new: String) -> String` | Replace first occurrence |
| `String_ReplaceAll` | `func String_ReplaceAll(s: String, old: String, new: String) -> String` | Replace all non-overlapping occurrences |
| `String_Format1` | `func String_Format1(pattern: String, a0: String) -> String` | Format with 1 arg (`{0}`) |
@@ -430,6 +439,7 @@ struct Map<K, V> {
| `Map_New<K,V>` | `func Map_New<K,V>(cap: uint) -> Map<K,V>` | Create map |
| `Map_Set<K,V>` | `func Map_Set<K,V>(m: *Map<K,V>, key: K, value: V)` | Insert/update |
| `Map_Get<K,V>` | `func Map_Get<K,V>(m: *Map<K,V>, key: K) -> V` | Get value (zero if missing) |
| `Map_GetOr<K,V>` | `func Map_GetOr<K,V>(m: *Map<K,V>, key: K, defaultVal: V) -> V` | Get or default if missing |
| `Map_Has<K,V>` | `func Map_Has<K,V>(m: *Map<K,V>, key: K) -> bool` | Check key exists |
| `Map_Remove<K,V>` | `func Map_Remove<K,V>(m: *Map<K,V>, key: K) -> bool` | Remove key (true if present) |
| `Map_Clear<K,V>` | `func Map_Clear<K,V>(m: *Map<K,V>)` | Remove all entries (keeps capacity) |
@@ -481,6 +491,7 @@ struct StringMap<V> {
| `StringMap_New<V>` | `func StringMap_New<V>(cap: uint) -> StringMap<V>` | Create map |
| `StringMap_Set<V>` | `func StringMap_Set<V>(m: *StringMap<V>, key: String, value: V)` | Insert/update |
| `StringMap_Get<V>` | `func StringMap_Get<V>(m: *StringMap<V>, key: String) -> V` | Get value |
| `StringMap_GetOr<V>` | `func StringMap_GetOr<V>(m: *StringMap<V>, key: String, defaultVal: V) -> V` | Get or default if missing |
| `StringMap_Has<V>` | `func StringMap_Has<V>(m: *StringMap<V>, key: String) -> bool` | Check key exists |
| `StringMap_Remove<V>` | `func StringMap_Remove<V>(m: *StringMap<V>, key: String) -> bool` | Remove key |
| `StringMap_Clear<V>` | `func StringMap_Clear<V>(m: *StringMap<V>)` | Clear all entries |
@@ -1010,6 +1021,7 @@ import Std::Test::*;
| `Test_AssertEqInt` | `func Test_AssertEqInt(a: int, b: int)` | Integer equality |
| `Test_AssertNeqInt` | `func Test_AssertNeqInt(a: int, b: int)` | Integer inequality |
| `Test_AssertEqString` | `func Test_AssertEqString(a: String, b: String)` | String equality |
| `Test_AssertNeqString` | `func Test_AssertNeqString(a: String, b: String)` | String inequality |
| `Test_AssertEqBool` | `func Test_AssertEqBool(a: bool, b: bool)` | Boolean equality |
| `Test_Fail` / `Test_Pass` | `func ...(msg: String)` | Explicit fail / log pass |
| `Test_Exit` | `func Test_Exit(code: int)` | Exit with code |
+72
View File
@@ -0,0 +1,72 @@
// Session 83 — Array RemoveAt/Insert/SwapRemove/Clone + String Cmp/IndexOf/case
import Std::Io::{PrintLine, PrintInt};
import Std::Array::{
Array, Array_New, Array_Push, Array_Get, Array_Len, Array_Free,
Array_RemoveAt, Array_Insert, Array_SwapRemove, Array_Clone
};
import Std::String::{
String_Eq, String_Cmp, String_IndexOf, String_ToUpper, String_ToLower
};
import Std::Map::{Map, Map_New, Map_Set, Map_GetOr, Map_Free};
import Std::Test::{
Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_AssertEqString, Test_Pass
};
func Main() -> int {
// --- Array_Insert / RemoveAt ---
var arr: Array<int> = Array_New<int>(4);
Array_Push<int>(&arr, 10);
Array_Push<int>(&arr, 30);
Array_Insert<int>(&arr, 1, 20); // [10, 20, 30]
Array_Insert<int>(&arr, 3, 40); // append via insert at len
Test_AssertEqInt(Array_Len<int>(&arr) as int, 4);
Test_AssertEqInt(Array_Get<int>(&arr, 0), 10);
Test_AssertEqInt(Array_Get<int>(&arr, 1), 20);
Test_AssertEqInt(Array_Get<int>(&arr, 2), 30);
Test_AssertEqInt(Array_Get<int>(&arr, 3), 40);
let rem: int = Array_RemoveAt<int>(&arr, 1); // remove 20 → [10, 30, 40]
Test_AssertEqInt(rem, 20);
Test_AssertEqInt(Array_Len<int>(&arr) as int, 3);
Test_AssertEqInt(Array_Get<int>(&arr, 1), 30);
// --- Array_SwapRemove (order not preserved) ---
let swapped: int = Array_SwapRemove<int>(&arr, 0); // remove 10, last→front
Test_AssertEqInt(swapped, 10);
Test_AssertEqInt(Array_Len<int>(&arr) as int, 2);
// --- Array_Clone ---
var clone: Array<int> = Array_Clone<int>(&arr);
Test_AssertEqInt(Array_Len<int>(&clone) as int, Array_Len<int>(&arr) as int);
Test_AssertEqInt(Array_Get<int>(&clone, 0), Array_Get<int>(&arr, 0));
Array_Push<int>(&clone, 99);
Test_AssertEqInt(Array_Len<int>(&clone) as int, 3);
Test_AssertEqInt(Array_Len<int>(&arr) as int, 2); // original unchanged
Array_Free<int>(&arr);
Array_Free<int>(&clone);
// --- String_Cmp / IndexOf / ToUpper / ToLower ---
Test_AssertEqInt(String_Cmp("abc", "abc"), 0);
Test_AssertTrue(String_Cmp("a", "b") < 0);
Test_AssertTrue(String_Cmp("z", "a") > 0);
Test_AssertEqInt(String_IndexOf("hello world", "world"), 6);
Test_AssertEqInt(String_IndexOf("hello", "xyz"), -1);
Test_AssertEqInt(String_IndexOf("aaa", "a"), 0);
Test_AssertEqString(String_ToUpper("Hello, Bux!"), "HELLO, BUX!");
Test_AssertEqString(String_ToLower("Hello, Bux!"), "hello, bux!");
Test_AssertEqString(String_ToUpper("123"), "123");
Test_AssertEqString(String_ToLower(""), "");
// --- Map_GetOr ---
var m: Map<int, int> = Map_New<int, int>(8);
Map_Set<int, int>(&m, 1, 100);
Test_AssertEqInt(Map_GetOr<int, int>(&m, 1, -1), 100);
Test_AssertEqInt(Map_GetOr<int, int>(&m, 99, -1), -1);
Map_Free<int, int>(&m);
PrintLine("collections_extra: all checks passed");
Test_Pass("collections_extra");
return 0;
}
+126
View File
@@ -0,0 +1,126 @@
// Session 8486 — delimiter-balanced :tt + expr-level $(…),* + juxta free-form
//
// 1) `$args:tt` multi-element group flattens when spliced as sole call arg:
// `(a, b)` tuple and `[a, b]` slice lit → `$f($args)` becomes `f(a, b)`.
// 2) Expression-level `$( $a ),*` inside call templates expands to N args.
// 3) Nested `id_tt!` rewrite still works (deeper nested expand).
// 4) Free-form juxta (session 86): pattern `$f:ident $args:tt` matches a single
// call arg `F(a, b)` as ident + arg-list group (no comma required at call site).
import Std::Io::{PrintLine, PrintInt};
import Std::Test::{Test_Pass};
// Parenthesized group as argument list (delimiter-balanced tt)
macro! apply_tt {
( $f:ident, $args:tt ) => {
$f($args)
}
}
// Juxtaposition pattern (no comma between fragments) — free-form paste
macro! apply_juxta {
( $f:ident $args:tt ) => {
$f($args)
}
}
// Expression-level repetition inside a call
macro! apply_rep {
( $f:ident, $($a:expr),* ) => {
$f( $($a),* )
}
}
// Nested rewrite of tt through another macro
macro! id_tt {
( $x:tt ) => { $x }
}
macro! outer_tt {
( $x:tt ) => { id_tt!($x) }
}
// Non-tuple tt still works as a single value
macro! wrap_tt {
( $x:tt ) => { ( $x ) + 1 }
}
// Contrast: :expr keeps the tuple as one argument
macro! apply_expr {
( $f:ident, $args:expr ) => {
$f($args)
}
}
func Add(a: int, b: int) -> int {
return a + b;
}
func Add3(a: int, b: int, c: int) -> int {
return a + b + c;
}
func SumPair(t: (int, int)) -> int {
return t.0 + t.1;
}
func Main() -> int {
// 1) tt group flatten: (3, 4) → Add(3, 4)
let a: int = apply_tt!(Add, (3, 4));
PrintInt(a);
PrintLine("");
// 2) expression-level rep: Add3(1, 2, 3)
let b: int = apply_rep!(Add3, 1, 2, 3);
PrintInt(b);
PrintLine("");
// empty rep → zero-arg call is invalid for Add3; use Add with 2 via rep
let b2: int = apply_rep!(Add, 10, 20);
PrintInt(b2);
PrintLine("");
// 3) nested tt rewrite
let c: int = outer_tt!(7 + 4);
PrintInt(c);
PrintLine("");
// 4) non-group tt value
let d: int = wrap_tt!(41);
PrintInt(d);
PrintLine("");
// 5) :expr keeps tuple as one arg
let e: int = apply_expr!(SumPair, (5, 6));
PrintInt(e);
PrintLine("");
// 6) nested group through outer_tt then apply
let f: int = apply_tt!(Add, (100, 1));
PrintInt(f);
PrintLine("");
// 7) slice-lit group flatten (session 85)
let g: int = apply_tt!(Add, [8, 9]);
PrintInt(g);
PrintLine("");
// 8) free-form juxta: single call site arg Add(2, 5) → $f + $args (session 86)
let h: int = apply_juxta!(Add(2, 5));
PrintInt(h);
PrintLine("");
// zero-arg juxta still works with empty group
// (skip — no zero-arg test func required)
// 9) juxta with three args
let i: int = apply_juxta!(Add3(1, 2, 4));
PrintInt(i);
PrintLine("");
if a != 7 || b != 6 || b2 != 30 || c != 11 || d != 42 || e != 11 || f != 101 || g != 17 || h != 7 || i != 7 {
PrintLine("FAIL macro_tt_raw");
return 1;
}
PrintLine("PASS macro_tt_raw");
Test_Pass("macro_tt_raw");
return 0;
}
+55
View File
@@ -0,0 +1,55 @@
// Session 87 — `$t:type` fragment for sizeof / cast type positions
import Std::Io::{PrintLine, PrintInt};
import Std::Array::{Array, Array_New, Array_Push, Array_Get, Array_Len, Array_Reverse, Array_Free};
import Std::Test::{
Test_AssertEqInt, Test_AssertEqString, Test_AssertNeqString, Test_Pass
};
import Std::String::{String_ToUpper};
macro! size_of {
( $t:type ) => { sizeof($t) as int }
}
macro! cast_zero {
( $t:type ) => { 0 as $t }
}
func Main() -> int {
let a: int = size_of!(int);
let b: int = size_of!(*int);
PrintInt(a);
PrintLine("");
PrintInt(b);
PrintLine("");
// 4 or 8 depending on ABI
if a != 4 && a != 8 {
PrintLine("FAIL size_of int");
return 1;
}
if b != 4 && b != 8 {
PrintLine("FAIL size_of *int");
return 1;
}
let z: int = cast_zero!(int);
Test_AssertEqInt(z, 0);
// Array_Reverse + Test_AssertNeqString
var arr: Array<int> = Array_New<int>(4);
Array_Push<int>(&arr, 1);
Array_Push<int>(&arr, 2);
Array_Push<int>(&arr, 3);
Array_Reverse<int>(&arr);
Test_AssertEqInt(Array_Get<int>(&arr, 0), 3);
Test_AssertEqInt(Array_Get<int>(&arr, 1), 2);
Test_AssertEqInt(Array_Get<int>(&arr, 2), 1);
Test_AssertEqInt(Array_Len<int>(&arr) as int, 3);
Array_Free<int>(&arr);
Test_AssertEqString(String_ToUpper("ok"), "OK");
Test_AssertNeqString("a", "b");
PrintLine("PASS macro_type");
Test_Pass("macro_type");
return 0;
}
+76
View File
@@ -140,4 +140,80 @@ module Std::Array {
}
}
/// Remove element at `index`, shifting later elements left. Returns the removed value.
func Array_RemoveAt<T>(self: *Array<T>, index: uint) -> T {
bux_bounds_check(index, self.len);
let val: T = self.data[index];
var i: uint = index;
while i + 1 < self.len {
self.data[i] = self.data[i + 1];
i = i + 1;
}
self.len = self.len - 1;
return val;
}
/// Insert `value` at `index` (`0..=len`), shifting later elements right.
func Array_Insert<T>(self: *Array<T>, index: uint, value: T) {
// allow index == len (append); panic if index > len
bux_bounds_check(index, self.len + 1);
if self.len >= self.cap {
var newCap: uint = self.cap * 2;
if newCap == 0 {
newCap = 4;
}
self.cap = newCap;
self.data = bux_realloc(self.data as *void, self.cap * sizeof(T)) as *T;
}
var i: uint = self.len;
while i > index {
self.data[i] = self.data[i - 1];
i = i - 1;
}
self.data[index] = value;
self.len = self.len + 1;
}
/// O(1) remove: swap `index` with last, then pop. Does not preserve order.
func Array_SwapRemove<T>(self: *Array<T>, index: uint) -> T {
bux_bounds_check(index, self.len);
let val: T = self.data[index];
self.len = self.len - 1;
if index < self.len {
self.data[index] = self.data[self.len];
}
return val;
}
/// Shallow clone: new buffer, elements copied by value.
func Array_Clone<T>(self: *Array<T>) -> Array<T> {
var cap: uint = self.len;
if cap == 0 {
cap = 1;
}
var out: Array<T> = Array_New<T>(cap);
var i: uint = 0;
while i < self.len {
Array_Push<T>(&out, self.data[i]);
i = i + 1;
}
return out;
}
/// Reverse elements in place.
func Array_Reverse<T>(self: *Array<T>) {
if self.len < 2 {
return;
}
var i: uint = 0;
var j: uint = self.len - 1;
while i < j {
let tmp: T = self.data[i];
self.data[i] = self.data[j];
self.data[j] = tmp;
i = i + 1;
j = j - 1;
}
}
}
+16
View File
@@ -63,6 +63,14 @@ module Std::Map {
return zero;
}
/// Like `Map_Get`, but returns `defaultVal` when the key is absent.
func Map_GetOr<K, V>(m: *Map<K, V>, key: K, defaultVal: V) -> V {
if Map_Has<K, V>(m, key) {
return Map_Get<K, V>(m, key);
}
return defaultVal;
}
func Map_Has<K, V>(m: *Map<K, V>, key: K) -> bool {
var keyPtr: *K = &key;
let hash: uint = bux_hash_bytes(keyPtr as *void, sizeof(K));
@@ -186,6 +194,14 @@ module Std::Map {
return zero;
}
/// Like `StringMap_Get`, but returns `defaultVal` when the key is absent.
func StringMap_GetOr<V>(m: *StringMap<V>, key: String, defaultVal: V) -> V {
if StringMap_Has<V>(m, key) {
return StringMap_Get<V>(m, key);
}
return defaultVal;
}
func StringMap_Has<V>(m: *StringMap<V>, key: String) -> bool {
let hash: uint = bux_hash_string(key);
var idx: uint = hash % m.cap;
+56
View File
@@ -71,6 +71,8 @@ module Std::String {
/// True if `s` begins with `prefix`.
func String_StartsWith(s: String, prefix: String) -> bool {
if s == null as String { return false; }
if prefix == null as String { return false; }
let s_len: uint = bux_strlen(s);
let p_len: uint = bux_strlen(prefix);
if p_len > s_len {
@@ -82,6 +84,8 @@ module Std::String {
/// True if `s` ends with `suffix`.
func String_EndsWith(s: String, suffix: String) -> bool {
if s == null as String { return false; }
if suffix == null as String { return false; }
let s_len: uint = bux_strlen(s);
let suf_len: uint = bux_strlen(suffix);
if suf_len > s_len {
@@ -294,4 +298,56 @@ module Std::String {
return bux_str_format(pattern, a0, a1, a2, "", "", "", "", "");
}
/// Lexicographic compare: `<0` if a<b, `0` if equal, `>0` if a>b (`strcmp`).
func String_Cmp(a: String, b: String) -> int {
return bux_strcmp(a, b);
}
/// Byte index of first `needle` in `s`, or `-1` if not found.
func String_IndexOf(s: String, needle: String) -> int {
let pos: String = bux_strstr(s, needle);
if String_IsNull(pos) {
return -1;
}
return String_Offset(pos, s) as int;
}
/// ASCII lower → upper (`a``z` only). Non-ASCII bytes unchanged.
func String_ToUpper(s: String) -> String {
let n: uint = bux_strlen(s);
var sb: StringBuilder = StringBuilder_NewCap(n + 1);
var i: uint = 0;
while i < n {
let c: int = s[i] as int;
if c >= 97 && c <= 122 {
StringBuilder_AppendChar(&sb, (c - 32) as char8);
} else {
StringBuilder_AppendChar(&sb, c as char8);
}
i = i + 1;
}
let result: String = StringBuilder_Build(&sb);
StringBuilder_Free(&sb);
return result;
}
/// ASCII upper → lower (`A``Z` only). Non-ASCII bytes unchanged.
func String_ToLower(s: String) -> String {
let n: uint = bux_strlen(s);
var sb: StringBuilder = StringBuilder_NewCap(n + 1);
var i: uint = 0;
while i < n {
let c: int = s[i] as int;
if c >= 65 && c <= 90 {
StringBuilder_AppendChar(&sb, (c + 32) as char8);
} else {
StringBuilder_AppendChar(&sb, c as char8);
}
i = i + 1;
}
let result: String = StringBuilder_Build(&sb);
StringBuilder_Free(&sb);
return result;
}
}
+9
View File
@@ -46,6 +46,15 @@ module Std::Test {
}
}
/// Assert two strings differ.
func Test_AssertNeqString(a: String, b: String) {
if String_Eq(a, b) {
PrintLine("ASSERT_NEQ_STRING FAILED: both are");
PrintLine(b);
bux_exit(1);
}
}
/// Assert two bools are equal.
func Test_AssertEqBool(a: bool, b: bool) {
if a != b {
+3
View File
@@ -130,6 +130,9 @@ module Ast {
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)
const ekMacroTt: int = 31; // `$x:tt` bound fragment (expand only; boolValue=group)
const ekMacroRep: int = 32; // `$( expr ),*` expression-level rep (expand only)
const ekMacroType: int = 33; // `$t:type` bound type (expand only; refType)
struct ExprList {
expr: *Expr,
+2
View File
@@ -141,6 +141,8 @@ module CBackend {
/// Normalize monomorphized type spellings for C (Array<int> → Array_int).
func CBE_NormalizeTypeName(name: String) -> String {
// Null typeName can appear on partial HIR field nodes; treat as empty.
if name == null as String { return ""; }
if String_Eq(name, "") { return name; }
if String_StartsWith(name, "Array<") && String_EndsWith(name, ">") {
let n: uint = String_Len(name);
+234 -10
View File
@@ -454,15 +454,85 @@ module MacroExpand {
if aexp.kind == ekMacroStmt || aexp.kind == ekMacroPat { return null as *Expr; }
return aexp;
}
// tt — any single call-site AST fragment (session 76; raw token trees later)
// tt — any single call-site AST fragment (session 76/84/85).
// Delimiter-balanced multi-element groups (tuple `(a,b)` / slice `[a,b]`)
// flatten when spliced as sole call arg: `$f($args)` → `f(a, b)`.
if String_Eq(kindStr, "tt") {
return aexp;
if aexp.kind == ekMacroTt { return aexp; }
let wrap: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
wrap.kind = ekMacroTt;
wrap.line = aexp.line;
wrap.column = aexp.column;
wrap.sourceFile = aexp.sourceFile;
wrap.child1 = aexp;
// group flag: tuple or non-empty slice lit
wrap.boolValue = aexp.kind == ekTuple ||
(aexp.kind == ekSlice && aexp.callArgCount > 0);
return wrap;
}
// type — session 87: named / pointer type from call-site expr shape
if String_Eq(kindStr, "type") {
if aexp.kind == ekMacroType { return aexp; }
var te: *TypeExpr = null as *TypeExpr;
if aexp.kind == ekIdent {
te = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te.kind = tekNamed;
te.line = aexp.line;
te.column = aexp.column;
te.typeName = aexp.strValue;
} else if aexp.kind == ekUnary && aexp.intValue == tkStar && aexp.child1 != null as *Expr {
// *T from unary star
let inner: *Expr = Macro_CoerceArg("type", aexp.child1);
if inner == null as *Expr || inner.refType == null as *TypeExpr {
return null as *Expr;
}
te = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te.kind = tekPointer;
te.line = aexp.line;
te.column = aexp.column;
te.pointerPointee = inner.refType;
if inner.refType != null as *TypeExpr {
te.typeName = String_Concat(inner.refType.typeName, "*");
}
} else {
return null as *Expr;
}
let tw: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
tw.kind = ekMacroType;
tw.line = aexp.line;
tw.column = aexp.column;
tw.refType = te;
return tw;
}
// default: treat as expr
if aexp.kind == ekMacroStmt || aexp.kind == ekMacroPat { return null as *Expr; }
return aexp;
}
// Substitute `$t:type` in TypeExpr (named type starting with $)
func Macro_SubstType(te: *TypeExpr, env: *MacroEnv) -> *TypeExpr {
if te == null as *TypeExpr { return null as *TypeExpr; }
if te.kind == tekNamed && String_StartsWith(te.typeName, "$") {
let bound: *Expr = Env_Lookup(env, te.typeName);
if bound != null as *Expr && bound.kind == ekMacroType && bound.refType != null as *TypeExpr {
return bound.refType;
}
}
if te.pointerPointee != null as *TypeExpr {
te.pointerPointee = Macro_SubstType(te.pointerPointee, env);
if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
te.typeName = String_Concat(te.pointerPointee.typeName, "*");
}
}
if te.sliceElement != null as *TypeExpr {
te.sliceElement = Macro_SubstType(te.sliceElement, env);
}
if te.funcRet != null as *TypeExpr {
te.funcRet = Macro_SubstType(te.funcRet, env);
}
return te;
}
// 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;
@@ -773,15 +843,117 @@ module MacroExpand {
// Substitute $frags in a cloned tree
// ---------------------------------------------------------------------------
// Build callArgs for a call/macro-call, flattening MacroRep and MacroTt groups.
func Macro_SubstCallArgs(oldArgs: *ExprList, env: *MacroEnv, file: String, line: uint32, col: uint32,
outCount: *int) -> *ExprList {
var first: *ExprList = null as *ExprList;
var last: *ExprList = null as *ExprList;
var count: int = 0;
var args: *ExprList = oldArgs;
while args != null as *ExprList {
let a: *Expr = args.expr;
var didFlat: bool = false;
// Expression-level `$( body ),*`
if a != null as *Expr && a.kind == ekMacroRep && a.child1 != null as *Expr {
let body: *Expr = a.child1;
let use0: bool = Macro_ExprUsesListName(body, env.listName0);
let use1: bool = Macro_ExprUsesListName(body, env.listName1);
if !use0 && !use1 {
let node: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node.expr = Subst_Expr(body, env, file, line, col);
node.next = null as *ExprList;
node.argName = "";
if first == null as *ExprList { first = node; last = node; }
else { last.next = node; last = node; }
count = count + 1;
} else {
var nRep: int = 0;
if use0 && env.listCount0 > nRep { nRep = env.listCount0; }
if use1 && env.listCount1 > nRep { nRep = env.listCount1; }
var li: int = 0;
while li < nRep {
var inner: MacroEnv = Env_New();
Env_CopySingles(&inner, env);
if use0 && li < env.listCount0 {
Env_SetNamed(&inner, env.listName0, Env_ListGet(env, 0, li));
}
if use1 && li < env.listCount1 {
Env_SetNamed(&inner, env.listName1, Env_ListGet(env, 1, li));
}
let node2: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node2.expr = Subst_Expr(body, &inner, file, line, col);
node2.next = null as *ExprList;
node2.argName = "";
if first == null as *ExprList { first = node2; last = node2; }
else { last.next = node2; last = node2; }
count = count + 1;
li = li + 1;
}
}
didFlat = true;
} else if a != null as *Expr && a.kind == ekIdent {
let bound: *Expr = Env_Lookup(env, a.strValue);
// Bare `$args:tt` group → flatten tuple/slice elements as call args
if bound != null as *Expr && bound.kind == ekMacroTt && bound.boolValue &&
bound.child1 != null as *Expr &&
(bound.child1.kind == ekTuple || bound.child1.kind == ekSlice) {
var tel: *ExprList = bound.child1.callArgs;
while tel != null as *ExprList {
let ce: *Expr = Ast_CloneExpr(tel.expr);
Graft_Expr(ce, file, line, col);
let node3: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node3.expr = ce;
node3.next = null as *ExprList;
node3.argName = "";
if first == null as *ExprList { first = node3; last = node3; }
else { last.next = node3; last = node3; }
count = count + 1;
tel = tel.next;
}
didFlat = true;
}
}
if !didFlat {
let node4: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node4.expr = Subst_Expr(a, env, file, line, col);
node4.next = null as *ExprList;
node4.argName = "";
if first == null as *ExprList { first = node4; last = node4; }
else { last.next = node4; last = node4; }
count = count + 1;
}
args = args.next;
}
*outCount = count;
return first;
}
func Subst_Expr(e: *Expr, env: *MacroEnv, file: String, line: uint32, col: uint32) -> *Expr {
if e == null as *Expr { return null as *Expr; }
if e.kind == ekIdent {
let bound: *Expr = Env_Lookup(env, e.strValue);
if bound != null as *Expr {
let n: *Expr = Ast_CloneExpr(bound);
// Value position: unwrap MacroTt wrapper
if bound.kind == ekMacroTt && bound.child1 != null as *Expr {
let n: *Expr = Ast_CloneExpr(bound.child1);
Graft_Expr(n, file, line, col);
return n;
}
// Type fragments are not values — leave a zero literal
if bound.kind == ekMacroType {
let z: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
z.kind = ekLiteral;
z.line = line;
z.column = col;
z.tokKind = 0;
z.tokText = "0";
z.intValue = 0;
return z;
}
let n2: *Expr = Ast_CloneExpr(bound);
Graft_Expr(n2, file, line, col);
return n2;
}
}
// In-place subst on clone of e
let c: *Expr = Ast_CloneExpr(e);
@@ -789,14 +961,24 @@ module MacroExpand {
c.child1 = Subst_Expr(c.child1, env, file, line, col);
c.child2 = Subst_Expr(c.child2, env, file, line, col);
c.child3 = Subst_Expr(c.child3, env, file, line, col);
// sizeof / cast / is type annotations
if c.refType != null as *TypeExpr {
c.refType = Macro_SubstType(c.refType, env);
}
if c.refBlock != null as *Block {
c.refBlock = Subst_Block_Flat(c.refBlock, env, file, line, col);
}
// callArgs list
var args: *ExprList = c.callArgs;
while args != null as *ExprList {
args.expr = Subst_Expr(args.expr, env, file, line, col);
args = args.next;
// callArgs: flatten MacroRep + MacroTt groups for calls
if c.kind == ekCall || c.kind == ekMacroCall {
var nCount: int = 0;
c.callArgs = Macro_SubstCallArgs(c.callArgs, env, file, line, col, &nCount);
c.callArgCount = nCount;
} else {
var args2: *ExprList = c.callArgs;
while args2 != null as *ExprList {
args2.expr = Subst_Expr(args2.expr, env, file, line, col);
args2 = args2.next;
}
}
var arm: *MatchArm = c.matchArms;
while arm != null as *MatchArm {
@@ -851,6 +1033,10 @@ module MacroExpand {
c.child1 = Subst_Expr(c.child1, env, file, line, col);
c.child2 = Subst_Expr(c.child2, env, file, line, col);
c.child3 = Subst_Expr(c.child3, env, file, line, col);
// let x: $t = …
if c.refStmtType != null as *TypeExpr {
c.refStmtType = Macro_SubstType(c.refStmtType, env);
}
if c.refStmtBlock != null as *Block {
c.refStmtBlock = Subst_Block(c.refStmtBlock, env, file, line, col);
}
@@ -1003,10 +1189,48 @@ module MacroExpand {
}
let useGroups: bool = nReps > 1 && nGroups > 1;
// Session 86 — juxta free-form: single arg F(a,b) → $f:ident + $args:tt
var ruleArgs: *ExprList = expArgs;
var ruleNargs: int = nargs;
if !useGroups && nReps == 0 && nSeg == 2 && nargs == 1 && expArgs != null as *ExprList {
let k0: String = Macro_KindAt(kinds, 0);
let k1: String = Macro_KindAt(kinds, 1);
let only: *Expr = expArgs.expr;
if String_Eq(k0, "ident") && String_Eq(k1, "tt") &&
only != null as *Expr && only.kind == ekCall &&
only.child1 != null as *Expr && only.child1.kind == ekIdent {
let callee: *Expr = only.child1;
// Build MacroTt group from call args (tuple of elements)
let inner: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
inner.kind = ekTuple;
inner.line = only.line;
inner.column = only.column;
inner.callArgs = only.callArgs;
inner.callArgCount = only.callArgCount;
let group: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
group.kind = ekMacroTt;
group.line = only.line;
group.column = only.column;
group.child1 = inner;
group.boolValue = true;
let n0: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
n0.expr = callee;
n0.next = null as *ExprList;
n0.argName = "";
let n1: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
n1.expr = group;
n1.next = null as *ExprList;
n1.argName = "";
n0.next = n1;
ruleArgs = n0;
ruleNargs = 2;
}
}
env = Env_New();
var ok: bool = true;
var argList: *ExprList = expArgs;
var flatLeft: int = nargs;
var argList: *ExprList = ruleArgs;
var flatLeft: int = ruleNargs;
var gIdx: int = 0;
var gOff: int = 0; // offset within current group when useGroups
var paramIdx: int = 0;
+69 -5
View File
@@ -723,6 +723,35 @@ module Parser {
return first;
}
// Slice / array literal: [a, b, c] (session 85 — also :tt group flatten)
if kind == tkLBracket {
discard parserAdvance(p);
let se: *Expr = parserMakeExpr(ekSlice, line, col);
var firstEl: *ExprList = null as *ExprList;
var lastEl: *ExprList = null as *ExprList;
var elCount: int = 0;
while !parserCheck(p, tkRBracket) {
let elem: *Expr = parserParseExpr(p);
let node: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node.expr = elem;
node.next = null as *ExprList;
node.argName = "";
if firstEl == null as *ExprList {
firstEl = node;
lastEl = node;
} else {
lastEl.next = node;
lastEl = node;
}
elCount = elCount + 1;
if !parserMatch(p, tkComma) { break; }
}
discard parserExpect(p, tkRBracket, "expected ']' to close slice");
se.callArgs = firstEl;
se.callArgCount = elCount;
return se;
}
// Empty-param closure: `||` is lexed as tkPipePipe (logical-or token).
// As a primary it can only mean a zero-param closure: || -> T { ... }
if kind == tkPipePipe {
@@ -1145,8 +1174,20 @@ module Parser {
while !parserCheck(p, tkRParen) {
var argExpr: *Expr = null as *Expr;
var argName: String = "";
// Expression-level `$( expr ),*` / `$( expr )*` in macro templates
if p.macroTemplateMode && parserCheck(p, tkDollar) && parserPeek(p, 1) == tkLParen {
discard parserAdvance(p); // $
discard parserAdvance(p); // (
let repBody: *Expr = parserParseExpr(p);
discard parserExpect(p, tkRParen, "expected ')' after expression macro rep body");
if parserCheck(p, tkComma) {
discard parserAdvance(p);
}
discard parserExpect(p, tkStar, "expected '*' after expression macro rep");
argExpr = parserMakeExpr(ekMacroRep, line, col);
argExpr.child1 = repBody;
} else if parserPeek(p, 0) == tkIdent && parserPeek(p, 1) == tkColon {
// Named argument: name: value
if parserPeek(p, 0) == tkIdent && parserPeek(p, 1) == tkColon {
let nameTok: LexToken = parserCurToken(p);
argName = nameTok.text;
discard parserAdvance(p); // ident
@@ -2413,13 +2454,19 @@ module Parser {
parserEmitDiag(p, fragTok.line, fragTok.column, "macro fragment must start with '$'");
}
discard parserExpect(p, tkColon, "expected ':' after fragment name");
var kname: String = "";
if parserCheck(p, tkType) {
discard parserAdvance(p);
kname = "type";
} else {
let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind");
var kname: String = kindTok.text;
kname = kindTok.text;
}
if String_Eq(kname, "lit") { kname = "literal"; }
if String_Eq(kname, "pattern") { kname = "pat"; }
if !(String_Eq(kname, "expr") || String_Eq(kname, "ident") || String_Eq(kname, "tt")
|| String_Eq(kname, "literal") || String_Eq(kname, "block")
|| String_Eq(kname, "stmt") || String_Eq(kname, "pat")) {
|| String_Eq(kname, "stmt") || String_Eq(kname, "pat") || String_Eq(kname, "type")) {
kname = "expr";
}
if nIn == 0 {
@@ -2460,13 +2507,20 @@ module Parser {
parserEmitDiag(p, fragTok.line, fragTok.column, "macro fragment must start with '$'");
}
discard parserExpect(p, tkColon, "expected ':' after fragment name");
// `type` is a keyword (tkType); other kinds are idents
var kname: String = "";
if parserCheck(p, tkType) {
discard parserAdvance(p);
kname = "type";
} else {
let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind");
var kname: String = kindTok.text;
kname = kindTok.text;
}
if String_Eq(kname, "lit") { kname = "literal"; }
if String_Eq(kname, "pattern") { kname = "pat"; }
if !(String_Eq(kname, "expr") || String_Eq(kname, "ident") || String_Eq(kname, "tt")
|| String_Eq(kname, "literal") || String_Eq(kname, "block")
|| String_Eq(kname, "stmt") || String_Eq(kname, "pat")) {
|| String_Eq(kname, "stmt") || String_Eq(kname, "pat") || String_Eq(kname, "type")) {
kname = "expr";
}
if rule.paramCount < 9 {
@@ -2478,6 +2532,16 @@ module Parser {
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if parserMatch(p, tkComma) { continue; }
if parserMatch(p, tkSemicolon) { continue; }
// Juxtaposition (session 86): `$f:ident $args:tt` without comma
if parserCheck(p, tkDollar) {
continue;
}
if parserPeek(p, 0) == tkIdent {
let nxt: LexToken = parserCurToken(p);
if String_StartsWith(nxt.text, "$") {
continue;
}
}
break;
}
}
+24 -2
View File
@@ -1,9 +1,10 @@
// Stdlib golden: Array helpers + Contains/IndexOf/Extend
// Stdlib golden: Array helpers + Contains/IndexOf/Extend + Insert/Remove/Clone
import Std::Io::{PrintLine};
import Std::Array::{
Array, Array_New, Array_Push, Array_Pop, Array_Clear, Array_IsEmpty,
Array_First, Array_Last, Array_Cap, Array_Reserve, Array_Len, Array_Get,
Array_Contains, Array_IndexOf, Array_Extend, Array_Free
Array_Contains, Array_IndexOf, Array_Extend, Array_Free,
Array_RemoveAt, Array_Insert, Array_SwapRemove, Array_Clone
};
import Std::Test::{
Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_Pass
@@ -38,12 +39,33 @@ func Main() -> int {
Test_AssertEqInt(Array_Len<int>(&arr) as int, 4);
Test_AssertEqInt(Array_Get<int>(&arr, 3), 50);
// Insert / RemoveAt
Array_Insert<int>(&arr, 1, 15); // [10, 15, 20, 40, 50]
Test_AssertEqInt(Array_Get<int>(&arr, 1), 15);
Test_AssertEqInt(Array_Len<int>(&arr) as int, 5);
let mid: int = Array_RemoveAt<int>(&arr, 1);
Test_AssertEqInt(mid, 15);
Test_AssertEqInt(Array_Get<int>(&arr, 1), 20);
// SwapRemove + Clone
let n: int = Array_Len<int>(&arr) as int;
let lastBefore: int = Array_Get<int>(&arr, (n - 1) as uint);
let sw: int = Array_SwapRemove<int>(&arr, 0);
Test_AssertEqInt(sw, 10);
Test_AssertEqInt(Array_Get<int>(&arr, 0), lastBefore);
var clone: Array<int> = Array_Clone<int>(&arr);
Test_AssertEqInt(Array_Len<int>(&clone) as int, Array_Len<int>(&arr) as int);
Array_Push<int>(&clone, 7);
Test_AssertTrue(Array_Len<int>(&clone) > Array_Len<int>(&arr));
Array_Clear<int>(&arr);
Test_AssertTrue(Array_IsEmpty<int>(&arr));
Test_AssertTrue(Array_Cap<int>(&arr) >= 8);
Array_Free<int>(&arr);
Array_Free<int>(&extra);
Array_Free<int>(&clone);
PrintLine("stdlib_array: ok");
Test_Pass("stdlib_array");
return 0;
+10 -2
View File
@@ -1,8 +1,9 @@
// Stdlib golden: String_IsEmpty / IsBlank / Repeat / ReplaceAll
// Stdlib golden: String_IsEmpty / IsBlank / Repeat / ReplaceAll + Cmp/IndexOf/case
import Std::Io::{PrintLine};
import Std::String::{
String_IsEmpty, String_IsBlank, String_Repeat, String_ReplaceAll,
String_Eq, String_Len, String_Contains, String_StartsWith, String_EndsWith
String_Eq, String_Len, String_Contains, String_StartsWith, String_EndsWith,
String_Cmp, String_IndexOf, String_ToUpper, String_ToLower
};
import Std::Test::{
Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_AssertEqString, Test_Pass
@@ -29,6 +30,13 @@ func Main() -> int {
Test_AssertTrue(String_StartsWith("hello", "he"));
Test_AssertTrue(String_EndsWith("hello", "lo"));
Test_AssertEqInt(String_Cmp("aa", "aa"), 0);
Test_AssertTrue(String_Cmp("a", "b") < 0);
Test_AssertEqInt(String_IndexOf("foo bar baz", "bar"), 4);
Test_AssertEqInt(String_IndexOf("foo", "zz"), -1);
Test_AssertEqString(String_ToUpper("AbC-12"), "ABC-12");
Test_AssertEqString(String_ToLower("AbC-12"), "abc-12");
PrintLine("stdlib_string: ok");
Test_Pass("stdlib_string");
return 0;
+39 -22
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env bash
# Session 75 — Linux / cloud / embedded smoke:
# Session 75 / 85 — Linux / cloud / embedded smoke:
# 1) BUX_RUNTIME=minimal (thin runtime, run hello)
# 2) --static --release (fully-static binary, file(1) check)
# 3) --target aarch64-linux-gnu (cross build if toolchain present)
# 4) CTFE CRC example under minimal runtime
# 4) --target riscv64-linux-gnu (cross if toolchain present; else SKIP)
# 5) CTFE CRC example under minimal runtime
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}"
@@ -38,6 +39,33 @@ EOF
echo "$d"
}
# Cross-compile helper: needs <triple>-gcc with target libc/headers.
# clang -target alone is not enough without a sysroot — we SKIP rather than fail.
CROSS_PKGS=()
try_cross() {
local triple="$1" label="$2" file_pat="$3"
local pkg bin file_out out
note "cross $triple"
if ! command -v "${triple}-gcc" >/dev/null 2>&1; then
echo "SKIP: ${triple}-gcc not on PATH (install gcc-${triple%%-*} or full cross-gcc)"
return 0
fi
pkg=$(mkpkg "hello_${label}" "$ROOT/examples/hello.bux")
CROSS_PKGS+=("$pkg")
out=$("$BUXC" --quiet --static --release --target "$triple" build "$pkg" 2>&1) || {
echo "$out" >&2
exit 1
}
bin="$pkg/build/hello_${label}"
[[ -x "$bin" ]] || bin="$pkg/build/hello_${label}.exe"
file_out=$(file "$bin")
echo "$file_out"
echo "$file_out" | grep -qiE "$file_pat"
echo "$file_out" | grep -qi 'statically linked\|static-pie\|static '
echo "PASS: cross $label"
pass=$((pass+1))
}
pass=0
fail=0
note() { echo "=== $* ==="; }
@@ -45,7 +73,10 @@ note() { echo "=== $* ==="; }
# ── 1) minimal runtime ──────────────────────────────────────────────────
note "minimal runtime (BUX_RUNTIME=minimal)"
PKG=$(mkpkg hello_min "$ROOT/examples/hello.bux")
trap 'rm -rf "$PKG" "${PKG2:-}" "${PKG3:-}" "${PKG4:-}"' EXIT
cleanup() {
rm -rf "$PKG" "${PKG2:-}" "${PKG4:-}" "${CROSS_PKGS[@]:-}"
}
trap cleanup EXIT
export BUX_RUNTIME=minimal
out=$("$BUXC" --quiet run "$PKG" 2>&1) || { echo "$out" >&2; exit 1; }
echo "$out" | grep -q 'Hello, Bux!'
@@ -78,26 +109,12 @@ echo "PASS: static link"
pass=$((pass+1))
# ── 3) cross aarch64 (optional toolchain) ───────────────────────────────
note "cross aarch64-linux-gnu"
PKG3=$(mkpkg hello_arm "$ROOT/examples/hello.bux")
if command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then
out=$("$BUXC" --quiet --static --release --target aarch64-linux-gnu build "$PKG3" 2>&1) || {
echo "$out" >&2
exit 1
}
BIN3="$PKG3/build/hello_arm"
[[ -x "$BIN3" ]] || BIN3="$PKG3/build/hello_arm.exe"
file_out=$(file "$BIN3")
echo "$file_out"
echo "$file_out" | grep -qi 'ARM aarch64\|aarch64'
echo "$file_out" | grep -qi 'statically linked\|static-pie\|static '
echo "PASS: cross aarch64"
pass=$((pass+1))
else
echo "SKIP: aarch64-linux-gnu-gcc not on PATH"
fi
try_cross "aarch64-linux-gnu" "aarch64" 'ARM aarch64|aarch64'
# ── 4) CTFE CRC under minimal runtime ───────────────────────────────────
# ── 4) cross riscv64 (optional toolchain) — session 85 ──────────────────
try_cross "riscv64-linux-gnu" "riscv64" 'RISC-V|riscv64|UCB RISC-V'
# ── 5) CTFE CRC under minimal runtime ───────────────────────────────────
note "ctfe_crc (minimal)"
if [[ -f "$ROOT/examples/ctfe_crc.bux" ]]; then
PKG4=$(mkpkg ctfe_crc "$ROOT/examples/ctfe_crc.bux")