Files
brolang/README.md
T
2026-07-18 00:12:12 +02:00

10 KiB

brolang

Prototype error-tolerant Brolang compiler written in Odin.

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.

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:

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:

./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 root/build.bro and compiles the program package it names. Without root, it searches the current directory and parents for the nearest build.bro. Build outputs are written to root/build/<name>.

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 build.bro. 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.bro, writes build/<name>-test, and reuses its C link inputs, libraries, include paths, and defines.

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:

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:

brolang --translate-c stdio.h stdlib.h unistd.h fcntl.h errno.h --output-dir ffi/c
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:

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:

call func(callback *func(value i32) i32, value i32) i32 {
	return callback(value)
}

Bodyless manual and imported C functions may be variadic:

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:

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/:

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:

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 hide to keep it local to its source file; leading underscores have no visibility meaning. Imports are always file-local and cannot be hidden or re-exported:

hide helper func() i32 { return 42 }
hide State :: struct { value i32 }

Current prototype features:

  • Newline-terminated, multiline statements; } may terminate a block's final statement
  • # comments
  • Immutable :: bindings, typed mutable = 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 compile-time folding of addition and unary negation trees
  • 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, and tagged unions—may be interleaved with runtime parameters, are erased from the ABI, and specialize from explicit arguments 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
  • Native function pointer values and types (*func(...) R, *func(...) R ! E, ?*func(...) R)
  • 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
  • Static, eager runtime, mutable runtime, and deferred problematic globals
  • Runtime diagnostics followed by llvm.trap

See LANGUAGE.md for the concise implemented and planned language feature ledger, and 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