17 KiB
"quick" / "easy" fixes
- for global initialization cycles, report also starting and ending lines
milestones
- interop type foundation (implemented)
- unsigned integers, floats, and target-dependent c scalar types
- atomic
c_*primitive types remain distinct until target-aware lowering c_funcand pointer-onlyc_struct;cremains an ordinary identifier- keep binding mutability (
::/=) separate from element or pointee mutability (mut) - arrays and indexing
[N]T: array withNlogical elements[N;S]T: array withNlogical elements followed by sentinelS
- pointers
@T/@mut T: non-null single-item pointer without arithmetic*T/*mut T: non-null many-item pointer with arithmetic- optional pointers represent nullable pointers (i.e.
?@T/?@mut T,?*T/?*mut T)
- slices and slicing
[]T: pointer and length[;S]T: pointer and length with a sentinel invariant- ordinary slices do not guarantee null termination
- string literals as immutable sentinel slices backed by static arrays (superseded by milestone 3.5)
- character literals
- optionals with trapping unwrap and fallback operations
- native structs with compiler-controlled layout
- pointer-only
c_structsupport with target c layoutSome :: c_struct { ... }: defined c-layout structSome :: c_struct: opaque c-layout struct- passing c structs by value was deferred until milestone 4.1
- restricted c header imports (implemented)
- treat an imported header as a synthetic, file-local package namespace
native :: import "relative/path/to/header.h"
- import functions, typedefs, scalar types, and pointers to opaque records
- keep implementation linking separate from header imports
- cache imports by canonical header path and target/include/define configuration
- diagnose unsupported declarations when referenced
- dynamically load libclang behind a replaceable c importer boundary
- c variadic calls (implemented)
- represent c variadics as a fixed parameter count plus a variadic flag
- apply c default argument promotions at call sites
- emit LLVM c-variadic declarations and calls
- keep native brolang variadics and tuple design separate
3.5. sentinel pointers and c strings (implemented)
- add sentinel many-item pointers:
[*;S]T - represent string literals as immutable pointers to statically stored sentinel arrays:
@[N;0]u8 - arrays expose
.lenbut no.ptr; slices and pointers-to-arrays expose sentinel-preserving.ptr - allow pointer-to-array
.len, indexing, slicing, pointer decay, and slice construction without explicit dereference - preserve or forget sentinel information through compatible pointer and slice coercions without copying arrays
- allow zero-terminated immutable byte pointer views to convert to immutable
*c_charand[*;0]c_char - keep
u8andc_chardistinct to preserve target-dependent scalar c semantics - reject general
u8/c_charinterchange, slice-to-pointer coercion, and conversion to mutable c character pointers
- advanced c interop
- by-value records and unions (implemented)
- complete plain imported structs/unions and manual
c_structvalues - fixed C arrays inside imported records
- keyed struct literals and exactly-one-field union literals
- field reads/writes, storage, and fixed-signature calls/returns
- aarch64-macos small aggregate, homogeneous float aggregate, and indirect ABI lowering
- keep incomplete, bitfield, packed, flexible-array, qualified-field, and otherwise non-plain records pointer-only
- keep C variadic record arguments unsupported
- complete plain imported structs/unions and manual
- function pointers and callbacks (implemented)
- imported C function pointer typedefs lower to nullable pointer types
- manual
?*c_func(...) Tcallback type spelling - concrete
c_funcdeclarations/definitions can be passed as callback values - postfix calls through non-null function pointers, including
callback?(...) - fixed and C-variadic callback ABI emission through LLVM indirect calls
- external variables (implemented)
- imported external C object variables lower to direct LLVM external global references
- top-level
constobject variables are read-only from brolang - mutable external scalars/records can be assigned through qualified package globals
- unsupported variable types remain lazy diagnostics when referenced
- object-like macro constants (implemented)
- scalar integer/float literal macros import as immutable globals
CLITERAL(Type){ ... }/(Type){ ... }record literal macros import as immutable globals- function-like macros and non-literal macro expressions remain unsupported
- static inline functions (implemented)
- control flow (implemented)
- boolean expressions (implemented)
booltype withtrue/falseliterals- comparison operators:
==,!=,<,<=,>,>=(numeric operands widen;boolsupports only==/!=) - operators:
and,or,! - lazy evaluation / short-circuit evaluation
- if statements (implemented). example:
if condition { ... } else if { ... } else { ... }- conditions must be
bool; block-scoped locals with shadowing across blocks - lowered through new
Label/Br/Cond_BrIR opcodes (alloca-backed locals, no phi nodes)
- conditions must be
- conditional unwrapping for optionals (
?T) (implemented):if val |v| { ... } else { ... }- unwrapvalintovif it is notnone- single immutable binding scoped to the then-block;
vnot visible inelseor after theif |lexes as a newPipetoken; the.Ifreuses ASTname/ HIRlocalto carry the binding (no new statement kind)- new
Optional_Is_Some/Optional_ValueIR opcodes (theUnwrappresence-test + extract, minus the trap)
- single immutable binding scoped to the then-block;
- conditional unwrapping with guard clause (implemented):
if val |v : v >= 10| { ... } else { ... }- enter the then-block whenvalis notnoneand the guard is true - multi-unwrap (implemented; see section below)
- while loops (implemented; operates on boolean conditions). examples:
while condition { ... }- iterate while the condition is truewhile condition : i = i + 1 { ... }- execute the update after each completed iteration- the condition and update may be parenthesized independently for visual clarity
- update targets must already be declared and mutable; loops do not introduce implicit induction variables
- update clauses support ordinary and compound assignment
- ranges (implemented; see section below)
- for loops (implemented; operates on ranges, arrays, slices, and pointers-to-arrays). examples:
for items |item| { ... }- capture just theitemvalue in the array/slice (uses copy semantics, i.e. gets aT)for (&items) |@item| { ... }- capture a pointer to each array element; its@T/@mut Tmutability follows the iterablefor items_slice |@item| { ... }- slices already refer to backing storage and support pointer capture directlyfor items |item, idx| { ... }- captureitemand its index index in the array/slicefor 0..10 |i| { ... }- iterate over the range0..10(exclusive)for 0..=10 |i| { ... }- iterate over the range0..10(inclusive)for 0..(len) |i| { ... }or equivalentlyfor 0..=(len - 1) |i| { ... }- calculating range bounds, expressions must be parenthesized
- for all conditionals/guards, parentheses are optional but allowed for visual clarity
- compound assignment:
+=,-=,*=,/=(implemented)
- added the binary arithmetic operators
-,*,/(previously only+existed);*//bind tighter than+/-, and prefix-(negation) is unchanged - compound assignments preserve the target, operator, and right-hand side explicitly through parsing and checking; lowering computes the target address once, then loads, applies the operation, and stores through that address
- side-effecting index, field-base, and dereference expressions are evaluated once in left-to-right order
- integer arithmetic traps on overflow (
Sub_Checked/Mul_Checkedvia the LLVM.with.overflowintrinsics) and integer/traps on divide-by-zero andINT_MIN / -1; floats follow IEEE (fadd/fsub/fmul/fdiv, no trap) - constant folding (global initializers) covers
-,*,/alongside+
- enums (native and c interop) (implemented; see below)
- native enums are nominal value types with integer runtime representations
- unbacked enums are non-empty, dense, zero-based, and use the smallest fitting unsigned backing
- explicitly backed enums require an integer type and strictly increasing literal values
- enum members support
Type.member,package.Type.member, and contextual.member - enum values support storage, calls/returns, and same-type equality/inequality
- explicitly backed native enums use their backing ABI in
c_funcsignatures and variadic promotion - imported C enum types alias libclang's target-selected integer backing and enumerators import as package constants
- distinct types (implemented; see below)
- nominal declarations preserve identity across packages and reuse the backing runtime representation
- construction uses
Type(value)with exactly one value of the exact backing type - no implicit conversion to or from the backing type
- backing-type operators and reverse explicit conversions remain deferred
- concrete runtime backing types are supported; unresolved,
int,void, function, and opaque backings are rejected
- allow pointer field access pass-through (implemented)
- having a pointer (
ptr) to a struct, we should allow access throughptr.fieldas opposed to mandatingptr^.field
- make slice expressions on array variables implicitly address-taking (implemented)
- zig's slice expression on an array variable handles the address-taking implicitly (nice ergonomics)
arr[a..b]on an array variable now slices without the explicit&; the explicit(&arr)[a..b]pointer-to-array form keeps working unchanged- array rvalues (e.g. a by-value array return) are materialized into a temporary before slicing, matching the for-loop iterable lowering
- c header imports and automatic native brolang bindings (implemented)
brolang translate-c <header.h> [--target ...] [--c-include-path ...] [--c-define ...]prints native.brobindings for a C header to stdout (the offline counterpart of the in-memorynative :: import "x.h"); reuses the libclangcimport.Result- emitter lives in
compiler/translatec;render_typemirrorsloader.translate_c_typeone-to-one so emitted source re-parses to identical types (guarded by a round-trip test) - added a native type-alias declaration
Name :: alias T(parser/lexer/token surface; thetypes.define_alias/.Aliasmachinery already existed) so C typedefs and callback typedefs round-trip - emits functions, complete/opaque structs (collapsing
typedef struct {...} Foo), typedef aliases, and scalar/aggregate/enum-member constants - C unions, external variables, static-inline functions, and unsupported declarations have
no hand-writable spelling and are emitted as
# unsupported in bindings:comments (functions that reference an un-spellable union therefore keep a dangling reference)
undefinedas inspired by zig (implemented):
- allow mutable local declarations with
undefined- undefined values are assigned a poison value (0xaa...)
- allows for something like:
a int = undefined if (condition) { a = 42 } else { a = -2 }
- disallow:
b :: undefinedsince assigning undefined to something that can't change defeats the purpose - disallow assigning
undefinedafter declaration; use optionals andnonefor values that intentionally move back to an empty state
- introduce
floatandrangetype constraints (theintfamily generalized) (implemented)
floatresolves a local binding to any float scalar (f32/f64) via static analysis; widensf32->f64across assignments, mirroring howintpicks the smallest integer- on a local declaration, integer literals satisfy
floatand default tof64(pi float = 3is3.0); a runtime integer (x float = some_i32) stays acannot implicitly converterror rangeis now a spellable type/constraint:r range :: 0..10resolves to the inferred range (element type preserved), andfunc(start, end int) range { return start..end }monomorphizes the result per call. this replaces the priorint-as-passthrough hack that was the only way to forward a range through a functionint/float/rangeconstraints now gate by family in every position (previously params/results were unchecked generic passthroughs):- a local initializer out of family errors instead of silently taking the natural type
- a param rejects an out-of-family argument (
cannot pass f64 to 'int' parameter 'x'); afloatparam accepts an integer-literal argument as f64 (e.g.f(3)) - a function result is narrowed to the constraint's family
-
broaden type inference to surrounding context
-
add slice-by-range
- allow the use of a range in slice expressions:
excl_range range :: 0..10 some_arr[excl_range] # slice by named exclusive range incl_range range :: 0..=10 some_arr[incl_range] # slice by named inclusive range
A word on multi-unwrap
Unwrap multiple optionals with and. This short-circuits: if the first optional is none, subsequent expressions are not evaluated.
name: ?[]u8 = get_name()
age: ?u8 = get_age()
if name and age |n, a| {
# both n and a are guaranteed non-none here
print("{s} is {d} years old", {n, a})
}
With guard clause on multiple values:
if name and hat |n, h : n == "Huginn" and h.brand == .gucci| {
print("{s}'s got that drip\n", {n})
}
Parentheses around the expression are optional, but can aid readability when combined with guards:
# without parentheses
if name and hat |n, h : guard| { ... }
# with parentheses for clarity
if (name and hat) |n, h : guard| { ... }
A word on lazy / short-circuit evaluation
The and in multi-unwrap short-circuits left-to-right:
if get_name() and get_hat() |n, h| {
# get_hat() is only called if get_name() returned non-none
}
This is important for avoiding unnecessary computation or side effects.
A word on ranges
Ranges represent a sequence of values, commonly used in for loops, and is itself a value type:
0..10 # exclusive: 0, 1, 2, ..., 9
0..=10 # inclusive: 0, 1, 2, ..., 10
Parenthesization rule: Each side of .. must be either a simple term (literal or identifier) or a parenthesized expression. This eliminates precedence ambiguity:
0..10 # OK: both sides are literals
0..n # OK: both sides are simple
0..(n + 1) # OK: complex expression is parenthesized
(a + 1)..(b - 1) # OK: both sides parenthesized
# 0..n + 1 # ERROR: must parenthesize complex expressions
This rule keeps the grammar simple and forces clarity at the call site — no precedence rules to remember. Also, being a value type, ranges can be assigned to variables and passed around like any other value. Range bounds are evaluated once, must have compatible concrete integer types, and descending ranges are empty.
For-loop captures are immutable and scoped to the loop body. Sequence index captures are usize. Pointer capture uses |@item|; arrays must be passed by pointer (for example &items), while slices can be used directly. Sentinel elements are not included in iteration.
A word on distinct types
Distinct types are considered distinct from their backing type. They do not implicitly coerce to their backing type.
# distinct type
UserID :: distinct u32
# instantiate distinct type
my_id UserID :: UserID(42) # value must have the exact backing type
A word on enums
# standard enums
Animal :: enum {
dog
cat
bird
lizard
}
# enums with backing type
Nat :: enum(u8) { # in this case, a maximum of 256 values are possible
one # default: implicitly starts from value 0
two
three
four
five
}
# enums with backing type with explicit associated values
# note: must not be jumbled (i.e. `first_val = 1` must come before `other_val = 2`), but is allowed to be discontiguous (i.e. `one = 1` can be followed by `three = 3` without `two = 2` in between)
Nat :: enum(u8) {
one = 1
two = 2
three = 3
# no four
five = 5
}
# enums with backing type with semi-implicit associated values
Nat :: enum(u8) {
one = 1 # starts from value 1
two # implicitly gets value 2
three # etc...
four
five
}
# using enums
dog_tag1 Animal :: Animal.dog
dog_tag2 Animal :: .dog # type inferred
Unbacked enums cannot assign explicit values. Backed enum values must be decimal integer literals, fit the backing type, and increase strictly; gaps are allowed.
Native enum types remain distinct from integers and from other enum types. They support
== and !=, but not arithmetic, ordering, casts, or backing-value extraction.
C enums follow C/Zig import semantics rather than native enum semantics:
native :: import "native.h"
value native.Imported_Enum :: native.IMPORTED_ENUM_VALUE
The imported enum type is an alias of its target-selected C integer backing, and imported enumerators are package-level constants.