feat: try/unwrap payload types, LSP format, macro paste, freestanding runtime
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled

- Type `?`/`!` as Result/Option Ok payload (not always int); fix unwrap C types
- LSP 0.18 document formatting (bux fmt) + VS Code format-on-save
- Macro `:type` generics (Array_New<$t>) and operators-only tt paste
- Ship runtime_freestanding.c + BUX_RUNTIME=freestanding + smokes/examples
This commit is contained in:
2026-07-28 16:56:35 +03:00
parent db7ba1dff2
commit ec5984762b
22 changed files with 1332 additions and 68 deletions
+13 -3
View File
@@ -5,12 +5,12 @@ BUILD_DIR := build
# Project-local nimcache so CI can cache compiles (default is ~/.cache/nim). # Project-local nimcache so CI can cache compiles (default is ~/.cache/nim).
NIMFLAGS ?= --nimcache:nimcache NIMFLAGS ?= --nimcache:nimcache
EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked ownership_release drop_early_return lifetime_elision ctfe 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. # 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 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 all: build
@@ -37,7 +37,7 @@ debug: dev
@echo "Debug binary: buxc_debug" @echo "Debug binary: buxc_debug"
# Full local / sequential suite (same coverage as split CI jobs combined). # 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). # Nim unit tests + tiny CLI smoke (needs Nim + buxc).
test-unit: ensure-buxc test-unit: ensure-buxc
@@ -212,6 +212,9 @@ test-lsp: lsp
@echo "=== LSP diagnostics (error underlines) smoke ===" @echo "=== LSP diagnostics (error underlines) smoke ==="
@chmod +x tools/smoke_lsp_diagnostics.sh @chmod +x tools/smoke_lsp_diagnostics.sh
@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 ===" @echo "=== LSP references / rename smoke ==="
@chmod +x tools/smoke_lsp_rename.sh @chmod +x tools/smoke_lsp_rename.sh
@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 @chmod +x tools/smoke_linux_targets.sh
@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 # Session 78 — Nexus HTTPS (self-signed) smoke
.PHONY: test-nexus-tls .PHONY: test-nexus-tls
test-nexus-tls: ensure-buxc test-nexus-tls: ensure-buxc
+8 -3
View File
@@ -16,6 +16,7 @@ type
RuntimeFlavor* = enum RuntimeFlavor* = enum
rfFull ## rt/runtime.c — POSIX + OpenSSL rfFull ## rt/runtime.c — POSIX + OpenSSL
rfMinimal ## rt/runtime_minimal.c — thin, static/container/embed friendly 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) rfWin ## rt/runtime_win.c — Windows/MinGW (historical)
GlobalOptions* = object GlobalOptions* = object
@@ -63,7 +64,7 @@ Registry / toolchain env:
BUX_REGISTRY_INSECURE=1 Allow self-signed HTTPS registry (dev/smoke) BUX_REGISTRY_INSECURE=1 Allow self-signed HTTPS registry (dev/smoke)
BUX_CFLAGS Extra flags appended to the C compiler line BUX_CFLAGS Extra flags appended to the C compiler line
BUX_CC C compiler binary (overrides --target pick) 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 BUX_STATIC=1 Same as --static
Global options: Global options:
@@ -136,8 +137,10 @@ proc resolveRuntimeFlavor(opts: GlobalOptions): RuntimeFlavor =
case e case e
of "full", "posix": of "full", "posix":
return rfFull return rfFull
of "minimal", "thin", "embed", "embedded", "freestanding": of "minimal", "thin", "embed", "embedded":
return rfMinimal return rfMinimal
of "freestanding", "bare", "nolibc":
return rfFreestanding
of "win", "windows": of "win", "windows":
return rfWin return rfWin
of "": of "":
@@ -159,10 +162,12 @@ proc runtimeFileName(flavor: RuntimeFlavor): string =
case flavor case flavor
of rfFull: "runtime.c" of rfFull: "runtime.c"
of rfMinimal: "runtime_minimal.c" of rfMinimal: "runtime_minimal.c"
of rfFreestanding: "runtime_freestanding.c"
of rfWin: "runtime_win.c" of rfWin: "runtime_win.c"
proc isThinRuntime(flavor: RuntimeFlavor): bool = 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 = proc findOnPath(bin: string): bool =
## True if `bin` resolves as an executable on PATH (or is an absolute path). ## True if `bin` resolves as an executable on PATH (or is an absolute path).
+117 -15
View File
@@ -2101,6 +2101,10 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
of ekTry: of ekTry:
let operand = ctx.lowerExpr(expr.exprTryOperand) let operand = ctx.lowerExpr(expr.exprTryOperand)
var operandType = ctx.resolveExprType(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 typeName = ""
var errTag = "" var errTag = ""
@@ -2112,7 +2116,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
# Upgrade bare generic enum name to concrete monomorphization. # Upgrade bare generic enum name to concrete monomorphization.
# Sema stores Result/Option without mangled type-args; try needs Result_int_String_Tag. # 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 # Prefer resolving call/ident TypeExpr with type args
if expr.exprTryOperand != nil: if expr.exprTryOperand != nil:
if expr.exprTryOperand.kind == ekIdent and ctx.varTypeExprs.hasKey(expr.exprTryOperand.exprIdent): 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 & "_")): (resolved.name == typeName or resolved.name.startsWith(typeName & "_")):
typeName = resolved.name typeName = resolved.name
# Enclosing function return type (must match for `?` propagation) # Enclosing function return type (must match for `?` propagation)
let stillBare = operandType == nil or operandType.kind != tkNamed or var stillBare = typeName == "Result" or typeName == "Option"
typeName == operandType.name
if stillBare and ctx.currentFuncRetType != nil and if stillBare and ctx.currentFuncRetType != nil and
ctx.currentFuncRetType.kind == tkNamed: ctx.currentFuncRetType.kind == tkNamed:
let rn = ctx.currentFuncRetType.name let rn = ctx.currentFuncRetType.name
if rn.startsWith(typeName & "_"): if rn.startsWith(typeName & "_"):
typeName = rn 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 # Err tag / Ok field from base or concrete name
let baseForTags = let baseForTags =
@@ -2157,6 +2168,41 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
errTag = typeName & "_Err" errTag = typeName & "_Err"
okField = "Ok_0" okField = "Ok_0"
# Payload type (Ok_0 / Some_0) — must match T of Result<T,E>, 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 tmpName = ctx.freshTryVar()
let tmpAlloca = hirAlloca(tmpName, operandType, loc) let tmpAlloca = hirAlloca(tmpName, operandType, loc)
let tmpVar = hirVar(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, let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr,
typ: makeNamed(typeName & "_Data"), loc: loc) typ: makeNamed(typeName & "_Data"), loc: loc)
let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: okField, let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: okField,
typ: makePointer(makeInt()), loc: loc) typ: makePointer(okType), loc: loc)
let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: makeInt(), loc: loc) let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: okType, loc: loc)
ctx.pendingStmts.add(tmpAlloca) ctx.pendingStmts.add(tmpAlloca)
ctx.pendingStmts.add(tmpStore) ctx.pendingStmts.add(tmpStore)
@@ -2193,14 +2239,66 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
var errTag = "Result_Err" var errTag = "Result_Err"
var typeName = "Result" var typeName = "Result"
if operandType.kind == tkNamed: var okField = "Ok_0"
if operandType != nil and operandType.kind == tkNamed:
typeName = operandType.name typeName = operandType.name
if typeName == "Option": if typeName == "Option" or typeName.startsWith("Option_"):
errTag = "Option_None" 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 tmpName = ctx.freshTryVar()
let tmpAlloca = hirAlloca(tmpName, operandType, loc) 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 tmpStore = hirStore(tmpVar, operand, loc)
let tagPtr = HirNode(kind: hFieldPtr, fieldPtrBase: tmpVar, fieldName: "tag", 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 errConst = hirVar(errTag, makeNamed(typeName & "_Tag"), loc)
let cond = hirBinary(tkEq, tagLoad, errConst, makeBool(), 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 panicTok = Token(kind: tkStringLiteral, text: "\"unwrap failed\"", loc: loc)
let panicMsg = HirNode(kind: hLit, litToken: panicTok, typ: makeStr(), loc: loc) let panicMsg = HirNode(kind: hLit, litToken: panicTok, typ: makeStr(), loc: loc)
let panicCall = hirCall("bux_panic", @[panicMsg], makeVoid(), 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, let ifNode = HirNode(kind: hIf, ifCond: cond, ifThen: thenBlock,
ifElse: nil, typ: makeVoid(), loc: loc) 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) typ: makePointer(makeNamed(typeName & "_Data")), loc: loc)
let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr, let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr,
typ: makeNamed(typeName & "_Data"), loc: loc) typ: makeNamed(typeName & "_Data"), loc: loc)
let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: "Ok_0", let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: okField,
typ: makePointer(makeInt()), loc: loc) typ: makePointer(okType), loc: loc)
let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: makeInt(), loc: loc) let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: okType, loc: loc)
ctx.pendingStmts.add(tmpAlloca) ctx.pendingStmts.add(tmpAlloca)
ctx.pendingStmts.add(tmpStore) ctx.pendingStmts.add(tmpStore)
+71 -3
View File
@@ -17,6 +17,17 @@ type
proc emitErr(res: var MacroExpandResult, loc: SourceLocation, msg: string) = proc emitErr(res: var MacroExpandResult, loc: SourceLocation, msg: string) =
res.diagnostics.add(MacroDiagnostic(loc: loc, message: msg)) 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*) # Deep clone (bootstrap has no Ast_Clone*)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -152,9 +163,12 @@ proc cloneExpr*(e: Expr): Expr =
for a in e.exprCallArgs: for a in e.exprCallArgs:
result.exprCallArgs.add(cloneExpr(a)) result.exprCallArgs.add(cloneExpr(a))
of ekGenericCall: of ekGenericCall:
var gtas: seq[TypeExpr] = @[]
for ta in e.exprGenericTypeArgs:
gtas.add(cloneTypeExpr(ta))
result = Expr(kind: ekGenericCall, loc: e.loc, result = Expr(kind: ekGenericCall, loc: e.loc,
exprGenericCallee: e.exprGenericCallee, exprGenericCallee: e.exprGenericCallee,
exprGenericTypeArgs: e.exprGenericTypeArgs) exprGenericTypeArgs: gtas)
of ekIndex: of ekIndex:
result = Expr(kind: ekIndex, loc: e.loc, result = Expr(kind: ekIndex, loc: e.loc,
exprIndexObj: cloneExpr(e.exprIndexObj), exprIndexObj: cloneExpr(e.exprIndexObj),
@@ -165,9 +179,12 @@ proc cloneExpr*(e: Expr): Expr =
exprFieldObj: cloneExpr(e.exprFieldObj), exprFieldObj: cloneExpr(e.exprFieldObj),
exprFieldName: e.exprFieldName) exprFieldName: e.exprFieldName)
of ekStructInit: of ekStructInit:
var stas: seq[TypeExpr] = @[]
for ta in e.exprStructInitTypeArgs:
stas.add(cloneTypeExpr(ta))
result = Expr(kind: ekStructInit, loc: e.loc, result = Expr(kind: ekStructInit, loc: e.loc,
exprStructInitName: e.exprStructInitName, exprStructInitName: e.exprStructInitName,
exprStructInitTypeArgs: e.exprStructInitTypeArgs, exprStructInitTypeArgs: stas,
exprStructInitFields: @[]) exprStructInitFields: @[])
for f in e.exprStructInitFields: for f in e.exprStructInitFields:
result.exprStructInitFields.add((f.name, cloneExpr(f.value))) result.exprStructInitFields.add((f.name, cloneExpr(f.value)))
@@ -895,6 +912,12 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
of ekRange: of ekRange:
c.exprRangeLo = substExpr(c.exprRangeLo, env, callLoc) c.exprRangeLo = substExpr(c.exprRangeLo, env, callLoc)
c.exprRangeHi = substExpr(c.exprRangeHi, 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: of ekCall:
c.exprCallCallee = substExpr(c.exprCallCallee, env, callLoc) c.exprCallCallee = substExpr(c.exprCallCallee, env, callLoc)
var args: seq[Expr] = @[] var args: seq[Expr] = @[]
@@ -950,12 +973,37 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
argNames.add("") argNames.add("")
c.exprCallArgs = args c.exprCallArgs = args
c.exprCallArgNames = argNames 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: of ekIndex:
c.exprIndexObj = substExpr(c.exprIndexObj, env, callLoc) c.exprIndexObj = substExpr(c.exprIndexObj, env, callLoc)
c.exprIndexIdx = substExpr(c.exprIndexIdx, env, callLoc) c.exprIndexIdx = substExpr(c.exprIndexIdx, env, callLoc)
of ekField: of ekField:
c.exprFieldObj = substExpr(c.exprFieldObj, env, callLoc) c.exprFieldObj = substExpr(c.exprFieldObj, env, callLoc)
of ekStructInit: 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]] = @[] var fields: seq[tuple[name: string, value: Expr]] = @[]
for f in c.exprStructInitFields: for f in c.exprStructInitFields:
fields.add((f.name, substExpr(f.value, env, callLoc))) 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) exprMacroTtInner: inner, exprMacroTtGroup: true)
result = @[callee, group] 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 matched: MacroRule
var env: MacroEnv var env: MacroEnv
var found = false var found = false
@@ -1257,7 +1324,8 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
var gi = 0 var gi = 0
var ai = 0 var ai = 0
let useGroups = nReps > 1 and groups.len > 1 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: for frag in rule.frags:
if failed: break if failed: break
+26
View File
@@ -434,10 +434,22 @@ proc isMacroStmtStart(p: Parser): bool =
p.peek() in {tkLet, tkVar, tkIf, tkWhile, tkFor, tkLoop, tkMatch, tkReturn, p.peek() in {tkLet, tkVar, tkIf, tkWhile, tkFor, tkLoop, tkMatch, tkReturn,
tkBreak, tkContinue, tkDefer, tkSwitch, tkDo} 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 = proc parseMacroArg(p: var Parser): Expr =
## Macro call argument: ## Macro call argument:
## - statement keywords → ekMacroStmt ## - statement keywords → ekMacroStmt
## - `_` / pattern-only starts → ekMacroPat (also `$p:pat` from expr via coerce) ## - `_` / pattern-only starts → ekMacroPat (also `$p:pat` from expr via coerce)
## - bare binary operators (`+`, `*`, `==`, …) → ekMacroTt (operators-only paste)
## - else expression ## - else expression
let loc = p.currentLoc let loc = p.currentLoc
if p.isMacroStmtStart(): if p.isMacroStmtStart():
@@ -447,6 +459,20 @@ proc parseMacroArg(p: var Parser): Expr =
if p.check(tkUnderscore): if p.check(tkUnderscore):
let pat = p.parsePattern() let pat = p.parsePattern()
return Expr(kind: ekMacroPat, loc: loc, exprMacroPat: pat) 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() p.parseExpr()
proc parseMacroRepExpr(p: var Parser): Expr = proc parseMacroRepExpr(p: var Parser): Expr =
+57 -7
View File
@@ -1078,6 +1078,58 @@ proc collectGlobals*(sema: var Sema) =
proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type
proc checkStmt(sema: var Sema, stmt: Stmt, 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,E>` → 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<T,E> / Option<T> 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 = proc typeImplements(sema: Sema, t: Type, interfaceName: string): bool =
## Check if a type implements an interface by verifying all required methods exist. ## Check if a type implements an interface by verifying all required methods exist.
if t.isUnknown: return true 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) discard sema.checkExpr(expr.exprIsOperand, scope)
return makeBool() return makeBool()
of ekTry: of ekTry:
discard sema.checkExpr(expr.exprTryOperand, scope) let opTy = sema.checkExpr(expr.exprTryOperand, scope)
# For now, assume Result<int, String> -> int # Payload of Result/Option; validates operand is Result or Option
# TODO: check operand is Result/Option and current function returns same type return sema.extractResultOptionPayload(opTy, expr.loc, "try operator (`?`)")
return makeInt()
of ekUnwrap: of ekUnwrap:
discard sema.checkExpr(expr.exprUnwrapOperand, scope) let opTy = sema.checkExpr(expr.exprUnwrapOperand, scope)
# Unwrap: extract Ok value or panic on Err return sema.extractResultOptionPayload(opTy, expr.loc, "unwrap operator (`!`)")
return makeInt()
of ekBlock: of ekBlock:
var blockScope = newScope(scope) var blockScope = newScope(scope)
var lastType = makeVoid() var lastType = makeVoid()
+18 -6
View File
@@ -181,13 +181,25 @@ no-libc freestanding firmware.
| Thin runtime (no pthread/OpenSSL) | ✅ | `rt/runtime_minimal.c` | | Thin runtime (no pthread/OpenSSL) | ✅ | `rt/runtime_minimal.c` |
| Static musl / distroless | ✅ | `make test-musl-static`, Dockerfiles | | Static musl / distroless | ✅ | `make test-musl-static`, Dockerfiles |
| Linux multi-arch cross | ✅ | aarch64 + riscv64 smokes (SKIP without gcc) | | Linux multi-arch cross | ✅ | aarch64 + riscv64 smokes (SKIP without gcc) |
| True freestanding (`-ffreestanding`, no libc) | 🔬 spike | Needs custom alloc, panic, and I/O stubs | | True freestanding (`-ffreestanding`, no libc) | spike shipped | `rt/runtime_freestanding.c` + `BUX_RUNTIME=freestanding` |
| Cortex-M / qemu-system | 🔬 spike | Same; plus linker scripts and startup | | 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 **Freestanding runtime** (`rt/runtime_freestanding.c`):
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** - No hosted libc: bump heap (default 256 KiB), freestanding string/arith helpers
import `Std::Net` / `Std::Task` / OpenSSL. - 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.
--- ---
+26 -1
View File
@@ -1,7 +1,32 @@
# Bux — План за подобрения (post-v1.0.0) # Bux — План за подобрения (post-v1.0.0)
> **Дата:** 2026-07-28 > **Дата:** 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<String, String>` + `?` | `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.
--- ---
+23 -7
View File
@@ -1,13 +1,14 @@
# Bux Language Server (`bux-lsp`) # 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`) > **Binary:** `tools/bux-lsp` (`make lsp`)
> **Editors:** VS Code extension in [`vscode/`](../vscode/README.md); any LSP client via stdio > **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** Bux already ships a real Language Server Protocol implementation. It is **not**
syntax-only: hover and outline use bootstrap semantic analysis when available, syntax-only: hover and outline use bootstrap semantic analysis when available,
and **error underlines (red squiggles)** come from **in-process** lex / parse / type-check **error underlines (red squiggles)** come from **in-process** lex / parse / type-check
of the **live editor buffer** on every open, edit, and save. 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 | | 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/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/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/completion` | ✅ | Trigger: `.` `:` |
| `textDocument/hover` | ✅ | Sema types for globals/stdlib; **scoped locals** + inferred `let` | | `textDocument/hover` | ✅ | Sema types for globals/stdlib; **scoped locals** + inferred `let` |
| `textDocument/definition` | ✅ | Go to definition | | `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 ## Limitations / not yet
Honest gaps (so Reddit / issue trackers stay accurate): 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 semantic tokens** provider (TextMate grammar handles highlighting in VS Code)
- **No code actions / lightbulbs** (quick-fixes) - **No code actions / lightbulbs** (quick-fixes)
- **No inlay hints** - **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 - Completion is useful but not a full IDE IntelliSense engine
- Single-process stdio only (no TCP/socket mode) - Single-process stdio only (no TCP/socket mode)
+49 -4
View File
@@ -997,6 +997,25 @@ func Compute() -> Result {
``` ```
`?` can be used on `Result` and `Option` types in any expression context. `?` can be used on `Result` and `Option` types in any expression context.
The type of `expr?` is the **Ok / Some payload** (`T` in `Result<T,E>` or
`Option<T>`), 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<String, String> {
return Result_NewOk<String, String>("bux");
}
func Run() -> Result<String, String> {
let n: String = GetName()?; // n: String
return Result_NewOk<String, String>(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 between fragments) matches a **single call-site argument** that is a call
expression: `apply_juxta!(Add(2, 5))` → binds `$f=Add`, `$args` = arg-list expression: `apply_juxta!(Add(2, 5))` → binds `$f=Add`, `$args` = arg-list
group, then `$f($args)` flattens to `Add(2, 5)`. group, then `$f($args)` flattens to `Add(2, 5)`.
- **Type fragments (session 87):** - **Type fragments (session 87+):** named, pointer, and **generic** types
(`Array<int>`, `*int`); `$t` substitutes in `sizeof` / cast / let types and
monomorph call type args (`Array_New<$t>`).
```bux ```bux
macro! size_of { macro! size_of {
( $t:type ) => { sizeof($t) as int } ( $t:type ) => { sizeof($t) as int }
} }
macro! new_array {
( $t:type, $cap:expr ) => { Array_New<$t>($cap) }
}
let n: int = size_of!(int); let n: int = size_of!(int);
let p: int = size_of!(*int); let p: int = size_of!(*int);
let s: int = size_of!(Array<int>);
var a: Array<int> = 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 ### Invocation
@@ -1415,8 +1453,15 @@ Examples: `examples/macro_hygiene.bux`, `examples/macro_unhygienic.bux`.
templates (not as a free-standing primary expression). templates (not as a free-standing primary expression).
- Raw delimiter-balanced `tt` covers **tuple** `(a, b)` and **slice lit** - Raw delimiter-balanced `tt` covers **tuple** `(a, b)` and **slice lit**
`[a, b]` groups, plus **juxta call-split** for `$f:ident $args:tt` matching `[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 `F(a, b)`.
without AST) remain out of scope. - **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>`, `*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: `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`.
+14 -2
View File
@@ -1472,10 +1472,22 @@ bootstrap + **buxc2** `macro_tt_raw` (incl. slice) PASS.
5. Tag `v1.0.0` after `make test` gate 5. Tag `v1.0.0` after `make test` gate
**Post-1.0 backlog (MINOR, not freeze blockers):** **Post-1.0 backlog (MINOR, not freeze blockers):**
- Generics in `:type` (`Array<int>`); operators-only tt paste - ~~Generics in `:type` (`Array<int>`); operators-only tt paste~~ ✅ session 5
- `runtime_freestanding.c` + Cortex-M research - ~~`runtime_freestanding.c`~~ ✅ session 5 (Cortex-M / board BSP still research)
- LSP / IDE versioning independent of language MAJOR - 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<T,E>` / 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 - Повече Windows examples / Win OpenSSL / Win sockets
+40
View File
@@ -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;
}
+44
View File
@@ -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<int>
let sz: int = size_of!(Array<int>);
PrintInt(sz);
PrintLine("");
if sz < 8 {
PrintLine("FAIL size_of Array<int>");
return 1;
}
// $t spliced into monomorphized call Array_New<$t>
var a: Array<int> = new_array!(int, 4);
Array_Push<int>(&a, 10);
Array_Push<int>(&a, 20);
Test_AssertEqInt(Array_Get<int>(&a, 0), 10);
Test_AssertEqInt(Array_Get<int>(&a, 1), 20);
Test_AssertEqInt(Array_Len<int>(&a) as int, 2);
Array_Free<int>(&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;
}
+32
View File
@@ -0,0 +1,32 @@
// Try operator `?` with generic Result<T,E> (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<String, String> {
if String_Eq(name, "") {
return Result_NewErr<String, String>("empty name");
}
return Result_NewOk<String, String>("hello");
}
func Greet(name: String) -> Result<String, String> {
let g: String = GetGreeting(name)?;
return Result_NewOk<String, String>(g);
}
func Main() -> int {
PrintLine("Try generic Result demo:");
let ok: Result<String, String> = Greet("bux");
if ok.tag == Result_Ok {
PrintLine(ok.data.Ok_0);
}
let err: Result<String, String> = Greet("");
if err.tag == Result_Err {
PrintLine(err.data.Err_0);
}
return 0;
}
+484
View File
@@ -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
+6 -2
View File
@@ -59,10 +59,13 @@ module Cli {
return "rt/runtime_win.c"; return "rt/runtime_win.c";
} }
if String_Eq(env, "minimal") || String_Eq(env, "thin") || if String_Eq(env, "minimal") || String_Eq(env, "thin") ||
String_Eq(env, "embed") || String_Eq(env, "embedded") || String_Eq(env, "embed") || String_Eq(env, "embedded") {
String_Eq(env, "freestanding") {
return "rt/runtime_minimal.c"; 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") { if String_Eq(env, "full") || String_Eq(env, "posix") {
return "rt/runtime.c"; return "rt/runtime.c";
} }
@@ -78,6 +81,7 @@ module Cli {
func Cli_IsThinRuntimePath(rtPath: String) -> bool { func Cli_IsThinRuntimePath(rtPath: String) -> bool {
if String_Contains(rtPath, "runtime_minimal.c") { return true; } 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; } if String_Contains(rtPath, "runtime_win.c") { return true; }
return false; return false;
} }
+68 -2
View File
@@ -470,16 +470,30 @@ module MacroExpand {
(aexp.kind == ekSlice && aexp.callArgCount > 0); (aexp.kind == ekSlice && aexp.callArgCount > 0);
return wrap; return wrap;
} }
// type — session 87: named / pointer type from call-site expr shape // type — session 87+: named / pointer / generic Array<int> from call-site
if String_Eq(kindStr, "type") { if String_Eq(kindStr, "type") {
if aexp.kind == ekMacroType { return aexp; } if aexp.kind == ekMacroType { return aexp; }
var te: *TypeExpr = null as *TypeExpr; 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 = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te.kind = tekNamed; te.kind = tekNamed;
te.line = aexp.line; te.line = aexp.line;
te.column = aexp.column; te.column = aexp.column;
if aexp.kind == ekGenericCall && !String_Eq(aexp.genericCallee, "") {
te.typeName = aexp.genericCallee;
} else {
te.typeName = aexp.strValue; te.typeName = aexp.strValue;
}
// Generic type args: Array<int> / Map<K,V> (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 { } else if aexp.kind == ekUnary && aexp.intValue == tkStar && aexp.child1 != null as *Expr {
// *T from unary star // *T from unary star
let inner: *Expr = Macro_CoerceArg("type", aexp.child1); let inner: *Expr = Macro_CoerceArg("type", aexp.child1);
@@ -518,6 +532,35 @@ module MacroExpand {
return bound.refType; 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 { if te.pointerPointee != null as *TypeExpr {
te.pointerPointee = Macro_SubstType(te.pointerPointee, env); te.pointerPointee = Macro_SubstType(te.pointerPointee, env);
if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr { if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
@@ -533,6 +576,27 @@ module MacroExpand {
return te; 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" // Fragment kind check: "ident" | "literal" | "block" | "stmt" | "pat" | "expr" | "tt"
func Macro_FragMatches(kindStr: String, aexp: *Expr) -> bool { func Macro_FragMatches(kindStr: String, aexp: *Expr) -> bool {
return Macro_CoerceArg(kindStr, aexp) != null as *Expr; 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.child1 = Subst_Expr(c.child1, env, file, line, col);
c.child2 = Subst_Expr(c.child2, env, file, line, col); c.child2 = Subst_Expr(c.child2, env, file, line, col);
c.child3 = Subst_Expr(c.child3, 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 // sizeof / cast / is type annotations
if c.refType != null as *TypeExpr { if c.refType != null as *TypeExpr {
c.refType = Macro_SubstType(c.refType, env); c.refType = Macro_SubstType(c.refType, env);
+71 -3
View File
@@ -21,9 +21,11 @@
# docs even when `extend T for I` has no methods (no open required). # 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 # 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. # 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 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 # JSON-RPC Transport
@@ -2514,6 +2516,64 @@ proc handleDocumentSymbol(stream: FileStream, id: JsonNode, paramsNode: JsonNode
}) })
sendResponse(stream, id, arr) 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) = proc handleWorkspaceSymbol(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
## workspace/symbol — fuzzy-ish substring filter over workspace + open docs. ## workspace/symbol — fuzzy-ish substring filter over workspace + open docs.
let query = if paramsNode.hasKey("query"): paramsNode["query"].getStr().toLowerAscii() else: "" let query = if paramsNode.hasKey("query"): paramsNode["query"].getStr().toLowerAscii() else: ""
@@ -3370,9 +3430,11 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
"workspaceSymbolProvider": true, "workspaceSymbolProvider": true,
"callHierarchyProvider": true, "callHierarchyProvider": true,
"implementationProvider": 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: if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull:
rootPath = paramsNode["rootPath"].getStr() rootPath = paramsNode["rootPath"].getStr()
@@ -3470,6 +3532,12 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
of "typeHierarchy/subtypes": of "typeHierarchy/subtypes":
handleTypeHierarchySubtypes(stream, id, paramsNode) handleTypeHierarchySubtypes(stream, id, paramsNode)
of "textDocument/formatting":
handleDocumentFormatting(stream, id, paramsNode)
of "textDocument/rangeFormatting":
handleDocumentRangeFormatting(stream, id, paramsNode)
else: else:
if id != nil: if id != nil:
sendError(stream, id, -32601, "method not found: " & methodName) sendError(stream, id, -32601, "method not found: " & methodName)
+66
View File
@@ -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"
+86
View File
@@ -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
+2 -2
View File
@@ -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 | | **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`, … | | **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 | | **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`) | | **Editor** | Bracket colorization, smart indent / on-enter, fold regions (`// region`), **format-on-save** default for `[bux]` |
| **Build** | `buxc` problem matcher for Tasks | | **Build** | `buxc` problem matcher for Tasks |
## Requirements ## Requirements
+7 -4
View File
@@ -1,8 +1,8 @@
{ {
"name": "bux-lang", "name": "bux-lang",
"displayName": "Bux Language Support", "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", "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.1", "version": "0.2.2",
"publisher": "bux-lang", "publisher": "bux-lang",
"license": "MIT", "license": "MIT",
"icon": "icon.png", "icon": "icon.png",
@@ -25,6 +25,7 @@
"Programming Languages", "Programming Languages",
"Snippets", "Snippets",
"Linters", "Linters",
"Formatters",
"Other" "Other"
], ],
"keywords": [ "keywords": [
@@ -32,7 +33,8 @@
"bux-lang", "bux-lang",
"programming-language", "programming-language",
"lsp", "lsp",
"syntax" "syntax",
"formatter"
], ],
"activationEvents": [ "activationEvents": [
"onLanguage:bux", "onLanguage:bux",
@@ -107,7 +109,7 @@
"bux.lsp.enabled": { "bux.lsp.enabled": {
"type": "boolean", "type": "boolean",
"default": true, "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": { "bux.lsp.path": {
"type": "string", "type": "string",
@@ -133,6 +135,7 @@
"editor.tabSize": 4, "editor.tabSize": 4,
"editor.insertSpaces": true, "editor.insertSpaces": true,
"editor.detectIndentation": false, "editor.detectIndentation": false,
"editor.formatOnSave": true,
"editor.quickSuggestions": { "editor.quickSuggestions": {
"other": true, "other": true,
"comments": false, "comments": false,