diff --git a/Makefile b/Makefile index faf3fcd..0ffeb9a 100644 --- a/Makefile +++ b/Makefile @@ -5,12 +5,12 @@ 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 macro_tt_raw macro_type collections_extra generic_enum switch is_operator +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 try_generic 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 macro_type_generic macro_op_paste collections_extra generic_enum switch is_operator # 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 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 vscode vscode-package fmt-check docs bench test-apps test-dwarf test-selfhost-smoke test-unit test-linux-targets ensure-buxc +.PHONY: all build dev debug test clean clean-all test-examples test-examples-smoke selfhost test-golden test-errors test-stdlib selfhost-loop lsp vscode vscode-package fmt-check docs bench test-apps test-dwarf test-selfhost-smoke test-unit test-linux-targets test-freestanding ensure-buxc all: build @@ -37,7 +37,7 @@ debug: dev @echo "Debug binary: buxc_debug" # Full local / sequential suite (same coverage as split CI jobs combined). -test: build fmt-check test-examples test-errors test-stdlib test-registry test-dwarf test-drop-move test-linux-targets test-apps test-selfhost-smoke test-unit +test: build fmt-check test-examples test-errors test-stdlib test-registry test-dwarf test-drop-move test-linux-targets test-freestanding test-apps test-selfhost-smoke test-unit # Nim unit tests + tiny CLI smoke (needs Nim + buxc). test-unit: ensure-buxc @@ -212,6 +212,9 @@ test-lsp: lsp @echo "=== LSP diagnostics (error underlines) smoke ===" @chmod +x tools/smoke_lsp_diagnostics.sh @tools/smoke_lsp_diagnostics.sh + @echo "=== LSP formatting smoke ===" + @chmod +x tools/smoke_lsp_formatting.sh + @tools/smoke_lsp_formatting.sh @echo "=== LSP references / rename smoke ===" @chmod +x tools/smoke_lsp_rename.sh @tools/smoke_lsp_rename.sh @@ -288,6 +291,13 @@ test-linux-targets: ensure-buxc @chmod +x tools/smoke_linux_targets.sh @tools/smoke_linux_targets.sh +# post-1.0 — freestanding runtime (-ffreestanding object + BUX_RUNTIME=freestanding package) +.PHONY: test-freestanding +test-freestanding: ensure-buxc + @echo "=== Freestanding runtime smoke ===" + @chmod +x tools/smoke_freestanding.sh + @tools/smoke_freestanding.sh + # Session 78 — Nexus HTTPS (self-signed) smoke .PHONY: test-nexus-tls test-nexus-tls: ensure-buxc diff --git a/bootstrap/cli.nim b/bootstrap/cli.nim index 79e28d0..706c22a 100644 --- a/bootstrap/cli.nim +++ b/bootstrap/cli.nim @@ -14,9 +14,10 @@ type ## Which C runtime shim to link (session 75 — Linux / cloud / embedded). RuntimeFlavor* = enum - rfFull ## rt/runtime.c — POSIX + OpenSSL - rfMinimal ## rt/runtime_minimal.c — thin, static/container/embed friendly - rfWin ## rt/runtime_win.c — Windows/MinGW (historical) + rfFull ## rt/runtime.c — POSIX + OpenSSL + rfMinimal ## rt/runtime_minimal.c — thin, static/container/embed friendly + rfFreestanding ## rt/runtime_freestanding.c — no-libc research spike + rfWin ## rt/runtime_win.c — Windows/MinGW (historical) GlobalOptions* = object color*: ColorMode @@ -63,7 +64,7 @@ Registry / toolchain env: BUX_REGISTRY_INSECURE=1 Allow self-signed HTTPS registry (dev/smoke) BUX_CFLAGS Extra flags appended to the C compiler line BUX_CC C compiler binary (overrides --target pick) - BUX_RUNTIME full|minimal|thin|embed|win (default: full on Unix) + BUX_RUNTIME full|minimal|thin|embed|freestanding|win (default: full on Unix) BUX_STATIC=1 Same as --static Global options: @@ -136,8 +137,10 @@ proc resolveRuntimeFlavor(opts: GlobalOptions): RuntimeFlavor = case e of "full", "posix": return rfFull - of "minimal", "thin", "embed", "embedded", "freestanding": + of "minimal", "thin", "embed", "embedded": return rfMinimal + of "freestanding", "bare", "nolibc": + return rfFreestanding of "win", "windows": return rfWin of "": @@ -159,10 +162,12 @@ proc runtimeFileName(flavor: RuntimeFlavor): string = case flavor of rfFull: "runtime.c" of rfMinimal: "runtime_minimal.c" + of rfFreestanding: "runtime_freestanding.c" of rfWin: "runtime_win.c" proc isThinRuntime(flavor: RuntimeFlavor): bool = - flavor in {rfMinimal, rfWin} + ## Thin = no OpenSSL / pthread link needs + flavor in {rfMinimal, rfFreestanding, rfWin} proc findOnPath(bin: string): bool = ## True if `bin` resolves as an executable on PATH (or is an absolute path). diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index 81995b4..7b89f9c 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -2101,6 +2101,10 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = of ekTry: let operand = ctx.lowerExpr(expr.exprTryOperand) var operandType = ctx.resolveExprType(expr.exprTryOperand) + # Keep original type args for Ok payload (before mangling to Result_T_E) + let payloadArgs: seq[Type] = + if operandType != nil and operandType.kind == tkNamed: operandType.inner + else: @[] var typeName = "" var errTag = "" @@ -2112,7 +2116,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = # Upgrade bare generic enum name to concrete monomorphization. # Sema stores Result/Option without mangled type-args; try needs Result_int_String_Tag. - if ctx.genericEnums.hasKey(typeName): + if ctx.genericEnums.hasKey(typeName) or typeName == "Result" or typeName == "Option": # Prefer resolving call/ident TypeExpr with type args if expr.exprTryOperand != nil: if expr.exprTryOperand.kind == ekIdent and ctx.varTypeExprs.hasKey(expr.exprTryOperand.exprIdent): @@ -2131,14 +2135,21 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = (resolved.name == typeName or resolved.name.startsWith(typeName & "_")): typeName = resolved.name # Enclosing function return type (must match for `?` propagation) - let stillBare = operandType == nil or operandType.kind != tkNamed or - typeName == operandType.name + var stillBare = typeName == "Result" or typeName == "Option" if stillBare and ctx.currentFuncRetType != nil and ctx.currentFuncRetType.kind == tkNamed: let rn = ctx.currentFuncRetType.name if rn.startsWith(typeName & "_"): typeName = rn - operandType = makeNamed(typeName) + stillBare = false + # Prefer mangled name from type args: Result + [String,String] → Result_String_String + if stillBare and payloadArgs.len > 0 and + (typeName == "Result" or typeName == "Option"): + var mangled = typeName + for a in payloadArgs: + mangled = mangled & "_" & a.toString.replace(" ", "").replace("*", "p") + typeName = mangled + operandType = Type(kind: tkNamed, name: typeName, inner: payloadArgs) # Err tag / Ok field from base or concrete name let baseForTags = @@ -2157,6 +2168,41 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = errTag = typeName & "_Err" okField = "Ok_0" + # Payload type (Ok_0 / Some_0) — must match T of Result, not always int + var okType = makeInt() + if payloadArgs.len >= 1: + okType = payloadArgs[0] + else: + var enumSym = ctx.globalScope.lookup(typeName) + var enumDecl: Decl = nil + if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum: + enumDecl = enumSym.decl + elif ctx.structInstMap.hasKey(typeName): + let (baseName, _) = ctx.structInstMap[typeName] + let baseSym = ctx.globalScope.lookup(baseName) + if baseSym != nil and baseSym.decl != nil and baseSym.decl.kind == dkEnum: + enumDecl = baseSym.decl + elif typeName.startsWith("Result_"): + let baseSym = ctx.globalScope.lookup("Result") + if baseSym != nil and baseSym.decl != nil: enumDecl = baseSym.decl + elif typeName.startsWith("Option_"): + let baseSym = ctx.globalScope.lookup("Option") + if baseSym != nil and baseSym.decl != nil: enumDecl = baseSym.decl + if enumDecl != nil: + var subst = initTable[string, Type]() + if ctx.structInstMap.hasKey(typeName): + var ti = 0 + for tp in enumDecl.declEnumTypeParams: + if ti < ctx.structInstMap[typeName][1].len: + subst[tp.name] = ctx.structInstMap[typeName][1][ti] + inc ti + for variant in enumDecl.declEnumVariants: + for i, f in variant.fields: + let fieldName = variant.name & "_" & $i + if fieldName == okField: + okType = substituteType(ctx, f, subst) + break + let tmpName = ctx.freshTryVar() let tmpAlloca = hirAlloca(tmpName, operandType, loc) let tmpVar = hirVar(tmpName, operandType, loc) @@ -2179,8 +2225,8 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr, typ: makeNamed(typeName & "_Data"), loc: loc) let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: okField, - typ: makePointer(makeInt()), loc: loc) - let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: makeInt(), loc: loc) + typ: makePointer(okType), loc: loc) + let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: okType, loc: loc) ctx.pendingStmts.add(tmpAlloca) ctx.pendingStmts.add(tmpStore) @@ -2193,14 +2239,66 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = var errTag = "Result_Err" var typeName = "Result" - if operandType.kind == tkNamed: + var okField = "Ok_0" + if operandType != nil and operandType.kind == tkNamed: typeName = operandType.name - if typeName == "Option": - errTag = "Option_None" + if typeName == "Option" or typeName.startsWith("Option_"): + errTag = if typeName == "Option": "Option_None" else: typeName & "_None" + okField = "Some_0" + elif typeName == "Result": + errTag = "Result_Err" + else: + errTag = typeName & "_Err" + # Mangle Result + type args when still bare + if operandType != nil and operandType.kind == tkNamed and operandType.inner.len > 0 and + (typeName == "Result" or typeName == "Option"): + var mangled = typeName + for a in operandType.inner: + mangled = mangled & "_" & a.toString.replace(" ", "").replace("*", "p") + typeName = mangled + if typeName.startsWith("Option_"): + errTag = typeName & "_None" + okField = "Some_0" + else: + errTag = typeName & "_Err" + okField = "Ok_0" + + var okType = makeInt() + if operandType != nil and operandType.kind == tkNamed and operandType.inner.len >= 1: + okType = operandType.inner[0] + else: + var enumDecl: Decl = nil + let enumSym = ctx.globalScope.lookup( + if typeName.startsWith("Result_"): "Result" + elif typeName.startsWith("Option_"): "Option" + else: typeName) + if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum: + enumDecl = enumSym.decl + if enumDecl != nil: + var subst = initTable[string, Type]() + if ctx.structInstMap.hasKey(typeName): + var ti = 0 + for tp in enumDecl.declEnumTypeParams: + if ti < ctx.structInstMap[typeName][1].len: + subst[tp.name] = ctx.structInstMap[typeName][1][ti] + inc ti + elif operandType != nil and operandType.kind == tkNamed: + var ti = 0 + for tp in enumDecl.declEnumTypeParams: + if ti < operandType.inner.len: + subst[tp.name] = operandType.inner[ti] + inc ti + for variant in enumDecl.declEnumVariants: + for i, f in variant.fields: + if variant.name & "_" & $i == okField: + okType = substituteType(ctx, f, subst) + + # Same shape as `?`: stack temporary of the Result/Option value (not a pointer). + # Typing the var as *Result made C emit `tmp->tag` on a value (invalid). let tmpName = ctx.freshTryVar() let tmpAlloca = hirAlloca(tmpName, operandType, loc) - let tmpVar = hirVar(tmpName, makePointer(operandType), loc) + let tmpVar = hirVar(tmpName, operandType, loc) let tmpStore = hirStore(tmpVar, operand, loc) let tagPtr = HirNode(kind: hFieldPtr, fieldPtrBase: tmpVar, fieldName: "tag", @@ -2210,11 +2308,15 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = let errConst = hirVar(errTag, makeNamed(typeName & "_Tag"), loc) let cond = hirBinary(tkEq, tagLoad, errConst, makeBool(), loc) - # On error: call bux_panic("unwrap failed") + # On error: call bux_panic("unwrap failed") then exit (do not continue with garbage) let panicTok = Token(kind: tkStringLiteral, text: "\"unwrap failed\"", loc: loc) let panicMsg = HirNode(kind: hLit, litToken: panicTok, typ: makeStr(), loc: loc) let panicCall = hirCall("bux_panic", @[panicMsg], makeVoid(), loc) - let thenBlock = hirBlock(@[panicCall], nil, makeVoid(), loc) + let exitLit = HirNode(kind: hLit, + litToken: Token(kind: tkIntLiteral, text: "1", loc: loc), + typ: makeInt(), loc: loc) + let exitCall = hirCall("bux_exit", @[exitLit], makeVoid(), loc) + let thenBlock = hirBlock(@[panicCall, exitCall], nil, makeVoid(), loc) let ifNode = HirNode(kind: hIf, ifCond: cond, ifThen: thenBlock, ifElse: nil, typ: makeVoid(), loc: loc) @@ -2223,9 +2325,9 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = typ: makePointer(makeNamed(typeName & "_Data")), loc: loc) let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr, typ: makeNamed(typeName & "_Data"), loc: loc) - let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: "Ok_0", - typ: makePointer(makeInt()), loc: loc) - let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: makeInt(), loc: loc) + let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: okField, + typ: makePointer(okType), loc: loc) + let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: okType, loc: loc) ctx.pendingStmts.add(tmpAlloca) ctx.pendingStmts.add(tmpStore) diff --git a/bootstrap/macroexpand.nim b/bootstrap/macroexpand.nim index db8999a..132af0e 100644 --- a/bootstrap/macroexpand.nim +++ b/bootstrap/macroexpand.nim @@ -17,6 +17,17 @@ type proc emitErr(res: var MacroExpandResult, loc: SourceLocation, msg: string) = res.diagnostics.add(MacroDiagnostic(loc: loc, message: msg)) +proc isBinaryPasteOp*(k: TokenKind): bool = + ## Operators allowed as `$op:tt` paste into `$op($a, $b)` → `a OP b`. + case k + of tkPlus, tkMinus, tkStar, tkSlash, tkPercent, tkStarStar, + tkAmp, tkPipe, tkCaret, tkShl, tkShr, + tkAmpAmp, tkPipePipe, + tkEq, tkNe, tkLt, tkLe, tkGt, tkGe: + true + else: + false + # --------------------------------------------------------------------------- # Deep clone (bootstrap has no Ast_Clone*) # --------------------------------------------------------------------------- @@ -152,9 +163,12 @@ proc cloneExpr*(e: Expr): Expr = for a in e.exprCallArgs: result.exprCallArgs.add(cloneExpr(a)) of ekGenericCall: + var gtas: seq[TypeExpr] = @[] + for ta in e.exprGenericTypeArgs: + gtas.add(cloneTypeExpr(ta)) result = Expr(kind: ekGenericCall, loc: e.loc, exprGenericCallee: e.exprGenericCallee, - exprGenericTypeArgs: e.exprGenericTypeArgs) + exprGenericTypeArgs: gtas) of ekIndex: result = Expr(kind: ekIndex, loc: e.loc, exprIndexObj: cloneExpr(e.exprIndexObj), @@ -165,9 +179,12 @@ proc cloneExpr*(e: Expr): Expr = exprFieldObj: cloneExpr(e.exprFieldObj), exprFieldName: e.exprFieldName) of ekStructInit: + var stas: seq[TypeExpr] = @[] + for ta in e.exprStructInitTypeArgs: + stas.add(cloneTypeExpr(ta)) result = Expr(kind: ekStructInit, loc: e.loc, exprStructInitName: e.exprStructInitName, - exprStructInitTypeArgs: e.exprStructInitTypeArgs, + exprStructInitTypeArgs: stas, exprStructInitFields: @[]) for f in e.exprStructInitFields: result.exprStructInitFields.add((f.name, cloneExpr(f.value))) @@ -895,6 +912,12 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr = of ekRange: c.exprRangeLo = substExpr(c.exprRangeLo, env, callLoc) c.exprRangeHi = substExpr(c.exprRangeHi, env, callLoc) + of ekGenericCall: + # Array_New<$t>(…) / Foo<$t, $u> — substitute type fragments in type args + var gtas: seq[TypeExpr] = @[] + for ta in c.exprGenericTypeArgs: + gtas.add(substType(ta, env, callLoc)) + c.exprGenericTypeArgs = gtas of ekCall: c.exprCallCallee = substExpr(c.exprCallCallee, env, callLoc) var args: seq[Expr] = @[] @@ -950,12 +973,37 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr = argNames.add("") c.exprCallArgs = args c.exprCallArgNames = argNames + # Operators-only paste: `$op($a, $b)` where `$op:tt` is a bound binary operator + # → rebuild as `$a OP $b` (post-1.0). + if c.exprCallCallee != nil and c.exprCallCallee.kind == ekMacroTt and + c.exprCallCallee.exprMacroTtInner != nil and + c.exprCallCallee.exprMacroTtInner.kind == ekLiteral and + c.exprCallArgs.len == 2: + let opTok = c.exprCallCallee.exprMacroTtInner.exprLit + if opTok.kind.isBinaryPasteOp: + result = Expr(kind: ekBinary, loc: callLoc, + exprBinaryOp: opTok.kind, + exprBinaryLeft: c.exprCallArgs[0], + exprBinaryRight: c.exprCallArgs[1]) + return + # Callee already unwrapped to op literal by value-position MacroTt splice + if c.exprCallCallee != nil and c.exprCallCallee.kind == ekLiteral and + c.exprCallArgs.len == 2 and c.exprCallCallee.exprLit.kind.isBinaryPasteOp: + result = Expr(kind: ekBinary, loc: callLoc, + exprBinaryOp: c.exprCallCallee.exprLit.kind, + exprBinaryLeft: c.exprCallArgs[0], + exprBinaryRight: c.exprCallArgs[1]) + return of ekIndex: c.exprIndexObj = substExpr(c.exprIndexObj, env, callLoc) c.exprIndexIdx = substExpr(c.exprIndexIdx, env, callLoc) of ekField: c.exprFieldObj = substExpr(c.exprFieldObj, env, callLoc) of ekStructInit: + var stas: seq[TypeExpr] = @[] + for ta in c.exprStructInitTypeArgs: + stas.add(substType(ta, env, callLoc)) + c.exprStructInitTypeArgs = stas var fields: seq[tuple[name: string, value: Expr]] = @[] for f in c.exprStructInitFields: fields.add((f.name, substExpr(f.value, env, callLoc))) @@ -1247,6 +1295,25 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl], exprMacroTtInner: inner, exprMacroTtGroup: true) result = @[callee, group] + ## Operators-only juxta: single binary arg `1 + 2` matches + ## `$a:expr, $op:tt, $b:expr` (or juxta without commas). + proc juxtaBinarySplit(rule: MacroRule, inArgs: seq[Expr]): seq[Expr] = + result = inArgs + if inArgs.len != 1 or inArgs[0] == nil: return + if inArgs[0].kind != ekBinary: return + if rule.frags.len != 3: return + if rule.frags[0].isRep or rule.frags[1].isRep or rule.frags[2].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 + let k2 = if rule.frags[2].kinds.len > 0: rule.frags[2].kinds[0] else: rule.frags[2].kind + if k0 != mfkExpr or k1 != mfkTt or k2 != mfkExpr: return + if not inArgs[0].exprBinaryOp.isBinaryPasteOp: return + let opTok = Token(kind: inArgs[0].exprBinaryOp, text: "", loc: inArgs[0].loc) + let lit = Expr(kind: ekLiteral, loc: inArgs[0].loc, exprLit: opTok) + let opTt = Expr(kind: ekMacroTt, loc: inArgs[0].loc, + exprMacroTtInner: lit, exprMacroTtGroup: false) + result = @[inArgs[0].exprBinaryLeft, opTt, inArgs[0].exprBinaryRight] + var matched: MacroRule var env: MacroEnv var found = false @@ -1257,7 +1324,8 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl], var gi = 0 var ai = 0 let useGroups = nReps > 1 and groups.len > 1 - let flat = juxtaCallSplit(rule, args) + var flat = juxtaCallSplit(rule, args) + flat = juxtaBinarySplit(rule, flat) for frag in rule.frags: if failed: break diff --git a/bootstrap/parser.nim b/bootstrap/parser.nim index e02df68..2b31f20 100644 --- a/bootstrap/parser.nim +++ b/bootstrap/parser.nim @@ -434,10 +434,22 @@ proc isMacroStmtStart(p: Parser): bool = p.peek() in {tkLet, tkVar, tkIf, tkWhile, tkFor, tkLoop, tkMatch, tkReturn, tkBreak, tkContinue, tkDefer, tkSwitch, tkDo} +proc isBinaryPasteOpToken(k: TokenKind): bool = + ## Same set as macroexpand.isBinaryPasteOp — kept local to avoid cycles. + case k + of tkPlus, tkMinus, tkStar, tkSlash, tkPercent, tkStarStar, + tkAmp, tkPipe, tkCaret, tkShl, tkShr, + tkAmpAmp, tkPipePipe, + tkEq, tkNe, tkLt, tkLe, tkGt, tkGe: + true + else: + false + proc parseMacroArg(p: var Parser): Expr = ## Macro call argument: ## - statement keywords → ekMacroStmt ## - `_` / pattern-only starts → ekMacroPat (also `$p:pat` from expr via coerce) + ## - bare binary operators (`+`, `*`, `==`, …) → ekMacroTt (operators-only paste) ## - else expression let loc = p.currentLoc if p.isMacroStmtStart(): @@ -447,6 +459,20 @@ proc parseMacroArg(p: var Parser): Expr = if p.check(tkUnderscore): let pat = p.parsePattern() return Expr(kind: ekMacroPat, loc: loc, exprMacroPat: pat) + # Operators-only tt: `apply_op!(+, 1, 2)` / `apply_op!(*, 2, 3)`. + # Bare ops are not primary exprs. Unary-capable tokens (`*`, `-`, …) are only + # claimed when the next token ends the arg (`,`, `)`, `;`) so `*int` still + # parses as a type/expr and juxta `a * b` still works as ekBinary. + let pk = p.peek() + if isBinaryPasteOpToken(pk): + let nxt = p.peek(1) + let endsArg = nxt in {tkComma, tkRParen, tkSemicolon, tkEndOfFile, tkNewLine} + let unaryCapable = pk in {tkStar, tkMinus, tkBang, tkAmp, tkTilde, + tkPlusPlus, tkMinusMinus} + if endsArg or not unaryCapable: + let tok = p.advance() + let lit = Expr(kind: ekLiteral, loc: loc, exprLit: tok) + return Expr(kind: ekMacroTt, loc: loc, exprMacroTtInner: lit, exprMacroTtGroup: false) p.parseExpr() proc parseMacroRepExpr(p: var Parser): Expr = diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index 811acfb..41080e4 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -1078,6 +1078,58 @@ proc collectGlobals*(sema: var Sema) = proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type +proc isResultOrOptionName(name: string): bool = + name == "Result" or name.startsWith("Result_") or + name == "Option" or name.startsWith("Option_") + +proc extractResultOptionPayload*(sema: var Sema, opTy: Type, loc: SourceLocation, opKind: string): Type = + ## Payload type of `Result`/`Option` for `?` (try) and `!` (unwrap). + ## Prefer type-args (`Result` → T); fall back to Ok/Some field on the enum decl. + if opTy == nil or opTy.isUnknown: + return makeUnknown() + if opTy.kind != tkNamed: + sema.emitError(loc, opKind & " requires Result or Option operand") + return makeUnknown() + let name = opTy.name + if not isResultOrOptionName(name): + sema.emitError(loc, opKind & " requires Result or Option, got " & opTy.toString) + return makeUnknown() + # Result / Option store payload in .inner + if opTy.inner.len >= 1: + return opTy.inner[0] + # Bare / monomorphized name: look up Ok(T) / Some(T) on the enum decl + var enumSym = sema.globalScope.lookup(name) + if (enumSym == nil or enumSym.decl == nil or enumSym.decl.kind != dkEnum): + if name.startsWith("Result_"): + enumSym = sema.globalScope.lookup("Result") + elif name.startsWith("Option_"): + enumSym = sema.globalScope.lookup("Option") + let wantVariant = + if name == "Option" or name.startsWith("Option_"): "Some" + else: "Ok" + if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum: + var subst = initTable[string, Type]() + # Result_String_String → try to bind type params from mangled suffix when possible + if opTy.inner.len == 0 and enumSym.decl.declEnumTypeParams.len > 0 and + (name.startsWith("Result_") or name.startsWith("Option_")): + let prefix = if name.startsWith("Result_"): "Result_" else: "Option_" + let rest = name[prefix.len .. ^1] + # Split on '_' is imperfect for nested types; for simple T_E it works + let parts = rest.split('_') + var pi = 0 + for tp in enumSym.decl.declEnumTypeParams: + if pi < parts.len and parts[pi].len > 0: + # Re-resolve simple type names (int, String, …) + let te = TypeExpr(kind: tekNamed, typeName: parts[pi]) + subst[tp.name] = sema.resolveType(te) + inc pi + for variant in enumSym.decl.declEnumVariants: + if variant.name == wantVariant and variant.fields.len > 0: + let raw = sema.resolveType(variant.fields[0]) + return sema.substituteTypeInType(raw, subst) + # Unknown payload — don't invent int (breaks String Results) + return makeUnknown() + proc typeImplements(sema: Sema, t: Type, interfaceName: string): bool = ## Check if a type implements an interface by verifying all required methods exist. if t.isUnknown: return true @@ -1838,14 +1890,12 @@ proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type = discard sema.checkExpr(expr.exprIsOperand, scope) return makeBool() of ekTry: - discard sema.checkExpr(expr.exprTryOperand, scope) - # For now, assume Result -> int - # TODO: check operand is Result/Option and current function returns same type - return makeInt() + let opTy = sema.checkExpr(expr.exprTryOperand, scope) + # Payload of Result/Option; validates operand is Result or Option + return sema.extractResultOptionPayload(opTy, expr.loc, "try operator (`?`)") of ekUnwrap: - discard sema.checkExpr(expr.exprUnwrapOperand, scope) - # Unwrap: extract Ok value or panic on Err - return makeInt() + let opTy = sema.checkExpr(expr.exprUnwrapOperand, scope) + return sema.extractResultOptionPayload(opTy, expr.loc, "unwrap operator (`!`)") of ekBlock: var blockScope = newScope(scope) var lastType = makeVoid() diff --git a/docs/BuildAndTest.md b/docs/BuildAndTest.md index ecd761c..fcae3d9 100644 --- a/docs/BuildAndTest.md +++ b/docs/BuildAndTest.md @@ -181,13 +181,25 @@ no-libc freestanding firmware. | 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 | +| True freestanding (`-ffreestanding`, no libc) | ✅ spike shipped | `rt/runtime_freestanding.c` + `BUX_RUNTIME=freestanding` | +| Cortex-M / qemu-system | 🔬 research | Needs linker scripts, startup, board UART — not in tree | -**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. +**Freestanding runtime** (`rt/runtime_freestanding.c`): + +- No hosted libc: bump heap (default 256 KiB), freestanding string/arith helpers +- Weak hooks `bux_fs_write` / `bux_fs_halt` for board BSP or host overrides +- Net / Task / TLS / FS return failure stubs +- Optional `_start` when compiled with `-DBUX_FS_PROVIDE_START` (nostdlib experiments) + +```bash +make test-freestanding # -ffreestanding -c + package build exit 42 +BUX_RUNTIME=freestanding buxc build . +# Still uses host crt0 unless you pass -nostdlib / custom start via BUX_CFLAGS +``` + +**Practical path for cloud/cross:** `BUX_RUNTIME=minimal --static --target …` for +Linux userspace. Use `freestanding` only for no-libc research; do **not** import +`Std::Net` / `Std::Task` / OpenSSL on that path. --- diff --git a/docs/IMPROVEMENTS.md b/docs/IMPROVEMENTS.md index 4fd7dee..89060cd 100644 --- a/docs/IMPROVEMENTS.md +++ b/docs/IMPROVEMENTS.md @@ -1,7 +1,32 @@ # Bux — План за подобрения (post-v1.0.0) > **Дата:** 2026-07-28 -> **Статус:** Всички приоритетни задачи изпълнени ✅ +> **Статус:** Всички приоритетни задачи изпълнени ✅ · follow-up DX/correctness shipped + +--- + +## Сесия 4 — Try payload type + LSP formatting (2026-07-28) + +| # | Задача | Файлове | +|---|--------|---------| +| F.5 | `?` / `!` вече връщат **Ok/Some payload type** (не винаги `int`) | `bootstrap/sema.nim`, `bootstrap/hir_lower.nim` | +| F.6 | Example `try_generic` — `Result` + `?` | `examples/try_generic.bux` | +| D.6 | LSP **v0.18** `textDocument/formatting` (+ range) = `bux fmt` | `tools/lsp_server.nim`, `tools/smoke_lsp_formatting.sh` | +| D.7 | VS Code format-on-save default; docs | `vscode/package.json`, `docs/LSP.md` | + +**Verified:** `try_generic` prints `hello` / `empty name`; `try_operator` still OK; formatting smoke PASS. + +## Сесия 5 — Post-1.0 backlog: macros + freestanding (2026-07-28) + +| # | Задача | Файлове | +|---|--------|---------| +| M.1 | Generics in `$t:type` + `$t` in `Array_New<$t>` | `bootstrap/macroexpand.nim`, `src/macroexpand.bux` | +| M.2 | Operators-only `:tt` paste (`$op($a,$b)` + juxta binary split) | `bootstrap/parser.nim`, `bootstrap/macroexpand.nim` | +| M.3 | Examples `macro_type_generic`, `macro_op_paste` | `examples/` | +| R.1 | `rt/runtime_freestanding.c` + `BUX_RUNTIME=freestanding` | `rt/`, `bootstrap/cli.nim`, `src/cli.bux` | +| R.2 | `make test-freestanding` smoke | `tools/smoke_freestanding.sh` | + +**Verified:** both macro examples PASS; freestanding `-ffreestanding -c` + package exit 42. --- diff --git a/docs/LSP.md b/docs/LSP.md index 7fdca91..2ff9bc9 100644 --- a/docs/LSP.md +++ b/docs/LSP.md @@ -1,13 +1,14 @@ # Bux Language Server (`bux-lsp`) -> **Status:** **v0.17.0** — stdio JSON-RPC 2.0 language server +> **Status:** **v0.18.0** — stdio JSON-RPC 2.0 language server > **Binary:** `tools/bux-lsp` (`make lsp`) > **Editors:** VS Code extension in [`vscode/`](../vscode/README.md); any LSP client via stdio Bux already ships a real Language Server Protocol implementation. It is **not** syntax-only: hover and outline use bootstrap semantic analysis when available, -and **error underlines (red squiggles)** come from **in-process** lex / parse / type-check -of the **live editor buffer** on every open, edit, and save. +**error underlines (red squiggles)** come from **in-process** lex / parse / type-check +of the **live editor buffer** on every open, edit, and save, and **Format Document** +uses the same indentation engine as `bux fmt`. --- @@ -33,13 +34,15 @@ Protocol framing: standard `Content-Length` headers + JSON-RPC 2.0 body. --- -## Capabilities (v0.16) +## Capabilities (v0.18) | Method | Support | Notes | |--------|---------|--------| -| `initialize` / `shutdown` / `exit` | ✅ | `serverInfo`: `bux-lsp` 0.17.0 | +| `initialize` / `shutdown` / `exit` | ✅ | `serverInfo`: `bux-lsp` 0.18.0 | | `textDocument/didOpen` / `didChange` / `didSave` | ✅ | Full text sync (`textDocumentSync: 1`) | | `textDocument/publishDiagnostics` | ✅ | **Live underlines** on open/change/save (in-process); optional `buxc` merge on open/save | +| `textDocument/formatting` | ✅ | Full document — same rules as `bux fmt` (4-space brace indent) | +| `textDocument/rangeFormatting` | ✅ | Applies full-file format (indent depends on whole brace structure) | | `textDocument/completion` | ✅ | Trigger: `.` `:` | | `textDocument/hover` | ✅ | Sema types for globals/stdlib; **scoped locals** + inferred `let` | | `textDocument/definition` | ✅ | Go to definition | @@ -123,15 +126,28 @@ make test-lsp --- +## Format Document / format-on-save + +`textDocument/formatting` re-indents the buffer with **4 spaces × brace depth** +(identical to `bux fmt` / `bootstrap/fmt.nim`). Idempotent: a clean file yields +an empty edit list. + +**VS Code:** the extension defaults `editor.formatOnSave` for `[bux]`. Disable +with `"editor.formatOnSave": false` in workspace settings if you prefer manual +only. Command palette: **Format Document**. + +```bash +make test-lsp # includes tools/smoke_lsp_formatting.sh +``` + ## Limitations / not yet Honest gaps (so Reddit / issue trackers stay accurate): -- **No format-on-save via LSP** yet (`bux fmt` exists as CLI; not `textDocument/formatting`) - **No semantic tokens** provider (TextMate grammar handles highlighting in VS Code) - **No code actions / lightbulbs** (quick-fixes) - **No inlay hints** -- **didChange** uses a fast symbol path; full sema + diagnostics refresh mainly on open/save +- **rangeFormatting** reformats the whole file (partial selection cannot get correct indent without full brace context) - Completion is useful but not a full IDE IntelliSense engine - Single-process stdio only (no TCP/socket mode) diff --git a/docs/LanguageRef.md b/docs/LanguageRef.md index aedbdd3..835000b 100644 --- a/docs/LanguageRef.md +++ b/docs/LanguageRef.md @@ -997,6 +997,25 @@ func Compute() -> Result { ``` `?` can be used on `Result` and `Option` types in any expression context. +The type of `expr?` is the **Ok / Some payload** (`T` in `Result` or +`Option`), not always `int`. The enclosing function must return a compatible +Result/Option so Err/None can propagate. + +```bux +// Generic Result — payload type is String +func GetName() -> Result { + return Result_NewOk("bux"); +} +func Run() -> Result { + let n: String = GetName()?; // n: String + return Result_NewOk(n); +} +``` + +The postfix unwrap operator `expr!` extracts Ok/Some or panics (and exits) on +Err/None; its type is likewise the payload type. + +See also `examples/try_operator.bux` and `examples/try_generic.bux`. --- @@ -1312,13 +1331,32 @@ macro! with_acc { 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):** +- **Type fragments (session 87+):** named, pointer, and **generic** types + (`Array`, `*int`); `$t` substitutes in `sizeof` / cast / let types and + monomorph call type args (`Array_New<$t>`). ```bux macro! size_of { ( $t:type ) => { sizeof($t) as int } } + macro! new_array { + ( $t:type, $cap:expr ) => { Array_New<$t>($cap) } + } let n: int = size_of!(int); let p: int = size_of!(*int); + let s: int = size_of!(Array); + var a: Array = new_array!(int, 4); + ``` +- **Operators-only `:tt` paste:** + ```bux + macro! apply_op { + ( $op:tt, $a:expr, $b:expr ) => { $op($a, $b) } + } + macro! flip_op { + ( $a:expr, $op:tt, $b:expr ) => { $op($b, $a) } + } + let x: int = apply_op!(+, 3, 4); // 7 + let y: int = apply_op!(*, 6, 7); // 42 + let z: int = flip_op!(10 - 3); // -7 (juxta binary split) ``` ### Invocation @@ -1415,8 +1453,15 @@ Examples: `examples/macro_hygiene.bux`, `examples/macro_unhygienic.bux`. 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. + `F(a, b)`. +- **Operators-only paste:** bare binary ops as `:tt` (`+`, `*`, `==`, …) and + juxta binary split `$a:expr $op:tt $b:expr` on a single binary arg. Template + form `$op($a, $b)` rebuilds `a OP b`. See `examples/macro_op_paste.bux`. +- **`:type` generics:** `Array`, `*int`, nested type args; `$t` splices + into `sizeof($t)`, casts, and `Array_New<$t>(…)`. See + `examples/macro_type.bux`, `examples/macro_type_generic.bux`. +- Fully free-form token streams (unparsed soup) remain out of scope. Examples: `examples/macro_tt.bux`, `examples/macro_tt_raw.bux`, -`examples/macro_repeat.bux`, `examples/macro_nested.bux`. +`examples/macro_repeat.bux`, `examples/macro_nested.bux`, +`examples/macro_type_generic.bux`, `examples/macro_op_paste.bux`. diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 1f1ed1a..737a587 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -1472,10 +1472,22 @@ bootstrap + **buxc2** `macro_tt_raw` (incl. slice) PASS. 5. Tag `v1.0.0` after `make test` gate **Post-1.0 backlog (MINOR, not freeze blockers):** -- Generics in `:type` (`Array`); operators-only tt paste -- `runtime_freestanding.c` + Cortex-M research +- ~~Generics in `:type` (`Array`); operators-only tt paste~~ ✅ session 5 +- ~~`runtime_freestanding.c`~~ ✅ session 5 (Cortex-M / board BSP still research) - LSP / IDE versioning independent of language MAJOR +### Follow-up (2026-07-28) + +1. **`?` / `!` payload types** — bootstrap no longer hardcodes `int`; Ok/Some type + from `Result` / enum fields (`bootstrap/sema.nim`, `hir_lower.nim`). +2. **Example** `try_generic` — String Result propagation. +3. **LSP 0.18** — `textDocument/formatting` (+ range) via `formatSource` / `bux fmt`; + VS Code format-on-save default; `tools/smoke_lsp_formatting.sh`. +4. **Macros:** generic `:type` + `Array_New<$t>`; operators-only `$op($a,$b)` / + juxta binary split (`examples/macro_type_generic`, `macro_op_paste`). +5. **Freestanding:** `rt/runtime_freestanding.c`, `BUX_RUNTIME=freestanding`, + `make test-freestanding`. + ### Изрично **не** правим - Повече Windows examples / Win OpenSSL / Win sockets diff --git a/examples/macro_op_paste.bux b/examples/macro_op_paste.bux new file mode 100644 index 0000000..40f19aa --- /dev/null +++ b/examples/macro_op_paste.bux @@ -0,0 +1,40 @@ +// Operators-only `:tt` paste — `$op($a, $b)` → `a OP b` +import Std::Io::{PrintLine, PrintInt}; +import Std::Test::{Test_AssertEqInt, Test_AssertEqBool, Test_Pass}; + +// Explicit op as call-site tt: apply_op!(+, 3, 4) → 7 +macro! apply_op { + ( $op:tt, $a:expr, $b:expr ) => { $op($a, $b) } +} + +// Juxta binary split: flip_op!(10 - 3) → 3 - 10 +macro! flip_op { + ( $a:expr, $op:tt, $b:expr ) => { $op($b, $a) } +} + +func Main() -> int { + let sum: int = apply_op!(+, 3, 4); + Test_AssertEqInt(sum, 7); + + let prod: int = apply_op!(*, 6, 7); + Test_AssertEqInt(prod, 42); + + let diff: int = flip_op!(10 - 3); + Test_AssertEqInt(diff, -7); + + let eq: bool = apply_op!(==, 5, 5); + Test_AssertEqBool(eq, true); + + let lt: bool = apply_op!(<, 2, 9); + Test_AssertEqBool(lt, true); + + PrintInt(sum); + PrintLine(""); + PrintInt(prod); + PrintLine(""); + PrintInt(diff); + PrintLine(""); + PrintLine("PASS macro_op_paste"); + Test_Pass("macro_op_paste"); + return 0; +} diff --git a/examples/macro_type_generic.bux b/examples/macro_type_generic.bux new file mode 100644 index 0000000..e70d272 --- /dev/null +++ b/examples/macro_type_generic.bux @@ -0,0 +1,44 @@ +// Generics in `$t:type` + type-arg splice into Array_New<$t> +import Std::Io::{PrintLine, PrintInt}; +import Std::Array::{Array, Array_New, Array_Push, Array_Get, Array_Len, Array_Free}; +import Std::Test::{Test_AssertEqInt, Test_Pass}; + +macro! size_of { + ( $t:type ) => { sizeof($t) as int } +} + +macro! new_array { + ( $t:type, $cap:expr ) => { + Array_New<$t>($cap) + } +} + +func Main() -> int { + // Generic type fragment: Array + let sz: int = size_of!(Array); + PrintInt(sz); + PrintLine(""); + if sz < 8 { + PrintLine("FAIL size_of Array"); + return 1; + } + + // $t spliced into monomorphized call Array_New<$t> + var a: Array = new_array!(int, 4); + Array_Push(&a, 10); + Array_Push(&a, 20); + Test_AssertEqInt(Array_Get(&a, 0), 10); + Test_AssertEqInt(Array_Get(&a, 1), 20); + Test_AssertEqInt(Array_Len(&a) as int, 2); + Array_Free(&a); + + let psz: int = size_of!(*int); + if psz != 4 && psz != 8 { + PrintLine("FAIL size_of *int"); + return 1; + } + + PrintLine("PASS macro_type_generic"); + Test_Pass("macro_type_generic"); + return 0; +} diff --git a/examples/try_generic.bux b/examples/try_generic.bux new file mode 100644 index 0000000..f66d29c --- /dev/null +++ b/examples/try_generic.bux @@ -0,0 +1,32 @@ +// Try operator `?` with generic Result (non-int Ok payload). +import Std::Io::{PrintLine}; +import Std::Result::{Result, Result_NewOk, Result_NewErr}; +import Std::String::{String_Eq}; + +func GetGreeting(name: String) -> Result { + if String_Eq(name, "") { + return Result_NewErr("empty name"); + } + return Result_NewOk("hello"); +} + +func Greet(name: String) -> Result { + let g: String = GetGreeting(name)?; + return Result_NewOk(g); +} + +func Main() -> int { + PrintLine("Try generic Result demo:"); + + let ok: Result = Greet("bux"); + if ok.tag == Result_Ok { + PrintLine(ok.data.Ok_0); + } + + let err: Result = Greet(""); + if err.tag == Result_Err { + PrintLine(err.data.Err_0); + } + + return 0; +} diff --git a/rt/runtime_freestanding.c b/rt/runtime_freestanding.c new file mode 100644 index 0000000..bed348a --- /dev/null +++ b/rt/runtime_freestanding.c @@ -0,0 +1,484 @@ +/* Bux Runtime — freestanding / bare-metal research spike (post-v1.0) + * + * Goal: link with `-ffreestanding -nostdlib` without pulling in a hosted libc. + * This is **not** a full embedded platform kit (no Cortex-M startup, no + * linker scripts, no UART drivers). It is a portable base for: + * - compile smoke under -ffreestanding + * - no-libc static link experiments (custom `_start`) + * - future board BSPs that replace the weak I/O hooks + * + * Select: BUX_RUNTIME=freestanding (bootstrap + selfhost) + * + * Memory: fixed bump allocator over a static arena (default 256 KiB). + * Override at compile time: -DBUX_FS_HEAP_BYTES=N + * + * I/O: weak stubs — default print is a no-op; panic spins. Host may override: + * void bux_fs_write(const char* s, unsigned n); + * void bux_fs_halt(int code); + */ + +#if defined(__STDC_HOSTED__) && __STDC_HOSTED__ == 1 && !defined(BUX_FS_FORCE) +/* When accidentally compiled as hosted without -ffreestanding, still avoid + * libc — we implement everything we need. */ +#endif + +/* ── Fixed-width types without stdint.h ────────────────────────────────── */ +typedef signed char bux_i8; +typedef unsigned char bux_u8; +typedef short bux_i16; +typedef unsigned short bux_u16; +typedef int bux_i32; +typedef unsigned int bux_u32; +#if defined(__LP64__) || defined(_WIN64) || defined(__x86_64__) || defined(__aarch64__) +typedef long bux_i64; +typedef unsigned long bux_u64; +typedef unsigned long bux_size; +#else +typedef long long bux_i64; +typedef unsigned long long bux_u64; +typedef unsigned int bux_size; +#endif +typedef bux_u8 bux_bool; +#ifndef NULL +# define NULL ((void*)0) +#endif +#ifndef true +# define true 1 +# define false 0 +#endif + +/* ── Heap ─────────────────────────────────────────────────────────────── */ +#ifndef BUX_FS_HEAP_BYTES +# define BUX_FS_HEAP_BYTES (256u * 1024u) +#endif + +static unsigned char bux_fs_heap[BUX_FS_HEAP_BYTES]; +static bux_size bux_fs_heap_off = 0; + +/* Weak hooks — board / host may override */ +__attribute__((weak)) void bux_fs_write(const char* s, unsigned n) { + (void)s; (void)n; +} +__attribute__((weak)) void bux_fs_halt(int code) { + (void)code; + for (;;) { /* spin */ } +} + +/* ── CLI args (empty under freestanding) ──────────────────────────────── */ +int g_argc = 0; +char** g_argv = NULL; + +int bux_argc(void) { return g_argc; } +char* bux_argv(int index) { + (void)index; + return (char*)""; +} + +/* ── Memory ───────────────────────────────────────────────────────────── */ +static void bux_fs_zero(void* p, bux_size n) { + unsigned char* b = (unsigned char*)p; + bux_size i; + for (i = 0; i < n; i++) b[i] = 0; +} + +void* bux_alloc(bux_size size) { + /* 8-byte align */ + bux_size off = (bux_fs_heap_off + 7u) & ~(bux_size)7u; + if (size == 0) size = 1; + if (off + size > (bux_size)BUX_FS_HEAP_BYTES) { + bux_fs_write("OOM\n", 4); + bux_fs_halt(1); + return NULL; + } + void* p = &bux_fs_heap[off]; + bux_fs_heap_off = off + size; + bux_fs_zero(p, size); + return p; +} + +void* bux_realloc(void* ptr, bux_size size) { + /* Bump allocator cannot free — allocate fresh + copy if needed */ + void* n = bux_alloc(size); + if (ptr != NULL && n != NULL && size > 0) { + unsigned char* d = (unsigned char*)n; + unsigned char* s = (unsigned char*)ptr; + bux_size i; + /* unknown old size — best effort copy of `size` bytes */ + for (i = 0; i < size; i++) d[i] = s[i]; + } + return n; +} + +void bux_free(void* ptr) { (void)ptr; /* bump: no-op */ } + +/* ── Basic I/O / panic ────────────────────────────────────────────────── */ +void bux_print(const char* s) { + if (!s) return; + unsigned n = 0; + while (s[n]) n++; + bux_fs_write(s, n); +} +void bux_println(const char* s) { + bux_print(s ? s : ""); + bux_fs_write("\n", 1); +} +void bux_print_int(bux_i64 n) { + char buf[32]; + int i = 0; + int neg = 0; + if (n < 0) { neg = 1; n = -n; } + if (n == 0) { buf[i++] = '0'; } + else { + while (n > 0 && i < 30) { + buf[i++] = (char)('0' + (n % 10)); + n /= 10; + } + } + if (neg) buf[i++] = '-'; + /* reverse */ + int a = 0, b = i - 1; + while (a < b) { + char t = buf[a]; buf[a] = buf[b]; buf[b] = t; + a++; b--; + } + bux_fs_write(buf, (unsigned)i); +} +void bux_print_float(double f) { + /* minimal: cast to int for freestanding */ + bux_print_int((bux_i64)f); +} +void bux_print_bool(bux_bool b) { bux_print(b ? "true" : "false"); } +void bux_print_char(char c) { bux_fs_write(&c, 1); } +void bux_panic(const char* msg) { + bux_fs_write("PANIC: ", 7); + if (msg) { + unsigned n = 0; + while (msg[n]) n++; + bux_fs_write(msg, n); + } + bux_fs_write("\n", 1); + bux_fs_halt(1); +} +void bux_exit(int code) { bux_fs_halt(code); } +void bux_assert(int cond, const char* file, int line, const char* expr) { + (void)file; (void)line; (void)expr; + if (!cond) bux_panic("assert failed"); +} + +/* ── Checked arithmetic ───────────────────────────────────────────────── */ +bux_i64 bux_div_i64(bux_i64 a, bux_i64 b) { + if (b == 0) bux_panic("division by zero"); + return a / b; +} +bux_i64 bux_mod_i64(bux_i64 a, bux_i64 b) { + if (b == 0) bux_panic("modulo by zero"); + return a % b; +} +bux_i64 bux_add_i64_checked(bux_i64 a, bux_i64 b) { return a + b; } +bux_i64 bux_sub_i64_checked(bux_i64 a, bux_i64 b) { return a - b; } +bux_i64 bux_mul_i64_checked(bux_i64 a, bux_i64 b) { return a * b; } +bux_i64 bux_neg_i64_checked(bux_i64 a) { return -a; } + +/* ── Strings (no libc) ────────────────────────────────────────────────── */ +unsigned int bux_strlen(const char* s) { + unsigned int n = 0; + if (!s) return 0; + while (s[n]) n++; + return n; +} +int bux_strlen_c(const char* s) { return (int)bux_strlen(s); } +int bux_strcmp(const char* a, const char* b) { + if (!a) a = ""; + if (!b) b = ""; + while (*a && *a == *b) { a++; b++; } + return (unsigned char)*a - (unsigned char)*b; +} +int bux_strncmp(const char* a, const char* b, unsigned int n) { + if (!a) a = ""; + if (!b) b = ""; + unsigned int i; + for (i = 0; i < n; i++) { + if (a[i] != b[i] || a[i] == 0) return (unsigned char)a[i] - (unsigned char)b[i]; + } + return 0; +} +char* bux_strcpy(char* dest, const char* src) { + if (!dest) return NULL; + if (!src) { dest[0] = 0; return dest; } + char* d = dest; + while ((*d++ = *src++)) {} + return dest; +} +char* bux_strcat(char* dest, const char* src) { + if (!dest) return NULL; + if (!src) return dest; + char* d = dest; + while (*d) d++; + while ((*d++ = *src++)) {} + return dest; +} +char* bux_strncpy(char* dest, const char* src, unsigned int n) { + if (!dest) return NULL; + unsigned int i = 0; + if (src) { + for (; i < n && src[i]; i++) dest[i] = src[i]; + } + for (; i < n; i++) dest[i] = 0; + return dest; +} +double bux_str_to_float(const char* s) { + (void)s; + return 0.0; +} +bux_i64 bux_str_to_int(const char* s) { + if (!s) return 0; + bux_i64 v = 0; + int neg = 0; + if (*s == '-') { neg = 1; s++; } + while (*s >= '0' && *s <= '9') { + v = v * 10 + (*s - '0'); + s++; + } + return neg ? -v : v; +} +const char* bux_strstr(const char* haystack, const char* needle) { + if (!haystack || !needle) return NULL; + if (!*needle) return haystack; + for (const char* h = haystack; *h; h++) { + const char* a = h; + const char* b = needle; + while (*a && *b && *a == *b) { a++; b++; } + if (!*b) return h; + } + return NULL; +} +unsigned int bux_str_offset(const char* pos, const char* base) { + if (!pos || !base) return 0; + return (unsigned int)(pos - base); +} +int bux_str_contains(const char* haystack, const char* needle) { + return bux_strstr(haystack, needle) != NULL; +} +int bux_str_is_null(const char* s) { return s == NULL; } + +char* bux_str_slice(const char* s, unsigned int start, unsigned int len) { + if (!s) s = ""; + unsigned int sl = bux_strlen(s); + if (start > sl) start = sl; + if (start + len > sl) len = sl - start; + char* out = (char*)bux_alloc(len + 1); + unsigned int i; + for (i = 0; i < len; i++) out[i] = s[start + i]; + out[len] = 0; + return out; +} +static int is_ws(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} +char* bux_str_trim_left(const char* s) { + if (!s) s = ""; + while (*s && is_ws(*s)) s++; + return bux_str_slice(s, 0, bux_strlen(s)); +} +char* bux_str_trim_right(const char* s) { + if (!s) s = ""; + unsigned int n = bux_strlen(s); + while (n > 0 && is_ws(s[n - 1])) n--; + return bux_str_slice(s, 0, n); +} +char* bux_str_trim(const char* s) { + char* a = bux_str_trim_left(s); + return bux_str_trim_right(a); +} +char* bux_int_to_str(bux_i64 n) { + char buf[32]; + int i = 0; + int neg = 0; + if (n < 0) { neg = 1; n = -n; } + if (n == 0) buf[i++] = '0'; + else while (n > 0 && i < 30) { buf[i++] = (char)('0' + (n % 10)); n /= 10; } + if (neg) buf[i++] = '-'; + int a = 0, b = i - 1; + while (a < b) { char t = buf[a]; buf[a] = buf[b]; buf[b] = t; a++; b--; } + buf[i] = 0; + return bux_str_slice(buf, 0, (unsigned)i); +} +char* bux_float_to_string(double f) { return bux_int_to_str((bux_i64)f); } + +unsigned int bux_str_split_count(const char* s, const char* delim) { + (void)s; (void)delim; + return 0; +} +char* bux_str_split_part(const char* s, const char* delim, unsigned int index) { + (void)s; (void)delim; (void)index; + return (char*)""; +} +char* bux_str_join2(const char* a, const char* b, const char* sep) { + unsigned int na = bux_strlen(a), nb = bux_strlen(b), ns = bux_strlen(sep); + char* out = (char*)bux_alloc(na + ns + nb + 1); + unsigned int i = 0, j; + for (j = 0; j < na; j++) out[i++] = a[j]; + for (j = 0; j < ns; j++) out[i++] = sep[j]; + for (j = 0; j < nb; j++) out[i++] = b[j]; + out[i] = 0; + return out; +} +char* bux_str_format(const char* fmt, const char* a0, const char* a1, const char* a2, const char* a3) { + (void)a1; (void)a2; (void)a3; + /* minimal: return fmt or a0 */ + if (a0 && a0[0]) return bux_str_slice(a0, 0, bux_strlen(a0)); + if (fmt) return bux_str_slice(fmt, 0, bux_strlen(fmt)); + return (char*)""; +} +char* bux_escape_c_string(const char* s, int len) { + (void)len; + return s ? bux_str_slice(s, 0, bux_strlen(s)) : (char*)""; +} + +/* StringBuilder stubs (shape-compatible enough for unused mono) */ +typedef struct { + char* data; + unsigned int len; + unsigned int cap; +} BuxStringBuilder; + +void bux_sb_append(BuxStringBuilder* sb, const char* s) { + (void)sb; (void)s; +} +void bux_sb_append_int(BuxStringBuilder* sb, bux_i64 n) { (void)sb; (void)n; } +void bux_sb_append_float(BuxStringBuilder* sb, double f) { (void)sb; (void)f; } +void bux_sb_append_char(BuxStringBuilder* sb, char c) { (void)sb; (void)c; } +const char* bux_sb_build(BuxStringBuilder* sb) { return sb && sb->data ? sb->data : ""; } +void bux_sb_free(BuxStringBuilder* sb) { (void)sb; } + +/* ── FS / path / OS — unavailable ─────────────────────────────────────── */ +char* bux_read_file(const char* path) { (void)path; return NULL; } +int bux_write_file(const char* path, const char* content) { + (void)path; (void)content; + return -1; +} +int bux_file_exists(const char* path) { (void)path; return 0; } +char* bux_path_join(const char* a, const char* b) { + return bux_str_join2(a ? a : "", b ? b : "", "/"); +} +char* bux_path_parent(const char* path) { + (void)path; + return (char*)"."; +} +char* bux_path_ext(const char* path) { + (void)path; + return (char*)""; +} +int bux_mkdir_if_needed(const char* path) { (void)path; return -1; } +int bux_dir_exists(const char* path) { (void)path; return 0; } +char** bux_list_dir(const char* dir, const char* ext, int* out_count) { + (void)dir; (void)ext; + if (out_count) *out_count = 0; + return NULL; +} + +/* ── Math (integer only; float ops soft-stub) ─────────────────────────── */ +double bux_sqrt(double x) { return x; } +double bux_pow(double x, double y) { (void)y; return x; } +bux_i64 bux_abs_i64(bux_i64 x) { return x < 0 ? -x : x; } +double bux_abs_f64(double x) { return x < 0 ? -x : x; } +bux_i64 bux_min_i64(bux_i64 a, bux_i64 b) { return a < b ? a : b; } +bux_i64 bux_max_i64(bux_i64 a, bux_i64 b) { return a > b ? a : b; } +double bux_min_f64(double a, double b) { return a < b ? a : b; } +double bux_max_f64(double a, double b) { return a > b ? a : b; } + +unsigned int bux_hash_bytes(const void* ptr, bux_size size) { + const unsigned char* p = (const unsigned char*)ptr; + unsigned int h = 2166136261u; + bux_size i; + if (!p) return 0; + for (i = 0; i < size; i++) { + h ^= p[i]; + h *= 16777619u; + } + return h; +} +int bux_mem_eq(const void* a, const void* b, bux_size size) { + const unsigned char* x = (const unsigned char*)a; + const unsigned char* y = (const unsigned char*)b; + bux_size i; + if (!x || !y) return x == y; + for (i = 0; i < size; i++) if (x[i] != y[i]) return 0; + return 1; +} +unsigned int bux_hash_string(const char* s) { + return bux_hash_bytes(s, bux_strlen(s)); +} + +const char* bux_getenv(const char* name) { (void)name; return ""; } +const char* bux_cc_ld_stable(void) { return ""; } +int bux_setenv(const char* name, const char* value) { + (void)name; (void)value; + return -1; +} +const char* bux_getcwd(void) { return "."; } +int bux_chdir(const char* path) { (void)path; return -1; } + +void bux_install_stop_handlers(void) {} +int bux_should_stop(void) { return 0; } +void bux_set_stop_listen_fd(int fd) { (void)fd; } + +bux_i64 bux_time_ms(void) { return 0; } +bux_i64 bux_time_us(void) { return 0; } + +/* ── Task / net / crypto — hard stubs ─────────────────────────────────── */ +int bux_task_spawn(void* fn, void* arg) { (void)fn; (void)arg; return -1; } +void bux_task_yield(void) {} +void bux_task_sleep_ms(int ms) { (void)ms; } + +int bux_chan_new(int cap) { (void)cap; return -1; } +int bux_chan_send(int id, void* msg) { (void)id; (void)msg; return -1; } +void* bux_chan_recv(int id) { (void)id; return NULL; } +void bux_chan_close(int id) { (void)id; } + +int bux_tcp_listen(int port) { (void)port; return -1; } +int bux_tcp_accept(int fd) { (void)fd; return -1; } +int bux_tcp_connect(const char* host, int port) { + (void)host; (void)port; + return -1; +} +int bux_tcp_send(int fd, const void* buf, int n) { + (void)fd; (void)buf; (void)n; + return -1; +} +int bux_tcp_recv(int fd, void* buf, int n) { + (void)fd; (void)buf; (void)n; + return -1; +} +void bux_tcp_close(int fd) { (void)fd; } + +void* bux_tls_server_ctx(const char* cert, const char* key) { + (void)cert; (void)key; + return NULL; +} +void* bux_tls_server_ctx_ex(const char* cert, const char* key, const char* ca) { + (void)cert; (void)key; (void)ca; + return NULL; +} +int bux_tls_accept(void* ctx, int fd) { (void)ctx; (void)fd; return -1; } +int bux_tls_send(int h, const void* buf, int n) { + (void)h; (void)buf; (void)n; + return -1; +} +int bux_tls_recv(int h, void* buf, int n) { + (void)h; (void)buf; (void)n; + return -1; +} +void bux_tls_close(int h) { (void)h; } +const char* bux_tls_error(void) { return "tls unavailable (freestanding)"; } + +/* ── Optional bare `_start` for -nostdlib link experiments ───────────── */ +#ifdef BUX_FS_PROVIDE_START +extern int main(int argc, char** argv); +void _start(void) { + g_argc = 0; + g_argv = NULL; + int code = main(0, NULL); + bux_fs_halt(code); +} +#endif diff --git a/src/cli.bux b/src/cli.bux index 8345a6e..23abff2 100644 --- a/src/cli.bux +++ b/src/cli.bux @@ -59,10 +59,13 @@ module Cli { return "rt/runtime_win.c"; } if String_Eq(env, "minimal") || String_Eq(env, "thin") || - String_Eq(env, "embed") || String_Eq(env, "embedded") || - String_Eq(env, "freestanding") { + String_Eq(env, "embed") || String_Eq(env, "embedded") { return "rt/runtime_minimal.c"; } + if String_Eq(env, "freestanding") || String_Eq(env, "bare") || + String_Eq(env, "nolibc") { + return "rt/runtime_freestanding.c"; + } if String_Eq(env, "full") || String_Eq(env, "posix") { return "rt/runtime.c"; } @@ -78,6 +81,7 @@ module Cli { func Cli_IsThinRuntimePath(rtPath: String) -> bool { if String_Contains(rtPath, "runtime_minimal.c") { return true; } + if String_Contains(rtPath, "runtime_freestanding.c") { return true; } if String_Contains(rtPath, "runtime_win.c") { return true; } return false; } diff --git a/src/macroexpand.bux b/src/macroexpand.bux index 2d13048..320686e 100644 --- a/src/macroexpand.bux +++ b/src/macroexpand.bux @@ -470,16 +470,30 @@ module MacroExpand { (aexp.kind == ekSlice && aexp.callArgCount > 0); return wrap; } - // type — session 87: named / pointer type from call-site expr shape + // type — session 87+: named / pointer / generic Array from call-site if String_Eq(kindStr, "type") { if aexp.kind == ekMacroType { return aexp; } var te: *TypeExpr = null as *TypeExpr; - if aexp.kind == ekIdent { + if aexp.kind == ekIdent || aexp.kind == ekGenericCall { te = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; te.kind = tekNamed; te.line = aexp.line; te.column = aexp.column; - te.typeName = aexp.strValue; + if aexp.kind == ekGenericCall && !String_Eq(aexp.genericCallee, "") { + te.typeName = aexp.genericCallee; + } else { + te.typeName = aexp.strValue; + } + // Generic type args: Array / Map (selfhost: up to 2 string args) + var gcount: int = aexp.genericTypeArgCount; + if gcount > 0 { + te.typeArgName0 = aexp.genericTypeArg0; + te.typeArgCount = 1; + if gcount > 1 { + te.typeArgName1 = aexp.genericTypeArg1; + te.typeArgCount = 2; + } + } } 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); @@ -518,6 +532,35 @@ module MacroExpand { return bound.refType; } } + // Array<$t> / Map<$k,$v> — substitute type-arg name slots + if te.kind == tekNamed && te.typeArgCount > 0 { + if String_StartsWith(te.typeArgName0, "$") { + let b0: *Expr = Env_Lookup(env, te.typeArgName0); + if b0 != null as *Expr && b0.kind == ekMacroType && b0.refType != null as *TypeExpr { + // Flatten simple named type arg (int, String, …) + if b0.refType.kind == tekNamed && b0.refType.typeArgCount == 0 { + te.typeArgName0 = b0.refType.typeName; + } else if b0.refType.kind == tekNamed { + // Nested generic: store mangled-ish "Array_int" for mono + te.typeArgName0 = b0.refType.typeName; + if b0.refType.typeArgCount > 0 { + te.typeArgName0 = String_Concat(te.typeArgName0, "_"); + te.typeArgName0 = String_Concat(te.typeArgName0, b0.refType.typeArgName0); + } + } else if b0.refType.kind == tekPointer && b0.refType.pointerPointee != null as *TypeExpr { + te.typeArgName0 = String_Concat(b0.refType.pointerPointee.typeName, "p"); + } + } + } + if te.typeArgCount > 1 && String_StartsWith(te.typeArgName1, "$") { + let b1: *Expr = Env_Lookup(env, te.typeArgName1); + if b1 != null as *Expr && b1.kind == ekMacroType && b1.refType != null as *TypeExpr { + if b1.refType.kind == tekNamed { + te.typeArgName1 = b1.refType.typeName; + } + } + } + } if te.pointerPointee != null as *TypeExpr { te.pointerPointee = Macro_SubstType(te.pointerPointee, env); if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr { @@ -533,6 +576,27 @@ module MacroExpand { return te; } + // Substitute `$t` in Expr generic type-arg slots (Array_New<$t>) + func Macro_SubstGenericTypeArgs(e: *Expr, env: *MacroEnv) { + if e == null as *Expr { return; } + if e.genericTypeArgCount > 0 && String_StartsWith(e.genericTypeArg0, "$") { + let b0: *Expr = Env_Lookup(env, e.genericTypeArg0); + if b0 != null as *Expr && b0.kind == ekMacroType && b0.refType != null as *TypeExpr { + if b0.refType.kind == tekNamed { + e.genericTypeArg0 = b0.refType.typeName; + } + } + } + if e.genericTypeArgCount > 1 && String_StartsWith(e.genericTypeArg1, "$") { + let b1: *Expr = Env_Lookup(env, e.genericTypeArg1); + if b1 != null as *Expr && b1.kind == ekMacroType && b1.refType != null as *TypeExpr { + if b1.refType.kind == tekNamed { + e.genericTypeArg1 = b1.refType.typeName; + } + } + } + } + // 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; @@ -961,6 +1025,8 @@ 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); + // Array_New<$t> / Foo<$t,$u> + Macro_SubstGenericTypeArgs(c, env); // sizeof / cast / is type annotations if c.refType != null as *TypeExpr { c.refType = Macro_SubstType(c.refType, env); diff --git a/tools/lsp_server.nim b/tools/lsp_server.nim index ea32992..dfcc5f7 100644 --- a/tools/lsp_server.nim +++ b/tools/lsp_server.nim @@ -21,9 +21,11 @@ # docs even when `extend T for I` has no methods (no open required). # v0.17.0: in-process diagnostics (lex/parse/sema) on open/change/save so the # editor underlines errors in the live buffer without needing buxc. +# v0.18.0: textDocument/formatting + rangeFormatting via bootstrap `formatSource` +# (same rules as `bux fmt` — 4-space brace indent). import std/[json, os, strutils, streams, tables, osproc, sequtils, sets] -import lexer, parser, ast, sema, types, scope, source_location +import lexer, parser, ast, sema, types, scope, source_location, fmt # --------------------------------------------------------------------------- # JSON-RPC Transport @@ -2514,6 +2516,64 @@ proc handleDocumentSymbol(stream: FileStream, id: JsonNode, paramsNode: JsonNode }) sendResponse(stream, id, arr) +# --------------------------------------------------------------------------- +# Document formatting (v0.18 — same engine as `bux fmt`) +# --------------------------------------------------------------------------- + +proc lineCountAndLastLen(s: string): tuple[lines: int, lastLen: int] = + ## 0-based end position after last character (for full-document TextEdit). + if s.len == 0: + return (0, 0) + var lines = 0 + var lastLen = 0 + var i = 0 + while i < s.len: + if s[i] == '\n': + inc lines + lastLen = 0 + else: + inc lastLen + inc i + # Trailing content without final newline still occupies a line + if s[^1] != '\n': + # last line is incomplete — end character is lastLen + discard + else: + # ends with newline: end is (lines, 0) in LSP (exclusive end after last line) + discard + result = (lines, lastLen) + +proc fullDocumentEdit(uri: string, content: string, formatted: string): JsonNode = + ## Single TextEdit replacing the whole buffer with formatted text. + if formatted == content: + return newJArray() + let (endLine, endChar) = lineCountAndLastLen(content) + var arr = newJArray() + arr.add(%*{ + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": endLine, "character": endChar} + }, + "newText": formatted + }) + discard uri + return arr + +proc handleDocumentFormatting(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = + ## textDocument/formatting — re-indent with 4 spaces (idempotent). + let uri = paramsNode["textDocument"]["uri"].getStr() + let doc = getDoc(uri) + let formatted = formatSource(doc.content) + sendResponse(stream, id, fullDocumentEdit(uri, doc.content, formatted)) + +proc handleDocumentRangeFormatting(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = + ## textDocument/rangeFormatting — whole-file format (indent is brace-global). + ## Editors that send a selection still get a consistent full reformat. + let uri = paramsNode["textDocument"]["uri"].getStr() + let doc = getDoc(uri) + let formatted = formatSource(doc.content) + sendResponse(stream, id, fullDocumentEdit(uri, doc.content, formatted)) + proc handleWorkspaceSymbol(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = ## workspace/symbol — fuzzy-ish substring filter over workspace + open docs. let query = if paramsNode.hasKey("query"): paramsNode["query"].getStr().toLowerAscii() else: "" @@ -3370,9 +3430,11 @@ proc handleMessage(stream: FileStream, msg: JsonNode) = "workspaceSymbolProvider": true, "callHierarchyProvider": true, "implementationProvider": true, - "typeHierarchyProvider": true + "typeHierarchyProvider": true, + "documentFormattingProvider": true, + "documentRangeFormattingProvider": true }, - "serverInfo": {"name": "bux-lsp", "version": "0.17.0"} + "serverInfo": {"name": "bux-lsp", "version": "0.18.0"} }) if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull: rootPath = paramsNode["rootPath"].getStr() @@ -3470,6 +3532,12 @@ proc handleMessage(stream: FileStream, msg: JsonNode) = of "typeHierarchy/subtypes": handleTypeHierarchySubtypes(stream, id, paramsNode) + of "textDocument/formatting": + handleDocumentFormatting(stream, id, paramsNode) + + of "textDocument/rangeFormatting": + handleDocumentRangeFormatting(stream, id, paramsNode) + else: if id != nil: sendError(stream, id, -32601, "method not found: " & methodName) diff --git a/tools/smoke_freestanding.sh b/tools/smoke_freestanding.sh new file mode 100755 index 0000000..5d9171d --- /dev/null +++ b/tools/smoke_freestanding.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Smoke: freestanding runtime compiles under -ffreestanding; optional package build. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +RT="$ROOT/rt/runtime_freestanding.c" +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +if [[ ! -f "$RT" ]]; then + echo "FAIL: missing $RT" + exit 1 +fi + +CC="${BUX_CC:-cc}" + +echo "=== freestanding: -ffreestanding -c runtime ===" +"$CC" -ffreestanding -std=c11 -Wall -Wextra -c "$RT" -o "$TMP/rt_fs.o" +echo "PASS: runtime_freestanding.o" + +echo "=== freestanding: BUX_RUNTIME=freestanding build hello-ish ===" +mkdir -p "$TMP/pkg/src" +cat > "$TMP/pkg/bux.toml" <<'EOF' +[Package] +Name = "fs_smoke" +Version = "0.1.0" +EOF +cat > "$TMP/pkg/src/Main.bux" <<'EOF' +func Main() -> int { + return 42; +} +EOF + +if [[ ! -x "$ROOT/buxc" ]]; then + echo "building buxc..." + (cd "$ROOT" && make build >/dev/null) +fi + +# Hosted link still uses libc for crt0; runtime body is freestanding. +BUX_RUNTIME=freestanding "$ROOT/buxc" build "$TMP/pkg" --release >/dev/null +OUT="$TMP/pkg/build/fs_smoke" +if [[ ! -x "$OUT" ]]; then + echo "FAIL: binary not produced" + exit 1 +fi +CODE=$("$OUT"; echo $?) +if [[ "$CODE" != "42" ]]; then + echo "FAIL: expected exit 42, got $CODE" + exit 1 +fi +echo "PASS: freestanding runtime package exit 42" + +# Optional: object-level nostdlib link experiment (may need extra crt — soft) +echo "=== freestanding: optional -ffreestanding object of Main.c ===" +# Generate C then compile Main only with freestanding flags +BUX_RUNTIME=freestanding "$ROOT/buxc" build "$TMP/pkg" --release >/dev/null +if [[ -f "$TMP/pkg/build/main.c" ]]; then + if "$CC" -ffreestanding -std=c11 -c "$TMP/pkg/build/main.c" -o "$TMP/main_fs.o" 2>"$TMP/main_fs.err"; then + echo "PASS: main.c compiles under -ffreestanding" + else + # Hosted headers in generated C may pull stdint — not a hard fail + echo "SKIP: main.c -ffreestanding (generated C may need hosted headers)" + head -5 "$TMP/main_fs.err" || true + fi +fi + +echo "PASS: freestanding smoke" diff --git a/tools/smoke_lsp_formatting.sh b/tools/smoke_lsp_formatting.sh new file mode 100755 index 0000000..d846937 --- /dev/null +++ b/tools/smoke_lsp_formatting.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Smoke: textDocument/formatting re-indents with 4 spaces (same as bux fmt). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +LSP="$ROOT/tools/bux-lsp" +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +if [[ ! -x "$LSP" ]]; then + echo "building bux-lsp..." + (cd "$ROOT" && make lsp >/dev/null) +fi + +# Intentionally bad indent (2 spaces) +cat > "$TMP/Main.bux" <<'EOF' +func Main() -> int { + let x: int = 1; + if x > 0 { + return 0; + } + return 1; +} +EOF + +rpc() { + local body="$1" + local len + len=$(printf '%s' "$body" | wc -c) + printf 'Content-Length: %s\r\n\r\n%s' "$len" "$body" +} + +CONTENT_JSON=$(python3 -c 'import json,sys; print(json.dumps(open(sys.argv[1]).read()))' "$TMP/Main.bux") +URI="file://$TMP/Main.bux" + +{ + rpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file://'"$TMP"'"}}' + rpc '{"jsonrpc":"2.0","method":"initialized","params":{}}' + rpc '{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"'"$URI"'","languageId":"bux","version":1,"text":'"$CONTENT_JSON"'}}}' + rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/formatting","params":{"textDocument":{"uri":"'"$URI"'"},"options":{"tabSize":4,"insertSpaces":true}}}' + rpc '{"jsonrpc":"2.0","id":3,"method":"shutdown","params":null}' + rpc '{"jsonrpc":"2.0","method":"exit","params":null}' +} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt" + +python3 - <<'PY' "$TMP/out.txt" +import json, sys, re +raw = open(sys.argv[1]).read() + +# Find response id=2 with result TextEdit array +# Prefer structured parse of JSON objects containing "newText" +ok = False +version_ok = "0.18.0" in raw or '"documentFormattingProvider":true' in raw.replace(" ", "") +if "0.18.0" not in raw and "documentFormattingProvider" not in raw: + # initialize result may be nested; still require a formatting response + pass + +# Extract TextEdit newText via regex / brace walk +edits = [] +for m in re.finditer(r'"newText"\s*:\s*"((?:[^"\\]|\\.)*)"', raw): + edits.append(bytes(m.group(1), "utf-8").decode("unicode_escape")) + +if not edits: + print("FAIL: no TextEdit newText in formatting response") + print(raw[:3000]) + sys.exit(1) + +formatted = edits[0] +# Expected: 4-space indent after func { +if " let x: int = 1;" not in formatted: + print("FAIL: expected 4-space indent on let") + print(repr(formatted)) + sys.exit(1) +if " if x > 0 {" not in formatted and " if x > 0 {" not in formatted: + # after let at indent 1, if should be at indent 1 (same block) = 4 spaces + print("FAIL: unexpected if indent") + print(repr(formatted)) + sys.exit(1) +# Nested body of if at 8 spaces +if " return 0;" not in formatted: + print("FAIL: expected 8-space indent on return inside if") + print(repr(formatted)) + sys.exit(1) + +print("PASS: LSP formatting smoke (4-space brace indent)") +if "0.18.0" in raw: + print("PASS: serverInfo version 0.18.0") +PY diff --git a/vscode/README.md b/vscode/README.md index aeb3b46..9a24dc9 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -8,8 +8,8 @@ Syntax highlighting, snippets, editor defaults, and **Language Server Protocol** |------|----------------| | **Syntax** | Keywords, types, `f"..."` interpolation, raw `` `...` `` strings, C-strings, macros (`macro!` / `name!()`), attributes (`@[Checked]`), numbers (hex/bin/oct + suffixes), lifetimes | | **Snippets** | `main`, `func`, `struct`, `enum`, `match`, `interface`, `extend`, `macro`, `checked`, … | -| **LSP** | **Live error underlines** (red squiggles on edit), completion, hover, go-to-definition, references, rename, document/workspace symbols, call hierarchy, type hierarchy, go-to-implementation | -| **Editor** | Bracket colorization, smart indent / on-enter, fold regions (`// region`) | +| **LSP** | **Live error underlines** (red squiggles on edit), **Format Document** (same as `bux fmt`), completion, hover, go-to-definition, references, rename, document/workspace symbols, call hierarchy, type hierarchy, go-to-implementation | +| **Editor** | Bracket colorization, smart indent / on-enter, fold regions (`// region`), **format-on-save** default for `[bux]` | | **Build** | `buxc` problem matcher for Tasks | ## Requirements diff --git a/vscode/package.json b/vscode/package.json index dd4afe1..4564695 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -1,8 +1,8 @@ { "name": "bux-lang", "displayName": "Bux Language Support", - "description": "Syntax highlighting, snippets, and full LSP support for Bux — live error underlines, hover, go-to-def, rename, call/type hierarchy", - "version": "0.2.1", + "description": "Syntax highlighting, snippets, and full LSP support for Bux — live error underlines, Format Document, hover, go-to-def, rename, call/type hierarchy", + "version": "0.2.2", "publisher": "bux-lang", "license": "MIT", "icon": "icon.png", @@ -25,6 +25,7 @@ "Programming Languages", "Snippets", "Linters", + "Formatters", "Other" ], "keywords": [ @@ -32,7 +33,8 @@ "bux-lang", "programming-language", "lsp", - "syntax" + "syntax", + "formatter" ], "activationEvents": [ "onLanguage:bux", @@ -107,7 +109,7 @@ "bux.lsp.enabled": { "type": "boolean", "default": true, - "description": "Enable the Bux language server (diagnostics, hover, go-to-definition, rename, hierarchy)." + "description": "Enable the Bux language server (diagnostics, formatting, hover, go-to-definition, rename, hierarchy)." }, "bux.lsp.path": { "type": "string", @@ -133,6 +135,7 @@ "editor.tabSize": 4, "editor.insertSpaces": true, "editor.detectIndentation": false, + "editor.formatOnSave": true, "editor.quickSuggestions": { "other": true, "comments": false,