feat: multi-instance closures, richer stdlib, and Rust-style diagnostics

Introduce fat function pointers (BuxFn {code, env}) so capturing closures
are heap-allocated per value in both bootstrap and selfhost. Expand
Array/Map/Set/String/Test/Result APIs, add proper tuple codegen and
error snippets with multi-char underlines, golden diagnostic tests, and
LSP diagnostics via buxc check.
This commit is contained in:
2026-07-15 16:00:21 +03:00
parent 94e6806dda
commit 61ac06ab5f
48 changed files with 2789 additions and 362 deletions
+74
View File
@@ -58,4 +58,78 @@ func Array_operator_index_set<T>(self: *Array<T>, idx: uint, value: T) {
Array_Set<T>(self, idx, value);
}
/* True if the array has no elements */
func Array_IsEmpty<T>(self: *Array<T>) -> bool {
return self.len == 0;
}
/* Current capacity (not length) */
func Array_Cap<T>(self: *Array<T>) -> uint {
return self.cap;
}
/* Drop length to zero; keeps allocated capacity */
func Array_Clear<T>(self: *Array<T>) {
self.len = 0;
}
/* Ensure capacity is at least minCap (does not shrink) */
func Array_Reserve<T>(self: *Array<T>, minCap: uint) {
if minCap <= self.cap {
return;
}
self.cap = minCap;
self.data = bux_realloc(self.data as *void, self.cap * sizeof(T)) as *T;
}
/* First element (panics if empty via bounds check) */
func Array_First<T>(self: *Array<T>) -> T {
return Array_Get<T>(self, 0);
}
/* Last element (panics if empty via bounds check) */
func Array_Last<T>(self: *Array<T>) -> T {
return Array_Get<T>(self, self.len - 1);
}
/* Remove and return the last element (panics if empty) */
func Array_Pop<T>(self: *Array<T>) -> T {
bux_bounds_check(0, self.len);
self.len = self.len - 1;
return self.data[self.len];
}
/* Linear search: true if value is present (uses ==) */
func Array_Contains<T>(self: *Array<T>, value: T) -> bool {
var i: uint = 0;
while i < self.len {
if self.data[i] == value {
return true;
}
i = i + 1;
}
return false;
}
/* Index of first equal element, or -1 if not found */
func Array_IndexOf<T>(self: *Array<T>, value: T) -> int {
var i: uint = 0;
while i < self.len {
if self.data[i] == value {
return i as int;
}
i = i + 1;
}
return -1;
}
/* Append all elements of other onto self */
func Array_Extend<T>(self: *Array<T>, other: *Array<T>) {
var i: uint = 0;
while i < other.len {
Array_Push<T>(self, other.data[i]);
i = i + 1;
}
}
}
+40
View File
@@ -67,4 +67,44 @@ func Iter_Take<T>(it: *Iter<T>, n: uint) -> Iter<T> {
return Iter<T> { data: it.data, len: endPos, pos: it.pos };
}
/* True if any remaining element equals value */
func Iter_AnyEq<T>(it: *Iter<T>, value: T) -> bool {
var i: uint = it.pos;
while i < it.len {
if it.data[i] == value {
return true;
}
i = i + 1;
}
return false;
}
/* True if every remaining element equals value (true if empty) */
func Iter_AllEq<T>(it: *Iter<T>, value: T) -> bool {
var i: uint = it.pos;
while i < it.len {
if it.data[i] != value {
return false;
}
i = i + 1;
}
return true;
}
/* Collect remaining elements into a new Array */
func Iter_Collect<T>(it: *Iter<T>) -> Array<T> {
let remaining: uint = it.len - it.pos;
var cap: uint = remaining;
if cap == 0 {
cap = 1;
}
var arr: Array<T> = Array_New<T>(cap);
var i: uint = it.pos;
while i < it.len {
Array_Push<T>(&arr, it.data[i]);
i = i + 1;
}
return arr;
}
}
+69
View File
@@ -80,6 +80,41 @@ func Map_Len<K, V>(m: *Map<K, V>) -> uint {
return m.len;
}
func Map_IsEmpty<K, V>(m: *Map<K, V>) -> bool {
return m.len == 0;
}
/* Remove key if present. Rebuilds the table to keep open-addressing correct. */
func Map_Remove<K, V>(m: *Map<K, V>, key: K) -> bool {
if !Map_Has<K, V>(m, key) {
return false;
}
var fresh: Map<K, V> = Map_New<K, V>(m.cap);
var i: uint = 0;
while i < m.cap {
if m.entries[i].occupied {
if m.entries[i].key != key {
Map_Set<K, V>(&fresh, m.entries[i].key, m.entries[i].value);
}
}
i = i + 1;
}
bux_free(m.entries as *void);
m.entries = fresh.entries;
m.cap = fresh.cap;
m.len = fresh.len;
return true;
}
func Map_Clear<K, V>(m: *Map<K, V>) {
var i: uint = 0;
while i < m.cap {
m.entries[i].occupied = false;
i = i + 1;
}
m.len = 0;
}
func Map_Free<K, V>(m: *Map<K, V>) {
bux_free(m.entries as *void);
m.entries = null as *MapEntry<K, V>;
@@ -163,6 +198,40 @@ func StringMap_Len<V>(m: *StringMap<V>) -> uint {
return m.len;
}
func StringMap_IsEmpty<V>(m: *StringMap<V>) -> bool {
return m.len == 0;
}
func StringMap_Remove<V>(m: *StringMap<V>, key: String) -> bool {
if !StringMap_Has<V>(m, key) {
return false;
}
var fresh: StringMap<V> = StringMap_New<V>(m.cap);
var i: uint = 0;
while i < m.cap {
if m.entries[i].occupied {
if !String_Eq(m.entries[i].key, key) {
StringMap_Set<V>(&fresh, m.entries[i].key, m.entries[i].value);
}
}
i = i + 1;
}
bux_free(m.entries as *void);
m.entries = fresh.entries;
m.cap = fresh.cap;
m.len = fresh.len;
return true;
}
func StringMap_Clear<V>(m: *StringMap<V>) {
var i: uint = 0;
while i < m.cap {
m.entries[i].occupied = false;
i = i + 1;
}
m.len = 0;
}
func StringMap_Free<V>(m: *StringMap<V>) {
bux_free(m.entries as *void);
m.entries = null as *StringMapEntry<V>;
+19
View File
@@ -1,6 +1,8 @@
module Std::Option {
import Std::Io::{PrintLine};
extern func bux_exit(code: int);
enum Option {
Some(int),
None,
@@ -39,4 +41,21 @@ func Option_UnwrapOr(o: Option, fallback: int) -> int {
return fallback;
}
/* Unwrap Some or panic with a custom message */
func Option_Expect(o: Option, msg: String) -> int {
if o.tag != Option_Some {
PrintLine(msg);
bux_exit(1);
}
return o.data.Some_0;
}
/* If o is Some return it, otherwise return other */
func Option_Or(o: Option, other: Option) -> Option {
if o.tag == Option_Some {
return o;
}
return other;
}
}
+6
View File
@@ -6,6 +6,7 @@ extern func bux_getenv(name: String) -> String;
extern func bux_setenv(name: String, value: String) -> int;
extern func bux_getcwd() -> String;
extern func bux_chdir(path: String) -> int;
extern func bux_exit(code: int);
func Os_ArgsCount() -> int {
return bux_argc();
@@ -31,4 +32,9 @@ func Os_Chdir(path: String) -> bool {
return bux_chdir(path) == 0;
}
/* Terminate the process with the given exit code */
func Os_Exit(code: int) {
bux_exit(code);
}
}
+28
View File
@@ -1,6 +1,8 @@
module Std::Result {
import Std::Io::{PrintLine};
extern func bux_exit(code: int);
enum Result {
Ok(int),
Err(String),
@@ -41,4 +43,30 @@ func Result_UnwrapOr(r: Result, fallback: int) -> int {
return fallback;
}
/* Unwrap Ok or panic with a custom message */
func Result_Expect(r: Result, msg: String) -> int {
if r.tag != Result_Ok {
PrintLine(msg);
bux_exit(1);
}
return r.data.Ok_0;
}
/* Extract Err payload (panics if Ok) */
func Result_UnwrapErr(r: Result) -> String {
if r.tag != Result_Err {
PrintLine("panic: unwrap_err on Ok");
return "";
}
return r.data.Err_0;
}
/* If r is Ok return it, otherwise return other */
func Result_Or(r: Result, other: Result) -> Result {
if r.tag == Result_Ok {
return r;
}
return other;
}
}
+37
View File
@@ -61,6 +61,43 @@ func Set_Len<T>(s: *Set<T>) -> uint {
return s.len;
}
func Set_IsEmpty<T>(s: *Set<T>) -> bool {
return s.len == 0;
}
/* Remove value if present. Rebuilds the table to keep open-addressing correct. */
func Set_Remove<T>(s: *Set<T>, value: T) -> bool {
if !Set_Has<T>(s, value) {
return false;
}
var fresh: Set<T> = Set_New<T>(s.cap);
var i: uint = 0;
while i < s.cap {
if s.entries[i].occupied {
var entryPtr: *T = &s.entries[i].value;
var valuePtr: *T = &value;
if bux_mem_eq(entryPtr as *void, valuePtr as *void, sizeof(T)) == 0 {
Set_Add<T>(&fresh, s.entries[i].value);
}
}
i = i + 1;
}
bux_free(s.entries as *void);
s.entries = fresh.entries;
s.cap = fresh.cap;
s.len = fresh.len;
return true;
}
func Set_Clear<T>(s: *Set<T>) {
var i: uint = 0;
while i < s.cap {
s.entries[i].occupied = false;
i = i + 1;
}
s.len = 0;
}
func Set_Free<T>(s: *Set<T>) {
bux_free(s.entries as *void);
s.entries = null as *SetEntry<T>;
+64
View File
@@ -34,6 +34,10 @@ func String_Len(s: String) -> uint {
return bux_strlen(s);
}
func String_IsEmpty(s: String) -> bool {
return bux_strlen(s) == 0;
}
func String_IsNull(s: String) -> bool {
return bux_str_is_null(s) != 0;
}
@@ -147,6 +151,39 @@ func StringBuilder_Free(sb: *StringBuilder) {
bux_sb_free(sb.handle);
}
/* True if empty or only whitespace (space, tab, CR, LF) */
func String_IsBlank(s: String) -> bool {
let n: uint = bux_strlen(s);
var i: uint = 0;
while i < n {
let ch: String = bux_str_slice(s, i, 1);
if !(String_Eq(ch, " ") || String_Eq(ch, "\t") || String_Eq(ch, "\n") || String_Eq(ch, "\r")) {
return false;
}
i = i + 1;
}
return true;
}
/* Repeat s, count times (count==0 → empty string) */
func String_Repeat(s: String, count: uint) -> String {
if count == 0 {
return "";
}
if count == 1 {
return s;
}
var sb: StringBuilder = StringBuilder_New();
var i: uint = 0;
while i < count {
StringBuilder_Append(&sb, s);
i = i + 1;
}
let result: String = StringBuilder_Build(&sb);
StringBuilder_Free(&sb);
return result;
}
// ---------------------------------------------------------------------------
// String split/join
// ---------------------------------------------------------------------------
@@ -194,6 +231,33 @@ func String_Replace(s: String, old: String, new: String) -> String {
return result;
}
/* Replace every non-overlapping occurrence of old with new.
Empty old is a no-op (returns s unchanged). Safe if new contains old. */
func String_ReplaceAll(s: String, old: String, new: String) -> String {
let oldLen: uint = bux_strlen(old);
if oldLen == 0 {
return s;
}
var sb: StringBuilder = StringBuilder_New();
var remaining: String = s;
while true {
let pos: String = bux_strstr(remaining, old);
if String_IsNull(pos) {
StringBuilder_Append(&sb, remaining);
break;
}
let prefixLen: uint = String_Offset(pos, remaining);
let prefix: String = bux_str_slice(remaining, 0, prefixLen);
StringBuilder_Append(&sb, prefix);
StringBuilder_Append(&sb, new);
let remLen: uint = bux_strlen(remaining);
remaining = bux_str_slice(remaining, prefixLen + oldLen, remLen - prefixLen - oldLen);
}
let result: String = StringBuilder_Build(&sb);
StringBuilder_Free(&sb);
return result;
}
extern func bux_str_to_float(s: String) -> float64;
func String_ToFloat(s: String) -> float64 {
+32 -1
View File
@@ -1,5 +1,6 @@
module Std::Test {
import Std::Io::{PrintLine, PrintInt};
import Std::String::{String_Eq};
extern func bux_exit(code: int);
extern func bux_assert(cond: int, file: String, line: int, expr: String);
@@ -14,7 +15,7 @@ func Test_Assert(cond: bool) {
func Test_AssertEqInt(a: int, b: int) {
if a != b {
PrintLine("ASSERT_EQ FAILED:");
PrintLine("ASSERT_EQ_INT FAILED:");
PrintInt(a);
PrintLine(" != ");
PrintInt(b);
@@ -22,6 +23,31 @@ func Test_AssertEqInt(a: int, b: int) {
}
}
func Test_AssertNeqInt(a: int, b: int) {
if a == b {
PrintLine("ASSERT_NEQ_INT FAILED: both are");
PrintInt(a);
bux_exit(1);
}
}
func Test_AssertEqString(a: String, b: String) {
if !String_Eq(a, b) {
PrintLine("ASSERT_EQ_STRING FAILED:");
PrintLine(a);
PrintLine(" != ");
PrintLine(b);
bux_exit(1);
}
}
func Test_AssertEqBool(a: bool, b: bool) {
if a != b {
PrintLine("ASSERT_EQ_BOOL FAILED");
bux_exit(1);
}
}
func Test_AssertTrue(cond: bool) {
if !cond {
PrintLine("ASSERT_TRUE FAILED");
@@ -42,4 +68,9 @@ func Test_Fail(msg: String) {
bux_exit(1);
}
func Test_Pass(msg: String) {
PrintLine("PASS:");
PrintLine(msg);
}
}