From d60ce2bc3f67e2472e4478405608daa5f1819873 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Mon, 27 Jul 2026 21:40:11 +0300 Subject: [PATCH] feat: stdlib daily APIs, macro tt/type paste, riscv64 cross smoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 8 +- bootstrap/ast.nim | 16 ++ bootstrap/macroexpand.nim | 238 ++++++++++++++++++++++- bootstrap/parser.nim | 40 +++- bootstrap/sema.nim | 4 +- docs/BuildAndTest.md | 37 +++- docs/LanguageRef.md | 29 ++- docs/QUALITY_PLAN.md | 123 +++++++++--- docs/Stdlib.md | 12 ++ examples/collections_extra.bux | 72 +++++++ examples/macro_tt_raw.bux | 126 ++++++++++++ examples/macro_type.bux | 55 ++++++ lib/Array.bux | 76 ++++++++ lib/Map.bux | 16 ++ lib/String.bux | 56 ++++++ lib/Test.bux | 9 + src/ast.bux | 3 + src/c_backend.bux | 2 + src/macroexpand.bux | 248 ++++++++++++++++++++++-- src/parser.bux | 80 +++++++- tests/stdlib_golden/array/src/Main.bux | 26 ++- tests/stdlib_golden/string/src/Main.bux | 12 +- tools/smoke_linux_targets.sh | 61 +++--- 23 files changed, 1263 insertions(+), 86 deletions(-) create mode 100644 examples/collections_extra.bux create mode 100644 examples/macro_tt_raw.bux create mode 100644 examples/macro_type.bux diff --git a/Makefile b/Makefile index 35ca58a..7adb4b0 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ BUILD_DIR := build # Project-local nimcache so CI can cache compiles (default is ~/.cache/nim). NIMFLAGS ?= --nimcache:nimcache -EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked ownership_release drop_early_return lifetime_elision ctfe 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 diff --git a/bootstrap/ast.nim b/bootstrap/ast.nim index 56174a8..06cc6a5 100644 --- a/bootstrap/ast.nim +++ b/bootstrap/ast.nim @@ -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) diff --git a/bootstrap/macroexpand.nim b/bootstrap/macroexpand.nim index 690253c..db8999a 100644 --- a/bootstrap/macroexpand.nim +++ b/bootstrap/macroexpand.nim @@ -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 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 diff --git a/bootstrap/parser.nim b/bootstrap/parser.nim index 3c4735e..65e5384 100644 --- a/bootstrap/parser.nim +++ b/bootstrap/parser.nim @@ -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") diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index 9ac7beb..811acfb 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -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 diff --git a/docs/BuildAndTest.md b/docs/BuildAndTest.md index fe58e0b..cd5f7da 100644 --- a/docs/BuildAndTest.md +++ b/docs/BuildAndTest.md @@ -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 ` 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 diff --git a/docs/LanguageRef.md b/docs/LanguageRef.md index d8b3c72..ad146d6 100644 --- a/docs/LanguageRef.md +++ b/docs/LanguageRef.md @@ -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`. diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 9be3e43..a2a585f 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -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 75–79 | -| **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 call’s 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`) ### P1 — Linux / cloud-native -6. ~~**Static path**~~ ✅ session 75–76 -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 75–81)~~ ✅ ### 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 ### Изрично **не** правим diff --git a/docs/Stdlib.md b/docs/Stdlib.md index 3766bdf..485c768 100644 --- a/docs/Stdlib.md +++ b/docs/Stdlib.md @@ -85,6 +85,11 @@ struct Array { | `Array_Contains` | `func Array_Contains(arr: *Array, value: T) -> bool` | Linear search for value | | `Array_IndexOf` | `func Array_IndexOf(arr: *Array, value: T) -> int` | First index or -1 | | `Array_Extend` | `func Array_Extend(arr: *Array, other: *Array)` | Append all from other | +| `Array_RemoveAt` | `func Array_RemoveAt(arr: *Array, index: uint) -> T` | Remove at index (shift left) | +| `Array_Insert` | `func Array_Insert(arr: *Array, index: uint, value: T)` | Insert at index (`0..=len`) | +| `Array_SwapRemove` | `func Array_SwapRemove(arr: *Array, index: uint) -> T` | O(1) remove (swap with last) | +| `Array_Clone` | `func Array_Clone(arr: *Array) -> Array` | Shallow clone (value copy) | +| `Array_Reverse` | `func Array_Reverse(arr: *Array)` | Reverse elements in place | | `Array_Get` | `func Array_Get(arr: *Array, index: uint) -> T` | Get element at index | | `Array_Set` | `func Array_Set(arr: *Array, index: uint, value: T)` | Set element at index | | `Array_First` | `func Array_First(arr: *Array) -> 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 { | `Map_New` | `func Map_New(cap: uint) -> Map` | Create map | | `Map_Set` | `func Map_Set(m: *Map, key: K, value: V)` | Insert/update | | `Map_Get` | `func Map_Get(m: *Map, key: K) -> V` | Get value (zero if missing) | +| `Map_GetOr` | `func Map_GetOr(m: *Map, key: K, defaultVal: V) -> V` | Get or default if missing | | `Map_Has` | `func Map_Has(m: *Map, key: K) -> bool` | Check key exists | | `Map_Remove` | `func Map_Remove(m: *Map, key: K) -> bool` | Remove key (true if present) | | `Map_Clear` | `func Map_Clear(m: *Map)` | Remove all entries (keeps capacity) | @@ -481,6 +491,7 @@ struct StringMap { | `StringMap_New` | `func StringMap_New(cap: uint) -> StringMap` | Create map | | `StringMap_Set` | `func StringMap_Set(m: *StringMap, key: String, value: V)` | Insert/update | | `StringMap_Get` | `func StringMap_Get(m: *StringMap, key: String) -> V` | Get value | +| `StringMap_GetOr` | `func StringMap_GetOr(m: *StringMap, key: String, defaultVal: V) -> V` | Get or default if missing | | `StringMap_Has` | `func StringMap_Has(m: *StringMap, key: String) -> bool` | Check key exists | | `StringMap_Remove` | `func StringMap_Remove(m: *StringMap, key: String) -> bool` | Remove key | | `StringMap_Clear` | `func StringMap_Clear(m: *StringMap)` | 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 | diff --git a/examples/collections_extra.bux b/examples/collections_extra.bux new file mode 100644 index 0000000..950a177 --- /dev/null +++ b/examples/collections_extra.bux @@ -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 = Array_New(4); + Array_Push(&arr, 10); + Array_Push(&arr, 30); + Array_Insert(&arr, 1, 20); // [10, 20, 30] + Array_Insert(&arr, 3, 40); // append via insert at len + Test_AssertEqInt(Array_Len(&arr) as int, 4); + Test_AssertEqInt(Array_Get(&arr, 0), 10); + Test_AssertEqInt(Array_Get(&arr, 1), 20); + Test_AssertEqInt(Array_Get(&arr, 2), 30); + Test_AssertEqInt(Array_Get(&arr, 3), 40); + + let rem: int = Array_RemoveAt(&arr, 1); // remove 20 → [10, 30, 40] + Test_AssertEqInt(rem, 20); + Test_AssertEqInt(Array_Len(&arr) as int, 3); + Test_AssertEqInt(Array_Get(&arr, 1), 30); + + // --- Array_SwapRemove (order not preserved) --- + let swapped: int = Array_SwapRemove(&arr, 0); // remove 10, last→front + Test_AssertEqInt(swapped, 10); + Test_AssertEqInt(Array_Len(&arr) as int, 2); + + // --- Array_Clone --- + var clone: Array = Array_Clone(&arr); + Test_AssertEqInt(Array_Len(&clone) as int, Array_Len(&arr) as int); + Test_AssertEqInt(Array_Get(&clone, 0), Array_Get(&arr, 0)); + Array_Push(&clone, 99); + Test_AssertEqInt(Array_Len(&clone) as int, 3); + Test_AssertEqInt(Array_Len(&arr) as int, 2); // original unchanged + + Array_Free(&arr); + Array_Free(&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 = Map_New(8); + Map_Set(&m, 1, 100); + Test_AssertEqInt(Map_GetOr(&m, 1, -1), 100); + Test_AssertEqInt(Map_GetOr(&m, 99, -1), -1); + Map_Free(&m); + + PrintLine("collections_extra: all checks passed"); + Test_Pass("collections_extra"); + return 0; +} diff --git a/examples/macro_tt_raw.bux b/examples/macro_tt_raw.bux new file mode 100644 index 0000000..2e5ce4b --- /dev/null +++ b/examples/macro_tt_raw.bux @@ -0,0 +1,126 @@ +// Session 84–86 — 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; +} diff --git a/examples/macro_type.bux b/examples/macro_type.bux new file mode 100644 index 0000000..8aaaf09 --- /dev/null +++ b/examples/macro_type.bux @@ -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 = Array_New(4); + Array_Push(&arr, 1); + Array_Push(&arr, 2); + Array_Push(&arr, 3); + Array_Reverse(&arr); + Test_AssertEqInt(Array_Get(&arr, 0), 3); + Test_AssertEqInt(Array_Get(&arr, 1), 2); + Test_AssertEqInt(Array_Get(&arr, 2), 1); + Test_AssertEqInt(Array_Len(&arr) as int, 3); + Array_Free(&arr); + + Test_AssertEqString(String_ToUpper("ok"), "OK"); + Test_AssertNeqString("a", "b"); + + PrintLine("PASS macro_type"); + Test_Pass("macro_type"); + return 0; +} diff --git a/lib/Array.bux b/lib/Array.bux index f4cd117..c0a98f2 100644 --- a/lib/Array.bux +++ b/lib/Array.bux @@ -140,4 +140,80 @@ module Std::Array { } } + /// Remove element at `index`, shifting later elements left. Returns the removed value. + func Array_RemoveAt(self: *Array, 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(self: *Array, 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(self: *Array, 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(self: *Array) -> Array { + var cap: uint = self.len; + if cap == 0 { + cap = 1; + } + var out: Array = Array_New(cap); + var i: uint = 0; + while i < self.len { + Array_Push(&out, self.data[i]); + i = i + 1; + } + return out; + } + + /// Reverse elements in place. + func Array_Reverse(self: *Array) { + 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; + } + } + } diff --git a/lib/Map.bux b/lib/Map.bux index c451580..e109e57 100644 --- a/lib/Map.bux +++ b/lib/Map.bux @@ -63,6 +63,14 @@ module Std::Map { return zero; } + /// Like `Map_Get`, but returns `defaultVal` when the key is absent. + func Map_GetOr(m: *Map, key: K, defaultVal: V) -> V { + if Map_Has(m, key) { + return Map_Get(m, key); + } + return defaultVal; + } + func Map_Has(m: *Map, 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(m: *StringMap, key: String, defaultVal: V) -> V { + if StringMap_Has(m, key) { + return StringMap_Get(m, key); + } + return defaultVal; + } + func StringMap_Has(m: *StringMap, key: String) -> bool { let hash: uint = bux_hash_string(key); var idx: uint = hash % m.cap; diff --git a/lib/String.bux b/lib/String.bux index afa158d..96350d8 100644 --- a/lib/String.bux +++ b/lib/String.bux @@ -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 a0` 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; + } + } diff --git a/lib/Test.bux b/lib/Test.bux index 2641692..e15f0b1 100644 --- a/lib/Test.bux +++ b/lib/Test.bux @@ -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 { diff --git a/src/ast.bux b/src/ast.bux index 98256ae..6d8dea8 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -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, diff --git a/src/c_backend.bux b/src/c_backend.bux index 9bff009..46016fa 100644 --- a/src/c_backend.bux +++ b/src/c_backend.bux @@ -141,6 +141,8 @@ module CBackend { /// Normalize monomorphized type spellings for C (Array → 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); diff --git a/src/macroexpand.bux b/src/macroexpand.bux index c2f0522..0af1963 100644 --- a/src/macroexpand.bux +++ b/src/macroexpand.bux @@ -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,14 +843,116 @@ 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); - Graft_Expr(n, file, line, col); - return n; + // 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 @@ -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; diff --git a/src/parser.bux b/src/parser.bux index ff499fa..3785118 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -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 = ""; - // Named argument: name: value - if parserPeek(p, 0) == tkIdent && parserPeek(p, 1) == tkColon { + // 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 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"); - let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind"); - var kname: String = kindTok.text; + var kname: String = ""; + if parserCheck(p, tkType) { + discard parserAdvance(p); + kname = "type"; + } else { + let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind"); + 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"); - let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind"); - var kname: String = kindTok.text; + // `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"); + 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; } } diff --git a/tests/stdlib_golden/array/src/Main.bux b/tests/stdlib_golden/array/src/Main.bux index bc1f2f9..c78709c 100644 --- a/tests/stdlib_golden/array/src/Main.bux +++ b/tests/stdlib_golden/array/src/Main.bux @@ -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(&arr) as int, 4); Test_AssertEqInt(Array_Get(&arr, 3), 50); + // Insert / RemoveAt + Array_Insert(&arr, 1, 15); // [10, 15, 20, 40, 50] + Test_AssertEqInt(Array_Get(&arr, 1), 15); + Test_AssertEqInt(Array_Len(&arr) as int, 5); + let mid: int = Array_RemoveAt(&arr, 1); + Test_AssertEqInt(mid, 15); + Test_AssertEqInt(Array_Get(&arr, 1), 20); + + // SwapRemove + Clone + let n: int = Array_Len(&arr) as int; + let lastBefore: int = Array_Get(&arr, (n - 1) as uint); + let sw: int = Array_SwapRemove(&arr, 0); + Test_AssertEqInt(sw, 10); + Test_AssertEqInt(Array_Get(&arr, 0), lastBefore); + + var clone: Array = Array_Clone(&arr); + Test_AssertEqInt(Array_Len(&clone) as int, Array_Len(&arr) as int); + Array_Push(&clone, 7); + Test_AssertTrue(Array_Len(&clone) > Array_Len(&arr)); + Array_Clear(&arr); Test_AssertTrue(Array_IsEmpty(&arr)); Test_AssertTrue(Array_Cap(&arr) >= 8); Array_Free(&arr); Array_Free(&extra); + Array_Free(&clone); PrintLine("stdlib_array: ok"); Test_Pass("stdlib_array"); return 0; diff --git a/tests/stdlib_golden/string/src/Main.bux b/tests/stdlib_golden/string/src/Main.bux index 03d79ee..a5a5435 100644 --- a/tests/stdlib_golden/string/src/Main.bux +++ b/tests/stdlib_golden/string/src/Main.bux @@ -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; diff --git a/tools/smoke_linux_targets.sh b/tools/smoke_linux_targets.sh index a93a843..5a78338 100755 --- a/tools/smoke_linux_targets.sh +++ b/tools/smoke_linux_targets.sh @@ -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 -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")