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
|
`-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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(
|
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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