Files
brolang/README.md
T

267 lines
11 KiB
Markdown

# brolang
Prototype error-tolerant Brolang compiler written in Odin.
```sh
mkdir -p build
odin build . -out:build/brolang
./build/brolang examples/programs/prototype -o build/prototype
./build/prototype
```
Programs may receive the system I/O capability explicitly. Standard-stream
helpers bind the provider, handle, and callback; `main func() ...` remains valid.
```bro
io :: import "@std/io"
process :: import "@std/process"
main func(init process.Init) void {
io.print(io.stdout(init.io), "hello {s} {d}\n", {"bro", 37}) catch |_| {
return
}
}
```
Bodyless `c_func` declarations bind exact external symbols and require concrete
types. C primitives use atomic target-dependent names and remain semantically
distinct from exact-width Brolang primitives:
```bro
strlen c_func(value *c_char) c_ulong
```
Additional native inputs and libraries are passed to the final `zig cc`
invocation in command-line order:
```sh
./build/brolang examples/interop/manual -o build/manual \
--c-link examples/interop/manual/native.c
```
`--c-link` accepts C sources, object files, archives, and direct library paths.
`--c-library-path <dir>` becomes `-L<dir>`, and `--c-library <name>` becomes
`-l<name>`. `--c-include-path <dir>` and `--c-define <name[=value]>` configure
C preprocessing.
Instead of passing these on the command line, a project can describe its build
in Brolang itself. `brolang new <project>` creates a project with local `std`
and `ffi` copies; `brolang init` does the same for the current directory without
overwriting existing files. `brolang build [root]` reads a `config` constant
from exactly one of `root/build.bro` or `root/build.hon` and compiles the
program package it names. Without `root`, it searches the current directory
and parents for the nearest build file. Build outputs are written to
`root/build/<name>`.
```bro
b :: import "@std/build"
config :: b.BuildConfig{
name = "manual",
source = "src",
libraries = &[],
lib_paths = &[],
includes = &[],
defines = &[],
links = &["examples/build/manual/native.c"],
}
```
`name` is a plain executable name, and `source` is the program package relative
to the build file. The list fields map to the matching C options (`libraries`
`-l`, `lib_paths``-L`, `includes``-I`, `defines` → C defines, `links`
linker inputs) and, like those flags, their paths are relative to the invocation
directory. Lists take the address of an array literal; empty lists are written
`&[]`. See `examples/build/` for runnable projects.
Projects can declare tests directly and run them with `brolang test [root]`.
The command reads the same build file—exactly one of `build.bro` or
`build.hon`—writes `build/<name>-test`, and reuses its C link inputs,
libraries, include paths, and defines.
```bro
math :: import "../math"
testing :: import "@std/testing"
test import "../math"
addition test {
try testing.expect(math.add(20, 22) == 42)
try testing.expect_equal(42, math.add(20, 22))
}
```
`test import` discovers tests transitively without creating a namespace;
calling package code still requires an ordinary import. Ordinary imports do
not discover dependency tests. Assertions report their source location, a
failure ends only the current test, and the runner continues with the suite.
Relative `.h` imports create synthetic package namespaces backed by libclang:
```bro
native :: import "../include/native.h"
main func() void {
_ = native.imported_add(20, 22)
}
```
Header imports expose supported external functions, typedefs, C scalars, fixed
arrays, complete plain structs and unions, function pointer typedefs, and
pointers to opaque records. C `void*` imports as nullable `anyopaque` pointers.
Plain records can be constructed with keyed
literals, accessed by field, and passed or returned by value through fixed C
signatures on `aarch64-macos`. Unsupported or incomplete records remain
pointer-only. Header imports never add linker inputs; implementations must still
be supplied explicitly with the C-prefixed linking options. Set
`BROLANG_LIBCLANG_PATH` when libclang is not installed in a standard location.
For offline bindings, `brolang --translate-c stdio.h` resolves standard C
headers through the Zig libc headers used by the backend. Multiple headers can
be generated into one deduplicated package:
```sh
brolang --translate-c stdio.h stdlib.h unistd.h fcntl.h errno.h --output-dir ffi/c
```
```bro
native :: import "../include/native.h"
pair native.Pair :: native.echo_pair(native.Pair { left = 20, right = 22 })
choice native.Choice :: native.Choice { integer = 42 }
```
Concrete `c_func` declarations and definitions can be passed to C function
pointer parameters. Imported C callback typedefs are nullable, so calling one
from Brolang requires an explicit unwrap:
```bro
native :: import "../include/native.h"
double c_func(value c_int) c_int {
return value + value
}
call_mapper func(mapper native.Imported_Mapper) c_int {
return mapper?(21)
}
```
Native Brolang function pointer values use `@func(...) R`, with fallible
channels written on the result:
```bro
call func(callback @func(value i32) i32, value i32) i32 {
return callback(value)
}
```
Bare `func(...) R` and `c_func(...) R` values are comptime-only declaration
identities. They implicitly materialize compatible pointers in runtime pointer
contexts; pointers do not convert back to bare identities. Aggregates containing
bare identities are likewise comptime-only.
Bodyless manual and imported C functions may be variadic:
```bro
log_values c_func(tag c_int, ...) c_int
```
Zero-terminated byte strings can be passed directly to immutable C character
pointers without making `u8` and `c_char` generally interchangeable:
```bro
printf c_func(format *c_char, ...) c_int
main func() void {
_ = printf("answer: %d\n", 42)
}
```
Arguments after `...` accept concrete scalars, pointers, and nullable pointers.
Narrow integers are promoted to the target C `int` or `unsigned int`, and
`f32`/`c_float` are promoted to `c_double`. Arrays, slices, structs, and other
compound values must be converted to an explicit C-compatible representation
before the call.
Compilation phases are isolated under `compiler/`:
```text
package loader -> per-file lexer/parser/AST -> checker/HIR -> lower/IR -> opt -> LLVM -> zig cc
```
Source diagnostics do not block executable generation. When recovery is
possible, errors lower to runtime diagnostic traps; warnings do not trap. Any
source diagnostic makes the compiler return status `1`. Infrastructure or
backend failures return status `2`.
Top-level function bodies are semantically checked lazily when a concrete
specialization is demanded.
Every immediate `.bro` or `.hon` file in the input directory belongs to the root
package. Imports are relative directory paths and are local to the file that
declares them. Imports beginning with `@` resolve from the project root:
```bro
import "../math"
other_math :: import "../math"
mem :: import "@std/mem"
value :: math.sum(other_math.value, 1)
```
Top-level declarations are public by default. Prefix a declaration with bare `@hide`
to make it package-local, or spell the scope explicitly with `@hide:package` or
`@hide:file`. The qualifier is contextual, so `hide`, `package`, and `file` remain
available as identifiers. Imports are always file-local and cannot use visibility
qualifiers or be re-exported:
```bro
@hide
shared_helper func() i32 { return 42 }
@hide:file
implementation_detail func() i32 { return shared_helper() }
```
Current prototype features:
- Newline-terminated, multiline statements; `}` may terminate a block's final statement
- `#` comments
- Immutable inferred/typed `::` bindings, mutable inferred/typed `:=` locals/globals, and `_` sinks
- Exact-width signed/unsigned integers, `f32`, `f64`, `isize`, `usize`, and loose integer-constrained `int`
- Target-dependent atomic `c_*` primitive types, `c_func`, complete `c_struct`, `opaque`, `anyopaque`, and V1 `ptrcast!(T, ptr)`
- Arrays, sentinel arrays, single-item pointers, many-item pointers, sentinel many-item pointers, slices, sentinel slices, strings, character literals, optionals, and native structs
- String literals as immutable pointers to static zero-terminated byte arrays
- Pointer-preserving `.ptr`/`.len`, pointer-to-array indexing and slicing, postfix pointer dereference and optional unwrap, and keyed struct literals
- Contextual integer constants and typed compile-time evaluation of arithmetic and Zig-style bitwise expressions
- Integer `~`, `&`, `|`, `xor`, guarded `<<` / `>>`, saturating `<<|`, and their compound assignments; postfix `^` remains pointer dereference
- Scalar-backed nominal `distinct` types with same-identity runtime/comptime operators, explicit backing extraction casts, integer bounds/indexing, reflection, and recursive standard formatting
- Directory packages with merged declarations and file-local relative imports
- Relative C header imports as synthetic package namespaces
- Plain imported C structs/unions, fixed arrays, and C function pointer typedefs, including keyed literals, field access, callbacks, and Apple Silicon by-value ABI lowering
- Qualified imported globals and functions with package-aware symbol mangling
- Demand-monomorphized Brolang and C-ABI functions
- Recursively stable comptime values—including booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, tagged unions, and bare function identities—may be interleaved with runtime parameters, are erased from the ABI, and specialize from explicit arguments, declaration identity, or exact inference provenance
- Forced typed comptime expressions (`$sum(1, 2)`, `$Point { x = 1, y = 2 }`) and comptime value blocks (`${ yield 4 }`)
- Zig-style comptime type factories returning anonymous native structs (`Box func($T type) type`, used as `Box(i32)`)
- Comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`/`errdefer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values
- Comptime-only native and C function identities (`func(...) R`, `c_func(...) R`) with structural comptime-only propagation through aggregates
- Native function pointer values and types (`@func(...) R`, `@func(...) R ! E`, `?@func(...) R`) with implicit bare-to-pointer materialization
- Typed allocation/reallocation through `std/mem` and generic dynamic arrays through `std/arraylist`
- Bodyless concrete C function declarations with exact external symbol names
- Bodyless manual and imported C variadic declarations with default argument promotions
- Ordered linking of additional C sources, objects, archives, and libraries
- Checked signed addition and unary negation
- Float-only `/` plus explicit `divtrunc!`, `divfloor!`, `divexact!`, `divceil!`, `rem!`, and `mod!` scalar intrinsics
- Runtime/comptime typed `memcopy!` and `memset!` over slices and pointers-to-arrays, with checked lengths and overlap
- Static, eager runtime, mutable runtime, and deferred problematic globals
- Runtime diagnostics followed by `llvm.trap`
See [LANGUAGE.md](LANGUAGE.md) for the concise implemented and planned language
feature ledger, and [TODO.md](TODO.md) for the implementation roadmap.
Compiler exit statuses:
- `0`: executable produced without source diagnostics
- `1`: executable produced with source diagnostics; errors may embed traps, warnings do not
- `2`: executable could not be produced