build system (first pass)
This commit is contained in:
@@ -30,6 +30,32 @@ invocation in command-line order:
|
||||
`-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 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:
|
||||
|
||||
```bro
|
||||
|
||||
@@ -710,7 +710,16 @@
|
||||
- native function pointers are non-variadic v1; C variadic function pointers stay
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -152,6 +152,10 @@ Checker :: struct {
|
||||
infer_stack: [dynamic]Infer_Frame,
|
||||
build_stack: [dynamic]Build_Expr_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,
|
||||
sink_symbol: symbol.Id,
|
||||
type_symbol: symbol.Id,
|
||||
@@ -3792,6 +3796,46 @@ build_compound_expr :: proc(
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
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)
|
||||
if !hir_is_location(checker, value) {
|
||||
id := source.add(checker.diagnostics, expr.span, "'&' requires an addressable location")
|
||||
@@ -8087,6 +8131,7 @@ check :: proc(
|
||||
checker.infer_stack.allocator = allocator
|
||||
checker.build_stack.allocator = allocator
|
||||
checker.cycle_stack.allocator = allocator
|
||||
checker.anon_globals.allocator = allocator
|
||||
build_symbol_indexes(&checker)
|
||||
checker.global_types = 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 {
|
||||
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)
|
||||
|
||||
main_template := find_template(&checker, checker.main_symbol, 0)
|
||||
|
||||
@@ -2551,6 +2551,31 @@ valid_program_compiles_and_runs :: proc(t: ^testing.T) {
|
||||
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)
|
||||
c_printf_accepts_a_string_literal :: proc(t: ^testing.T) {
|
||||
output := "/tmp/brolang-test-printf"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
b :: import "@std/build"
|
||||
|
||||
config :: b.BuildConfig{
|
||||
name = "hello",
|
||||
source = "src",
|
||||
libraries = &[],
|
||||
lib_paths = &[],
|
||||
includes = &[],
|
||||
defines = &[],
|
||||
links = &[],
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
main func() i32 {
|
||||
return 0
|
||||
}
|
||||
@@ -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"],
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
int foreign_add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
foreign_add c_func(a, b i32) i32
|
||||
|
||||
main func() i32 {
|
||||
return foreign_add(20, 22)
|
||||
}
|
||||
@@ -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"],
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+1221
File diff suppressed because it is too large
Load Diff
@@ -92,12 +92,19 @@ print_usage :: proc() {
|
||||
fmt.eprintln(
|
||||
" 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 {
|
||||
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) {
|
||||
rest := text
|
||||
prefix := ".lib_dir = \""
|
||||
@@ -233,6 +240,10 @@ run_translate_c :: proc(args: []string) -> int {
|
||||
}
|
||||
|
||||
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]) {
|
||||
os2.exit(run_translate_c(os2.args))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user