build system (first pass)

This commit is contained in:
2026-07-04 16:23:24 +02:00
parent 4ebe9c90e9
commit cdde49e68b
15 changed files with 1914 additions and 1 deletions
+26
View File
@@ -30,6 +30,32 @@ invocation in command-line order:
`-l<name>`. `--c-include-path <dir>` and `--c-define <name[=value]>` configure `-l<name>`. `--c-include-path <dir>` and `--c-define <name[=value]>` configure
C preprocessing. C preprocessing.
Instead of passing these on the command line, a project can describe its build
in Brolang itself. `brolang build [root]` (root defaults to the current
directory) reads a `config` constant from `root/build.bro` and compiles the
program package it names:
```bro
b :: import "@std/build"
config :: b.BuildConfig{
name = "manual",
source = "src",
libraries = &[],
lib_paths = &[],
includes = &[],
defines = &[],
links = &["examples/build/manual/native.c"],
}
```
`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.
Relative `.h` imports create synthetic package namespaces backed by libclang: Relative `.h` imports create synthetic package namespaces backed by libclang:
```bro ```bro
+10 -1
View File
@@ -710,7 +710,16 @@
- native function pointers are non-variadic v1; C variadic function pointers stay - native function pointers are non-variadic v1; C variadic function pointers stay
under `*c_func(...) R` under `*c_func(...) R`
28. brolang build system (requires comptime execution) 28. brolang build system — v0 shipped
- `brolang build [root]` reads a declarative `config` constant from
`root/build.bro` (importing `@std/build`'s `BuildConfig`) and compiles the
program package it names. The config is read from the checked HIR — build.bro
is never lowered — so it is literal-only.
- enabled `&<array literal>` (Zig's `&.{...}`): the literal is promoted to an
anonymous global whose address decays to a slice, so list fields like
`libraries = &["raylib"]` work; empty lists are `&[]`
- deferred: build graph / steps / caching, multiple artifacts, computed paths
(needs string building), struct field defaults to drop `&[]` on empty lists
## A word on multi-unwrap ## A word on multi-unwrap
+252
View File
@@ -0,0 +1,252 @@
package compiler
import "./checker"
import "./cimport"
import "./hir"
import "./linker"
import "./loader"
import "./source"
import "./symbol"
import "./target"
import "./types"
import "core:fmt"
import vmem "core:mem/virtual"
import "core:path/filepath"
import "core:strings"
// BuildConfig is the native, extracted form of std/build's BuildConfig: all
// strings are cloned into context.allocator so they outlive the build module's
// arena (freed at the end of run_build). Free with destroy_build_config.
BuildConfig :: struct {
output_name: string,
source_dir: string,
link_arguments: []linker.Argument,
c_options: cimport.Options,
}
destroy_build_config :: proc(cfg: ^BuildConfig) {
delete(cfg.output_name)
delete(cfg.source_dir)
for arg in cfg.link_arguments {
delete(arg.value)
}
delete(cfg.link_arguments)
for path in cfg.c_options.include_paths {
delete(path)
}
delete(cfg.c_options.include_paths)
for define in cfg.c_options.defines {
delete(define)
}
delete(cfg.c_options.defines)
}
// run_build implements `brolang build [root]`: it loads and type-checks
// `root/build.bro`, reads its `config` constant, and compiles the program
// package the config names. build.bro is only checked (never lowered/emitted),
// so the config is read straight from the HIR.
run_build :: proc(root: string) -> int {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
arena: vmem.Arena
if err := vmem.arena_init_growing(&arena); err != nil {
fmt.eprintln("failed to initialize build arena:", err)
return 2
}
defer vmem.arena_destroy(&arena)
a := vmem.arena_allocator(&arena)
ast_module, loaded := loader.load(root, &sources, &diagnostics, &symbols, a, a, cimport.Options{}, target.DEFAULT)
if !loaded {
source.print_all(&diagnostics)
fmt.eprintln("failed to load build root:", root)
return 2
}
// check needs no `main`: it synthesizes a trap main and emits one benign
// "missing main" diagnostic, which is expected for build.bro. Suppress that
// one but surface any real errors in build.bro (and fail on them).
hir_module := checker.check(&ast_module, &diagnostics, &symbols, target.DEFAULT, a)
if build_bro_has_errors(&diagnostics) {
source.print_all(&diagnostics)
return 2
}
cfg, ok := extract_build_config(&hir_module, &symbols)
if !ok {
return 2
}
defer destroy_build_config(&cfg)
program := filepath.join({root, cfg.source_dir})
defer delete(program)
return compile_package(program, cfg.output_name, cfg.link_arguments, target.DEFAULT, cfg.c_options)
}
// build_bro_has_errors reports whether checking build.bro produced any diagnostic
// other than the benign "missing or unusable main function" (build.bro has no
// main by design; that one is emitted with an empty span).
build_bro_has_errors :: proc(diagnostics: ^source.Diagnostics) -> bool {
for item in diagnostics.items {
if item.span == (source.Span{}) && item.message == "missing or unusable main function" {
continue
}
return true
}
return false
}
// extract_build_config finds the top-level `config` constant and reads its
// BuildConfig{...} fields out of the HIR. All returned strings are cloned into
// context.allocator.
extract_build_config :: proc(m: ^hir.Module, symbols: ^symbol.Table) -> (BuildConfig, bool) {
config_id := symbol.intern(symbols, "config")
config_expr := hir.INVALID_EXPR
found := false
for g in m.globals {
if g.name == config_id {
config_expr = g.expr
found = true
break
}
}
if !found {
fmt.eprintln("build.bro: missing top-level 'config' constant")
return {}, false
}
root := unwrap_coercions(m, config_expr)
if root == hir.INVALID_EXPR || m.exprs[root].kind != .Struct {
fmt.eprintln("build.bro: 'config' must be a BuildConfig{...} literal")
return {}, false
}
args := m.exprs[root].args
fields := types.fields_for(&m.types, m.exprs[root].type)
cfg: BuildConfig
links: [dynamic]linker.Argument
includes: [dynamic]string
defines: [dynamic]string
for field, i in fields {
if i >= len(args) {
break
}
switch symbol.resolve(symbols, symbol.Id(field.name)) {
case "name":
if s, sok := read_string(m, args[i]); sok {
cfg.output_name = strings.clone(s)
}
case "source":
if s, sok := read_string(m, args[i]); sok {
cfg.source_dir = strings.clone(s)
}
case "libraries":
list := read_string_list(m, args[i])
for v in list {
append(&links, linker.Argument{kind = .Library, value = strings.clone(v)})
}
delete(list)
case "lib_paths":
list := read_string_list(m, args[i])
for v in list {
append(&links, linker.Argument{kind = .Library_Path, value = strings.clone(v)})
}
delete(list)
case "links":
list := read_string_list(m, args[i])
for v in list {
append(&links, linker.Argument{kind = .Input, value = strings.clone(v)})
}
delete(list)
case "includes":
list := read_string_list(m, args[i])
for v in list {
append(&includes, strings.clone(v))
}
delete(list)
case "defines":
list := read_string_list(m, args[i])
for v in list {
append(&defines, strings.clone(v))
}
delete(list)
}
}
cfg.link_arguments = links[:]
cfg.c_options.include_paths = includes[:]
cfg.c_options.defines = defines[:]
if len(cfg.output_name) == 0 || len(cfg.source_dir) == 0 {
fmt.eprintln("build.bro: config requires non-empty 'name' and 'source'")
destroy_build_config(&cfg)
return {}, false
}
return cfg, true
}
// unwrap_coercions strips implicit coercion wrappers (each stores its inner
// expr in `.left`) to reach the underlying value expression.
unwrap_coercions :: proc(m: ^hir.Module, id: hir.Expr_Id) -> hir.Expr_Id {
cur := id
for cur != hir.INVALID_EXPR && int(cur) < len(m.exprs) {
#partial switch m.exprs[cur].kind {
case .Retype, .Weaken_Slice, .Weaken_Pointer, .Decay_Array_Pointer, .Slice_Ptr,
.Widen, .Sum_Widen, .Optional_Some, .C_Coerce, .Scalar_Cast:
cur = m.exprs[cur].left
case:
return cur
}
}
return cur
}
read_string :: proc(m: ^hir.Module, id: hir.Expr_Id) -> (string, bool) {
e := unwrap_coercions(m, id)
if e == hir.INVALID_EXPR || m.exprs[e].kind != .String {
return "", false
}
sid := m.exprs[e].integer
if sid < 0 || int(sid) >= len(m.strings) {
return "", false
}
return m.strings[int(sid)], true
}
// read_string_list reads a `&[...]` list field: the value is an address of an
// anonymous global array (see checker `&<array literal>` promotion), whose
// elements are strings. Returned strings alias m.strings; callers clone them.
// The returned slice is owned by the caller (delete it).
read_string_list :: proc(m: ^hir.Module, id: hir.Expr_Id) -> []string {
addr := unwrap_coercions(m, id)
if addr == hir.INVALID_EXPR || m.exprs[addr].kind != .Address {
return nil
}
g := unwrap_coercions(m, m.exprs[addr].left)
if g == hir.INVALID_EXPR || m.exprs[g].kind != .Global {
return nil
}
gid := hir.as_global(m.exprs[g].target)
if gid == hir.INVALID_GLOBAL || int(gid) >= len(m.globals) {
return nil
}
arr := m.globals[gid].expr
if arr == hir.INVALID_EXPR || m.exprs[arr].kind != .Array {
return nil
}
elems := m.exprs[arr].args
out := make([]string, len(elems))
for a, i in elems {
s, ok := read_string(m, a)
if !ok {
delete(out)
return nil
}
out[i] = s
}
return out
}
+51
View File
@@ -152,6 +152,10 @@ Checker :: struct {
infer_stack: [dynamic]Infer_Frame, infer_stack: [dynamic]Infer_Frame,
build_stack: [dynamic]Build_Expr_Frame, build_stack: [dynamic]Build_Expr_Frame,
cycle_stack: [dynamic]Cycle_Frame, cycle_stack: [dynamic]Cycle_Frame,
// Anonymous globals synthesized for `&<array literal>` (Zig's `&.{...}`). Staged
// here during global/function building and flushed into module.globals AFTER
// build_globals, so the 1:1 module.globals <-> ast.globals index identity holds.
anon_globals: [dynamic]hir.Global,
main_symbol: symbol.Id, main_symbol: symbol.Id,
sink_symbol: symbol.Id, sink_symbol: symbol.Id,
type_symbol: symbol.Id, type_symbol: symbol.Id,
@@ -3792,6 +3796,46 @@ build_compound_expr :: proc(
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
case .Address: case .Address:
// `&<array literal>` (Zig's `&.{...}`): the operand is an rvalue with no
// address, so promote it to an anonymous global constant and take *its*
// address. Reuses the existing non-scalar-global storage path; only the
// stable global address enters the expression, so it never dangles. The
// resulting `*[N]T` then decays to a slice via the usual coercion.
if expr.left != ast.INVALID_EXPR && checker.ast_module.exprs[expr.left].kind == .Array {
operand := checker.ast_module.exprs[expr.left]
// Propagate an element-expected type through `&` so literal elements
// coerce to the target slice's element type (e.g. string -> []u8).
// Without this, `&["x"]` infers `*[1]*[N:0]u8`, which won't decay to
// `[][]u8` because can_decay_array_pointer requires child equality.
element := types.INVALID
if node, ok := types.node(store, expected); ok && (node.kind == .Slice || node.kind == .Array) {
element = node.child
}
synth_expected := types.INVALID
if types.is_valid(element) {
synth_expected = types.array(store, element, u64(len(operand.args)), false)
}
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, synth_expected, pkg, file)
array_type := checker.module.exprs[value].type
hidden_id := hir.Global_Id(len(checker.ast_module.globals) + len(checker.anon_globals))
append(&checker.anon_globals, hir.Global{
name = symbol.intern(checker.symbols, "__anon.array"),
type = array_type,
expr = value,
writable = false,
external = false,
diagnostic = source.INVALID_DIAGNOSTIC,
})
add_unique_global(global_reads, hidden_id)
global_ref := add_hir_expr(checker, hir.Expr{
kind=.Global, span=expr.span, type=array_type, target=hir.global_ref(hidden_id),
left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
return add_hir_expr(checker, hir.Expr{
kind=.Address, span=expr.span, type=types.pointer(store, array_type, false, false),
left=global_ref, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
if !hir_is_location(checker, value) { if !hir_is_location(checker, value) {
id := source.add(checker.diagnostics, expr.span, "'&' requires an addressable location") id := source.add(checker.diagnostics, expr.span, "'&' requires an addressable location")
@@ -8087,6 +8131,7 @@ check :: proc(
checker.infer_stack.allocator = allocator checker.infer_stack.allocator = allocator
checker.build_stack.allocator = allocator checker.build_stack.allocator = allocator
checker.cycle_stack.allocator = allocator checker.cycle_stack.allocator = allocator
checker.anon_globals.allocator = allocator
build_symbol_indexes(&checker) build_symbol_indexes(&checker)
checker.global_types = make([]types.Type, len(ast_module.globals), allocator) checker.global_types = make([]types.Type, len(ast_module.globals), allocator)
checker.global_demands = make([]types.Type, len(ast_module.globals), allocator) checker.global_demands = make([]types.Type, len(ast_module.globals), allocator)
@@ -8167,6 +8212,12 @@ check :: proc(
for index := 0; index < len(checker.specs); index += 1 { for index := 0; index < len(checker.specs); index += 1 {
build_function(&checker, spec_id(index)) build_function(&checker, spec_id(index))
} }
// Flush anonymous globals synthesized for `&<array literal>`. Appended only now
// (after every ast global was built at its identity-mapped index) so their ids,
// pre-assigned as len(ast.globals)+stage_index, land exactly.
for anon in checker.anon_globals {
append(&checker.module.globals, anon)
}
propagate_global_reads(&checker) propagate_global_reads(&checker)
main_template := find_template(&checker, checker.main_symbol, 0) main_template := find_template(&checker, checker.main_symbol, 0)
+25
View File
@@ -2551,6 +2551,31 @@ valid_program_compiles_and_runs :: proc(t: ^testing.T) {
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
} }
@(test)
build_command_is_recognized :: proc(t: ^testing.T) {
testing.expect(t, is_build_command("build"))
testing.expect(t, !is_build_command("translate-c"))
testing.expect(t, !is_build_command("--build"))
}
@(test)
build_subcommand_compiles_and_runs :: proc(t: ^testing.T) {
defer _ = os.remove("hello")
status := compiler_core.run_build("examples/build/hello")
testing.expect_value(t, status, 0)
state := run_executable("./hello")
testing.expect_value(t, state.exit_code, 0)
}
@(test)
build_subcommand_links_c_source_via_list_field :: proc(t: ^testing.T) {
defer _ = os.remove("manual_build")
status := compiler_core.run_build("examples/build/manual")
testing.expect_value(t, status, 0)
state := run_executable("./manual_build")
testing.expect_value(t, state.exit_code, 42)
}
@(test) @(test)
c_printf_accepts_a_string_literal :: proc(t: ^testing.T) { c_printf_accepts_a_string_literal :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-printf" output := "/tmp/brolang-test-printf"
+11
View File
@@ -0,0 +1,11 @@
b :: import "@std/build"
config :: b.BuildConfig{
name = "hello",
source = "src",
libraries = &[],
lib_paths = &[],
includes = &[],
defines = &[],
links = &[],
}
+3
View File
@@ -0,0 +1,3 @@
main func() i32 {
return 0
}
+11
View File
@@ -0,0 +1,11 @@
b :: import "@std/build"
config :: b.BuildConfig{
name = "manual_build",
source = "src",
libraries = &[],
lib_paths = &[],
includes = &[],
defines = &[],
links = &["examples/build/manual/native.c"],
}
+3
View File
@@ -0,0 +1,3 @@
int foreign_add(int a, int b) {
return a + b;
}
+5
View File
@@ -0,0 +1,5 @@
foreign_add c_func(a, b i32) i32
main func() i32 {
return foreign_add(20, 22)
}
+18
View File
@@ -0,0 +1,18 @@
# Illustrative build.bro for a raylib program (not run in CI: needs raylib
# installed and the game sources under ./game).
#
# Generate the raylib bindings once with:
# brolang translate-c /opt/homebrew/Cellar/raylib/*/include/raylib.h > game/raylib.bro
# then:
# brolang build examples/build/raylib
b :: import "@std/build"
config :: b.BuildConfig{
name = "game",
source = "source",
libraries = &["raylib"],
lib_paths = &["/opt/homebrew/lib"],
includes = &["/opt/homebrew/include"],
defines = &[],
links = &["-framework", "Cocoa", "-framework", "IOKit", "-framework", "CoreVideo", "-framework", "OpenGL"],
}
+250
View File
@@ -0,0 +1,250 @@
# Bouncing-shapes sandbox — a tour of brolang on top of raylib.
#
# Click to spawn a shape under the cursor, WASD/arrows to blow them around,
# SPACE to clear. The shape nearest the cursor is highlighted with its stats.
#
# Feature tour: native structs, enums, tagged unions + match (value, statement,
# payload capture, void variants, contextual construction), optionals + unwrap,
# value-loops (`yield :blk`), fallible functions with try/catch, defer, ranged
# and pointer-capturing for-loops, while, break/continue, compound assignment,
# scalar C interop, and multi-line strings.
rl :: import "@vendor/raylib"
# --- screen and physics constants -------------------------------------------
W :: 900
H :: 540
CAP usize :: 64
GRAV :: 0.18 # downward pull per frame
DAMP :: 0.82 # energy kept on a wall bounce
FORCE :: 0.9 # wind impulse from a key press
SPINMAX :: 3.0 # spin magnitude cap
RING_PAD :: 6.0 # highlight ring spacing
# --- shapes -----------------------------------------------------------------
Kind :: enum {
circle
square
triangle
}
Ball :: struct {
x f32
y f32
dx f32
dy f32
radius f32
kind Kind
}
# A frame's worth of player intent, as a tagged union. Each arm carries exactly
# the data that action needs (or `void` when it needs none).
Command :: union(enum) {
spawn Vector2 # spawn a shape at this point
push struct { dx f32, dy f32 } # blow every shape this way
clear void
idle void
}
# Spawning can fail when the backing array is full; the error carries the cap so
# the caller can report it.
SpawnError :: union(enum) {
full struct { cap usize }
}
# value-match used as an expression source: each arm yields a Color.
color_for func(k Kind) Color {
col :: match k {
.circle: Color{ r = 235, g = 90, b = 90, a = 255 }
.square: Color{ r = 90, g = 205, b = 130, a = 255 }
.triangle: Color{ r = 105, g = 160, b = 245, a = 255 }
}
return col
}
# value-match dispatching to a contextual return, used to cycle spawn kind.
next_kind func(k Kind) Kind {
return match k {
.circle: .square
.square: .triangle
.triangle: .circle
}
}
# Read this frame's input into a single Command (contextual union construction:
# `.clear`, `.spawn{...}`, `.push{...}` are built against the return type).
read_command func() Command {
if rl.IsMouseButtonPressed(MOUSE_BUTTON_LEFT) return .spawn{ GetMousePosition() }
if rl.IsKeyPressed(KEY_SPACE) return .clear
fx f32 = 0.0
fy f32 = 0.0
if rl.IsKeyDown(KEY_A) fx -= FORCE
if rl.IsKeyDown(KEY_D) fx += FORCE
if rl.IsKeyDown(KEY_W) fy -= FORCE
if rl.IsKeyDown(KEY_S) fy += FORCE
if rl.IsKeyDown(KEY_LEFT) fx -= FORCE
if rl.IsKeyDown(KEY_RIGHT) fx += FORCE
if rl.IsKeyDown(KEY_UP) fy -= FORCE
if rl.IsKeyDown(KEY_DOWN) fy += FORCE
moved :: fx != 0.0 or fy != 0.0
if (moved) return .push{ dx = fx, dy = fy }
return .idle
}
# Fallible capacity check: returns the slot index to fill, or fails `.full`.
reserve func(used usize) usize ! SpawnError {
if (used >= CAP) return .full{ cap = CAP }
return used
}
# Advance one ball: gravity, integrate, bounce off the four walls with damping.
# `b` is a pointer into the array, so the writes land in place.
step func(b @mut Ball) void {
b.dy += GRAV
b.x += b.dx
b.y += b.dy
if (b.x < b.radius) {
b.x = b.radius
b.dx = -b.dx * DAMP
}
right :: f32(W) - b.radius
if (b.x > right) {
b.x = right
b.dx = -b.dx * DAMP
}
if (b.y < b.radius) {
b.y = b.radius
b.dy = -b.dy * DAMP
}
floor :: f32(H) - b.radius
if (b.y > floor) {
b.y = floor
b.dy = -b.dy * DAMP
}
}
draw_ball func(b @Ball, highlight bool) void {
col :: color_for(b.kind)
center Vector2 = Vector2{ x = b.x, y = b.y }
match b.kind {
.circle: rl.DrawCircleV(center, b.radius, col)
.square: rl.DrawPoly(center, 4, b.radius, 45.0, col)
.triangle: rl.DrawPoly(center, 3, b.radius, 0.0, col)
}
if highlight {
ring rl.Color = rl.Color{ r = 250, g = 245, b = 200, a = 255 }
rl.DrawPoly(center, 24, b.radius + RING_PAD, 0.0, ring)
}
}
main func() i32 {
rl.SetConfigFlags(FLAG_MSAA_4X_HINT)
rl.InitWindow(W, H, "brolang — bouncing shapes")
defer rl.CloseWindow() # runs on every exit path out of main
rl.SetTargetFPS(60)
help ::
`[click] spawn a shape [WASD/arrows] blow wind
`[space] clear
balls [CAP]mut Ball = undefined
count usize = 0 # number of live balls, in slots 0..count
kc Kind = .circle # next kind to spawn
spin f32 = 1.0 # rotates spawn velocity for variety
at_cap bool = false # show the "at capacity" banner
bg :: rl.Color{ r = 24, g = 26, b = 34, a = 255 }
text :: rl.Color{ r = 225, g = 225, b = 230, a = 255 }
warn :: rl.Color{ r = 245, g = 180, b = 90, a = 255 }
while !rl.WindowShouldClose() {
# --- input -> command -----------------------------------------------
cmd :: read_command()
match cmd {
.spawn |at|: {
slot :: reserve(count) catch |e| {
match e {
.full |info|: at_cap = true
}
yield CAP # sentinel: >= CAP means "didn't fit"
}
if (slot < CAP) {
balls[slot] = Ball{
x = f32(at.x), y = f32(at.y),
dx = FORCE * 6.0 * spin,
dy = -FORCE * 5.0,
radius = 18.0,
kind = kc,
}
count += 1
kc = next_kind(kc)
spin = -spin * 1.2
if (spin > SPINMAX or spin < -SPINMAX) spin = 1.0
at_cap = false
}
}
.push |f|: {
for (&balls) |@b, i| {
if (i >= count) break
b.dx += f.dx
b.dy += f.dy
}
}
.clear: {
count = 0
at_cap = false
}
.idle: {}
}
# --- physics --------------------------------------------------------
for (&balls) |@b, i| {
if (i >= count) break
step(b)
}
# --- which shape is under the cursor? (optional via a value-loop) ---
mouse :: rl.GetMousePosition()
sel :: for 0..(count) |i| blk: {
c Vector2 = Vector2{ x = balls[i].x, y = balls[i].y }
if rl.CheckCollisionPointCircle(mouse, c, balls[i].radius) yield :blk i
yield none
}
# --- draw -----------------------------------------------------------
rl.BeginDrawing()
rl.ClearBackground(bg)
for (&balls) |@b, i| {
if (i >= count) break
hot bool = false
if sel |s| {
if (s == i) hot = true # true only for the hovered ball
}
draw_ball(b, hot)
}
rl.DrawText(help, 16, 16, 20, text)
if sel |s| {
label :: match balls[s].kind {
.circle: "circle"
.square: "square"
.triangle: "triangle"
}
rl.DrawText(label, 16, H - 36, 20, text)
}
if (at_cap) rl.DrawText("at capacity", W - 170, 16, 20, warn)
rl.DrawFPS(W - 90, H - 28)
rl.EndDrawing()
}
return 0
}
File diff suppressed because it is too large Load Diff
+11
View File
@@ -92,12 +92,19 @@ print_usage :: proc() {
fmt.eprintln( fmt.eprintln(
" brolang translate-c|--translate-c <header.h> [--target aarch64-macos] [--c-include-path <dir> | --c-define <name[=value]>]...", " brolang translate-c|--translate-c <header.h> [--target aarch64-macos] [--c-include-path <dir> | --c-define <name[=value]>]...",
) )
fmt.eprintln(
" brolang build [root] (reads root/build.bro; root defaults to the current directory)",
)
} }
is_translate_c_command :: proc(arg: string) -> bool { is_translate_c_command :: proc(arg: string) -> bool {
return arg == "translate-c" || arg == "--translate-c" return arg == "translate-c" || arg == "--translate-c"
} }
is_build_command :: proc(arg: string) -> bool {
return arg == "build"
}
zig_lib_dir_from_env_output :: proc(text: string, allocator := context.allocator) -> (string, bool) { zig_lib_dir_from_env_output :: proc(text: string, allocator := context.allocator) -> (string, bool) {
rest := text rest := text
prefix := ".lib_dir = \"" prefix := ".lib_dir = \""
@@ -233,6 +240,10 @@ run_translate_c :: proc(args: []string) -> int {
} }
main :: proc() { main :: proc() {
if len(os2.args) >= 2 && is_build_command(os2.args[1]) {
root := os2.args[2] if len(os2.args) >= 3 else "."
os2.exit(compiler.run_build(root))
}
if len(os2.args) >= 2 && is_translate_c_command(os2.args[1]) { if len(os2.args) >= 2 && is_translate_c_command(os2.args[1]) {
os2.exit(run_translate_c(os2.args)) os2.exit(run_translate_c(os2.args))
} }
+17
View File
@@ -0,0 +1,17 @@
# Build configuration surface for `brolang build` (v0).
#
# A project's `build.bro` imports this module and declares a top-level constant
# named `config` of type `BuildConfig`. `brolang build [root]` type-checks
# build.bro, reads the config, and compiles the program package it names.
#
# Declarative and literal-only: one executable per build. List fields take an
# address-of an array literal (`&["raylib"]`); empty lists are written `&[]`.
BuildConfig :: struct {
name []u8 # output executable name
source []u8 # program package directory, relative to build.bro
libraries [][]u8 # library names to link (-l)
lib_paths [][]u8 # library search directories (-L)
includes [][]u8 # C include directories (-I)
defines [][]u8 # C preprocessor defines (name or name=value)
links [][]u8 # extra linker inputs (object/source files, -framework pairs)
}