diff --git a/.gitignore b/.gitignore index 77e3fbc..6802b69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,14 @@ -brolang +/brolang # examples -prototype -main_i32 -cycle_used -cycle_unused -invalid_transitive_used_global -invalid_transitive_unused_global -runtime_global -missing_main -mutable_local -overflow +/prototype +/main_i32 +/cycle_used +/cycle_unused +/invalid_transitive_used_global +/invalid_transitive_unused_global +/runtime_global +/missing_main +/mutable_local +/overflow +/deep diff --git a/README.md b/README.md index 693f103..a1ac86f 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,42 @@ Prototype error-tolerant Brolang compiler written in Odin. ```sh odin build . -out:brolang -./brolang examples/prototype.bro -o /tmp/prototype +./brolang examples/programs/prototype -o /tmp/prototype /tmp/prototype ``` Compilation phases are isolated under `compiler/`: ```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 possible, invalid code lowers to runtime diagnostic traps and the compiler 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: -- 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 - `i8`, `i16`, `i32`, `i64`, and loose integer-constrained `int` - 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 - Checked signed addition - Static, eager runtime, and deferred problematic globals diff --git a/TODO.md b/TODO.md index 26a72c9..f3fe42d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,22 +1,44 @@ -# "quick" fixes +# "quick"/"easy" fixes - 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 -1. import system: - - 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: +1. get c interop working: - link with c / compile c code into binary alongside brolang code - 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 + - 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 + ``` diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 9a5183f..af33ff1 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -26,6 +26,7 @@ Expr_Kind :: enum { Expr :: struct { kind: Expr_Kind, span: source.Span, + qualifier: string, text: string, integer: i64, left: int, @@ -61,6 +62,8 @@ Stmt :: struct { Function :: struct { span: source.Span, name: string, + pkg: int, + file: int, c_abi: bool, params: []Param, result: Type_Syntax, @@ -71,17 +74,45 @@ Function :: struct { Global :: struct { span: source.Span, name: string, + pkg: int, + file: int, type: Type_Syntax, immutable: bool, expr: 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 { exprs: [dynamic]Expr, statements: [dynamic]Stmt, functions: [dynamic]Function, globals: [dynamic]Global, + imports: [dynamic]Import, + files: [dynamic]File, + packages: [dynamic]Package, allocator: mem.Allocator, } @@ -92,6 +123,9 @@ init_module :: proc(allocator := context.allocator) -> Module { module.statements.allocator = allocator module.functions.allocator = allocator module.globals.allocator = allocator + module.imports.allocator = allocator + module.files.allocator = allocator + module.packages.allocator = allocator return module } @@ -103,8 +137,18 @@ destroy_module :: proc(module: ^Module) { delete(function.params, 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.statements) delete(module.functions) delete(module.globals) + delete(module.imports) + delete(module.files) + delete(module.packages) } diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 137cbf6..3c5959d 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -7,6 +7,7 @@ import "../types" import "base:intrinsics" import "core:fmt" import "core:mem" +import "core:slice" import "core:strings" Spec :: struct { @@ -40,14 +41,23 @@ Constant :: struct { value: i128, } +Symbol_Index_Entry :: struct { + scope: int, + name: string, + id: int, +} + Checker :: struct { - ast_module: ^ast.Module, - diagnostics: ^source.Diagnostics, - module: hir.Module, - specs: [dynamic]Spec, - global_types: []types.Type, - constants: []Constant, - allocator: mem.Allocator, + ast_module: ^ast.Module, + diagnostics: ^source.Diagnostics, + module: hir.Module, + specs: [dynamic]Spec, + function_index: []Symbol_Index_Entry, + global_index: []Symbol_Index_Entry, + import_index: []Symbol_Index_Entry, + global_types: []types.Type, + constants: []Constant, + allocator: mem.Allocator, } 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 } -find_template :: proc(checker: ^Checker, name: string) -> int { - for function, index in checker.ast_module.functions { - if function.name == name { - return index +symbol_index_less :: proc(left, right: Symbol_Index_Entry) -> bool { + if left.scope != right.scope { + return left.scope < right.scope + } + 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 } -find_global :: proc(checker: ^Checker, name: string) -> int { - for global, index in checker.ast_module.globals { - if global.name == name { - return index - } +build_symbol_indexes :: proc(checker: ^Checker) { + checker.function_index = make([]Symbol_Index_Entry, len(checker.ast_module.functions), checker.allocator) + for function, id in checker.ast_module.functions { + 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 { @@ -143,41 +254,31 @@ contains_name :: proc(names: []string, name: string) -> bool { 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) { return } expr := checker.ast_module.exprs[expr_id] switch expr.kind { case .Name: - if !contains_name(locals, expr.text) && find_global(checker, expr.text) < 0 { - source.addf(checker.diagnostics, expr.span, "unresolved name '%s'", expr.text) + if expr.qualifier != "" { + _ = find_import(checker, file, expr.qualifier, true) } case .Call: - if find_template(checker, expr.text) < 0 { - source.addf(checker.diagnostics, expr.span, "unresolved function '%s'", expr.text) + if expr.qualifier != "" { + _ = find_import(checker, file, expr.qualifier, true) } for arg in expr.args { - validate_expr_names(checker, arg, locals) + mark_expr_imports_used(checker, arg, file) } case .Add: - validate_expr_names(checker, expr.left, locals) - validate_expr_names(checker, expr.right, locals) + mark_expr_imports_used(checker, expr.left, file) + mark_expr_imports_used(checker, expr.right, file) case .Invalid, .Integer: } } -validate_templates :: 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) - } +validate_declarations :: proc(checker: ^Checker) { for function in checker.ast_module.functions { locals: [dynamic]string locals.allocator = checker.allocator @@ -202,36 +303,8 @@ validate_templates :: proc(checker: ^Checker) { for statement_id in function.body { statement := checker.ast_module.statements[statement_id] switch statement.kind { - case .Declaration: - validate_expr_names(checker, statement.expr, locals[:]) - 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 .Declaration, .Assignment, .Return, .Expression: + mark_expr_imports_used(checker, statement.expr, function.file) case .Invalid: } } @@ -268,6 +341,19 @@ specialized_param_type :: proc(syntax: ast.Type_Syntax, actual: types.Type) -> t 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 { function := checker.ast_module.functions[template] 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) - if function.name == "main" && function.result == .Int { + if function.pkg == 0 && function.name == "main" && function.result == .Int { result = types.I32 } index := len(checker.specs) @@ -297,7 +383,7 @@ ensure_spec :: proc(checker: ^Checker, template: int, actual_args: []types.Type) 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) { return types.INVALID } @@ -315,27 +401,49 @@ infer_expr :: proc(checker: ^Checker, expr_id: int, locals: []Infer_Local) -> ty case .Integer: return types.smallest_signed_for_literal(expr.integer) case .Name: - local_type := find_infer_local(locals, expr.text) - if types.is_valid(local_type) { - return local_type + if expr.qualifier == "" { + local_type := find_infer_local(locals, expr.text) + 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 { return checker.global_types[global] } return types.INVALID case .Add: - left := infer_expr(checker, expr.left, locals) - right := infer_expr(checker, expr.right, locals) + left := infer_expr(checker, expr.left, locals, pkg, file) + right := infer_expr(checker, expr.right, locals, pkg, file) return types.widest(left, right) 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 { return types.INVALID } args := make([]types.Type, len(expr.args), checker.allocator) 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) delete(args, checker.allocator) @@ -348,7 +456,7 @@ infer_spec_result :: proc(checker: ^Checker, spec_id: int) -> types.Type { spec := checker.specs[spec_id] function := checker.ast_module.functions[spec.template] 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 } @@ -368,17 +476,17 @@ infer_spec_result :: proc(checker: ^Checker, spec_id: int) -> types.Type { statement := checker.ast_module.statements[statement_id] #partial switch statement.kind { 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) if declared_local.kind == .Concrete { value_type = declared_local } append(&locals, Infer_Local{name = statement.name, type = value_type}) case .Assignment, .Expression: - _ = infer_expr(checker, statement.expr, locals[:]) + _ = infer_expr(checker, statement.expr, locals[:], function.pkg, function.file) case .Return: 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) { result = returned } else { @@ -393,6 +501,22 @@ infer_spec_result :: proc(checker: ^Checker, spec_id: int) -> types.Type { 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) { for global, index in checker.ast_module.globals { declared := type_from_syntax(global.type) @@ -400,40 +524,25 @@ infer_all :: proc(checker: ^Checker) { 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 { 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 spec_count := len(checker.specs) - for spec_id in 0 ..< spec_count { - inferred := infer_spec_result(checker, spec_id) - if types.is_valid(inferred) && !types.equal(checker.specs[spec_id].result, inferred) { - checker.specs[spec_id].result = inferred - changed = true + for global, index in checker.ast_module.globals { + if type_from_syntax(global.type).kind == .Concrete { + continue } + 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 { changed = true @@ -442,14 +551,6 @@ infer_all :: proc(checker: ^Checker) { 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 { @@ -589,6 +690,8 @@ build_expr :: proc( global_reads: ^[dynamic]int, calls: ^[dynamic]int, expected := types.INVALID, + pkg := 0, + file := 0, ) -> int { if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { id := source.add(checker.diagnostics, source.Span{}, "missing expression") @@ -605,21 +708,28 @@ build_expr :: proc( case .Integer: unreachable() case .Name: - if local, ok := find_build_local(locals, expr.text); ok { - return add_hir_expr( - checker, - hir.Expr { - kind = .Local, - span = expr.span, - type = local.type, - target = local.id, - left = -1, - right = -1, - diagnostic = -1, - }, - ) + if expr.qualifier == "" { + if local, ok := find_build_local(locals, expr.text); ok { + return add_hir_expr( + checker, + hir.Expr { + kind = .Local, + span = expr.span, + type = local.type, + target = local.id, + left = -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 { add_unique(global_reads, global) 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) case .Add: - left := build_expr(checker, expr.left, locals, global_reads, calls) - right := build_expr(checker, expr.right, 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, types.INVALID, pkg, file) left_type := checker.module.exprs[left].type right_type := checker.module.exprs[right].type result := types.widest(left_type, right_type) @@ -666,14 +776,14 @@ build_expr :: proc( }, ) 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 { - id := source.addf( - checker.diagnostics, - expr.span, - "unresolved function '%s'", - expr.text, - ) + id := add_call_resolution_diagnostic(checker, expr, target_pkg) return invalid_hir_expr(checker, expr.span, id) } if len(expr.args) != len(checker.ast_module.functions[template].params) { @@ -701,6 +811,8 @@ build_expr :: proc( global_reads, calls, arg_expected, + pkg, + file, ) 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 { spec := checker.specs[spec_id] 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) } - 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) defer strings.builder_destroy(&builder) - if !function.c_abi { - strings.write_string(&builder, "bro__") - } + strings.write_string(&builder, "bro_c__" if function.c_abi else "bro__") + fmt.sbprintf(&builder, "p%d__", function.pkg) strings.write_string(&builder, function.name) for arg in spec.args { strings.write_string(&builder, "__") @@ -783,7 +884,7 @@ build_function :: proc(checker: ^Checker, spec_id: int) { spec := checker.specs[spec_id] function := checker.ast_module.functions[spec.template] 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 spec.result = types.I64 signature_diagnostic = source.addf( @@ -862,6 +963,8 @@ build_function :: proc(checker: ^Checker, spec_id: int) { &global_reads, &calls, expected, + function.pkg, + function.file, ) value_type := checker.module.exprs[value].type if declared.kind == .Concrete { @@ -929,7 +1032,7 @@ build_function :: proc(checker: ^Checker, spec_id: int) { problematic = problematic || checker.module.exprs[value].kind == .Invalid case .Assignment: 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 { id := source.add( checker.diagnostics, @@ -1013,6 +1116,8 @@ build_function :: proc(checker: ^Checker, spec_id: int) { &global_reads, &calls, local.type, + function.pkg, + function.file, ) value = coerce_expr(checker, value, local.type, statement.span) append(&body, len(checker.module.statements)) @@ -1090,6 +1195,8 @@ build_function :: proc(checker: ^Checker, spec_id: int) { &global_reads, &calls, spec.result, + function.pkg, + function.file, ) value = coerce_expr(checker, value, spec.result, statement.span) 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 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 { id := source.add( checker.diagnostics, @@ -1173,8 +1280,8 @@ build_function :: proc(checker: ^Checker, spec_id: int) { hir.Function { name = function.name, link_name = make_link_name(checker, spec_id), - c_abi = function.c_abi || function.name == "main", - is_main = function.name == "main", + c_abi = function.c_abi || (function.pkg == 0 && function.name == "main"), + is_main = function.pkg == 0 && function.name == "main", params = params[:], result = spec.result, locals = hir_locals[:], @@ -1221,7 +1328,7 @@ build_globals :: proc(checker: ^Checker) { if declared.kind == .Concrete { 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] if declared.kind == .Concrete { 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) { global_type = checker.module.exprs[expr].type } - if !types.is_concrete_integer(global_type) { - global_type = types.I64 - } 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 { diagnostic = source.add( checker.diagnostics, @@ -1389,16 +1503,14 @@ detect_global_cycles_visit :: proc(checker: ^Checker, global_id: int, states: [] return } if states[global_id] == 1 { - if !checker.module.globals[global_id].problematic { - id := source.addf( - checker.diagnostics, - checker.ast_module.globals[global_id].span, - "global initialization cycle involving '%s'", - checker.module.globals[global_id].name, - ) - checker.module.globals[global_id].diagnostic = id - checker.module.globals[global_id].problematic = true - } + id := source.addf( + checker.diagnostics, + checker.ast_module.globals[global_id].span, + "global initialization cycle involving '%s'", + checker.module.globals[global_id].name, + ) + checker.module.globals[global_id].diagnostic = id + checker.module.globals[global_id].problematic = true return } states[global_id] = 1 @@ -1478,6 +1590,7 @@ check :: proc( allocator = allocator, } checker.specs.allocator = allocator + build_symbol_indexes(&checker) checker.global_types = make([]types.Type, len(ast_module.globals), allocator) checker.constants = make([]Constant, len(ast_module.exprs), allocator) defer { @@ -1485,26 +1598,34 @@ check :: proc( delete(spec.args, allocator) } 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.constants, allocator) } for function, index in ast_module.functions { 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) } } + 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 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) } } } - validate_templates(&checker) + validate_declarations(&checker) infer_all(&checker) build_globals(&checker) for spec_id := 0; spec_id < len(checker.specs); spec_id += 1 { @@ -1513,10 +1634,10 @@ check :: proc( resolve_call_targets(&checker) propagate_global_reads(&checker) - main_template := find_template(&checker, "main") + main_template := find_template(&checker, "main", 0) main_declarations := 0 for function in ast_module.functions { - if function.name == "main" { + if function.pkg == 0 && function.name == "main" { main_declarations += 1 } } @@ -1543,5 +1664,10 @@ check :: proc( } delete(states, allocator) 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 } diff --git a/compiler/compiler.odin b/compiler/compiler.odin index 59a2911..9f3084d 100644 --- a/compiler/compiler.odin +++ b/compiler/compiler.odin @@ -2,27 +2,20 @@ package compiler import "./backend" import "./checker" -import "./lexer" import "./llvm" +import "./loader" import "./lower" import "./opt" -import "./parser" import "./source" import "core:fmt" import vmem "core:mem/virtual" import "core:os" import "core:os/os2" -compile_file :: proc(input_path, output_path: string) -> int { - source_bytes, ok := os.read_entire_file(input_path) - if !ok { - fmt.eprintln("failed to read input:", input_path) - return 2 - } - defer delete(source_bytes) - - source_file := source.Source{path=input_path, text=string(source_bytes)} - diagnostics := source.init_diagnostics(&source_file) +compile_package :: proc(input_path, output_path: string) -> int { + sources := source.init_store() + defer source.destroy_store(&sources) + diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) lexer_arena: vmem.Arena @@ -50,8 +43,17 @@ compile_file :: proc(input_path, output_path: string) -> int { } defer vmem.arena_destroy(&lower_arena) - tokens := lexer.lex(&source_file, &diagnostics, vmem.arena_allocator(&lexer_arena)) - ast_module := parser.parse(&tokens, &diagnostics, vmem.arena_allocator(&parser_arena)) + ast_module, loaded := loader.load( + 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) hir_module := checker.check(&ast_module, &diagnostics, vmem.arena_allocator(&checker_arena)) vmem.arena_free_all(&parser_arena) diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index 193314b..c7069f9 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -15,6 +15,7 @@ keyword_kind :: proc(text: string) -> token.Kind { switch text { case "c": return .Keyword_C case "func": return .Keyword_Func + case "import": return .Keyword_Import case "return": return .Keyword_Return case "void": return .Keyword_Void case "int": return .Keyword_Int @@ -36,7 +37,7 @@ append_token :: proc( ) { append(&stream.items, token.Token{ 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], diagnostic=diagnostic, }) @@ -71,7 +72,7 @@ lex :: proc( cursor += 1 append_token(&stream, source_file, .Colon_Colon, start, cursor) } 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) } case '=': @@ -80,6 +81,9 @@ lex :: proc( case '+': append_token(&stream, source_file, .Plus, cursor, cursor+1) cursor += 1 + case '.': + append_token(&stream, source_file, .Dot, cursor, cursor+1) + cursor += 1 case '(': append_token(&stream, source_file, .Left_Paren, cursor, cursor+1) cursor += 1 @@ -95,10 +99,41 @@ lex :: proc( case ',': append_token(&stream, source_file, .Comma, cursor, 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 ';': id := source.add( 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", ) append_token(&stream, source_file, .Invalid, cursor, cursor+1, id) @@ -120,7 +155,7 @@ lex :: proc( } else { id := source.addf( diagnostics, - source.Span{start=cursor, end=cursor+1}, + source.Span{file=source_file.id, start=cursor, end=cursor+1}, "invalid source byte 0x%02x", value, ) diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 19b1e44..7d8d823 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -65,10 +65,14 @@ diagnostic_message :: proc(emitter: ^Emitter, diagnostic: int, span: source.Span delete(message, emitter.allocator) 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( "%s:%d:%d: runtime trap: %s", - emitter.diagnostics.source.path, + source_file.path, line, column, fallback, diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin new file mode 100644 index 0000000..13ff847 --- /dev/null +++ b/compiler/loader/loader.odin @@ -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..= 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 +} diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 139f3de..edc2cac 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -3,16 +3,24 @@ package parser import "../ast" import "../source" import "../token" +import "core:fmt" import "core:strconv" +import "core:strings" Parser :: struct { tokens: ^token.Stream, diagnostics: ^source.Diagnostics, module: ast.Module, + pkg: int, + file: int, cursor: 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 { 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 } -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) 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{ 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, args=args[:], left=ast.INVALID_ID, @@ -143,14 +152,24 @@ parse_primary :: proc(parser: ^Parser) -> int { diagnostic=-1, }) 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 { - return parse_call(parser, tok) + return parse_call(parser, qualifier, first, name) } return add_expr(parser, ast.Expr{ kind=.Name, - span=tok.span, - text=tok.text, + span=span_from(first.span, name.span), + qualifier=qualifier, + text=name.text, left=ast.INVALID_ID, right=ast.INVALID_ID, diagnostic=-1, @@ -198,7 +217,7 @@ parse_expression :: proc(parser: ^Parser) -> int { right_expr := parser.module.exprs[right] left = add_expr(parser, ast.Expr{ 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, right=right, diagnostic=-1, @@ -210,12 +229,12 @@ parse_expression :: proc(parser: ^Parser) -> int { return left } -finish_statement :: proc(parser: ^Parser) -> int { +finish_statement :: proc(parser: ^Parser, allow_closing_brace := false) -> int { if current(parser).kind == .Newline { skip_newlines(parser) return -1 } - if current(parser).kind == .Eof { + if current(parser).kind == .Eof || allow_closing_brace && current(parser).kind == .Right_Brace { return -1 } diagnostic := source.add( @@ -240,7 +259,7 @@ parse_return :: proc(parser: ^Parser) -> int { id := len(parser.module.statements) append(&parser.module.statements, ast.Stmt{ kind=.Return, - span=source.Span{start=start.span.start, end=end.span.end}, + span=span_from(start.span, end.span), name="_", expr=ast.INVALID_ID, diagnostic=-1, @@ -251,7 +270,7 @@ parse_return :: proc(parser: ^Parser) -> int { id := len(parser.module.statements) append(&parser.module.statements, ast.Stmt{ 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, diagnostic=-1, }) @@ -286,7 +305,7 @@ parse_statement :: proc(parser: ^Parser) -> int { id := len(parser.module.statements) append(&parser.module.statements, ast.Stmt{ 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, type=type_syntax, immutable=immutable, @@ -367,7 +386,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { skip_newlines(parser) for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { 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) append(&parser.module.statements, ast.Stmt{ kind=.Invalid, @@ -384,8 +403,10 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { end = func_token } 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, + pkg=parser.pkg, + file=parser.file, c_abi=c_abi, params=params, 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) { + if current(parser).kind == .Keyword_Import { + start := advance(parser) + parse_import(parser, token.Token{}, start) + return + } if current(parser).kind != .Identifier { source.add(parser.diagnostics, current(parser).span, "expected a top-level declaration") for current(parser).kind != .Newline && current(parser).kind != .Eof { @@ -404,6 +481,17 @@ parse_top_level :: proc(parser: ^Parser) { return } 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 if is_type_token(current(parser).kind) { type_syntax = parse_type(parser) @@ -430,8 +518,10 @@ parse_top_level :: proc(parser: ^Parser) { expr := parse_expression(parser) 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, + pkg=parser.pkg, + file=parser.file, type=type_syntax, immutable=operator.kind == .Colon_Colon, expr=expr, @@ -457,3 +547,24 @@ parse :: proc( } 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 +} diff --git a/compiler/source/source.odin b/compiler/source/source.odin index fb69ad7..2d8d70e 100644 --- a/compiler/source/source.odin +++ b/compiler/source/source.odin @@ -4,24 +4,57 @@ import "core:fmt" import "core:mem" Span :: struct { + file: int, start: int, end: int, } Source :: struct { + id: int, path: string, text: string, } +Store :: struct { + items: [dynamic]Source, + allocator: mem.Allocator, +} + Diagnostic :: struct { span: Span, message: string, } Diagnostics :: struct { - source: ^Source, - items: [dynamic]Diagnostic, - allocator: mem.Allocator, + source: ^Source, + store: ^Store, + 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 { @@ -32,6 +65,14 @@ init_diagnostics :: proc(source_file: ^Source, allocator := context.allocator) - 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) { for diagnostic in diagnostics.items { delete(diagnostic.message, diagnostics.allocator) @@ -79,15 +120,32 @@ line_and_column :: proc(source_file: ^Source, offset: int) -> (line, column: int 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 { if id < 0 || id >= len(diagnostics.items) { - return fmt.aprintf("%s: compiler recovery error", diagnostics.source.path, allocator=allocator) + path := "" + 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] - 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(": error: %s", diagnostic.message, allocator=allocator) + } + line, column := line_and_column(source_file, diagnostic.span.start) return fmt.aprintf( "%s:%d:%d: error: %s", - diagnostics.source.path, + source_file.path, line, column, diagnostic.message, diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 2f4f685..43593c9 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -8,10 +8,12 @@ Kind :: enum { Newline, Identifier, Integer, + String, Underscore, Colon_Colon, Equal, Plus, + Dot, Left_Paren, Right_Paren, Left_Brace, @@ -19,6 +21,7 @@ Kind :: enum { Comma, Keyword_C, Keyword_Func, + Keyword_Import, Keyword_Return, Keyword_Void, Keyword_Int, diff --git a/compiler_tests.odin b/compiler_tests.odin index 36c7991..4e7b3c7 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -7,12 +7,14 @@ import "./compiler/checker" import "./compiler/hir" import "./compiler/ir" import "./compiler/lexer" +import "./compiler/loader" import "./compiler/llvm" import "./compiler/lower" import "./compiler/parser" import "./compiler/source" import "./compiler/token" import "./compiler/types" +import "core:fmt" import "core:os" import "core:os/os2" import "core:strings" @@ -53,6 +55,153 @@ main :: func() void {} 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) pipeline_emits_specialized_calling_conventions_and_checked_add :: proc(t: ^testing.T) { 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, 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 internal fastcc i8 @bro__sum_bro__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__p0__sum_bro__i8__i8")) testing.expect(t, strings.contains(llvm_text, "@llvm.sadd.with.overflow.i8")) } @@ -123,7 +272,7 @@ main :: func() void { defer delete(llvm_text) 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 instruction in function.instructions { 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) } +@(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 { state, stdout, stderr, _ := os2.process_exec( 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) { output := "/tmp/brolang-test-valid" 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) state := run_executable(output) 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) folded_constant_addition_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-constant-fold" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-invalid-unused" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-invalid-used" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-main-int" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-main-i32" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-transitive-unused" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-overflow" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-constant-context-error" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-constant-i64-overflow" 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) state := run_executable(output) testing.expect(t, !state.success) @@ -384,7 +629,7 @@ constant_beyond_i64_produces_trap_executable :: proc(t: ^testing.T) { @(test) 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} diagnostics := source.init_diagnostics(&source_file) 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) { output := "/tmp/brolang-test-missing-main" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-mutable" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-narrowing" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-runtime-global" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-cycle-unused" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-cycle-used" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-malformed-typed" 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) state := run_executable(output) testing.expect(t, !state.success) } @(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" defer _ = os.remove(output) - status := compiler_core.compile_file("examples/invalid_unused_function.bro", output) - testing.expect_value(t, status, 1) + status := compiler_core.compile_package("examples/programs/invalid_unused_function", output) + testing.expect_value(t, status, 0) state := run_executable(output) 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) function_mediated_problematic_global_is_deferred :: proc(t: ^testing.T) { output := "/tmp/brolang-test-function-global-unused" 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) state := run_executable(output) 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) { output := "/tmp/brolang-test-function-global-used" 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) state := run_executable(output) 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) +} diff --git a/examples/packages/absolute/app/main.bro b/examples/packages/absolute/app/main.bro new file mode 100644 index 0000000..b3385d9 --- /dev/null +++ b/examples/packages/absolute/app/main.bro @@ -0,0 +1,3 @@ +import "/definitely/not/a/brolang/package" + +main :: func() void {} diff --git a/examples/packages/alias_conflict/app/main.bro b/examples/packages/alias_conflict/app/main.bro new file mode 100644 index 0000000..fe4c46b --- /dev/null +++ b/examples/packages/alias_conflict/app/main.bro @@ -0,0 +1,6 @@ +math i32 :: 7 +import "../math" + +main :: func() i32 { + return math +} diff --git a/examples/packages/alias_conflict/math/math.bro b/examples/packages/alias_conflict/math/math.bro new file mode 100644 index 0000000..6663c0e --- /dev/null +++ b/examples/packages/alias_conflict/math/math.bro @@ -0,0 +1 @@ +value i32 :: 2 diff --git a/examples/packages/aliases/app/main.bro b/examples/packages/aliases/app/main.bro new file mode 100644 index 0000000..4762be6 --- /dev/null +++ b/examples/packages/aliases/app/main.bro @@ -0,0 +1,6 @@ +left :: import "../math" +right :: import "../math" + +main :: func() i32 { + return left.value + right.value +} diff --git a/examples/packages/aliases/math/math.bro b/examples/packages/aliases/math/math.bro new file mode 100644 index 0000000..6663c0e --- /dev/null +++ b/examples/packages/aliases/math/math.bro @@ -0,0 +1 @@ +value i32 :: 2 diff --git a/examples/packages/basic/app/ignored.txt b/examples/packages/basic/app/ignored.txt new file mode 100644 index 0000000..cd503a9 --- /dev/null +++ b/examples/packages/basic/app/ignored.txt @@ -0,0 +1 @@ +This non-Brolang file must not be loaded as part of the package. diff --git a/examples/packages/basic/app/main.bro b/examples/packages/basic/app/main.bro new file mode 100644 index 0000000..89772b8 --- /dev/null +++ b/examples/packages/basic/app/main.bro @@ -0,0 +1,5 @@ +import "../math" + +main :: func() i32 { + return math.sum(local_value, math.value) +} diff --git a/examples/packages/basic/app/nested/ignored.bro b/examples/packages/basic/app/nested/ignored.bro new file mode 100644 index 0000000..efcc6d4 --- /dev/null +++ b/examples/packages/basic/app/nested/ignored.bro @@ -0,0 +1 @@ +this is deliberately invalid and must not be loaded diff --git a/examples/packages/basic/app/value.bro b/examples/packages/basic/app/value.bro new file mode 100644 index 0000000..32f72ed --- /dev/null +++ b/examples/packages/basic/app/value.bro @@ -0,0 +1 @@ +local_value i32 :: 1 diff --git a/examples/packages/basic/math/math.bro b/examples/packages/basic/math/math.bro new file mode 100644 index 0000000..9e56a04 --- /dev/null +++ b/examples/packages/basic/math/math.bro @@ -0,0 +1,5 @@ +value i32 :: 2 + +sum :: func(a, b i32) i32 { + return a + b +} diff --git a/examples/packages/c_symbols/app/main.bro b/examples/packages/c_symbols/app/main.bro new file mode 100644 index 0000000..e978b67 --- /dev/null +++ b/examples/packages/c_symbols/app/main.bro @@ -0,0 +1,7 @@ +import "../left" +import "../right" + +main :: func() void { + _ = left.same() + _ = right.same() +} diff --git a/examples/packages/c_symbols/left/left.bro b/examples/packages/c_symbols/left/left.bro new file mode 100644 index 0000000..b2f7c92 --- /dev/null +++ b/examples/packages/c_symbols/left/left.bro @@ -0,0 +1,3 @@ +same :: c func() int { + return 1 +} diff --git a/examples/packages/c_symbols/right/right.bro b/examples/packages/c_symbols/right/right.bro new file mode 100644 index 0000000..1983aae --- /dev/null +++ b/examples/packages/c_symbols/right/right.bro @@ -0,0 +1,3 @@ +same :: c func() int { + return 2 +} diff --git a/examples/packages/cycle/a/a.bro b/examples/packages/cycle/a/a.bro new file mode 100644 index 0000000..e351559 --- /dev/null +++ b/examples/packages/cycle/a/a.bro @@ -0,0 +1,7 @@ +import "../b" + +seed i32 :: 4 + +run :: func() i32 { + return b.value +} diff --git a/examples/packages/cycle/app/main.bro b/examples/packages/cycle/app/main.bro new file mode 100644 index 0000000..5ec8a3b --- /dev/null +++ b/examples/packages/cycle/app/main.bro @@ -0,0 +1,5 @@ +import "../a" + +main :: func() i32 { + return a.run() +} diff --git a/examples/packages/cycle/b/b.bro b/examples/packages/cycle/b/b.bro new file mode 100644 index 0000000..7a4406d --- /dev/null +++ b/examples/packages/cycle/b/b.bro @@ -0,0 +1,3 @@ +import "../a" + +value i32 :: a.seed diff --git a/examples/packages/deep/app/level_one/level_two/level_three/three.bro b/examples/packages/deep/app/level_one/level_two/level_three/three.bro new file mode 100644 index 0000000..fec2219 --- /dev/null +++ b/examples/packages/deep/app/level_one/level_two/level_three/three.bro @@ -0,0 +1 @@ +value i32 :: 9 diff --git a/examples/packages/deep/app/level_one/level_two/two.bro b/examples/packages/deep/app/level_one/level_two/two.bro new file mode 100644 index 0000000..e380a83 --- /dev/null +++ b/examples/packages/deep/app/level_one/level_two/two.bro @@ -0,0 +1,5 @@ +import "./level_three" + +value :: func() i32 { + return level_three.value +} diff --git a/examples/packages/deep/app/level_one/one.bro b/examples/packages/deep/app/level_one/one.bro new file mode 100644 index 0000000..9d66bb0 --- /dev/null +++ b/examples/packages/deep/app/level_one/one.bro @@ -0,0 +1,5 @@ +import "./level_two" + +value :: func() i32 { + return level_two.value() +} diff --git a/examples/packages/deep/app/main.bro b/examples/packages/deep/app/main.bro new file mode 100644 index 0000000..6e788d7 --- /dev/null +++ b/examples/packages/deep/app/main.bro @@ -0,0 +1,5 @@ +import "./level_one" + +main :: func() i32 { + return level_one.value() +} diff --git a/examples/packages/default_alias_invalid/app/main.bro b/examples/packages/default_alias_invalid/app/main.bro new file mode 100644 index 0000000..999cd26 --- /dev/null +++ b/examples/packages/default_alias_invalid/app/main.bro @@ -0,0 +1,3 @@ +import "../bad-name" + +main :: func() void {} diff --git a/examples/packages/default_alias_invalid/bad-name/value.bro b/examples/packages/default_alias_invalid/bad-name/value.bro new file mode 100644 index 0000000..2040ab0 --- /dev/null +++ b/examples/packages/default_alias_invalid/bad-name/value.bro @@ -0,0 +1 @@ +value i32 :: 3 diff --git a/examples/packages/duplicate/app/main.bro b/examples/packages/duplicate/app/main.bro new file mode 100644 index 0000000..04a8f1b --- /dev/null +++ b/examples/packages/duplicate/app/main.bro @@ -0,0 +1,6 @@ +math :: import "../math" +math :: import "../math" + +main :: func() i32 { + return math.value +} diff --git a/examples/packages/duplicate/math/math.bro b/examples/packages/duplicate/math/math.bro new file mode 100644 index 0000000..6663c0e --- /dev/null +++ b/examples/packages/duplicate/math/math.bro @@ -0,0 +1 @@ +value i32 :: 2 diff --git a/examples/packages/eager_unused/app/main.bro b/examples/packages/eager_unused/app/main.bro new file mode 100644 index 0000000..c8cc601 --- /dev/null +++ b/examples/packages/eager_unused/app/main.bro @@ -0,0 +1,3 @@ +import "../eager" + +main :: func() void {} diff --git a/examples/packages/eager_unused/eager/eager.bro b/examples/packages/eager_unused/eager/eager.bro new file mode 100644 index 0000000..cdfecc6 --- /dev/null +++ b/examples/packages/eager_unused/eager/eager.bro @@ -0,0 +1,2 @@ +base i8 :: 127 +overflow i8 :: base + 1 diff --git a/examples/packages/empty/README.txt b/examples/packages/empty/README.txt new file mode 100644 index 0000000..f0ea70c --- /dev/null +++ b/examples/packages/empty/README.txt @@ -0,0 +1 @@ +An empty root package has no immediate .bro files and is an infrastructure error. diff --git a/examples/packages/empty_import/app/main.bro b/examples/packages/empty_import/app/main.bro new file mode 100644 index 0000000..2f367ad --- /dev/null +++ b/examples/packages/empty_import/app/main.bro @@ -0,0 +1,3 @@ +import "../empty" + +main :: func() void {} diff --git a/examples/packages/empty_import/empty/README.txt b/examples/packages/empty_import/empty/README.txt new file mode 100644 index 0000000..ae212e5 --- /dev/null +++ b/examples/packages/empty_import/empty/README.txt @@ -0,0 +1 @@ +This imported package intentionally has no .bro files. diff --git a/examples/packages/explicit_alias/app/main.bro b/examples/packages/explicit_alias/app/main.bro new file mode 100644 index 0000000..9b3596b --- /dev/null +++ b/examples/packages/explicit_alias/app/main.bro @@ -0,0 +1,5 @@ +good :: import "../bad-name" + +main :: func() i32 { + return good.value +} diff --git a/examples/packages/explicit_alias/bad-name/value.bro b/examples/packages/explicit_alias/bad-name/value.bro new file mode 100644 index 0000000..2040ab0 --- /dev/null +++ b/examples/packages/explicit_alias/bad-name/value.bro @@ -0,0 +1 @@ +value i32 :: 3 diff --git a/examples/packages/file_import/app/main.bro b/examples/packages/file_import/app/main.bro new file mode 100644 index 0000000..62e216e --- /dev/null +++ b/examples/packages/file_import/app/main.bro @@ -0,0 +1,3 @@ +import "../not_package.bro" + +main :: func() void {} diff --git a/examples/packages/file_import/not_package.bro b/examples/packages/file_import/not_package.bro new file mode 100644 index 0000000..d7a03b8 --- /dev/null +++ b/examples/packages/file_import/not_package.bro @@ -0,0 +1 @@ +main :: func() void {} diff --git a/examples/packages/file_local/app/a.bro b/examples/packages/file_local/app/a.bro new file mode 100644 index 0000000..b4296b4 --- /dev/null +++ b/examples/packages/file_local/app/a.bro @@ -0,0 +1,5 @@ +import "../math" + +used_here :: func() int { + return math.value +} diff --git a/examples/packages/file_local/app/b.bro b/examples/packages/file_local/app/b.bro new file mode 100644 index 0000000..8cda583 --- /dev/null +++ b/examples/packages/file_local/app/b.bro @@ -0,0 +1,8 @@ +not_imported_here :: func() int { + return math.value +} + +main :: func() void { + _ = used_here() + _ = not_imported_here() +} diff --git a/examples/packages/file_local/math/math.bro b/examples/packages/file_local/math/math.bro new file mode 100644 index 0000000..377931b --- /dev/null +++ b/examples/packages/file_local/math/math.bro @@ -0,0 +1 @@ +value :: 1 diff --git a/examples/packages/generic/app/main.bro b/examples/packages/generic/app/main.bro new file mode 100644 index 0000000..bbdcffa --- /dev/null +++ b/examples/packages/generic/app/main.bro @@ -0,0 +1,5 @@ +import "../math" + +main :: func() i32 { + return math.identity(127 + 1) +} diff --git a/examples/packages/generic/math/math.bro b/examples/packages/generic/math/math.bro new file mode 100644 index 0000000..e47087e --- /dev/null +++ b/examples/packages/generic/math/math.bro @@ -0,0 +1,3 @@ +identity :: func(value int) int { + return value +} diff --git a/examples/packages/global_cycle/a/a.bro b/examples/packages/global_cycle/a/a.bro new file mode 100644 index 0000000..e92080f --- /dev/null +++ b/examples/packages/global_cycle/a/a.bro @@ -0,0 +1,3 @@ +import "../b" + +value i32 :: b.value diff --git a/examples/packages/global_cycle/app/main.bro b/examples/packages/global_cycle/app/main.bro new file mode 100644 index 0000000..08cfde4 --- /dev/null +++ b/examples/packages/global_cycle/app/main.bro @@ -0,0 +1,5 @@ +import "../a" + +main :: func() void { + _ = a.value +} diff --git a/examples/packages/global_cycle/b/b.bro b/examples/packages/global_cycle/b/b.bro new file mode 100644 index 0000000..dee75f4 --- /dev/null +++ b/examples/packages/global_cycle/b/b.bro @@ -0,0 +1,3 @@ +import "../a" + +value i32 :: a.value diff --git a/examples/packages/import_order/app/main.bro b/examples/packages/import_order/app/main.bro new file mode 100644 index 0000000..5ee2804 --- /dev/null +++ b/examples/packages/import_order/app/main.bro @@ -0,0 +1,5 @@ +main :: func() i32 { + return math.value +} + +import "../math" diff --git a/examples/packages/import_order/math/math.bro b/examples/packages/import_order/math/math.bro new file mode 100644 index 0000000..094b323 --- /dev/null +++ b/examples/packages/import_order/math/math.bro @@ -0,0 +1 @@ +value i32 :: 6 diff --git a/examples/packages/imported_main/app/main.bro b/examples/packages/imported_main/app/main.bro new file mode 100644 index 0000000..dd48b1e --- /dev/null +++ b/examples/packages/imported_main/app/main.bro @@ -0,0 +1,5 @@ +import "../dep" + +main :: func() i32 { + return dep.main() +} diff --git a/examples/packages/imported_main/dep/dep.bro b/examples/packages/imported_main/dep/dep.bro new file mode 100644 index 0000000..d3ae0cb --- /dev/null +++ b/examples/packages/imported_main/dep/dep.bro @@ -0,0 +1,3 @@ +main :: func() i32 { + return 7 +} diff --git a/examples/packages/lazy_import/app/main.bro b/examples/packages/lazy_import/app/main.bro new file mode 100644 index 0000000..452f4dd --- /dev/null +++ b/examples/packages/lazy_import/app/main.bro @@ -0,0 +1,7 @@ +import "../math" + +unused :: func() int { + return math.missing +} + +main :: func() void {} diff --git a/examples/packages/lazy_import/math/math.bro b/examples/packages/lazy_import/math/math.bro new file mode 100644 index 0000000..b8b40f9 --- /dev/null +++ b/examples/packages/lazy_import/math/math.bro @@ -0,0 +1 @@ +value :: 7 diff --git a/examples/packages/missing_root_main/app/value.bro b/examples/packages/missing_root_main/app/value.bro new file mode 100644 index 0000000..a57f4eb --- /dev/null +++ b/examples/packages/missing_root_main/app/value.bro @@ -0,0 +1,3 @@ +import "../dep" + +value :: dep.main() diff --git a/examples/packages/missing_root_main/dep/main.bro b/examples/packages/missing_root_main/dep/main.bro new file mode 100644 index 0000000..3d18c2d --- /dev/null +++ b/examples/packages/missing_root_main/dep/main.bro @@ -0,0 +1,3 @@ +main :: func() i32 { + return 1 +} diff --git a/examples/packages/missing_unused/app/main.bro b/examples/packages/missing_unused/app/main.bro new file mode 100644 index 0000000..1f56dad --- /dev/null +++ b/examples/packages/missing_unused/app/main.bro @@ -0,0 +1,3 @@ +import "../missing" + +main :: func() void {} diff --git a/examples/packages/missing_used/app/main.bro b/examples/packages/missing_used/app/main.bro new file mode 100644 index 0000000..0a272e0 --- /dev/null +++ b/examples/packages/missing_used/app/main.bro @@ -0,0 +1,5 @@ +import "../missing" + +main :: func() void { + _ = missing.value +} diff --git a/examples/packages/non_transitive/app/main.bro b/examples/packages/non_transitive/app/main.bro new file mode 100644 index 0000000..c31fde6 --- /dev/null +++ b/examples/packages/non_transitive/app/main.bro @@ -0,0 +1,5 @@ +import "../bridge" + +main :: func() void { + _ = bridge.value +} diff --git a/examples/packages/non_transitive/bridge/bridge.bro b/examples/packages/non_transitive/bridge/bridge.bro new file mode 100644 index 0000000..85bb814 --- /dev/null +++ b/examples/packages/non_transitive/bridge/bridge.bro @@ -0,0 +1,5 @@ +import "../math" + +read :: func() i32 { + return math.value +} diff --git a/examples/packages/non_transitive/math/math.bro b/examples/packages/non_transitive/math/math.bro new file mode 100644 index 0000000..6663c0e --- /dev/null +++ b/examples/packages/non_transitive/math/math.bro @@ -0,0 +1 @@ +value i32 :: 2 diff --git a/examples/packages/problematic_unused/app/main.bro b/examples/packages/problematic_unused/app/main.bro new file mode 100644 index 0000000..84c9722 --- /dev/null +++ b/examples/packages/problematic_unused/app/main.bro @@ -0,0 +1,3 @@ +import "../broken" + +main :: func() void {} diff --git a/examples/packages/problematic_unused/broken/broken.bro b/examples/packages/problematic_unused/broken/broken.bro new file mode 100644 index 0000000..9b98e7f --- /dev/null +++ b/examples/packages/problematic_unused/broken/broken.bro @@ -0,0 +1 @@ +bad = 1 diff --git a/examples/packages/qualified_shadow/app/main.bro b/examples/packages/qualified_shadow/app/main.bro new file mode 100644 index 0000000..92f57b4 --- /dev/null +++ b/examples/packages/qualified_shadow/app/main.bro @@ -0,0 +1,9 @@ +import "../math" + +read :: func(value i8) int { + return math.value +} + +main :: func() i32 { + return read(1) +} diff --git a/examples/packages/qualified_shadow/math/math.bro b/examples/packages/qualified_shadow/math/math.bro new file mode 100644 index 0000000..cb99956 --- /dev/null +++ b/examples/packages/qualified_shadow/math/math.bro @@ -0,0 +1 @@ +value i16 :: 300 diff --git a/examples/packages/recursive/a/a.bro b/examples/packages/recursive/a/a.bro new file mode 100644 index 0000000..718d810 --- /dev/null +++ b/examples/packages/recursive/a/a.bro @@ -0,0 +1,5 @@ +import "../b" + +one :: func(value int) i32 { + return b.two(value) +} diff --git a/examples/packages/recursive/app/main.bro b/examples/packages/recursive/app/main.bro new file mode 100644 index 0000000..bb74994 --- /dev/null +++ b/examples/packages/recursive/app/main.bro @@ -0,0 +1,5 @@ +import "../a" + +main :: func() i32 { + return a.one(1) +} diff --git a/examples/packages/recursive/b/b.bro b/examples/packages/recursive/b/b.bro new file mode 100644 index 0000000..dce89e7 --- /dev/null +++ b/examples/packages/recursive/b/b.bro @@ -0,0 +1,5 @@ +import "../a" + +two :: func(value int) i32 { + return a.one(value) +} diff --git a/examples/packages/repeated/app/a.bro b/examples/packages/repeated/app/a.bro new file mode 100644 index 0000000..911138f --- /dev/null +++ b/examples/packages/repeated/app/a.bro @@ -0,0 +1,5 @@ +import "../math" + +from_a :: func() i32 { + return math.value +} diff --git a/examples/packages/repeated/app/b.bro b/examples/packages/repeated/app/b.bro new file mode 100644 index 0000000..b3fe0bc --- /dev/null +++ b/examples/packages/repeated/app/b.bro @@ -0,0 +1,5 @@ +import "../math" + +main :: func() i32 { + return from_a() + math.value +} diff --git a/examples/packages/repeated/math/math.bro b/examples/packages/repeated/math/math.bro new file mode 100644 index 0000000..6663c0e --- /dev/null +++ b/examples/packages/repeated/math/math.bro @@ -0,0 +1 @@ +value i32 :: 2 diff --git a/examples/packages/self_import/app/main.bro b/examples/packages/self_import/app/main.bro new file mode 100644 index 0000000..fac5d6c --- /dev/null +++ b/examples/packages/self_import/app/main.bro @@ -0,0 +1,7 @@ +self :: import "." + +value i32 :: 5 + +main :: func() i32 { + return self.value +} diff --git a/examples/packages/unused/app/main.bro b/examples/packages/unused/app/main.bro new file mode 100644 index 0000000..fc93dfd --- /dev/null +++ b/examples/packages/unused/app/main.bro @@ -0,0 +1,3 @@ +import "../math" + +main :: func() void {} diff --git a/examples/packages/unused/math/math.bro b/examples/packages/unused/math/math.bro new file mode 100644 index 0000000..377931b --- /dev/null +++ b/examples/packages/unused/math/math.bro @@ -0,0 +1 @@ +value :: 1 diff --git a/examples/constant_context_error.bro b/examples/programs/constant_context_error/main.bro similarity index 100% rename from examples/constant_context_error.bro rename to examples/programs/constant_context_error/main.bro diff --git a/examples/constant_fold.bro b/examples/programs/constant_fold/main.bro similarity index 100% rename from examples/constant_fold.bro rename to examples/programs/constant_fold/main.bro diff --git a/examples/constant_i64_overflow.bro b/examples/programs/constant_i64_overflow/main.bro similarity index 100% rename from examples/constant_i64_overflow.bro rename to examples/programs/constant_i64_overflow/main.bro diff --git a/examples/cycle_unused.bro b/examples/programs/cycle_unused/main.bro similarity index 100% rename from examples/cycle_unused.bro rename to examples/programs/cycle_unused/main.bro diff --git a/examples/cycle_used.bro b/examples/programs/cycle_used/main.bro similarity index 100% rename from examples/cycle_used.bro rename to examples/programs/cycle_used/main.bro diff --git a/examples/function_global_unused.bro b/examples/programs/function_global_unused/main.bro similarity index 100% rename from examples/function_global_unused.bro rename to examples/programs/function_global_unused/main.bro diff --git a/examples/function_global_used.bro b/examples/programs/function_global_used/main.bro similarity index 100% rename from examples/function_global_used.bro rename to examples/programs/function_global_used/main.bro diff --git a/examples/invalid_transitive_unused_global.bro b/examples/programs/invalid_transitive_unused_global/main.bro similarity index 100% rename from examples/invalid_transitive_unused_global.bro rename to examples/programs/invalid_transitive_unused_global/main.bro diff --git a/examples/invalid_transitive_used_global.bro b/examples/programs/invalid_transitive_used_global/main.bro similarity index 100% rename from examples/invalid_transitive_used_global.bro rename to examples/programs/invalid_transitive_used_global/main.bro diff --git a/examples/invalid_unused_function.bro b/examples/programs/invalid_unused_function/main.bro similarity index 100% rename from examples/invalid_unused_function.bro rename to examples/programs/invalid_unused_function/main.bro diff --git a/examples/invalid_unused_global.bro b/examples/programs/invalid_unused_global/main.bro similarity index 100% rename from examples/invalid_unused_global.bro rename to examples/programs/invalid_unused_global/main.bro diff --git a/examples/programs/invalid_used_function/main.bro b/examples/programs/invalid_used_function/main.bro new file mode 100644 index 0000000..ed7f354 --- /dev/null +++ b/examples/programs/invalid_used_function/main.bro @@ -0,0 +1,7 @@ +broken :: func(value int) int { + return missing + value +} + +main :: func() void { + _ = broken(1) +} diff --git a/examples/invalid_used_global.bro b/examples/programs/invalid_used_global/main.bro similarity index 100% rename from examples/invalid_used_global.bro rename to examples/programs/invalid_used_global/main.bro diff --git a/examples/main_i32.bro b/examples/programs/main_i32/main.bro similarity index 100% rename from examples/main_i32.bro rename to examples/programs/main_i32/main.bro diff --git a/examples/main_int.bro b/examples/programs/main_int/main.bro similarity index 100% rename from examples/main_int.bro rename to examples/programs/main_int/main.bro diff --git a/examples/malformed_typed_recovery.bro b/examples/programs/malformed_typed_recovery/main.bro similarity index 100% rename from examples/malformed_typed_recovery.bro rename to examples/programs/malformed_typed_recovery/main.bro diff --git a/examples/missing_main.bro b/examples/programs/missing_main/main.bro similarity index 100% rename from examples/missing_main.bro rename to examples/programs/missing_main/main.bro diff --git a/examples/mutable_local.bro b/examples/programs/mutable_local/main.bro similarity index 100% rename from examples/mutable_local.bro rename to examples/programs/mutable_local/main.bro diff --git a/examples/narrowing_error.bro b/examples/programs/narrowing_error/main.bro similarity index 100% rename from examples/narrowing_error.bro rename to examples/programs/narrowing_error/main.bro diff --git a/examples/programs/one_line/main.bro b/examples/programs/one_line/main.bro new file mode 100644 index 0000000..1aafe30 --- /dev/null +++ b/examples/programs/one_line/main.bro @@ -0,0 +1 @@ +main :: func() i32 { return 7 } diff --git a/examples/overflow.bro b/examples/programs/overflow/main.bro similarity index 100% rename from examples/overflow.bro rename to examples/programs/overflow/main.bro diff --git a/examples/prototype.bro b/examples/programs/prototype/main.bro similarity index 100% rename from examples/prototype.bro rename to examples/programs/prototype/main.bro diff --git a/examples/runtime_global.bro b/examples/programs/runtime_global/main.bro similarity index 100% rename from examples/runtime_global.bro rename to examples/programs/runtime_global/main.bro diff --git a/main.odin b/main.odin index e0762b0..5daf450 100644 --- a/main.odin +++ b/main.odin @@ -6,10 +6,10 @@ import "core:os/os2" main :: proc() { if len(os2.args) != 4 || os2.args[2] != "-o" { - fmt.eprintln("usage: brolang -o ") + fmt.eprintln("usage: brolang -o ") os2.exit(2) } - status := compiler.compile_file(os2.args[1], os2.args[3]) + status := compiler.compile_package(os2.args[1], os2.args[3]) if status != 0 { os2.exit(status) }