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
+71 -3
View File
@@ -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)
+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