package-type imports

This commit is contained in:
2026-06-10 00:08:33 +02:00
parent 087fdb45d5
commit d6e03b6f08
104 changed files with 1799 additions and 273 deletions
+12 -11
View File
@@ -1,13 +1,14 @@
brolang /brolang
# examples # examples
prototype /prototype
main_i32 /main_i32
cycle_used /cycle_used
cycle_unused /cycle_unused
invalid_transitive_used_global /invalid_transitive_used_global
invalid_transitive_unused_global /invalid_transitive_unused_global
runtime_global /runtime_global
missing_main /missing_main
mutable_local /mutable_local
overflow /overflow
/deep
+19 -3
View File
@@ -4,26 +4,42 @@ Prototype error-tolerant Brolang compiler written in Odin.
```sh ```sh
odin build . -out:brolang odin build . -out:brolang
./brolang examples/prototype.bro -o /tmp/prototype ./brolang examples/programs/prototype -o /tmp/prototype
/tmp/prototype /tmp/prototype
``` ```
Compilation phases are isolated under `compiler/`: Compilation phases are isolated under `compiler/`:
```text ```text
source -> lexer -> parser/AST -> checker/HIR -> lower/IR -> opt -> LLVM -> zig cc 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 Source diagnostics do not block executable generation. When recovery is
possible, invalid code lowers to runtime diagnostic traps and the compiler possible, invalid code lowers to runtime diagnostic traps and the compiler
returns status `1`. Infrastructure or backend failures return status `2`. returns 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` file in the input directory belongs to the root
package. Imports are relative directory paths and are local to the file that
declares them:
```bro
import "../math"
other_math :: import "../math"
value :: math.sum(other_math.value, 1)
```
Current prototype features: Current prototype features:
- Newline-terminated, multiline statements and `#` comments - Newline-terminated, multiline statements; `}` may terminate a block's final statement
- `#` comments
- Immutable `::` bindings, mutable function-local `=` bindings, and `_` sinks - Immutable `::` bindings, mutable function-local `=` bindings, and `_` sinks
- `i8`, `i16`, `i32`, `i64`, and loose integer-constrained `int` - `i8`, `i16`, `i32`, `i64`, and loose integer-constrained `int`
- Contextual integer constants and compile-time folding of literal addition trees - Contextual integer constants and compile-time folding of literal addition trees
- Directory packages with merged declarations and file-local relative imports
- Qualified imported globals and functions with package-aware symbol mangling
- Demand-monomorphized Brolang and C-ABI functions - Demand-monomorphized Brolang and C-ABI functions
- Checked signed addition - Checked signed addition
- Static, eager runtime, and deferred problematic globals - Static, eager runtime, and deferred problematic globals
+36 -14
View File
@@ -1,22 +1,44 @@
# "quick" fixes # "quick"/"easy" fixes
- for global initialization cycles, report also starting and ending lines - for global initialization cycles, report also starting and ending lines
- intern strings across all phases of the compiler and reference strings by their hash/id
- makes string comparison and equality checks faster and takes up less memory
# milestones # milestones
1. import system: 1. get c interop working:
- directory level package style like odin (and go?)
- globals in different files under same package are automatically merged into same namespace
- globals defined in different packages must be imported via their package
```
import "some_package" # automatically imports all public globals under `some_package`
other_name :: import "some_package" # imports all public globals under `some_package` into `other_name` namespace
some_package.hello # accesses the `hello` global from `some_package`
other_name.hello # accesses the `hello` global from `some_package` via `other_name` namespace
```
2. get c interop working:
- link with c / compile c code into binary alongside brolang code - link with c / compile c code into binary alongside brolang code
- create bindings from c headers - create bindings from c headers
- figure out how to represent variadic arguments
- proposal (from an older document):
```
# functions with variadic arguments
printf :: c func(fmt []u8, args ...) int # `...` is essentially an anonomous tuple type used in function arguments. `...` collects remaining arguments in a type inferred tuple (infer from anchor)
```
- notes on structs and tuples from same older document:
```
# structs carry only data, no methods, no behavior
Stuff :: struct {
first u32
second float
third [5]i32
}
# tuples are just structs without field names (accessed via index: ``some_tuple.0``, ``some_tuple.1``, etc.)
# anonomous tuple type inferred from literal and type anchors?
some_tuple :: { 4, "hello" }
```
- find out how this should co-exist with the import system - find out how this should co-exist with the import system
- maybe c header files should just be treated as individual packages
- that is also typically how they're used in c projects as they define an interface to a module (or package)
- that means that bindings should be automatically generated by the compiler when the user imports a c header file:
- `import "relative/path/to/some_header.h"` - wraps the c header file in a brolang package called `some_header`
- `other_header :: import "relative/path/to/some_header.h"` - wraps the c header file in a brolang package called `some_header` into `other_header` namespace
- implementation of the functions declared by the generated bindings is provided by the c implementation and requires the c implementation to be linked with the brolang binary
- this requires the ability to declare a "bare" function (as an interface) that is linked to the c implementation
```
# declare the bare extern function for c interop (notice only the signature is provided, not the implementation)
# this should only be allowed for extern functions, i.e. `c func`
extern_c_sum :: c func(a, b int) int
```
+44
View File
@@ -26,6 +26,7 @@ Expr_Kind :: enum {
Expr :: struct { Expr :: struct {
kind: Expr_Kind, kind: Expr_Kind,
span: source.Span, span: source.Span,
qualifier: string,
text: string, text: string,
integer: i64, integer: i64,
left: int, left: int,
@@ -61,6 +62,8 @@ Stmt :: struct {
Function :: struct { Function :: struct {
span: source.Span, span: source.Span,
name: string, name: string,
pkg: int,
file: int,
c_abi: bool, c_abi: bool,
params: []Param, params: []Param,
result: Type_Syntax, result: Type_Syntax,
@@ -71,17 +74,45 @@ Function :: struct {
Global :: struct { Global :: struct {
span: source.Span, span: source.Span,
name: string, name: string,
pkg: int,
file: int,
type: Type_Syntax, type: Type_Syntax,
immutable: bool, immutable: bool,
expr: int, expr: int,
diagnostic: int, diagnostic: int,
} }
Import :: struct {
span: source.Span,
alias: string,
path: string,
pkg: int,
file: int,
target: int,
valid: bool,
used: bool,
diagnostic: int,
}
File :: struct {
source: int,
pkg: int,
}
Package :: struct {
path: string,
name: string,
available: bool,
}
Module :: struct { Module :: struct {
exprs: [dynamic]Expr, exprs: [dynamic]Expr,
statements: [dynamic]Stmt, statements: [dynamic]Stmt,
functions: [dynamic]Function, functions: [dynamic]Function,
globals: [dynamic]Global, globals: [dynamic]Global,
imports: [dynamic]Import,
files: [dynamic]File,
packages: [dynamic]Package,
allocator: mem.Allocator, allocator: mem.Allocator,
} }
@@ -92,6 +123,9 @@ init_module :: proc(allocator := context.allocator) -> Module {
module.statements.allocator = allocator module.statements.allocator = allocator
module.functions.allocator = allocator module.functions.allocator = allocator
module.globals.allocator = allocator module.globals.allocator = allocator
module.imports.allocator = allocator
module.files.allocator = allocator
module.packages.allocator = allocator
return module return module
} }
@@ -103,8 +137,18 @@ destroy_module :: proc(module: ^Module) {
delete(function.params, module.allocator) delete(function.params, module.allocator)
delete(function.body, module.allocator) delete(function.body, module.allocator)
} }
for import_item in module.imports {
delete(import_item.path, module.allocator)
}
for pkg in module.packages {
delete(pkg.path, module.allocator)
delete(pkg.name, module.allocator)
}
delete(module.exprs) delete(module.exprs)
delete(module.statements) delete(module.statements)
delete(module.functions) delete(module.functions)
delete(module.globals) delete(module.globals)
delete(module.imports)
delete(module.files)
delete(module.packages)
} }
+302 -176
View File
@@ -7,6 +7,7 @@ import "../types"
import "base:intrinsics" import "base:intrinsics"
import "core:fmt" import "core:fmt"
import "core:mem" import "core:mem"
import "core:slice"
import "core:strings" import "core:strings"
Spec :: struct { Spec :: struct {
@@ -40,14 +41,23 @@ Constant :: struct {
value: i128, value: i128,
} }
Symbol_Index_Entry :: struct {
scope: int,
name: string,
id: int,
}
Checker :: struct { Checker :: struct {
ast_module: ^ast.Module, ast_module: ^ast.Module,
diagnostics: ^source.Diagnostics, diagnostics: ^source.Diagnostics,
module: hir.Module, module: hir.Module,
specs: [dynamic]Spec, specs: [dynamic]Spec,
global_types: []types.Type, function_index: []Symbol_Index_Entry,
constants: []Constant, global_index: []Symbol_Index_Entry,
allocator: mem.Allocator, import_index: []Symbol_Index_Entry,
global_types: []types.Type,
constants: []Constant,
allocator: mem.Allocator,
} }
eval_constant :: proc(checker: ^Checker, expr_id: int) -> Constant { eval_constant :: proc(checker: ^Checker, expr_id: int) -> Constant {
@@ -116,22 +126,123 @@ type_from_syntax :: proc(value: ast.Type_Syntax) -> types.Type {
return types.INVALID return types.INVALID
} }
find_template :: proc(checker: ^Checker, name: string) -> int { symbol_index_less :: proc(left, right: Symbol_Index_Entry) -> bool {
for function, index in checker.ast_module.functions { if left.scope != right.scope {
if function.name == name { return left.scope < right.scope
return index }
if left.name != right.name {
return left.name < right.name
}
return left.id < right.id
}
find_symbol :: proc(index: []Symbol_Index_Entry, scope: int, name: string) -> int {
low := 0
high := len(index)
for low < high {
middle := low + (high-low)/2
entry := index[middle]
if entry.scope < scope || entry.scope == scope && entry.name < name {
low = middle + 1
} else {
high = middle
} }
} }
if low < len(index) && index[low].scope == scope && index[low].name == name {
return index[low].id
}
return -1 return -1
} }
find_global :: proc(checker: ^Checker, name: string) -> int { build_symbol_indexes :: proc(checker: ^Checker) {
for global, index in checker.ast_module.globals { checker.function_index = make([]Symbol_Index_Entry, len(checker.ast_module.functions), checker.allocator)
if global.name == name { for function, id in checker.ast_module.functions {
return index checker.function_index[id] = Symbol_Index_Entry{scope=function.pkg, name=function.name, id=id}
}
} }
return -1 slice.sort_by(checker.function_index, symbol_index_less)
checker.global_index = make([]Symbol_Index_Entry, len(checker.ast_module.globals), checker.allocator)
for global, id in checker.ast_module.globals {
checker.global_index[id] = Symbol_Index_Entry{scope=global.pkg, name=global.name, id=id}
}
slice.sort_by(checker.global_index, symbol_index_less)
checker.import_index = make([]Symbol_Index_Entry, len(checker.ast_module.imports), checker.allocator)
for import_item, id in checker.ast_module.imports {
checker.import_index[id] = Symbol_Index_Entry{scope=import_item.file, name=import_item.alias, id=id}
}
slice.sort_by(checker.import_index, symbol_index_less)
}
find_template :: proc(checker: ^Checker, name: string, pkg := 0) -> int {
return find_symbol(checker.function_index, pkg, name)
}
find_global :: proc(checker: ^Checker, name: string, pkg := 0) -> int {
return find_symbol(checker.global_index, pkg, name)
}
find_import :: proc(checker: ^Checker, file: int, alias: string, mark_used := false) -> int {
id := find_symbol(checker.import_index, file, alias)
if id >= 0 && mark_used {
checker.ast_module.imports[id].used = true
}
return id
}
expr_package :: proc(checker: ^Checker, expr: ast.Expr, pkg, file: int, mark_used := false) -> (int, bool) {
if expr.qualifier == "" {
return pkg, true
}
import_id := find_import(checker, file, expr.qualifier, mark_used)
if import_id < 0 {
return -1, false
}
import_item := checker.ast_module.imports[import_id]
if import_item.target < 0 || import_item.target >= len(checker.ast_module.packages) ||
!checker.ast_module.packages[import_item.target].available {
return import_item.target, false
}
return import_item.target, true
}
add_package_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, file: int) -> int {
if find_import(checker, file, expr.qualifier) < 0 {
return source.addf(checker.diagnostics, expr.span, "unknown package alias '%s'", expr.qualifier)
}
return source.addf(checker.diagnostics, expr.span, "unavailable imported package '%s'", expr.qualifier)
}
add_name_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: int) -> int {
if find_template(checker, expr.text, target_pkg) >= 0 {
return source.addf(checker.diagnostics, expr.span, "'%s' is a function, not a global value", expr.text)
}
if expr.qualifier != "" {
return source.addf(
checker.diagnostics,
expr.span,
"package '%s' has no member '%s'",
expr.qualifier,
expr.text,
)
}
return source.addf(checker.diagnostics, expr.span, "unresolved global '%s'", expr.text)
}
add_call_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: int) -> int {
if find_global(checker, expr.text, target_pkg) >= 0 {
return source.addf(checker.diagnostics, expr.span, "'%s' is a global, not a function", expr.text)
}
if expr.qualifier != "" {
return source.addf(
checker.diagnostics,
expr.span,
"package '%s' has no member '%s'",
expr.qualifier,
expr.text,
)
}
return source.addf(checker.diagnostics, expr.span, "unresolved function '%s'", expr.text)
} }
contains_name :: proc(names: []string, name: string) -> bool { contains_name :: proc(names: []string, name: string) -> bool {
@@ -143,41 +254,31 @@ contains_name :: proc(names: []string, name: string) -> bool {
return false return false
} }
validate_expr_names :: proc(checker: ^Checker, expr_id: int, locals: []string) { mark_expr_imports_used :: proc(checker: ^Checker, expr_id, file: int) {
if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) {
return return
} }
expr := checker.ast_module.exprs[expr_id] expr := checker.ast_module.exprs[expr_id]
switch expr.kind { switch expr.kind {
case .Name: case .Name:
if !contains_name(locals, expr.text) && find_global(checker, expr.text) < 0 { if expr.qualifier != "" {
source.addf(checker.diagnostics, expr.span, "unresolved name '%s'", expr.text) _ = find_import(checker, file, expr.qualifier, true)
} }
case .Call: case .Call:
if find_template(checker, expr.text) < 0 { if expr.qualifier != "" {
source.addf(checker.diagnostics, expr.span, "unresolved function '%s'", expr.text) _ = find_import(checker, file, expr.qualifier, true)
} }
for arg in expr.args { for arg in expr.args {
validate_expr_names(checker, arg, locals) mark_expr_imports_used(checker, arg, file)
} }
case .Add: case .Add:
validate_expr_names(checker, expr.left, locals) mark_expr_imports_used(checker, expr.left, file)
validate_expr_names(checker, expr.right, locals) mark_expr_imports_used(checker, expr.right, file)
case .Invalid, .Integer: case .Invalid, .Integer:
} }
} }
validate_templates :: proc(checker: ^Checker) { validate_declarations :: proc(checker: ^Checker) {
for global in checker.ast_module.globals {
if global.type == .Void {
source.add(
checker.diagnostics,
global.span,
"void is only valid as a function result type",
)
}
validate_expr_names(checker, global.expr, nil)
}
for function in checker.ast_module.functions { for function in checker.ast_module.functions {
locals: [dynamic]string locals: [dynamic]string
locals.allocator = checker.allocator locals.allocator = checker.allocator
@@ -202,36 +303,8 @@ validate_templates :: proc(checker: ^Checker) {
for statement_id in function.body { for statement_id in function.body {
statement := checker.ast_module.statements[statement_id] statement := checker.ast_module.statements[statement_id]
switch statement.kind { switch statement.kind {
case .Declaration: case .Declaration, .Assignment, .Return, .Expression:
validate_expr_names(checker, statement.expr, locals[:]) mark_expr_imports_used(checker, statement.expr, function.file)
if statement.type == .Void {
source.add(
checker.diagnostics,
statement.span,
"void is only valid as a function result type",
)
}
if contains_name(locals[:], statement.name) {
source.addf(
checker.diagnostics,
statement.span,
"duplicate local '%s'",
statement.name,
)
}
append(&locals, statement.name)
case .Assignment:
validate_expr_names(checker, statement.expr, locals[:])
if statement.name != "_" && !contains_name(locals[:], statement.name) {
source.addf(
checker.diagnostics,
statement.span,
"cannot assign unresolved local '%s'",
statement.name,
)
}
case .Return, .Expression:
validate_expr_names(checker, statement.expr, locals[:])
case .Invalid: case .Invalid:
} }
} }
@@ -268,6 +341,19 @@ specialized_param_type :: proc(syntax: ast.Type_Syntax, actual: types.Type) -> t
return declared return declared
} }
can_specialize :: proc(function: ast.Function, actual_args: []types.Type) -> bool {
for param, index in function.params {
actual := types.INVALID
if index < len(actual_args) {
actual = actual_args[index]
}
if !types.is_concrete_integer(specialized_param_type(param.type, actual)) {
return false
}
}
return true
}
ensure_spec :: proc(checker: ^Checker, template: int, actual_args: []types.Type) -> int { ensure_spec :: proc(checker: ^Checker, template: int, actual_args: []types.Type) -> int {
function := checker.ast_module.functions[template] function := checker.ast_module.functions[template]
signature: [dynamic]types.Type signature: [dynamic]types.Type
@@ -286,7 +372,7 @@ ensure_spec :: proc(checker: ^Checker, template: int, actual_args: []types.Type)
} }
} }
result := type_from_syntax(function.result) result := type_from_syntax(function.result)
if function.name == "main" && function.result == .Int { if function.pkg == 0 && function.name == "main" && function.result == .Int {
result = types.I32 result = types.I32
} }
index := len(checker.specs) index := len(checker.specs)
@@ -297,7 +383,7 @@ ensure_spec :: proc(checker: ^Checker, template: int, actual_args: []types.Type)
return index return index
} }
infer_expr :: proc(checker: ^Checker, expr_id: int, locals: []Infer_Local) -> types.Type { infer_expr :: proc(checker: ^Checker, expr_id: int, locals: []Infer_Local, pkg := 0, file := 0) -> types.Type {
if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) {
return types.INVALID return types.INVALID
} }
@@ -315,27 +401,49 @@ infer_expr :: proc(checker: ^Checker, expr_id: int, locals: []Infer_Local) -> ty
case .Integer: case .Integer:
return types.smallest_signed_for_literal(expr.integer) return types.smallest_signed_for_literal(expr.integer)
case .Name: case .Name:
local_type := find_infer_local(locals, expr.text) if expr.qualifier == "" {
if types.is_valid(local_type) { local_type := find_infer_local(locals, expr.text)
return local_type if types.is_valid(local_type) {
return local_type
}
} }
global := find_global(checker, expr.text) target_pkg, available := expr_package(checker, expr, pkg, file)
if !available {
return types.INVALID
}
global := find_global(checker, expr.text, target_pkg)
if global >= 0 { if global >= 0 {
return checker.global_types[global] return checker.global_types[global]
} }
return types.INVALID return types.INVALID
case .Add: case .Add:
left := infer_expr(checker, expr.left, locals) left := infer_expr(checker, expr.left, locals, pkg, file)
right := infer_expr(checker, expr.right, locals) right := infer_expr(checker, expr.right, locals, pkg, file)
return types.widest(left, right) return types.widest(left, right)
case .Call: case .Call:
template := find_template(checker, expr.text) target_pkg, available := expr_package(checker, expr, pkg, file)
if !available {
return types.INVALID
}
template := find_template(checker, expr.text, target_pkg)
if template < 0 { if template < 0 {
return types.INVALID return types.INVALID
} }
args := make([]types.Type, len(expr.args), checker.allocator) args := make([]types.Type, len(expr.args), checker.allocator)
for arg, index in expr.args { for arg, index in expr.args {
args[index] = infer_expr(checker, arg, locals) args[index] = infer_expr(checker, arg, locals, pkg, file)
}
function := checker.ast_module.functions[template]
if !can_specialize(function, args) {
delete(args, checker.allocator)
declared := type_from_syntax(function.result)
if function.pkg == 0 && function.name == "main" && function.result == .Int {
return types.I32
}
if declared.kind == .Concrete || declared.kind == .Void {
return declared
}
return types.INVALID
} }
spec := ensure_spec(checker, template, args) spec := ensure_spec(checker, template, args)
delete(args, checker.allocator) delete(args, checker.allocator)
@@ -348,7 +456,7 @@ infer_spec_result :: proc(checker: ^Checker, spec_id: int) -> types.Type {
spec := checker.specs[spec_id] spec := checker.specs[spec_id]
function := checker.ast_module.functions[spec.template] function := checker.ast_module.functions[spec.template]
declared := type_from_syntax(function.result) declared := type_from_syntax(function.result)
if function.name == "main" && function.result == .Int { if function.pkg == 0 && function.name == "main" && function.result == .Int {
declared = types.I32 declared = types.I32
} }
@@ -368,17 +476,17 @@ infer_spec_result :: proc(checker: ^Checker, spec_id: int) -> types.Type {
statement := checker.ast_module.statements[statement_id] statement := checker.ast_module.statements[statement_id]
#partial switch statement.kind { #partial switch statement.kind {
case .Declaration: case .Declaration:
value_type := infer_expr(checker, statement.expr, locals[:]) value_type := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file)
declared_local := type_from_syntax(statement.type) declared_local := type_from_syntax(statement.type)
if declared_local.kind == .Concrete { if declared_local.kind == .Concrete {
value_type = declared_local value_type = declared_local
} }
append(&locals, Infer_Local{name = statement.name, type = value_type}) append(&locals, Infer_Local{name = statement.name, type = value_type})
case .Assignment, .Expression: case .Assignment, .Expression:
_ = infer_expr(checker, statement.expr, locals[:]) _ = infer_expr(checker, statement.expr, locals[:], function.pkg, function.file)
case .Return: case .Return:
if statement.expr >= 0 { if statement.expr >= 0 {
returned := infer_expr(checker, statement.expr, locals[:]) returned := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file)
if !types.is_valid(result) { if !types.is_valid(result) {
result = returned result = returned
} else { } else {
@@ -393,6 +501,22 @@ infer_spec_result :: proc(checker: ^Checker, spec_id: int) -> types.Type {
return declared return declared
} }
merge_inferred_type :: proc(current: ^types.Type, inferred: types.Type) -> bool {
if !types.is_concrete_integer(inferred) {
return false
}
if !types.is_concrete_integer(current^) {
current^ = inferred
return true
}
merged := types.widest(current^, inferred)
if types.is_concrete_integer(merged) && !types.equal(current^, merged) {
current^ = merged
return true
}
return false
}
infer_all :: proc(checker: ^Checker) { infer_all :: proc(checker: ^Checker) {
for global, index in checker.ast_module.globals { for global, index in checker.ast_module.globals {
declared := type_from_syntax(global.type) declared := type_from_syntax(global.type)
@@ -400,40 +524,25 @@ infer_all :: proc(checker: ^Checker) {
checker.global_types[index] = declared checker.global_types[index] = declared
} }
} }
for _ in 0 ..< max(4, len(checker.ast_module.globals) + 1) {
changed := false
for global, index in checker.ast_module.globals {
if types.is_valid(checker.global_types[index]) {
continue
}
inferred := infer_expr(checker, global.expr, nil)
if types.is_valid(inferred) {
checker.global_types[index] = inferred
changed = true
}
}
if !changed {
break
}
}
main_template := find_template(checker, "main") main_template := find_template(checker, "main", 0)
if main_template >= 0 { if main_template >= 0 {
ensure_spec(checker, main_template, nil) ensure_spec(checker, main_template, nil)
} }
for global in checker.ast_module.globals {
_ = infer_expr(checker, global.expr, nil)
}
for _ in 0 ..< 64 { for {
changed := false changed := false
spec_count := len(checker.specs) spec_count := len(checker.specs)
for spec_id in 0 ..< spec_count { for global, index in checker.ast_module.globals {
inferred := infer_spec_result(checker, spec_id) if type_from_syntax(global.type).kind == .Concrete {
if types.is_valid(inferred) && !types.equal(checker.specs[spec_id].result, inferred) { continue
checker.specs[spec_id].result = inferred
changed = true
} }
inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file)
changed = merge_inferred_type(&checker.global_types[index], inferred) || changed
}
for spec_id := 0; spec_id < len(checker.specs); spec_id += 1 {
inferred := infer_spec_result(checker, spec_id)
changed = merge_inferred_type(&checker.specs[spec_id].result, inferred) || changed
} }
if len(checker.specs) != spec_count { if len(checker.specs) != spec_count {
changed = true changed = true
@@ -442,14 +551,6 @@ infer_all :: proc(checker: ^Checker) {
break break
} }
} }
for global, index in checker.ast_module.globals {
if type_from_syntax(global.type).kind != .Concrete {
inferred := infer_expr(checker, global.expr, nil)
if types.is_concrete_integer(inferred) {
checker.global_types[index] = inferred
}
}
}
} }
add_hir_expr :: proc(checker: ^Checker, expr: hir.Expr) -> int { add_hir_expr :: proc(checker: ^Checker, expr: hir.Expr) -> int {
@@ -589,6 +690,8 @@ build_expr :: proc(
global_reads: ^[dynamic]int, global_reads: ^[dynamic]int,
calls: ^[dynamic]int, calls: ^[dynamic]int,
expected := types.INVALID, expected := types.INVALID,
pkg := 0,
file := 0,
) -> int { ) -> int {
if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) {
id := source.add(checker.diagnostics, source.Span{}, "missing expression") id := source.add(checker.diagnostics, source.Span{}, "missing expression")
@@ -605,21 +708,28 @@ build_expr :: proc(
case .Integer: case .Integer:
unreachable() unreachable()
case .Name: case .Name:
if local, ok := find_build_local(locals, expr.text); ok { if expr.qualifier == "" {
return add_hir_expr( if local, ok := find_build_local(locals, expr.text); ok {
checker, return add_hir_expr(
hir.Expr { checker,
kind = .Local, hir.Expr {
span = expr.span, kind = .Local,
type = local.type, span = expr.span,
target = local.id, type = local.type,
left = -1, target = local.id,
right = -1, left = -1,
diagnostic = -1, right = -1,
}, diagnostic = -1,
) },
)
}
} }
global := find_global(checker, expr.text) target_pkg, available := expr_package(checker, expr, pkg, file, true)
if !available {
id := add_package_resolution_diagnostic(checker, expr, file)
return invalid_hir_expr(checker, expr.span, id)
}
global := find_global(checker, expr.text, target_pkg)
if global >= 0 { if global >= 0 {
add_unique(global_reads, global) add_unique(global_reads, global)
return add_hir_expr( return add_hir_expr(
@@ -635,11 +745,11 @@ build_expr :: proc(
}, },
) )
} }
id := source.addf(checker.diagnostics, expr.span, "unresolved name '%s'", expr.text) id := add_name_resolution_diagnostic(checker, expr, target_pkg)
return invalid_hir_expr(checker, expr.span, id) return invalid_hir_expr(checker, expr.span, id)
case .Add: case .Add:
left := build_expr(checker, expr.left, locals, global_reads, calls) left := build_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
right := build_expr(checker, expr.right, locals, global_reads, calls) right := build_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
left_type := checker.module.exprs[left].type left_type := checker.module.exprs[left].type
right_type := checker.module.exprs[right].type right_type := checker.module.exprs[right].type
result := types.widest(left_type, right_type) result := types.widest(left_type, right_type)
@@ -666,14 +776,14 @@ build_expr :: proc(
}, },
) )
case .Call: case .Call:
template := find_template(checker, expr.text) target_pkg, available := expr_package(checker, expr, pkg, file, true)
if !available {
id := add_package_resolution_diagnostic(checker, expr, file)
return invalid_hir_expr(checker, expr.span, id)
}
template := find_template(checker, expr.text, target_pkg)
if template < 0 { if template < 0 {
id := source.addf( id := add_call_resolution_diagnostic(checker, expr, target_pkg)
checker.diagnostics,
expr.span,
"unresolved function '%s'",
expr.text,
)
return invalid_hir_expr(checker, expr.span, id) return invalid_hir_expr(checker, expr.span, id)
} }
if len(expr.args) != len(checker.ast_module.functions[template].params) { if len(expr.args) != len(checker.ast_module.functions[template].params) {
@@ -701,6 +811,8 @@ build_expr :: proc(
global_reads, global_reads,
calls, calls,
arg_expected, arg_expected,
pkg,
file,
) )
arg_types[index] = checker.module.exprs[built_args[index]].type arg_types[index] = checker.module.exprs[built_args[index]].type
} }
@@ -750,24 +862,13 @@ build_expr :: proc(
make_link_name :: proc(checker: ^Checker, spec_id: int) -> string { make_link_name :: proc(checker: ^Checker, spec_id: int) -> string {
spec := checker.specs[spec_id] spec := checker.specs[spec_id]
function := checker.ast_module.functions[spec.template] function := checker.ast_module.functions[spec.template]
if function.name == "main" { if function.pkg == 0 && function.name == "main" {
return fmt.aprintf("main", allocator = checker.allocator) return fmt.aprintf("main", allocator = checker.allocator)
} }
has_generic_params := false
for param in function.params {
if param.type == .Int {
has_generic_params = true
break
}
}
if function.c_abi && !has_generic_params {
return fmt.aprintf("%s", function.name, allocator = checker.allocator)
}
builder := strings.builder_make(checker.allocator) builder := strings.builder_make(checker.allocator)
defer strings.builder_destroy(&builder) defer strings.builder_destroy(&builder)
if !function.c_abi { strings.write_string(&builder, "bro_c__" if function.c_abi else "bro__")
strings.write_string(&builder, "bro__") fmt.sbprintf(&builder, "p%d__", function.pkg)
}
strings.write_string(&builder, function.name) strings.write_string(&builder, function.name)
for arg in spec.args { for arg in spec.args {
strings.write_string(&builder, "__") strings.write_string(&builder, "__")
@@ -783,7 +884,7 @@ build_function :: proc(checker: ^Checker, spec_id: int) {
spec := checker.specs[spec_id] spec := checker.specs[spec_id]
function := checker.ast_module.functions[spec.template] function := checker.ast_module.functions[spec.template]
signature_diagnostic := -1 signature_diagnostic := -1
if !types.is_valid(spec.result) { if spec.result.kind != .Void && !types.is_concrete_integer(spec.result) {
checker.specs[spec_id].result = types.I64 checker.specs[spec_id].result = types.I64
spec.result = types.I64 spec.result = types.I64
signature_diagnostic = source.addf( signature_diagnostic = source.addf(
@@ -862,6 +963,8 @@ build_function :: proc(checker: ^Checker, spec_id: int) {
&global_reads, &global_reads,
&calls, &calls,
expected, expected,
function.pkg,
function.file,
) )
value_type := checker.module.exprs[value].type value_type := checker.module.exprs[value].type
if declared.kind == .Concrete { if declared.kind == .Concrete {
@@ -929,7 +1032,7 @@ build_function :: proc(checker: ^Checker, spec_id: int) {
problematic = problematic || checker.module.exprs[value].kind == .Invalid problematic = problematic || checker.module.exprs[value].kind == .Invalid
case .Assignment: case .Assignment:
if statement.name == "_" { if statement.name == "_" {
value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls) value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls, types.INVALID, function.pkg, function.file)
if checker.module.exprs[value].type.kind == .Void { if checker.module.exprs[value].type.kind == .Void {
id := source.add( id := source.add(
checker.diagnostics, checker.diagnostics,
@@ -1013,6 +1116,8 @@ build_function :: proc(checker: ^Checker, spec_id: int) {
&global_reads, &global_reads,
&calls, &calls,
local.type, local.type,
function.pkg,
function.file,
) )
value = coerce_expr(checker, value, local.type, statement.span) value = coerce_expr(checker, value, local.type, statement.span)
append(&body, len(checker.module.statements)) append(&body, len(checker.module.statements))
@@ -1090,6 +1195,8 @@ build_function :: proc(checker: ^Checker, spec_id: int) {
&global_reads, &global_reads,
&calls, &calls,
spec.result, spec.result,
function.pkg,
function.file,
) )
value = coerce_expr(checker, value, spec.result, statement.span) value = coerce_expr(checker, value, spec.result, statement.span)
append(&body, len(checker.module.statements)) append(&body, len(checker.module.statements))
@@ -1105,7 +1212,7 @@ build_function :: proc(checker: ^Checker, spec_id: int) {
) )
problematic = problematic || checker.module.exprs[value].kind == .Invalid problematic = problematic || checker.module.exprs[value].kind == .Invalid
case .Expression: case .Expression:
value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls) value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls, types.INVALID, function.pkg, function.file)
if checker.module.exprs[value].type.kind != .Void { if checker.module.exprs[value].type.kind != .Void {
id := source.add( id := source.add(
checker.diagnostics, checker.diagnostics,
@@ -1173,8 +1280,8 @@ build_function :: proc(checker: ^Checker, spec_id: int) {
hir.Function { hir.Function {
name = function.name, name = function.name,
link_name = make_link_name(checker, spec_id), link_name = make_link_name(checker, spec_id),
c_abi = function.c_abi || function.name == "main", c_abi = function.c_abi || (function.pkg == 0 && function.name == "main"),
is_main = function.name == "main", is_main = function.pkg == 0 && function.name == "main",
params = params[:], params = params[:],
result = spec.result, result = spec.result,
locals = hir_locals[:], locals = hir_locals[:],
@@ -1221,7 +1328,7 @@ build_globals :: proc(checker: ^Checker) {
if declared.kind == .Concrete { if declared.kind == .Concrete {
expected = declared expected = declared
} }
expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected) expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected, global.pkg, global.file)
global_type := checker.global_types[global_id] global_type := checker.global_types[global_id]
if declared.kind == .Concrete { if declared.kind == .Concrete {
expr = coerce_expr(checker, expr, declared, global.span) expr = coerce_expr(checker, expr, declared, global.span)
@@ -1229,10 +1336,17 @@ build_globals :: proc(checker: ^Checker) {
} else if types.is_concrete_integer(checker.module.exprs[expr].type) { } else if types.is_concrete_integer(checker.module.exprs[expr].type) {
global_type = checker.module.exprs[expr].type global_type = checker.module.exprs[expr].type
} }
if !types.is_concrete_integer(global_type) {
global_type = types.I64
}
diagnostic := -1 diagnostic := -1
if !types.is_concrete_integer(global_type) {
diagnostic = source.addf(
checker.diagnostics,
global.span,
"could not resolve a concrete type for global '%s'",
global.name,
)
global_type = types.I64
expr = invalid_hir_expr(checker, global.span, diagnostic, global_type)
}
if global.type == .Void { if global.type == .Void {
diagnostic = source.add( diagnostic = source.add(
checker.diagnostics, checker.diagnostics,
@@ -1389,16 +1503,14 @@ detect_global_cycles_visit :: proc(checker: ^Checker, global_id: int, states: []
return return
} }
if states[global_id] == 1 { if states[global_id] == 1 {
if !checker.module.globals[global_id].problematic { id := source.addf(
id := source.addf( checker.diagnostics,
checker.diagnostics, checker.ast_module.globals[global_id].span,
checker.ast_module.globals[global_id].span, "global initialization cycle involving '%s'",
"global initialization cycle involving '%s'", checker.module.globals[global_id].name,
checker.module.globals[global_id].name, )
) checker.module.globals[global_id].diagnostic = id
checker.module.globals[global_id].diagnostic = id checker.module.globals[global_id].problematic = true
checker.module.globals[global_id].problematic = true
}
return return
} }
states[global_id] = 1 states[global_id] = 1
@@ -1478,6 +1590,7 @@ check :: proc(
allocator = allocator, allocator = allocator,
} }
checker.specs.allocator = allocator checker.specs.allocator = allocator
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.constants = make([]Constant, len(ast_module.exprs), allocator) checker.constants = make([]Constant, len(ast_module.exprs), allocator)
defer { defer {
@@ -1485,26 +1598,34 @@ check :: proc(
delete(spec.args, allocator) delete(spec.args, allocator)
} }
delete(checker.specs) delete(checker.specs)
delete(checker.function_index, allocator)
delete(checker.global_index, allocator)
delete(checker.import_index, allocator)
delete(checker.global_types, allocator) delete(checker.global_types, allocator)
delete(checker.constants, allocator) delete(checker.constants, allocator)
} }
for function, index in ast_module.functions { for function, index in ast_module.functions {
for previous in ast_module.functions[:index] { for previous in ast_module.functions[:index] {
if previous.name == function.name { if previous.pkg == function.pkg && previous.name == function.name {
source.addf(diagnostics, function.span, "duplicate function '%s'", function.name) source.addf(diagnostics, function.span, "duplicate function '%s'", function.name)
} }
} }
for global in ast_module.globals {
if global.pkg == function.pkg && global.name == function.name {
source.addf(diagnostics, function.span, "package declaration '%s' conflicts with a global", function.name)
}
}
} }
for global, index in ast_module.globals { for global, index in ast_module.globals {
for previous in ast_module.globals[:index] { for previous in ast_module.globals[:index] {
if previous.name == global.name { if previous.pkg == global.pkg && previous.name == global.name {
source.addf(diagnostics, global.span, "duplicate global '%s'", global.name) source.addf(diagnostics, global.span, "duplicate global '%s'", global.name)
} }
} }
} }
validate_templates(&checker) validate_declarations(&checker)
infer_all(&checker) infer_all(&checker)
build_globals(&checker) build_globals(&checker)
for spec_id := 0; spec_id < len(checker.specs); spec_id += 1 { for spec_id := 0; spec_id < len(checker.specs); spec_id += 1 {
@@ -1513,10 +1634,10 @@ check :: proc(
resolve_call_targets(&checker) resolve_call_targets(&checker)
propagate_global_reads(&checker) propagate_global_reads(&checker)
main_template := find_template(&checker, "main") main_template := find_template(&checker, "main", 0)
main_declarations := 0 main_declarations := 0
for function in ast_module.functions { for function in ast_module.functions {
if function.name == "main" { if function.pkg == 0 && function.name == "main" {
main_declarations += 1 main_declarations += 1
} }
} }
@@ -1543,5 +1664,10 @@ check :: proc(
} }
delete(states, allocator) delete(states, allocator)
propagate_problems(&checker) propagate_problems(&checker)
for import_item in ast_module.imports {
if import_item.valid && !import_item.used {
source.addf(diagnostics, import_item.span, "unused import '%s'", import_item.alias)
}
}
return checker.module return checker.module
} }
+16 -14
View File
@@ -2,27 +2,20 @@ package compiler
import "./backend" import "./backend"
import "./checker" import "./checker"
import "./lexer"
import "./llvm" import "./llvm"
import "./loader"
import "./lower" import "./lower"
import "./opt" import "./opt"
import "./parser"
import "./source" import "./source"
import "core:fmt" import "core:fmt"
import vmem "core:mem/virtual" import vmem "core:mem/virtual"
import "core:os" import "core:os"
import "core:os/os2" import "core:os/os2"
compile_file :: proc(input_path, output_path: string) -> int { compile_package :: proc(input_path, output_path: string) -> int {
source_bytes, ok := os.read_entire_file(input_path) sources := source.init_store()
if !ok { defer source.destroy_store(&sources)
fmt.eprintln("failed to read input:", input_path) diagnostics := source.init_store_diagnostics(&sources)
return 2
}
defer delete(source_bytes)
source_file := source.Source{path=input_path, text=string(source_bytes)}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics) defer source.destroy_diagnostics(&diagnostics)
lexer_arena: vmem.Arena lexer_arena: vmem.Arena
@@ -50,8 +43,17 @@ compile_file :: proc(input_path, output_path: string) -> int {
} }
defer vmem.arena_destroy(&lower_arena) defer vmem.arena_destroy(&lower_arena)
tokens := lexer.lex(&source_file, &diagnostics, vmem.arena_allocator(&lexer_arena)) ast_module, loaded := loader.load(
ast_module := parser.parse(&tokens, &diagnostics, vmem.arena_allocator(&parser_arena)) input_path,
&sources,
&diagnostics,
vmem.arena_allocator(&lexer_arena),
vmem.arena_allocator(&parser_arena),
)
if !loaded {
fmt.eprintln("failed to load root package directory:", input_path)
return 2
}
vmem.arena_free_all(&lexer_arena) vmem.arena_free_all(&lexer_arena)
hir_module := checker.check(&ast_module, &diagnostics, vmem.arena_allocator(&checker_arena)) hir_module := checker.check(&ast_module, &diagnostics, vmem.arena_allocator(&checker_arena))
vmem.arena_free_all(&parser_arena) vmem.arena_free_all(&parser_arena)
+39 -4
View File
@@ -15,6 +15,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
switch text { switch text {
case "c": return .Keyword_C case "c": return .Keyword_C
case "func": return .Keyword_Func case "func": return .Keyword_Func
case "import": return .Keyword_Import
case "return": return .Keyword_Return case "return": return .Keyword_Return
case "void": return .Keyword_Void case "void": return .Keyword_Void
case "int": return .Keyword_Int case "int": return .Keyword_Int
@@ -36,7 +37,7 @@ append_token :: proc(
) { ) {
append(&stream.items, token.Token{ append(&stream.items, token.Token{
kind=kind, kind=kind,
span=source.Span{start=start, end=end}, span=source.Span{file=source_file.id, start=start, end=end},
text=source_file.text[start:end], text=source_file.text[start:end],
diagnostic=diagnostic, diagnostic=diagnostic,
}) })
@@ -71,7 +72,7 @@ lex :: proc(
cursor += 1 cursor += 1
append_token(&stream, source_file, .Colon_Colon, start, cursor) append_token(&stream, source_file, .Colon_Colon, start, cursor)
} else { } else {
id := source.add(diagnostics, source.Span{start=start, end=cursor}, "expected a second ':'") id := source.add(diagnostics, source.Span{file=source_file.id, start=start, end=cursor}, "expected a second ':'")
append_token(&stream, source_file, .Invalid, start, cursor, id) append_token(&stream, source_file, .Invalid, start, cursor, id)
} }
case '=': case '=':
@@ -80,6 +81,9 @@ lex :: proc(
case '+': case '+':
append_token(&stream, source_file, .Plus, cursor, cursor+1) append_token(&stream, source_file, .Plus, cursor, cursor+1)
cursor += 1 cursor += 1
case '.':
append_token(&stream, source_file, .Dot, cursor, cursor+1)
cursor += 1
case '(': case '(':
append_token(&stream, source_file, .Left_Paren, cursor, cursor+1) append_token(&stream, source_file, .Left_Paren, cursor, cursor+1)
cursor += 1 cursor += 1
@@ -95,10 +99,41 @@ lex :: proc(
case ',': case ',':
append_token(&stream, source_file, .Comma, cursor, cursor+1) append_token(&stream, source_file, .Comma, cursor, cursor+1)
cursor += 1 cursor += 1
case '"':
start := cursor
cursor += 1
valid := true
for cursor < len(bytes) && bytes[cursor] != '"' && bytes[cursor] != '\n' {
if bytes[cursor] == '\\' {
cursor += 1
if cursor >= len(bytes) || (bytes[cursor] != '\\' && bytes[cursor] != '"') {
source.add(
diagnostics,
source.Span{file=source_file.id, start=max(cursor-1, start), end=min(cursor+1, len(bytes))},
"import strings only support '\\\\' and '\\\"' escapes",
)
valid = false
}
}
if cursor < len(bytes) && bytes[cursor] != '\n' {
cursor += 1
}
}
if cursor < len(bytes) && bytes[cursor] == '"' {
cursor += 1
append_token(&stream, source_file, .String if valid else .Invalid, start, cursor)
} else {
id := source.add(
diagnostics,
source.Span{file=source_file.id, start=start, end=cursor},
"unterminated import string",
)
append_token(&stream, source_file, .Invalid, start, cursor, id)
}
case ';': case ';':
id := source.add( id := source.add(
diagnostics, diagnostics,
source.Span{start=cursor, end=cursor+1}, source.Span{file=source_file.id, start=cursor, end=cursor+1},
"semicolons are invalid; terminate statements with a newline", "semicolons are invalid; terminate statements with a newline",
) )
append_token(&stream, source_file, .Invalid, cursor, cursor+1, id) append_token(&stream, source_file, .Invalid, cursor, cursor+1, id)
@@ -120,7 +155,7 @@ lex :: proc(
} else { } else {
id := source.addf( id := source.addf(
diagnostics, diagnostics,
source.Span{start=cursor, end=cursor+1}, source.Span{file=source_file.id, start=cursor, end=cursor+1},
"invalid source byte 0x%02x", "invalid source byte 0x%02x",
value, value,
) )
+6 -2
View File
@@ -65,10 +65,14 @@ diagnostic_message :: proc(emitter: ^Emitter, diagnostic: int, span: source.Span
delete(message, emitter.allocator) delete(message, emitter.allocator)
return id return id
} }
line, column := source.line_and_column(emitter.diagnostics.source, span.start) source_file := source.source_for_span(emitter.diagnostics, span)
if source_file == nil {
return register_message(emitter, fallback)
}
line, column := source.line_and_column(source_file, span.start)
message := fmt.aprintf( message := fmt.aprintf(
"%s:%d:%d: runtime trap: %s", "%s:%d:%d: runtime trap: %s",
emitter.diagnostics.source.path, source_file.path,
line, line,
column, column,
fallback, fallback,
+264
View File
@@ -0,0 +1,264 @@
package loader
import "../ast"
import "../lexer"
import "../parser"
import "../source"
import "core:mem"
import "core:os"
import "core:path/filepath"
import "core:slice"
import "core:strings"
State :: struct {
module: ^ast.Module,
sources: ^source.Store,
diagnostics: ^source.Diagnostics,
token_allocator: mem.Allocator,
allocator: mem.Allocator,
root_failed: bool,
}
is_identifier :: proc(value: string) -> bool {
if len(value) == 0 {
return false
}
is_start := proc(value: byte) -> bool {
return value == '_' || value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z'
}
if !is_start(value[0]) {
return false
}
for byte_value in transmute([]byte)value[1:] {
if !is_start(byte_value) && !(byte_value >= '0' && byte_value <= '9') {
return false
}
}
return true
}
find_package :: proc(state: ^State, path: string) -> int {
for pkg, id in state.module.packages {
if pkg.path == path {
return id
}
}
return -1
}
add_placeholder :: proc(state: ^State, path: string) -> int {
if existing := find_package(state, path); existing >= 0 {
return existing
}
id := len(state.module.packages)
append(&state.module.packages, ast.Package{
path=strings.clone(path, state.allocator),
name=strings.clone(filepath.base(path), state.allocator),
available=false,
})
return id
}
read_package_files :: proc(state: ^State, path: string) -> ([]os.File_Info, bool) {
handle, open_error := os.open(path, os.O_RDONLY)
if open_error != nil {
return nil, false
}
defer os.close(handle)
entries, read_error := os.read_dir(handle, -1, state.allocator)
if read_error != nil {
return nil, false
}
slice.sort_by(entries, proc(a, b: os.File_Info) -> bool {
return a.name < b.name
})
files: [dynamic]os.File_Info
files.allocator = state.allocator
for entry in entries {
if !entry.is_dir && filepath.ext(entry.name) == ".bro" {
append(&files, entry)
} else {
os.file_info_delete(entry, state.allocator)
}
}
delete(entries, state.allocator)
return files[:], true
}
resolve_import_path :: proc(state: ^State, importing_path, import_path: string) -> (string, bool) {
if filepath.is_abs(import_path) {
return "", false
}
joined, join_error := filepath.join({importing_path, import_path}, state.allocator)
if join_error != nil {
return "", false
}
canonical, ok := filepath.abs(joined, state.allocator)
if ok {
delete(joined, state.allocator)
return canonical, true
}
return joined, false
}
load_package :: proc(state: ^State, path: string, import_span: source.Span, is_root := false) -> int {
canonical, ok := filepath.abs(path, state.allocator)
if !ok || !os.is_dir(path) {
if is_root {
state.root_failed = true
if len(canonical) > 0 {
delete(canonical, state.allocator)
}
return -1
}
placeholder := path
if len(canonical) > 0 {
placeholder = canonical
}
id := add_placeholder(state, placeholder)
source.addf(state.diagnostics, import_span, "could not import package directory '%s'", path)
if len(canonical) > 0 {
delete(canonical, state.allocator)
}
return id
}
if existing := find_package(state, canonical); existing >= 0 {
delete(canonical, state.allocator)
return existing
}
pkg_id := len(state.module.packages)
append(&state.module.packages, ast.Package{
path=canonical,
name=strings.clone(filepath.base(canonical), state.allocator),
available=true,
})
files, files_ok := read_package_files(state, canonical)
if !files_ok {
state.root_failed = true
return pkg_id
}
if len(files) == 0 {
if is_root {
state.root_failed = true
} else {
source.addf(state.diagnostics, import_span, "package '%s' contains no readable .bro files", canonical)
state.module.packages[pkg_id].available = false
}
os.file_info_slice_delete(files, state.allocator)
return pkg_id
}
for file_info in files {
bytes, read_ok := os.read_entire_file(file_info.fullpath)
if !read_ok {
state.root_failed = true
continue
}
source_id := source.add_source(state.sources, file_info.fullpath, string(bytes))
delete(bytes)
file_id := len(state.module.files)
append(&state.module.files, ast.File{source=source_id, pkg=pkg_id})
stream := lexer.lex(&state.sources.items[source_id], state.diagnostics, state.token_allocator)
parser.parse_into(&stream, state.diagnostics, state.module, pkg_id, file_id)
delete(stream.items)
}
os.file_info_slice_delete(files, state.allocator)
import_count := len(state.module.imports)
for import_id in 0..<import_count {
import_item := state.module.imports[import_id]
if import_item.pkg != pkg_id || import_item.target >= 0 {
continue
}
if filepath.is_abs(import_item.path) {
state.module.imports[import_id].diagnostic = source.add(state.diagnostics, import_item.span, "absolute import paths are invalid")
state.module.imports[import_id].valid = false
state.module.imports[import_id].target = add_placeholder(state, import_item.path)
continue
}
target_path, target_ok := resolve_import_path(state, canonical, import_item.path)
target := load_package(state, target_path, import_item.span)
state.module.imports[import_id].target = target
if !target_ok || target < 0 || !state.module.packages[target].available {
state.module.imports[import_id].valid = false
}
delete(target_path, state.allocator)
}
return pkg_id
}
declaration_conflicts :: proc(module: ^ast.Module, pkg: int, name: string) -> bool {
for function in module.functions {
if function.pkg == pkg && function.name == name {
return true
}
}
for global in module.globals {
if global.pkg == pkg && global.name == name {
return true
}
}
return false
}
validate_imports :: proc(state: ^State) {
for import_item, import_id in state.module.imports {
if import_item.alias == "" && import_item.target >= 0 {
state.module.imports[import_id].alias = state.module.packages[import_item.target].name
}
alias := state.module.imports[import_id].alias
if !is_identifier(alias) {
state.module.imports[import_id].diagnostic = source.add(
state.diagnostics,
import_item.span,
"import requires an explicit valid identifier alias",
)
state.module.imports[import_id].valid = false
}
if declaration_conflicts(state.module, import_item.pkg, alias) {
state.module.imports[import_id].diagnostic = source.addf(
state.diagnostics,
import_item.span,
"import alias '%s' conflicts with a package declaration",
alias,
)
state.module.imports[import_id].valid = false
}
for previous in state.module.imports[:import_id] {
if previous.file == import_item.file && previous.alias == alias {
state.module.imports[import_id].diagnostic = source.addf(
state.diagnostics,
import_item.span,
"duplicate import alias '%s' in the same file",
alias,
)
state.module.imports[import_id].valid = false
break
}
}
}
}
load :: proc(
root_path: string,
sources: ^source.Store,
diagnostics: ^source.Diagnostics,
token_allocator := context.allocator,
allocator := context.allocator,
) -> (ast.Module, bool) {
module := ast.init_module(allocator)
state := State{
module=&module,
sources=sources,
diagnostics=diagnostics,
token_allocator=token_allocator,
allocator=allocator,
}
root := load_package(&state, root_path, source.Span{}, true)
if root != 0 && root >= 0 {
state.root_failed = true
}
validate_imports(&state)
return module, !state.root_failed
}
+126 -15
View File
@@ -3,16 +3,24 @@ package parser
import "../ast" import "../ast"
import "../source" import "../source"
import "../token" import "../token"
import "core:fmt"
import "core:strconv" import "core:strconv"
import "core:strings"
Parser :: struct { Parser :: struct {
tokens: ^token.Stream, tokens: ^token.Stream,
diagnostics: ^source.Diagnostics, diagnostics: ^source.Diagnostics,
module: ast.Module, module: ast.Module,
pkg: int,
file: int,
cursor: int, cursor: int,
delimiter_depth: int, delimiter_depth: int,
} }
span_from :: proc(first, last: source.Span) -> source.Span {
return source.Span{file=first.file, start=first.start, end=last.end}
}
current :: proc(parser: ^Parser) -> token.Token { current :: proc(parser: ^Parser) -> token.Token {
return parser.tokens.items[min(parser.cursor, len(parser.tokens.items)-1)] return parser.tokens.items[min(parser.cursor, len(parser.tokens.items)-1)]
} }
@@ -93,7 +101,7 @@ parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax {
return .Invalid return .Invalid
} }
parse_call :: proc(parser: ^Parser, name: token.Token) -> int { parse_call :: proc(parser: ^Parser, qualifier: string, first, name: token.Token) -> int {
left_paren := advance(parser) left_paren := advance(parser)
parser.delimiter_depth += 1 parser.delimiter_depth += 1
defer parser.delimiter_depth -= 1 defer parser.delimiter_depth -= 1
@@ -116,7 +124,8 @@ parse_call :: proc(parser: ^Parser, name: token.Token) -> int {
} }
return add_expr(parser, ast.Expr{ return add_expr(parser, ast.Expr{
kind=.Call, kind=.Call,
span=source.Span{start=name.span.start, end=right_paren.span.end}, span=source.Span{file=name.span.file, start=first.span.start, end=right_paren.span.end},
qualifier=qualifier,
text=name.text, text=name.text,
args=args[:], args=args[:],
left=ast.INVALID_ID, left=ast.INVALID_ID,
@@ -143,14 +152,24 @@ parse_primary :: proc(parser: ^Parser) -> int {
diagnostic=-1, diagnostic=-1,
}) })
case .Identifier: case .Identifier:
advance(parser) first := advance(parser)
name := first
qualifier := ""
if _, ok := allow(parser, .Dot); ok {
if current(parser).kind != .Identifier {
return invalid_expr(parser, current(parser).span, "expected a package member after '.'")
}
qualifier = first.text
name = advance(parser)
}
if current(parser).kind == .Left_Paren { if current(parser).kind == .Left_Paren {
return parse_call(parser, tok) return parse_call(parser, qualifier, first, name)
} }
return add_expr(parser, ast.Expr{ return add_expr(parser, ast.Expr{
kind=.Name, kind=.Name,
span=tok.span, span=span_from(first.span, name.span),
text=tok.text, qualifier=qualifier,
text=name.text,
left=ast.INVALID_ID, left=ast.INVALID_ID,
right=ast.INVALID_ID, right=ast.INVALID_ID,
diagnostic=-1, diagnostic=-1,
@@ -198,7 +217,7 @@ parse_expression :: proc(parser: ^Parser) -> int {
right_expr := parser.module.exprs[right] right_expr := parser.module.exprs[right]
left = add_expr(parser, ast.Expr{ left = add_expr(parser, ast.Expr{
kind=.Add, kind=.Add,
span=source.Span{start=left_expr.span.start, end=right_expr.span.end}, span=span_from(left_expr.span, right_expr.span),
left=left, left=left,
right=right, right=right,
diagnostic=-1, diagnostic=-1,
@@ -210,12 +229,12 @@ parse_expression :: proc(parser: ^Parser) -> int {
return left return left
} }
finish_statement :: proc(parser: ^Parser) -> int { finish_statement :: proc(parser: ^Parser, allow_closing_brace := false) -> int {
if current(parser).kind == .Newline { if current(parser).kind == .Newline {
skip_newlines(parser) skip_newlines(parser)
return -1 return -1
} }
if current(parser).kind == .Eof { if current(parser).kind == .Eof || allow_closing_brace && current(parser).kind == .Right_Brace {
return -1 return -1
} }
diagnostic := source.add( diagnostic := source.add(
@@ -240,7 +259,7 @@ parse_return :: proc(parser: ^Parser) -> int {
id := len(parser.module.statements) id := len(parser.module.statements)
append(&parser.module.statements, ast.Stmt{ append(&parser.module.statements, ast.Stmt{
kind=.Return, kind=.Return,
span=source.Span{start=start.span.start, end=end.span.end}, span=span_from(start.span, end.span),
name="_", name="_",
expr=ast.INVALID_ID, expr=ast.INVALID_ID,
diagnostic=-1, diagnostic=-1,
@@ -251,7 +270,7 @@ parse_return :: proc(parser: ^Parser) -> int {
id := len(parser.module.statements) id := len(parser.module.statements)
append(&parser.module.statements, ast.Stmt{ append(&parser.module.statements, ast.Stmt{
kind=.Return, kind=.Return,
span=source.Span{start=start.span.start, end=parser.module.exprs[expr].span.end}, span=span_from(start.span, parser.module.exprs[expr].span),
expr=expr, expr=expr,
diagnostic=-1, diagnostic=-1,
}) })
@@ -286,7 +305,7 @@ parse_statement :: proc(parser: ^Parser) -> int {
id := len(parser.module.statements) id := len(parser.module.statements)
append(&parser.module.statements, ast.Stmt{ append(&parser.module.statements, ast.Stmt{
kind=kind, kind=kind,
span=source.Span{start=name.span.start, end=parser.module.exprs[expr].span.end}, span=span_from(name.span, parser.module.exprs[expr].span),
name=name.text, name=name.text,
type=type_syntax, type=type_syntax,
immutable=immutable, immutable=immutable,
@@ -367,7 +386,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
skip_newlines(parser) skip_newlines(parser)
for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { for current(parser).kind != .Right_Brace && current(parser).kind != .Eof {
append(&body, parse_statement(parser)) append(&body, parse_statement(parser))
if diagnostic := finish_statement(parser); diagnostic >= 0 { if diagnostic := finish_statement(parser, true); diagnostic >= 0 {
statement_id := len(parser.module.statements) statement_id := len(parser.module.statements)
append(&parser.module.statements, ast.Stmt{ append(&parser.module.statements, ast.Stmt{
kind=.Invalid, kind=.Invalid,
@@ -384,8 +403,10 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
end = func_token end = func_token
} }
append(&parser.module.functions, ast.Function{ append(&parser.module.functions, ast.Function{
span=source.Span{start=name.span.start, end=end.span.end}, span=span_from(name.span, end.span),
name=name.text, name=name.text,
pkg=parser.pkg,
file=parser.file,
c_abi=c_abi, c_abi=c_abi,
params=params, params=params,
result=result, result=result,
@@ -394,7 +415,63 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
}) })
} }
decode_import_path :: proc(parser: ^Parser, tok: token.Token) -> string {
if len(tok.text) < 2 {
return fmt.aprintf("", allocator=parser.module.allocator)
}
builder := strings.builder_make(parser.module.allocator)
defer strings.builder_destroy(&builder)
for index := 1; index < len(tok.text)-1; index += 1 {
value := tok.text[index]
if value == '\\' && index+1 < len(tok.text)-1 {
index += 1
value = tok.text[index]
}
strings.write_byte(&builder, value)
}
return fmt.aprintf("%s", strings.to_string(builder), allocator=parser.module.allocator)
}
parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token) {
skip_newlines(parser)
path_token := current(parser)
if path_token.kind != .String {
source.add(parser.diagnostics, path_token.span, "expected an import path string")
if path_token.kind != .Newline && path_token.kind != .Eof {
advance(parser)
}
append(&parser.module.imports, ast.Import{
span=start.span,
alias=alias.text,
pkg=parser.pkg,
file=parser.file,
target=-1,
valid=false,
diagnostic=-1,
})
_ = finish_statement(parser)
return
}
advance(parser)
append(&parser.module.imports, ast.Import{
span=span_from(start.span, path_token.span),
alias=alias.text,
path=decode_import_path(parser, path_token),
pkg=parser.pkg,
file=parser.file,
target=-1,
valid=true,
diagnostic=-1,
})
_ = finish_statement(parser)
}
parse_top_level :: proc(parser: ^Parser) { parse_top_level :: proc(parser: ^Parser) {
if current(parser).kind == .Keyword_Import {
start := advance(parser)
parse_import(parser, token.Token{}, start)
return
}
if current(parser).kind != .Identifier { if current(parser).kind != .Identifier {
source.add(parser.diagnostics, current(parser).span, "expected a top-level declaration") source.add(parser.diagnostics, current(parser).span, "expected a top-level declaration")
for current(parser).kind != .Newline && current(parser).kind != .Eof { for current(parser).kind != .Newline && current(parser).kind != .Eof {
@@ -404,6 +481,17 @@ parse_top_level :: proc(parser: ^Parser) {
return return
} }
name := advance(parser) name := advance(parser)
if current(parser).kind == .Colon_Colon {
saved := parser.cursor
advance(parser)
skip_newlines(parser)
if current(parser).kind == .Keyword_Import {
start := advance(parser)
parse_import(parser, name, start)
return
}
parser.cursor = saved
}
type_syntax := ast.Type_Syntax.Invalid type_syntax := ast.Type_Syntax.Invalid
if is_type_token(current(parser).kind) { if is_type_token(current(parser).kind) {
type_syntax = parse_type(parser) type_syntax = parse_type(parser)
@@ -430,8 +518,10 @@ parse_top_level :: proc(parser: ^Parser) {
expr := parse_expression(parser) expr := parse_expression(parser)
append(&parser.module.globals, ast.Global{ append(&parser.module.globals, ast.Global{
span=source.Span{start=name.span.start, end=parser.module.exprs[expr].span.end}, span=span_from(name.span, parser.module.exprs[expr].span),
name=name.text, name=name.text,
pkg=parser.pkg,
file=parser.file,
type=type_syntax, type=type_syntax,
immutable=operator.kind == .Colon_Colon, immutable=operator.kind == .Colon_Colon,
expr=expr, expr=expr,
@@ -457,3 +547,24 @@ parse :: proc(
} }
return parser.module return parser.module
} }
parse_into :: proc(
stream: ^token.Stream,
diagnostics: ^source.Diagnostics,
module: ^ast.Module,
pkg, file: int,
) {
parser := Parser{
tokens=stream,
diagnostics=diagnostics,
module=module^,
pkg=pkg,
file=file,
}
skip_newlines(&parser)
for current(&parser).kind != .Eof {
parse_top_level(&parser)
skip_newlines(&parser)
}
module^ = parser.module
}
+64 -6
View File
@@ -4,24 +4,57 @@ import "core:fmt"
import "core:mem" import "core:mem"
Span :: struct { Span :: struct {
file: int,
start: int, start: int,
end: int, end: int,
} }
Source :: struct { Source :: struct {
id: int,
path: string, path: string,
text: string, text: string,
} }
Store :: struct {
items: [dynamic]Source,
allocator: mem.Allocator,
}
Diagnostic :: struct { Diagnostic :: struct {
span: Span, span: Span,
message: string, message: string,
} }
Diagnostics :: struct { Diagnostics :: struct {
source: ^Source, source: ^Source,
items: [dynamic]Diagnostic, store: ^Store,
allocator: mem.Allocator, items: [dynamic]Diagnostic,
allocator: mem.Allocator,
}
init_store :: proc(allocator := context.allocator) -> Store {
result: Store
result.allocator = allocator
result.items.allocator = allocator
return result
}
destroy_store :: proc(store: ^Store) {
for item in store.items {
delete(item.path, store.allocator)
delete(item.text, store.allocator)
}
delete(store.items)
}
add_source :: proc(store: ^Store, path, text: string) -> int {
id := len(store.items)
append(&store.items, Source{
id=id,
path=fmt.aprintf("%s", path, allocator=store.allocator),
text=fmt.aprintf("%s", text, allocator=store.allocator),
})
return id
} }
init_diagnostics :: proc(source_file: ^Source, allocator := context.allocator) -> Diagnostics { init_diagnostics :: proc(source_file: ^Source, allocator := context.allocator) -> Diagnostics {
@@ -32,6 +65,14 @@ init_diagnostics :: proc(source_file: ^Source, allocator := context.allocator) -
return result return result
} }
init_store_diagnostics :: proc(store: ^Store, allocator := context.allocator) -> Diagnostics {
result: Diagnostics
result.store = store
result.allocator = allocator
result.items.allocator = allocator
return result
}
destroy_diagnostics :: proc(diagnostics: ^Diagnostics) { destroy_diagnostics :: proc(diagnostics: ^Diagnostics) {
for diagnostic in diagnostics.items { for diagnostic in diagnostics.items {
delete(diagnostic.message, diagnostics.allocator) delete(diagnostic.message, diagnostics.allocator)
@@ -79,15 +120,32 @@ line_and_column :: proc(source_file: ^Source, offset: int) -> (line, column: int
return return
} }
source_for_span :: proc(diagnostics: ^Diagnostics, span: Span) -> ^Source {
if diagnostics.store != nil && span.file >= 0 && span.file < len(diagnostics.store.items) {
return &diagnostics.store.items[span.file]
}
return diagnostics.source
}
format :: proc(diagnostics: ^Diagnostics, id: int, allocator := context.allocator) -> string { format :: proc(diagnostics: ^Diagnostics, id: int, allocator := context.allocator) -> string {
if id < 0 || id >= len(diagnostics.items) { if id < 0 || id >= len(diagnostics.items) {
return fmt.aprintf("%s: compiler recovery error", diagnostics.source.path, allocator=allocator) path := "<unknown>"
if diagnostics.source != nil {
path = diagnostics.source.path
} else if diagnostics.store != nil && len(diagnostics.store.items) > 0 {
path = diagnostics.store.items[0].path
}
return fmt.aprintf("%s: compiler recovery error", path, allocator=allocator)
} }
diagnostic := diagnostics.items[id] diagnostic := diagnostics.items[id]
line, column := line_and_column(diagnostics.source, diagnostic.span.start) source_file := source_for_span(diagnostics, diagnostic.span)
if source_file == nil {
return fmt.aprintf("<unknown>: error: %s", diagnostic.message, allocator=allocator)
}
line, column := line_and_column(source_file, diagnostic.span.start)
return fmt.aprintf( return fmt.aprintf(
"%s:%d:%d: error: %s", "%s:%d:%d: error: %s",
diagnostics.source.path, source_file.path,
line, line,
column, column,
diagnostic.message, diagnostic.message,
+3
View File
@@ -8,10 +8,12 @@ Kind :: enum {
Newline, Newline,
Identifier, Identifier,
Integer, Integer,
String,
Underscore, Underscore,
Colon_Colon, Colon_Colon,
Equal, Equal,
Plus, Plus,
Dot,
Left_Paren, Left_Paren,
Right_Paren, Right_Paren,
Left_Brace, Left_Brace,
@@ -19,6 +21,7 @@ Kind :: enum {
Comma, Comma,
Keyword_C, Keyword_C,
Keyword_Func, Keyword_Func,
Keyword_Import,
Keyword_Return, Keyword_Return,
Keyword_Void, Keyword_Void,
Keyword_Int, Keyword_Int,
+622 -26
View File
@@ -7,12 +7,14 @@ import "./compiler/checker"
import "./compiler/hir" import "./compiler/hir"
import "./compiler/ir" import "./compiler/ir"
import "./compiler/lexer" import "./compiler/lexer"
import "./compiler/loader"
import "./compiler/llvm" import "./compiler/llvm"
import "./compiler/lower" import "./compiler/lower"
import "./compiler/parser" import "./compiler/parser"
import "./compiler/source" import "./compiler/source"
import "./compiler/token" import "./compiler/token"
import "./compiler/types" import "./compiler/types"
import "core:fmt"
import "core:os" import "core:os"
import "core:os/os2" import "core:os/os2"
import "core:strings" import "core:strings"
@@ -53,6 +55,153 @@ main :: func() void {}
testing.expect_value(t, len(module.functions[0].params), 2) testing.expect_value(t, len(module.functions[0].params), 2)
} }
@(test)
parser_accepts_single_statement_one_line_functions :: proc(t: ^testing.T) {
text := `give :: func() i8 { return 7 }
main :: func() void { _ = give() }
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
stream := lexer.lex(&source_file, &diagnostics)
defer delete(stream.items)
module := parser.parse(&stream, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.functions), 2)
testing.expect_value(t, len(module.functions[0].body), 1)
testing.expect_value(t, len(module.functions[1].body), 1)
testing.expect_value(t, module.statements[module.functions[0].body[0]].kind, ast.Stmt_Kind.Return)
testing.expect_value(t, module.statements[module.functions[1].body[0]].kind, ast.Stmt_Kind.Assignment)
}
@(test)
parser_accepts_bare_and_aliased_imports :: proc(t: ^testing.T) {
text := `import
"../math"
other :: import "../math"
escaped :: import "dir\"name\\tail"
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
stream := lexer.lex(&source_file, &diagnostics)
defer delete(stream.items)
module := parser.parse(&stream, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.imports), 3)
testing.expect_value(t, module.imports[0].alias, "")
testing.expect_value(t, module.imports[0].path, "../math")
testing.expect_value(t, module.imports[1].alias, "other")
testing.expect_value(t, module.imports[2].path, "dir\"name\\tail")
}
@(test)
parser_rejects_chained_package_access :: proc(t: ^testing.T) {
text := `main :: func() void {
_ = first.second.value
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
stream := lexer.lex(&source_file, &diagnostics)
defer delete(stream.items)
module := parser.parse(&stream, &diagnostics)
defer ast.destroy_module(&module)
testing.expect(t, len(diagnostics.items) > 0)
}
@(test)
lexer_diagnoses_invalid_import_strings :: proc(t: ^testing.T) {
text := "import \"bad\\q\"\nimport \"unterminated\n"
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
stream := lexer.lex(&source_file, &diagnostics)
defer delete(stream.items)
testing.expect_value(t, len(diagnostics.items), 2)
}
@(test)
package_loader_discovers_lexical_immediate_bro_files :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
module, loaded := loader.load("examples/packages/basic/app", &sources, &diagnostics)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.packages), 2)
testing.expect_value(t, len(module.files), 3)
testing.expect(t, strings.has_suffix(sources.items[module.files[0].source].path, "/main.bro"))
testing.expect(t, strings.has_suffix(sources.items[module.files[1].source].path, "/value.bro"))
testing.expect(t, strings.has_suffix(sources.items[module.files[2].source].path, "/math.bro"))
}
@(test)
multi_source_diagnostics_report_the_originating_file :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
module, loaded := loader.load("examples/packages/file_local/app", &sources, &diagnostics)
defer ast.destroy_module(&module)
hir_module := checker.check(&module, &diagnostics)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
found := false
for _, diagnostic_id in diagnostics.items {
message := source.format(&diagnostics, diagnostic_id)
if strings.contains(message, "/b.bro:2:9:") &&
strings.contains(message, "unknown package alias 'math'") {
found = true
}
delete(message)
}
testing.expect(t, found)
}
@(test)
semantic_lookup_diagnoses_wrong_declaration_kinds :: proc(t: ^testing.T) {
text := `value :: 1
give :: func() i8 {
return 1
}
main :: func() void {
_ = value()
_ = give
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
stream := lexer.lex(&source_file, &diagnostics)
defer delete(stream.items)
ast_module := parser.parse(&stream, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics)
defer hir.destroy_module(&hir_module)
found_global := false
found_function := false
for diagnostic in diagnostics.items {
found_global = found_global || strings.contains(diagnostic.message, "'value' is a global, not a function")
found_function = found_function || strings.contains(diagnostic.message, "'give' is a function, not a global value")
}
testing.expect(t, found_global)
testing.expect(t, found_function)
}
@(test) @(test)
pipeline_emits_specialized_calling_conventions_and_checked_add :: proc(t: ^testing.T) { pipeline_emits_specialized_calling_conventions_and_checked_add :: proc(t: ^testing.T) {
text := `sum_c :: c func(a, b int) int { text := `sum_c :: c func(a, b int) int {
@@ -84,8 +233,8 @@ main :: func() void {
testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, llvm_text, second_llvm_text) testing.expect_value(t, llvm_text, second_llvm_text)
testing.expect(t, strings.contains(llvm_text, "define i8 @sum_c__i8__i8")) testing.expect(t, strings.contains(llvm_text, "define i8 @bro_c__p0__sum_c__i8__i8"))
testing.expect(t, strings.contains(llvm_text, "define internal fastcc i8 @bro__sum_bro__i8__i8")) testing.expect(t, strings.contains(llvm_text, "define internal fastcc i8 @bro__p0__sum_bro__i8__i8"))
testing.expect(t, strings.contains(llvm_text, "@llvm.sadd.with.overflow.i8")) testing.expect(t, strings.contains(llvm_text, "@llvm.sadd.with.overflow.i8"))
} }
@@ -123,7 +272,7 @@ main :: func() void {
defer delete(llvm_text) defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "@bro__take_int__i16")) testing.expect(t, strings.contains(llvm_text, "@bro__p0__take_int__i16"))
for function in ir_module.functions { for function in ir_module.functions {
for instruction in function.instructions { for instruction in function.instructions {
testing.expect(t, instruction.op != ir.Opcode.Add_Checked) testing.expect(t, instruction.op != ir.Opcode.Add_Checked)
@@ -272,6 +421,92 @@ main :: func() void {
testing.expect_value(t, len(hir_module.functions), 3) testing.expect_value(t, len(hir_module.functions), 3)
} }
@(test)
long_generic_call_chain_reaches_a_fixed_point :: proc(t: ^testing.T) {
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
for index in 0 ..< 70 {
fmt.sbprintf(&builder, "f%d :: func(value int) int ", index)
strings.write_string(&builder, "{ return ")
if index == 69 {
strings.write_string(&builder, "value")
} else {
fmt.sbprintf(&builder, "f%d(value)", index+1)
}
strings.write_string(&builder, " }\n")
}
strings.write_string(&builder, "main :: func() i32 { return f0(1) }\n")
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
stream := lexer.lex(&source_file, &diagnostics)
defer delete(stream.items)
ast_module := parser.parse(&stream, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 71)
}
@(test)
globals_and_generic_results_reach_a_shared_fixed_point :: proc(t: ^testing.T) {
text := `derived :: identity(base)
base :: make()
identity :: func(value int) int {
return value
}
make :: func() int {
return 1
}
main :: func() i32 {
return derived
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
stream := lexer.lex(&source_file, &diagnostics)
defer delete(stream.items)
ast_module := parser.parse(&stream, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 3)
for global in hir_module.globals {
testing.expect(t, types.equal(global.type, types.I8))
}
}
@(test)
unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) {
text := `broken :: func(value, value i8, nope void) void {}
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
stream := lexer.lex(&source_file, &diagnostics)
defer delete(stream.items)
ast_module := parser.parse(&stream, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics)
defer hir.destroy_module(&hir_module)
found_duplicate := false
found_void := false
for diagnostic in diagnostics.items {
found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate parameter 'value'")
found_void = found_void || strings.contains(diagnostic.message, "void is only valid as a function result type")
}
testing.expect(t, found_duplicate)
testing.expect(t, found_void)
}
run_executable :: proc(path: string) -> os2.Process_State { run_executable :: proc(path: string) -> os2.Process_State {
state, stdout, stderr, _ := os2.process_exec( state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{path}}, os2.Process_Desc{command=[]string{path}},
@@ -286,17 +521,27 @@ run_executable :: proc(path: string) -> os2.Process_State {
valid_program_compiles_and_runs :: proc(t: ^testing.T) { valid_program_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-valid" output := "/tmp/brolang-test-valid"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/prototype.bro", output) status := compiler_core.compile_package("examples/programs/prototype", output)
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
} }
@(test)
one_line_main_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-one-line-main"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/one_line", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 7)
}
@(test) @(test)
folded_constant_addition_compiles_and_runs :: proc(t: ^testing.T) { folded_constant_addition_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-constant-fold" output := "/tmp/brolang-test-constant-fold"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/constant_fold.bro", output) status := compiler_core.compile_package("examples/programs/constant_fold", output)
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
@@ -306,7 +551,7 @@ folded_constant_addition_compiles_and_runs :: proc(t: ^testing.T) {
unused_invalid_global_does_not_trap :: proc(t: ^testing.T) { unused_invalid_global_does_not_trap :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-invalid-unused" output := "/tmp/brolang-test-invalid-unused"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/invalid_unused_global.bro", output) status := compiler_core.compile_package("examples/programs/invalid_unused_global", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
@@ -316,7 +561,7 @@ unused_invalid_global_does_not_trap :: proc(t: ^testing.T) {
used_invalid_global_traps :: proc(t: ^testing.T) { used_invalid_global_traps :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-invalid-used" output := "/tmp/brolang-test-invalid-used"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/invalid_used_global.bro", output) status := compiler_core.compile_package("examples/programs/invalid_used_global", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect(t, !state.success) testing.expect(t, !state.success)
@@ -326,7 +571,7 @@ used_invalid_global_traps :: proc(t: ^testing.T) {
main_int_is_constrained_to_i32 :: proc(t: ^testing.T) { main_int_is_constrained_to_i32 :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-main-int" output := "/tmp/brolang-test-main-int"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/main_int.bro", output) status := compiler_core.compile_package("examples/programs/main_int", output)
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 3) testing.expect_value(t, state.exit_code, 3)
@@ -336,7 +581,7 @@ main_int_is_constrained_to_i32 :: proc(t: ^testing.T) {
main_i32_returns_directly :: proc(t: ^testing.T) { main_i32_returns_directly :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-main-i32" output := "/tmp/brolang-test-main-i32"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/main_i32.bro", output) status := compiler_core.compile_package("examples/programs/main_i32", output)
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 4) testing.expect_value(t, state.exit_code, 4)
@@ -346,7 +591,7 @@ main_i32_returns_directly :: proc(t: ^testing.T) {
transitive_problematic_global_is_deferred :: proc(t: ^testing.T) { transitive_problematic_global_is_deferred :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-transitive-unused" output := "/tmp/brolang-test-transitive-unused"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/invalid_transitive_unused_global.bro", output) status := compiler_core.compile_package("examples/programs/invalid_transitive_unused_global", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
@@ -356,7 +601,7 @@ transitive_problematic_global_is_deferred :: proc(t: ^testing.T) {
checked_addition_traps_on_overflow :: proc(t: ^testing.T) { checked_addition_traps_on_overflow :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-overflow" output := "/tmp/brolang-test-overflow"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/overflow.bro", output) status := compiler_core.compile_package("examples/programs/overflow", output)
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
testing.expect(t, !state.success) testing.expect(t, !state.success)
@@ -366,7 +611,7 @@ checked_addition_traps_on_overflow :: proc(t: ^testing.T) {
constant_that_does_not_fit_context_produces_trap_executable :: proc(t: ^testing.T) { constant_that_does_not_fit_context_produces_trap_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-constant-context-error" output := "/tmp/brolang-test-constant-context-error"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/constant_context_error.bro", output) status := compiler_core.compile_package("examples/programs/constant_context_error", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect(t, !state.success) testing.expect(t, !state.success)
@@ -376,7 +621,7 @@ constant_that_does_not_fit_context_produces_trap_executable :: proc(t: ^testing.
constant_beyond_i64_produces_trap_executable :: proc(t: ^testing.T) { constant_beyond_i64_produces_trap_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-constant-i64-overflow" output := "/tmp/brolang-test-constant-i64-overflow"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/constant_i64_overflow.bro", output) status := compiler_core.compile_package("examples/programs/constant_i64_overflow", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect(t, !state.success) testing.expect(t, !state.success)
@@ -384,7 +629,7 @@ constant_beyond_i64_produces_trap_executable :: proc(t: ^testing.T) {
@(test) @(test)
same_line_statements_are_diagnosed :: proc(t: ^testing.T) { same_line_statements_are_diagnosed :: proc(t: ^testing.T) {
text := "main :: func() void { _ = 1 _ = 2\n}\n" text := "main :: func() void { _ = 1 _ = 2 }\n"
source_file := source.Source{path="test.bro", text=text} source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file) diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics) defer source.destroy_diagnostics(&diagnostics)
@@ -399,7 +644,7 @@ same_line_statements_are_diagnosed :: proc(t: ^testing.T) {
missing_main_produces_trap_executable :: proc(t: ^testing.T) { missing_main_produces_trap_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-missing-main" output := "/tmp/brolang-test-missing-main"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/missing_main.bro", output) status := compiler_core.compile_package("examples/programs/missing_main", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect(t, !state.success) testing.expect(t, !state.success)
@@ -422,7 +667,7 @@ backend_failure_preserves_existing_output :: proc(t: ^testing.T) {
mutable_local_reassignment_uses_runtime_storage :: proc(t: ^testing.T) { mutable_local_reassignment_uses_runtime_storage :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-mutable" output := "/tmp/brolang-test-mutable"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/mutable_local.bro", output) status := compiler_core.compile_package("examples/programs/mutable_local", output)
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 3) testing.expect_value(t, state.exit_code, 3)
@@ -432,7 +677,7 @@ mutable_local_reassignment_uses_runtime_storage :: proc(t: ^testing.T) {
implicit_narrowing_produces_trap_executable :: proc(t: ^testing.T) { implicit_narrowing_produces_trap_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-narrowing" output := "/tmp/brolang-test-narrowing"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/narrowing_error.bro", output) status := compiler_core.compile_package("examples/programs/narrowing_error", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect(t, !state.success) testing.expect(t, !state.success)
@@ -442,7 +687,7 @@ implicit_narrowing_produces_trap_executable :: proc(t: ^testing.T) {
valid_runtime_global_initializes_before_main :: proc(t: ^testing.T) { valid_runtime_global_initializes_before_main :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-runtime-global" output := "/tmp/brolang-test-runtime-global"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/runtime_global.bro", output) status := compiler_core.compile_package("examples/programs/runtime_global", output)
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
@@ -452,7 +697,7 @@ valid_runtime_global_initializes_before_main :: proc(t: ^testing.T) {
unused_global_cycle_is_deferred :: proc(t: ^testing.T) { unused_global_cycle_is_deferred :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-cycle-unused" output := "/tmp/brolang-test-cycle-unused"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/cycle_unused.bro", output) status := compiler_core.compile_package("examples/programs/cycle_unused", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
@@ -462,7 +707,7 @@ unused_global_cycle_is_deferred :: proc(t: ^testing.T) {
used_global_cycle_traps :: proc(t: ^testing.T) { used_global_cycle_traps :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-cycle-used" output := "/tmp/brolang-test-cycle-used"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/cycle_used.bro", output) status := compiler_core.compile_package("examples/programs/cycle_used", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect(t, !state.success) testing.expect(t, !state.success)
@@ -472,27 +717,37 @@ used_global_cycle_traps :: proc(t: ^testing.T) {
malformed_typed_values_still_produce_executable :: proc(t: ^testing.T) { malformed_typed_values_still_produce_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-malformed-typed" output := "/tmp/brolang-test-malformed-typed"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/malformed_typed_recovery.bro", output) status := compiler_core.compile_package("examples/programs/malformed_typed_recovery", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect(t, !state.success) testing.expect(t, !state.success)
} }
@(test) @(test)
unused_invalid_function_is_diagnosed_but_not_reached :: proc(t: ^testing.T) { unused_invalid_function_body_is_not_checked :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-invalid-unused-function" output := "/tmp/brolang-test-invalid-unused-function"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/invalid_unused_function.bro", output) status := compiler_core.compile_package("examples/programs/invalid_unused_function", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
} }
@(test)
used_invalid_function_is_diagnosed_and_traps :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-invalid-used-function"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/invalid_used_function", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test) @(test)
function_mediated_problematic_global_is_deferred :: proc(t: ^testing.T) { function_mediated_problematic_global_is_deferred :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-function-global-unused" output := "/tmp/brolang-test-function-global-unused"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/function_global_unused.bro", output) status := compiler_core.compile_package("examples/programs/function_global_unused", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
@@ -502,8 +757,349 @@ function_mediated_problematic_global_is_deferred :: proc(t: ^testing.T) {
function_mediated_problematic_global_traps_when_used :: proc(t: ^testing.T) { function_mediated_problematic_global_traps_when_used :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-function-global-used" output := "/tmp/brolang-test-function-global-used"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_file("examples/function_global_used.bro", output) status := compiler_core.compile_package("examples/programs/function_global_used", output)
testing.expect_value(t, status, 1) testing.expect_value(t, status, 1)
state := run_executable(output) state := run_executable(output)
testing.expect(t, !state.success) testing.expect(t, !state.success)
} }
@(test)
package_files_merge_and_qualified_imports_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-basic"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/basic/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 3)
}
@(test)
unused_import_is_diagnosed_but_remains_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-unused"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/unused/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
unused_missing_package_does_not_trap :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-missing-unused"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/missing_unused/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
referenced_missing_package_traps_at_reference :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-missing-used"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/missing_used/app", output)
testing.expect_value(t, status, 1)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect(t, !state.success)
testing.expect(t, strings.contains(string(stderr), "/missing_used/app/main.bro:4:6:"))
}
@(test)
imports_are_file_local :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-file-local"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/file_local/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
package_import_cycles_are_valid :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-cycle"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/cycle/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 4)
}
@(test)
imported_main_is_an_ordinary_callable_function :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-imported-main"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/imported_main/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 7)
}
@(test)
same_named_c_functions_in_packages_do_not_collide :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-c-symbols"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/c_symbols/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
invalid_root_package_preserves_existing_output :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-preserved-output"
defer _ = os.remove(output)
previous := "previous artifact"
testing.expect(t, os.write_entire_file(output, transmute([]byte)previous))
testing.expect_value(t, compiler_core.compile_package("examples/programs/prototype/main.bro", output), 2)
data, ok := os.read_entire_file(output)
defer delete(data)
testing.expect(t, ok)
testing.expect_value(t, string(data), previous)
}
@(test)
empty_root_package_is_an_infrastructure_error :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-empty"
defer _ = os.remove(output)
testing.expect_value(t, compiler_core.compile_package("examples/packages/empty", output), 2)
testing.expect(t, !os.exists(output))
}
@(test)
same_package_can_be_imported_under_distinct_aliases :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-aliases"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/aliases/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 4)
}
@(test)
duplicate_same_file_import_keeps_first_binding :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-duplicate"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/duplicate/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 2)
}
@(test)
identical_imports_in_different_files_are_independent :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-repeated"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/repeated/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 4)
}
@(test)
imports_do_not_reexport_members :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-non-transitive"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/non_transitive/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
self_import_via_dot_is_valid :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-self"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/self_import/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 5)
}
@(test)
unused_absolute_import_is_diagnosed_without_trapping :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-absolute"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/absolute/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
unreferenced_problematic_imported_global_is_deferred :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-problematic-unused"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/problematic_unused/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
valid_eager_global_in_unused_package_still_initializes :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-eager-unused"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/eager_unused/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
cross_package_global_initialization_cycle_traps_when_used :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-global-cycle"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/global_cycle/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
cross_package_generic_specializes_from_folded_argument :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-generic"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/generic/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 128)
}
@(test)
lazy_function_body_marks_import_as_used_without_resolving_it :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-lazy-import"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/lazy_import/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
qualified_global_inference_ignores_same_named_local :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-qualified-shadow"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/qualified_shadow/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 300)
}
@(test)
cross_package_recursive_specialization_reaches_fixed_point :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-recursive"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/recursive/app", output)
testing.expect_value(t, status, 0)
testing.expect(t, os.exists(output))
}
@(test)
imported_main_does_not_satisfy_root_main_requirement :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-missing-root-main"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/missing_root_main/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
package_llvm_is_deterministic_and_symbols_include_package_ids :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
ast_module, loaded := loader.load("examples/packages/c_symbols/app", &sources, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics)
defer delete(llvm_text)
second_llvm_text := llvm.emit(&ir_module, &diagnostics)
defer delete(second_llvm_text)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, llvm_text, second_llvm_text)
testing.expect(t, strings.contains(llvm_text, "define i8 @bro_c__p1__same"))
testing.expect(t, strings.contains(llvm_text, "define i8 @bro_c__p2__same"))
testing.expect(t, strings.contains(llvm_text, "define i32 @main()"))
}
@(test)
invalid_default_alias_requires_an_explicit_alias :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-default-alias-invalid"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/default_alias_invalid/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
explicit_alias_allows_invalid_directory_basename :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-explicit-alias"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/explicit_alias/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 3)
}
@(test)
import_alias_conflict_is_diagnosed_without_trapping :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-alias-conflict"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/alias_conflict/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 7)
}
@(test)
empty_imported_package_is_a_source_diagnostic :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-empty-import"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/empty_import/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
non_directory_import_is_a_source_diagnostic :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-file-import"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/file_import/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
import_is_available_before_its_textual_declaration :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-import-order"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/import_order/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 6)
}
@(test)
deeply_nested_child_packages_load_recursively :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-deep"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/deep/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 9)
}
+3
View File
@@ -0,0 +1,3 @@
import "/definitely/not/a/brolang/package"
main :: func() void {}
@@ -0,0 +1,6 @@
math i32 :: 7
import "../math"
main :: func() i32 {
return math
}
@@ -0,0 +1 @@
value i32 :: 2
+6
View File
@@ -0,0 +1,6 @@
left :: import "../math"
right :: import "../math"
main :: func() i32 {
return left.value + right.value
}
+1
View File
@@ -0,0 +1 @@
value i32 :: 2
+1
View File
@@ -0,0 +1 @@
This non-Brolang file must not be loaded as part of the package.
+5
View File
@@ -0,0 +1,5 @@
import "../math"
main :: func() i32 {
return math.sum(local_value, math.value)
}
@@ -0,0 +1 @@
this is deliberately invalid and must not be loaded
+1
View File
@@ -0,0 +1 @@
local_value i32 :: 1
+5
View File
@@ -0,0 +1,5 @@
value i32 :: 2
sum :: func(a, b i32) i32 {
return a + b
}
+7
View File
@@ -0,0 +1,7 @@
import "../left"
import "../right"
main :: func() void {
_ = left.same()
_ = right.same()
}
@@ -0,0 +1,3 @@
same :: c func() int {
return 1
}
@@ -0,0 +1,3 @@
same :: c func() int {
return 2
}
+7
View File
@@ -0,0 +1,7 @@
import "../b"
seed i32 :: 4
run :: func() i32 {
return b.value
}
+5
View File
@@ -0,0 +1,5 @@
import "../a"
main :: func() i32 {
return a.run()
}
+3
View File
@@ -0,0 +1,3 @@
import "../a"
value i32 :: a.seed
@@ -0,0 +1 @@
value i32 :: 9
@@ -0,0 +1,5 @@
import "./level_three"
value :: func() i32 {
return level_three.value
}
@@ -0,0 +1,5 @@
import "./level_two"
value :: func() i32 {
return level_two.value()
}
+5
View File
@@ -0,0 +1,5 @@
import "./level_one"
main :: func() i32 {
return level_one.value()
}
@@ -0,0 +1,3 @@
import "../bad-name"
main :: func() void {}
@@ -0,0 +1 @@
value i32 :: 3
+6
View File
@@ -0,0 +1,6 @@
math :: import "../math"
math :: import "../math"
main :: func() i32 {
return math.value
}
@@ -0,0 +1 @@
value i32 :: 2
@@ -0,0 +1,3 @@
import "../eager"
main :: func() void {}
@@ -0,0 +1,2 @@
base i8 :: 127
overflow i8 :: base + 1
+1
View File
@@ -0,0 +1 @@
An empty root package has no immediate .bro files and is an infrastructure error.
@@ -0,0 +1,3 @@
import "../empty"
main :: func() void {}
@@ -0,0 +1 @@
This imported package intentionally has no .bro files.
@@ -0,0 +1,5 @@
good :: import "../bad-name"
main :: func() i32 {
return good.value
}
@@ -0,0 +1 @@
value i32 :: 3
@@ -0,0 +1,3 @@
import "../not_package.bro"
main :: func() void {}
@@ -0,0 +1 @@
main :: func() void {}
+5
View File
@@ -0,0 +1,5 @@
import "../math"
used_here :: func() int {
return math.value
}
+8
View File
@@ -0,0 +1,8 @@
not_imported_here :: func() int {
return math.value
}
main :: func() void {
_ = used_here()
_ = not_imported_here()
}
@@ -0,0 +1 @@
value :: 1
+5
View File
@@ -0,0 +1,5 @@
import "../math"
main :: func() i32 {
return math.identity(127 + 1)
}
+3
View File
@@ -0,0 +1,3 @@
identity :: func(value int) int {
return value
}
+3
View File
@@ -0,0 +1,3 @@
import "../b"
value i32 :: b.value
@@ -0,0 +1,5 @@
import "../a"
main :: func() void {
_ = a.value
}
+3
View File
@@ -0,0 +1,3 @@
import "../a"
value i32 :: a.value
@@ -0,0 +1,5 @@
main :: func() i32 {
return math.value
}
import "../math"
@@ -0,0 +1 @@
value i32 :: 6
@@ -0,0 +1,5 @@
import "../dep"
main :: func() i32 {
return dep.main()
}
@@ -0,0 +1,3 @@
main :: func() i32 {
return 7
}
@@ -0,0 +1,7 @@
import "../math"
unused :: func() int {
return math.missing
}
main :: func() void {}
@@ -0,0 +1 @@
value :: 7
@@ -0,0 +1,3 @@
import "../dep"
value :: dep.main()
@@ -0,0 +1,3 @@
main :: func() i32 {
return 1
}
@@ -0,0 +1,3 @@
import "../missing"
main :: func() void {}
@@ -0,0 +1,5 @@
import "../missing"
main :: func() void {
_ = missing.value
}
@@ -0,0 +1,5 @@
import "../bridge"
main :: func() void {
_ = bridge.value
}
@@ -0,0 +1,5 @@
import "../math"
read :: func() i32 {
return math.value
}
@@ -0,0 +1 @@
value i32 :: 2
@@ -0,0 +1,3 @@
import "../broken"
main :: func() void {}
@@ -0,0 +1 @@
bad = 1
@@ -0,0 +1,9 @@
import "../math"
read :: func(value i8) int {
return math.value
}
main :: func() i32 {
return read(1)
}
@@ -0,0 +1 @@
value i16 :: 300
+5
View File
@@ -0,0 +1,5 @@
import "../b"
one :: func(value int) i32 {
return b.two(value)
}
+5
View File
@@ -0,0 +1,5 @@
import "../a"
main :: func() i32 {
return a.one(1)
}
+5
View File
@@ -0,0 +1,5 @@
import "../a"
two :: func(value int) i32 {
return a.one(value)
}
+5
View File
@@ -0,0 +1,5 @@
import "../math"
from_a :: func() i32 {
return math.value
}
+5
View File
@@ -0,0 +1,5 @@
import "../math"
main :: func() i32 {
return from_a() + math.value
}
+1
View File
@@ -0,0 +1 @@
value i32 :: 2
@@ -0,0 +1,7 @@
self :: import "."
value i32 :: 5
main :: func() i32 {
return self.value
}
+3
View File
@@ -0,0 +1,3 @@
import "../math"
main :: func() void {}
+1
View File
@@ -0,0 +1 @@
value :: 1
@@ -0,0 +1,7 @@
broken :: func(value int) int {
return missing + value
}
main :: func() void {
_ = broken(1)
}
+1
View File
@@ -0,0 +1 @@
main :: func() i32 { return 7 }

Some files were not shown because too many files have changed in this diff Show More