From 087fdb45d5c700a2dd826c23563ee60bf75a5972 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Tue, 9 Jun 2026 17:28:28 +0200 Subject: [PATCH] initial draft --- .gitignore | 13 + README.md | 36 + TODO.md | 22 + compiler/ast/ast.odin | 110 ++ compiler/backend/backend.odin | 46 + compiler/checker/checker.odin | 1547 +++++++++++++++++ compiler/compiler.odin | 80 + compiler/hir/hir.odin | 121 ++ compiler/ir/ir.odin | 91 + compiler/lexer/lexer.odin | 135 ++ compiler/llvm/llvm.odin | 389 +++++ compiler/lower/lower.odin | 356 ++++ compiler/opt/opt.odin | 12 + compiler/parser/parser.odin | 459 +++++ compiler/source/source.odin | 104 ++ compiler/token/token.odin | 40 + compiler/types/types.odin | 139 ++ compiler_tests.odin | 509 ++++++ examples/constant_context_error.bro | 4 + examples/constant_fold.bro | 3 + examples/constant_i64_overflow.bro | 3 + examples/cycle_unused.bro | 6 + examples/cycle_used.bro | 6 + examples/function_global_unused.bro | 9 + examples/function_global_used.bro | 11 + examples/invalid_transitive_unused_global.bro | 11 + examples/invalid_transitive_used_global.bro | 11 + examples/invalid_unused_function.bro | 7 + examples/invalid_unused_global.bro | 5 + examples/invalid_used_global.bro | 5 + examples/main_i32.bro | 3 + examples/main_int.bro | 3 + examples/malformed_typed_recovery.bro | 12 + examples/missing_main.bro | 1 + examples/mutable_local.bro | 5 + examples/narrowing_error.bro | 7 + examples/overflow.bro | 4 + examples/prototype.bro | 20 + examples/runtime_global.bro | 10 + main.odin | 16 + 40 files changed, 4371 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 TODO.md create mode 100644 compiler/ast/ast.odin create mode 100644 compiler/backend/backend.odin create mode 100644 compiler/checker/checker.odin create mode 100644 compiler/compiler.odin create mode 100644 compiler/hir/hir.odin create mode 100644 compiler/ir/ir.odin create mode 100644 compiler/lexer/lexer.odin create mode 100644 compiler/llvm/llvm.odin create mode 100644 compiler/lower/lower.odin create mode 100644 compiler/opt/opt.odin create mode 100644 compiler/parser/parser.odin create mode 100644 compiler/source/source.odin create mode 100644 compiler/token/token.odin create mode 100644 compiler/types/types.odin create mode 100644 compiler_tests.odin create mode 100644 examples/constant_context_error.bro create mode 100644 examples/constant_fold.bro create mode 100644 examples/constant_i64_overflow.bro create mode 100644 examples/cycle_unused.bro create mode 100644 examples/cycle_used.bro create mode 100644 examples/function_global_unused.bro create mode 100644 examples/function_global_used.bro create mode 100644 examples/invalid_transitive_unused_global.bro create mode 100644 examples/invalid_transitive_used_global.bro create mode 100644 examples/invalid_unused_function.bro create mode 100644 examples/invalid_unused_global.bro create mode 100644 examples/invalid_used_global.bro create mode 100644 examples/main_i32.bro create mode 100644 examples/main_int.bro create mode 100644 examples/malformed_typed_recovery.bro create mode 100644 examples/missing_main.bro create mode 100644 examples/mutable_local.bro create mode 100644 examples/narrowing_error.bro create mode 100644 examples/overflow.bro create mode 100644 examples/prototype.bro create mode 100644 examples/runtime_global.bro create mode 100644 main.odin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77e3fbc --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +brolang + +# examples +prototype +main_i32 +cycle_used +cycle_unused +invalid_transitive_used_global +invalid_transitive_unused_global +runtime_global +missing_main +mutable_local +overflow diff --git a/README.md b/README.md new file mode 100644 index 0000000..693f103 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# brolang + +Prototype error-tolerant Brolang compiler written in Odin. + +```sh +odin build . -out:brolang +./brolang examples/prototype.bro -o /tmp/prototype +/tmp/prototype +``` + +Compilation phases are isolated under `compiler/`: + +```text +source -> 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`. + +Current prototype features: + +- Newline-terminated, multiline statements and `#` 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 +- Demand-monomorphized Brolang and C-ABI functions +- Checked signed addition +- Static, eager runtime, and deferred problematic globals +- Runtime diagnostics followed by `llvm.trap` + +Compiler exit statuses: + +- `0`: executable produced without source diagnostics +- `1`: executable produced with source diagnostics and embedded traps +- `2`: executable could not be produced diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..26a72c9 --- /dev/null +++ b/TODO.md @@ -0,0 +1,22 @@ +# "quick" fixes + +- for global initialization cycles, report also starting and ending lines + +# 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: + - link with c / compile c code into binary alongside brolang code + - create bindings from c headers + - find out how this should co-exist with the import system diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin new file mode 100644 index 0000000..9a5183f --- /dev/null +++ b/compiler/ast/ast.odin @@ -0,0 +1,110 @@ +package ast + +import "../source" +import "core:mem" + +INVALID_ID :: -1 + +Type_Syntax :: enum { + Invalid, + Int, + I8, + I16, + I32, + I64, + Void, +} + +Expr_Kind :: enum { + Invalid, + Integer, + Name, + Add, + Call, +} + +Expr :: struct { + kind: Expr_Kind, + span: source.Span, + text: string, + integer: i64, + left: int, + right: int, + args: []int, + diagnostic: int, +} + +Param :: struct { + name: string, + span: source.Span, + type: Type_Syntax, +} + +Stmt_Kind :: enum { + Invalid, + Declaration, + Assignment, + Return, + Expression, +} + +Stmt :: struct { + kind: Stmt_Kind, + span: source.Span, + name: string, + type: Type_Syntax, + immutable: bool, + expr: int, + diagnostic: int, +} + +Function :: struct { + span: source.Span, + name: string, + c_abi: bool, + params: []Param, + result: Type_Syntax, + body: []int, + diagnostic: int, +} + +Global :: struct { + span: source.Span, + name: string, + type: Type_Syntax, + immutable: bool, + expr: int, + diagnostic: int, +} + +Module :: struct { + exprs: [dynamic]Expr, + statements: [dynamic]Stmt, + functions: [dynamic]Function, + globals: [dynamic]Global, + allocator: mem.Allocator, +} + +init_module :: proc(allocator := context.allocator) -> Module { + module: Module + module.allocator = allocator + module.exprs.allocator = allocator + module.statements.allocator = allocator + module.functions.allocator = allocator + module.globals.allocator = allocator + return module +} + +destroy_module :: proc(module: ^Module) { + for expr in module.exprs { + delete(expr.args, module.allocator) + } + for function in module.functions { + delete(function.params, module.allocator) + delete(function.body, module.allocator) + } + delete(module.exprs) + delete(module.statements) + delete(module.functions) + delete(module.globals) +} diff --git a/compiler/backend/backend.odin b/compiler/backend/backend.odin new file mode 100644 index 0000000..1efe6e3 --- /dev/null +++ b/compiler/backend/backend.odin @@ -0,0 +1,46 @@ +package backend + +import "core:fmt" +import "core:os" +import "core:os/os2" + +compile :: proc(llvm_path, output_path: string) -> bool { + pid := os2.get_pid() + temporary_output := fmt.tprintf("%s.brolang-tmp-%d", output_path, pid) + defer _ = os.remove(temporary_output) + + command := []string{ + "/usr/bin/env", + "ZIG_LOCAL_CACHE_DIR=/tmp/brolang-zig-cache", + "ZIG_GLOBAL_CACHE_DIR=/tmp/brolang-zig-global-cache", + "zig", + "cc", + "-Wno-override-module", + llvm_path, + "-o", + temporary_output, + } + state, stdout, stderr, err := os2.process_exec( + os2.Process_Desc{command=command}, + context.allocator, + ) + defer delete(stdout) + defer delete(stderr) + if len(stdout) > 0 { + fmt.print(string(stdout)) + } + if len(stderr) > 0 { + fmt.eprint(string(stderr)) + } + if err != nil || state.exit_code != 0 { + if err != nil { + fmt.eprintln("failed to execute zig cc:", err) + } + return false + } + if rename_err := os2.rename(temporary_output, output_path); rename_err != nil { + fmt.eprintln("failed to atomically replace output:", rename_err) + return false + } + return true +} diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin new file mode 100644 index 0000000..137cbf6 --- /dev/null +++ b/compiler/checker/checker.odin @@ -0,0 +1,1547 @@ +package checker + +import "../ast" +import "../hir" +import "../source" +import "../types" +import "base:intrinsics" +import "core:fmt" +import "core:mem" +import "core:strings" + +Spec :: struct { + template: int, + args: []types.Type, + result: types.Type, + hir_id: int, +} + +Infer_Local :: struct { + name: string, + type: types.Type, +} + +Build_Local :: struct { + name: string, + type: types.Type, + mutable: bool, + id: int, +} + +Constant_Kind :: enum { + Unknown, + Not_Constant, + Value, + Overflow, +} + +Constant :: struct { + kind: Constant_Kind, + value: i128, +} + +Checker :: struct { + ast_module: ^ast.Module, + diagnostics: ^source.Diagnostics, + module: hir.Module, + specs: [dynamic]Spec, + global_types: []types.Type, + constants: []Constant, + allocator: mem.Allocator, +} + +eval_constant :: proc(checker: ^Checker, expr_id: int) -> Constant { + if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { + return Constant{kind = .Not_Constant} + } + if checker.constants[expr_id].kind != .Unknown { + return checker.constants[expr_id] + } + expr := checker.ast_module.exprs[expr_id] + result: Constant + switch expr.kind { + case .Integer: + result = Constant{kind = .Value, value = i128(expr.integer)} + case .Add: + left := eval_constant(checker, expr.left) + right := eval_constant(checker, expr.right) + if left.kind == .Overflow || right.kind == .Overflow { + result = Constant{kind = .Overflow} + } else if left.kind != .Value || right.kind != .Value { + result = Constant{kind = .Not_Constant} + } else { + value, overflow := intrinsics.overflow_add(left.value, right.value) + if overflow { + result = Constant{kind = .Overflow} + } else { + result = Constant{kind = .Value, value = value} + } + } + case .Invalid, .Name, .Call: + result = Constant{kind = .Not_Constant} + } + checker.constants[expr_id] = result + return result +} + +fits_signed_type :: proc(value: i128, target: types.Type) -> bool { + if !types.is_signed(target) { + return false + } + limit := i128(1) << u32(target.bits - 1) + return value >= -limit && value < limit +} + +fits_i64 :: proc(value: i128) -> bool { + return fits_signed_type(value, types.I64) +} + +type_from_syntax :: proc(value: ast.Type_Syntax) -> types.Type { + switch value { + case .Int: + return types.INT + case .I8: + return types.I8 + case .I16: + return types.I16 + case .I32: + return types.I32 + case .I64: + return types.I64 + case .Void: + return types.VOID + case .Invalid: + return types.INVALID + } + 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 + } + } + return -1 +} + +find_global :: proc(checker: ^Checker, name: string) -> int { + for global, index in checker.ast_module.globals { + if global.name == name { + return index + } + } + return -1 +} + +contains_name :: proc(names: []string, name: string) -> bool { + for existing in names { + if existing == name { + return true + } + } + return false +} + +validate_expr_names :: proc(checker: ^Checker, expr_id: int, locals: []string) { + 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) + } + case .Call: + if find_template(checker, expr.text) < 0 { + source.addf(checker.diagnostics, expr.span, "unresolved function '%s'", expr.text) + } + for arg in expr.args { + validate_expr_names(checker, arg, locals) + } + case .Add: + validate_expr_names(checker, expr.left, locals) + validate_expr_names(checker, expr.right, locals) + 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) + } + for function in checker.ast_module.functions { + locals: [dynamic]string + locals.allocator = checker.allocator + for param in function.params { + if param.type == .Void { + source.add( + checker.diagnostics, + param.span, + "void is only valid as a function result type", + ) + } + if contains_name(locals[:], param.name) { + source.addf( + checker.diagnostics, + param.span, + "duplicate parameter '%s'", + param.name, + ) + } + append(&locals, param.name) + } + 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 .Invalid: + } + } + delete(locals) + } +} + +find_infer_local :: proc(locals: []Infer_Local, name: string) -> types.Type { + for index := len(locals) - 1; index >= 0; index -= 1 { + if locals[index].name == name { + return locals[index].type + } + } + return types.INVALID +} + +spec_signature_equal :: proc(spec: Spec, template: int, args: []types.Type) -> bool { + if spec.template != template || len(spec.args) != len(args) { + return false + } + for arg, index in args { + if !types.equal(spec.args[index], arg) { + return false + } + } + return true +} + +specialized_param_type :: proc(syntax: ast.Type_Syntax, actual: types.Type) -> types.Type { + declared := type_from_syntax(syntax) + if declared.kind == .Int_Constraint { + return actual + } + return declared +} + +ensure_spec :: proc(checker: ^Checker, template: int, actual_args: []types.Type) -> int { + function := checker.ast_module.functions[template] + signature: [dynamic]types.Type + signature.allocator = checker.allocator + for param, index in function.params { + actual := types.INVALID + if index < len(actual_args) { + actual = actual_args[index] + } + append(&signature, specialized_param_type(param.type, actual)) + } + for spec, index in checker.specs { + if spec_signature_equal(spec, template, signature[:]) { + delete(signature) + return index + } + } + result := type_from_syntax(function.result) + if function.name == "main" && function.result == .Int { + result = types.I32 + } + index := len(checker.specs) + append( + &checker.specs, + Spec{template = template, args = signature[:], result = result, hir_id = -1}, + ) + return index +} + +infer_expr :: proc(checker: ^Checker, expr_id: int, locals: []Infer_Local) -> types.Type { + if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { + return types.INVALID + } + constant := eval_constant(checker, expr_id) + if constant.kind == .Overflow || (constant.kind == .Value && !fits_i64(constant.value)) { + return types.I64 + } + if constant.kind == .Value { + return types.smallest_signed_for_literal(i64(constant.value)) + } + expr := checker.ast_module.exprs[expr_id] + switch expr.kind { + case .Invalid: + return types.INVALID + 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 + } + global := find_global(checker, expr.text) + 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) + return types.widest(left, right) + case .Call: + template := find_template(checker, expr.text) + 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) + } + spec := ensure_spec(checker, template, args) + delete(args, checker.allocator) + return checker.specs[spec].result + } + return types.INVALID +} + +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 { + declared = types.I32 + } + + locals: [dynamic]Infer_Local + locals.allocator = checker.allocator + defer delete(locals) + for param, index in function.params { + param_type := types.INVALID + if index < len(spec.args) { + param_type = spec.args[index] + } + append(&locals, Infer_Local{name = param.name, type = param_type}) + } + + result := types.INVALID + for statement_id in function.body { + statement := checker.ast_module.statements[statement_id] + #partial switch statement.kind { + case .Declaration: + value_type := infer_expr(checker, statement.expr, locals[:]) + 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[:]) + case .Return: + if statement.expr >= 0 { + returned := infer_expr(checker, statement.expr, locals[:]) + if !types.is_valid(result) { + result = returned + } else { + result = types.widest(result, returned) + } + } + } + } + if declared.kind == .Int_Constraint { + return result + } + return declared +} + +infer_all :: proc(checker: ^Checker) { + for global, index in checker.ast_module.globals { + declared := type_from_syntax(global.type) + if declared.kind == .Concrete { + 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") + 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 { + 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 + } + } + if len(checker.specs) != spec_count { + changed = true + } + if !changed { + 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 { + id := len(checker.module.exprs) + append(&checker.module.exprs, expr) + return id +} + +invalid_hir_expr :: proc( + checker: ^Checker, + span: source.Span, + diagnostic: int, + recovery_type := types.INVALID, +) -> int { + return add_hir_expr( + checker, + hir.Expr { + kind = .Invalid, + span = span, + type = recovery_type, + target = -1, + left = -1, + right = -1, + diagnostic = diagnostic, + }, + ) +} + +add_unique :: proc(values: ^[dynamic]int, value: int) { + for existing in values { + if existing == value { + return + } + } + append(values, value) +} + +find_build_local :: proc(locals: []Build_Local, name: string) -> (Build_Local, bool) { + for index := len(locals) - 1; index >= 0; index -= 1 { + if locals[index].name == name { + return locals[index], true + } + } + return Build_Local{}, false +} + +coerce_expr :: proc( + checker: ^Checker, + expr_id: int, + expected: types.Type, + span: source.Span, +) -> int { + if expr_id < 0 { + return expr_id + } + actual := checker.module.exprs[expr_id].type + if types.equal(actual, expected) { + return expr_id + } + if types.can_widen(actual, expected) { + return add_hir_expr( + checker, + hir.Expr { + kind = .Widen, + span = span, + type = expected, + left = expr_id, + target = -1, + right = -1, + diagnostic = -1, + }, + ) + } + id := source.addf( + checker.diagnostics, + span, + "cannot implicitly convert %s to %s", + types.name(actual), + types.name(expected), + ) + return invalid_hir_expr(checker, span, id, expected) +} + +build_constant_expr :: proc( + checker: ^Checker, + expr: ast.Expr, + constant: Constant, + expected: types.Type, +) -> int { + recovery_type := types.I64 + if types.is_signed(expected) { + recovery_type = expected + } + if constant.kind == .Overflow || !fits_i64(constant.value) { + id := source.add( + checker.diagnostics, + expr.span, + "integer constant expression exceeds signed i64 range", + ) + return invalid_hir_expr(checker, expr.span, id, recovery_type) + } + + value := i64(constant.value) + result_type := types.smallest_signed_for_literal(value) + if types.is_signed(expected) { + if !fits_signed_type(constant.value, expected) { + id := source.addf( + checker.diagnostics, + expr.span, + "integer constant %d does not fit in %s", + constant.value, + types.name(expected), + ) + return invalid_hir_expr(checker, expr.span, id, expected) + } + result_type = expected + } + return add_hir_expr( + checker, + hir.Expr { + kind = .Integer, + span = expr.span, + type = result_type, + integer = value, + target = -1, + left = -1, + right = -1, + diagnostic = -1, + }, + ) +} + +build_expr :: proc( + checker: ^Checker, + expr_id: int, + locals: []Build_Local, + global_reads: ^[dynamic]int, + calls: ^[dynamic]int, + expected := types.INVALID, +) -> int { + if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { + id := source.add(checker.diagnostics, source.Span{}, "missing expression") + return invalid_hir_expr(checker, source.Span{}, id) + } + expr := checker.ast_module.exprs[expr_id] + constant := eval_constant(checker, expr_id) + if constant.kind == .Value || constant.kind == .Overflow { + return build_constant_expr(checker, expr, constant, expected) + } + switch expr.kind { + case .Invalid: + return invalid_hir_expr(checker, expr.span, expr.diagnostic) + 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, + }, + ) + } + global := find_global(checker, expr.text) + if global >= 0 { + add_unique(global_reads, global) + return add_hir_expr( + checker, + hir.Expr { + kind = .Global, + span = expr.span, + type = checker.global_types[global], + target = global, + left = -1, + right = -1, + diagnostic = -1, + }, + ) + } + id := source.addf(checker.diagnostics, expr.span, "unresolved name '%s'", expr.text) + 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_type := checker.module.exprs[left].type + right_type := checker.module.exprs[right].type + result := types.widest(left_type, right_type) + if !types.is_signed(result) { + id := source.add( + checker.diagnostics, + expr.span, + "addition requires compatible signed integers", + ) + return invalid_hir_expr(checker, expr.span, id) + } + left = coerce_expr(checker, left, result, checker.module.exprs[left].span) + right = coerce_expr(checker, right, result, checker.module.exprs[right].span) + return add_hir_expr( + checker, + hir.Expr { + kind = .Add, + span = expr.span, + type = result, + left = left, + right = right, + target = -1, + diagnostic = -1, + }, + ) + case .Call: + template := find_template(checker, expr.text) + if template < 0 { + id := source.addf( + checker.diagnostics, + expr.span, + "unresolved function '%s'", + expr.text, + ) + return invalid_hir_expr(checker, expr.span, id) + } + if len(expr.args) != len(checker.ast_module.functions[template].params) { + id := source.addf( + checker.diagnostics, + expr.span, + "function '%s' expects %d arguments, got %d", + expr.text, + len(checker.ast_module.functions[template].params), + len(expr.args), + ) + return invalid_hir_expr(checker, expr.span, id) + } + built_args := make([]int, len(expr.args), checker.allocator) + arg_types := make([]types.Type, len(expr.args), checker.allocator) + for arg, index in expr.args { + arg_expected := type_from_syntax(checker.ast_module.functions[template].params[index].type) + if arg_expected.kind != .Concrete { + arg_expected = types.INVALID + } + built_args[index] = build_expr( + checker, + arg, + locals, + global_reads, + calls, + arg_expected, + ) + arg_types[index] = checker.module.exprs[built_args[index]].type + } + spec := ensure_spec(checker, template, arg_types) + delete(arg_types, checker.allocator) + for _, index in built_args { + built_args[index] = coerce_expr( + checker, + built_args[index], + checker.specs[spec].args[index], + checker.module.exprs[built_args[index]].span, + ) + } + add_unique(calls, spec) + result := checker.specs[spec].result + if !types.is_valid(result) { + id := source.addf( + checker.diagnostics, + expr.span, + "could not resolve result type for specialization of '%s'", + expr.text, + ) + delete(built_args, checker.allocator) + return invalid_hir_expr(checker, expr.span, id) + } + return add_hir_expr( + checker, + hir.Expr { + kind = .Call, + span = expr.span, + type = result, + target = spec, + left = -1, + right = -1, + args = built_args, + diagnostic = -1, + }, + ) + } + return invalid_hir_expr( + checker, + expr.span, + source.add(checker.diagnostics, expr.span, "invalid expression"), + ) +} + +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" { + 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, function.name) + for arg in spec.args { + strings.write_string(&builder, "__") + strings.write_string(&builder, types.name(arg)) + } + return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator) +} + +build_function :: proc(checker: ^Checker, spec_id: int) { + if checker.specs[spec_id].hir_id >= 0 { + return + } + spec := checker.specs[spec_id] + function := checker.ast_module.functions[spec.template] + signature_diagnostic := -1 + if !types.is_valid(spec.result) { + checker.specs[spec_id].result = types.I64 + spec.result = types.I64 + signature_diagnostic = source.addf( + checker.diagnostics, + function.span, + "could not resolve a concrete result type for '%s'", + function.name, + ) + } + for arg in spec.args { + if !types.is_concrete_integer(arg) { + signature_diagnostic = source.addf( + checker.diagnostics, + function.span, + "could not resolve a concrete parameter type for '%s'", + function.name, + ) + break + } + } + hir_id := len(checker.module.functions) + checker.specs[spec_id].hir_id = hir_id + + locals: [dynamic]Build_Local + locals.allocator = checker.allocator + hir_locals: [dynamic]hir.Local + hir_locals.allocator = checker.allocator + params: [dynamic]int + params.allocator = checker.allocator + body: [dynamic]int + body.allocator = checker.allocator + global_reads: [dynamic]int + global_reads.allocator = checker.allocator + calls: [dynamic]int + calls.allocator = checker.allocator + + for param, index in function.params { + local_id := len(hir_locals) + param_type := types.INVALID + if index < len(spec.args) { + param_type = spec.args[index] + } + append(&hir_locals, hir.Local{name = param.name, type = param_type, parameter = true}) + append(&locals, Build_Local{name = param.name, type = param_type, id = local_id}) + append(¶ms, local_id) + } + + problematic := signature_diagnostic >= 0 + has_return := false + if signature_diagnostic >= 0 { + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Trap, + span = function.span, + expr = -1, + local = -1, + diagnostic = signature_diagnostic, + }, + ) + } + for statement_id in function.body { + statement := checker.ast_module.statements[statement_id] + switch statement.kind { + case .Declaration: + declared := type_from_syntax(statement.type) + expected := types.INVALID + if declared.kind == .Concrete { + expected = declared + } + value := build_expr( + checker, + statement.expr, + locals[:], + &global_reads, + &calls, + expected, + ) + value_type := checker.module.exprs[value].type + if declared.kind == .Concrete { + value = coerce_expr(checker, value, declared, statement.span) + value_type = checker.module.exprs[value].type + } else if declared.kind == .Void { + id := source.add( + checker.diagnostics, + statement.span, + "locals cannot have type void", + ) + value = invalid_hir_expr(checker, statement.span, id) + value_type = types.INVALID + } + if _, found := find_build_local(locals[:], statement.name); found { + id := source.addf( + checker.diagnostics, + statement.span, + "duplicate local '%s'", + statement.name, + ) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Trap, + span = statement.span, + expr = -1, + local = -1, + diagnostic = id, + }, + ) + problematic = true + continue + } + local_id := len(hir_locals) + append( + &hir_locals, + hir.Local { + name = statement.name, + type = value_type, + mutable = !statement.immutable, + }, + ) + append( + &locals, + Build_Local { + name = statement.name, + type = value_type, + mutable = !statement.immutable, + id = local_id, + }, + ) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Declaration, + span = statement.span, + local = local_id, + expr = value, + diagnostic = -1, + }, + ) + problematic = problematic || checker.module.exprs[value].kind == .Invalid + case .Assignment: + if statement.name == "_" { + value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls) + if checker.module.exprs[value].type.kind == .Void { + id := source.add( + checker.diagnostics, + statement.span, + "cannot assign a void expression to '_'", + ) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Trap, + span = statement.span, + expr = -1, + local = -1, + diagnostic = id, + }, + ) + problematic = true + } else { + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Sink, + span = statement.span, + expr = value, + local = -1, + diagnostic = -1, + }, + ) + } + continue + } + local, found := find_build_local(locals[:], statement.name) + if !found { + id := source.addf( + checker.diagnostics, + statement.span, + "cannot assign unresolved local '%s'", + statement.name, + ) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Trap, + span = statement.span, + expr = -1, + local = -1, + diagnostic = id, + }, + ) + problematic = true + continue + } + if !local.mutable { + id := source.addf( + checker.diagnostics, + statement.span, + "cannot assign immutable local '%s'", + statement.name, + ) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Trap, + span = statement.span, + expr = -1, + local = -1, + diagnostic = id, + }, + ) + problematic = true + continue + } + value := build_expr( + checker, + statement.expr, + locals[:], + &global_reads, + &calls, + local.type, + ) + value = coerce_expr(checker, value, local.type, statement.span) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Assignment, + span = statement.span, + expr = value, + local = local.id, + diagnostic = -1, + }, + ) + problematic = problematic || checker.module.exprs[value].kind == .Invalid + case .Return: + has_return = true + if statement.expr < 0 { + if spec.result.kind != .Void { + id := source.add( + checker.diagnostics, + statement.span, + "'return _' is only valid in a void function", + ) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Trap, + span = statement.span, + expr = -1, + local = -1, + diagnostic = id, + }, + ) + problematic = true + } else { + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Return, + span = statement.span, + expr = -1, + local = -1, + diagnostic = -1, + }, + ) + } + continue + } + if spec.result.kind == .Void { + id := source.add( + checker.diagnostics, + statement.span, + "void function cannot return a value", + ) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Trap, + span = statement.span, + expr = -1, + local = -1, + diagnostic = id, + }, + ) + problematic = true + continue + } + value := build_expr( + checker, + statement.expr, + locals[:], + &global_reads, + &calls, + spec.result, + ) + value = coerce_expr(checker, value, spec.result, statement.span) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Return, + span = statement.span, + expr = value, + local = -1, + diagnostic = -1, + }, + ) + problematic = problematic || checker.module.exprs[value].kind == .Invalid + case .Expression: + value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls) + if checker.module.exprs[value].type.kind != .Void { + id := source.add( + checker.diagnostics, + statement.span, + "non-void expression result must be consumed or assigned to '_'", + ) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Trap, + span = statement.span, + expr = -1, + local = -1, + diagnostic = id, + }, + ) + problematic = true + } else { + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Expression, + span = statement.span, + expr = value, + local = -1, + diagnostic = -1, + }, + ) + } + case .Invalid: + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt { + kind = .Trap, + span = statement.span, + expr = -1, + local = -1, + diagnostic = statement.diagnostic, + }, + ) + problematic = true + } + } + + if spec.result.kind != .Void && !has_return { + id := source.addf( + checker.diagnostics, + function.span, + "function '%s' does not return a value", + function.name, + ) + append(&body, len(checker.module.statements)) + append( + &checker.module.statements, + hir.Stmt{kind = .Trap, span = function.span, expr = -1, local = -1, diagnostic = id}, + ) + problematic = true + } + + append( + &checker.module.functions, + 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", + params = params[:], + result = spec.result, + locals = hir_locals[:], + body = body[:], + direct_global_reads = global_reads[:], + calls = calls[:], + problematic = problematic, + diagnostic = -1, + }, + ) + delete(locals) +} + +expr_problematic :: proc(module: ^hir.Module, expr_id: int) -> bool { + if expr_id < 0 || expr_id >= len(module.exprs) { + return true + } + expr := module.exprs[expr_id] + if expr.kind == .Invalid { + return true + } + if expr.left >= 0 && expr_problematic(module, expr.left) { + return true + } + if expr.right >= 0 && expr_problematic(module, expr.right) { + return true + } + for arg in expr.args { + if expr_problematic(module, arg) { + return true + } + } + return false +} + +build_globals :: proc(checker: ^Checker) { + for global, global_id in checker.ast_module.globals { + dependencies: [dynamic]int + dependencies.allocator = checker.allocator + calls: [dynamic]int + calls.allocator = checker.allocator + declared := type_from_syntax(global.type) + expected := types.INVALID + if declared.kind == .Concrete { + expected = declared + } + expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected) + global_type := checker.global_types[global_id] + if declared.kind == .Concrete { + expr = coerce_expr(checker, expr, declared, global.span) + global_type = checker.module.exprs[expr].type + } 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 global.type == .Void { + diagnostic = source.add( + checker.diagnostics, + global.span, + "void is only valid as a function result type", + ) + expr = invalid_hir_expr(checker, global.span, diagnostic) + } + if !global.immutable { + diagnostic = source.add( + checker.diagnostics, + global.span, + "mutable declarations are only valid inside functions", + ) + expr = invalid_hir_expr(checker, global.span, diagnostic) + } + is_static := checker.module.exprs[expr].kind == .Integer && diagnostic < 0 + static_value: i64 + if is_static { + static_value = checker.module.exprs[expr].integer + } + append( + &checker.module.globals, + hir.Global { + name = global.name, + type = global_type, + expr = expr, + static_value = static_value, + is_static = is_static, + dependencies = dependencies[:], + calls = calls[:], + direct_problem = expr_problematic(&checker.module, expr), + problematic = expr_problematic(&checker.module, expr), + diagnostic = diagnostic, + }, + ) + } +} + +propagate_problems :: proc(checker: ^Checker) { + changed := true + for changed { + changed = false + for &function in checker.module.functions { + if function.problematic { + continue + } + for call in function.calls { + if call >= 0 && call < len(checker.specs) { + hir_id := checker.specs[call].hir_id + if hir_id >= 0 && checker.module.functions[hir_id].problematic { + function.problematic = true + changed = true + break + } + } + } + } + for &global in checker.module.globals { + if global.problematic { + continue + } + for dependency in global.dependencies { + if dependency >= 0 && + dependency < len(checker.module.globals) && + checker.module.globals[dependency].problematic { + global.problematic = true + changed = true + break + } + } + if global.problematic { + continue + } + for call in global.calls { + if call >= 0 && call < len(checker.specs) { + hir_id := checker.specs[call].hir_id + if hir_id >= 0 && checker.module.functions[hir_id].problematic { + global.problematic = true + changed = true + break + } + } + } + } + } +} + +resolve_call_targets :: proc(checker: ^Checker) { + for &expr in checker.module.exprs { + if expr.kind == .Call && expr.target >= 0 && expr.target < len(checker.specs) { + expr.target = checker.specs[expr.target].hir_id + } + } +} + +append_unique_slice :: proc(values: ^[]int, value: int, allocator: mem.Allocator) -> bool { + for existing in values^ { + if existing == value { + return false + } + } + replacement := make([]int, len(values^) + 1, allocator) + copy(replacement, values^) + replacement[len(values^)] = value + delete(values^, allocator) + values^ = replacement + return true +} + +propagate_global_reads :: proc(checker: ^Checker) { + changed := true + for changed { + changed = false + for &function in checker.module.functions { + for call in function.calls { + if call < 0 || call >= len(checker.specs) { + continue + } + callee := checker.specs[call].hir_id + if callee < 0 || callee >= len(checker.module.functions) { + continue + } + for global_id in checker.module.functions[callee].direct_global_reads { + if append_unique_slice( + &function.direct_global_reads, + global_id, + checker.allocator, + ) { + changed = true + } + } + } + } + } + for &global in checker.module.globals { + for call in global.calls { + if call < 0 || call >= len(checker.specs) { + continue + } + function_id := checker.specs[call].hir_id + if function_id < 0 || function_id >= len(checker.module.functions) { + continue + } + for dependency in checker.module.functions[function_id].direct_global_reads { + _ = append_unique_slice(&global.dependencies, dependency, checker.allocator) + } + } + } +} + +detect_global_cycles_visit :: proc(checker: ^Checker, global_id: int, states: []u8) { + if states[global_id] == 2 { + 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 + } + return + } + states[global_id] = 1 + for dependency in checker.module.globals[global_id].dependencies { + if dependency >= 0 && dependency < len(states) { + detect_global_cycles_visit(checker, dependency, states) + if checker.module.globals[dependency].problematic { + checker.module.globals[global_id].problematic = true + } + } + } + states[global_id] = 2 +} + +synthesize_trap_main :: proc(checker: ^Checker) { + id := source.add(checker.diagnostics, source.Span{}, "missing or unusable main function") + statement_id := len(checker.module.statements) + append( + &checker.module.statements, + hir.Stmt{kind = .Trap, span = source.Span{}, expr = -1, local = -1, diagnostic = id}, + ) + body := make([]int, 1, checker.allocator) + body[0] = statement_id + append( + &checker.module.functions, + hir.Function { + name = "main", + link_name = fmt.aprintf("main", allocator = checker.allocator), + c_abi = true, + is_main = true, + result = types.VOID, + body = body, + problematic = true, + diagnostic = id, + }, + ) +} + +replace_main_with_trap :: proc(checker: ^Checker, diagnostic: int) { + for &function in checker.module.functions { + if !function.is_main { + continue + } + delete(function.params, checker.allocator) + delete(function.body, checker.allocator) + function.params = nil + function.result = types.VOID + function.problematic = true + function.diagnostic = diagnostic + statement_id := len(checker.module.statements) + append( + &checker.module.statements, + hir.Stmt { + kind = .Trap, + span = source.Span{}, + expr = -1, + local = -1, + diagnostic = diagnostic, + }, + ) + function.body = make([]int, 1, checker.allocator) + function.body[0] = statement_id + return + } + synthesize_trap_main(checker) +} + +check :: proc( + ast_module: ^ast.Module, + diagnostics: ^source.Diagnostics, + allocator := context.allocator, +) -> hir.Module { + checker := Checker { + ast_module = ast_module, + diagnostics = diagnostics, + module = hir.init_module(allocator), + allocator = allocator, + } + checker.specs.allocator = allocator + checker.global_types = make([]types.Type, len(ast_module.globals), allocator) + checker.constants = make([]Constant, len(ast_module.exprs), allocator) + defer { + for spec in checker.specs { + delete(spec.args, allocator) + } + delete(checker.specs) + 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 { + source.addf(diagnostics, function.span, "duplicate function '%s'", function.name) + } + } + } + for global, index in ast_module.globals { + for previous in ast_module.globals[:index] { + if previous.name == global.name { + source.addf(diagnostics, global.span, "duplicate global '%s'", global.name) + } + } + } + + validate_templates(&checker) + infer_all(&checker) + build_globals(&checker) + for spec_id := 0; spec_id < len(checker.specs); spec_id += 1 { + build_function(&checker, spec_id) + } + resolve_call_targets(&checker) + propagate_global_reads(&checker) + + main_template := find_template(&checker, "main") + main_declarations := 0 + for function in ast_module.functions { + if function.name == "main" { + main_declarations += 1 + } + } + if main_declarations == 0 { + synthesize_trap_main(&checker) + } else { + template := ast_module.functions[main_template] + if main_declarations != 1 || + len(template.params) != 0 || + !(template.result == .Void || template.result == .I32 || template.result == .Int) { + id := source.add( + diagnostics, + template.span, + "main must be unique, take no parameters, and return void, i32, or int", + ) + replace_main_with_trap(&checker, id) + } + } + + propagate_problems(&checker) + states := make([]u8, len(checker.module.globals), allocator) + for global_id in 0 ..< len(checker.module.globals) { + detect_global_cycles_visit(&checker, global_id, states) + } + delete(states, allocator) + propagate_problems(&checker) + return checker.module +} diff --git a/compiler/compiler.odin b/compiler/compiler.odin new file mode 100644 index 0000000..59a2911 --- /dev/null +++ b/compiler/compiler.odin @@ -0,0 +1,80 @@ +package compiler + +import "./backend" +import "./checker" +import "./lexer" +import "./llvm" +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) + defer source.destroy_diagnostics(&diagnostics) + + lexer_arena: vmem.Arena + if err := vmem.arena_init_growing(&lexer_arena); err != nil { + fmt.eprintln("failed to initialize lexer arena:", err) + return 2 + } + defer vmem.arena_destroy(&lexer_arena) + parser_arena: vmem.Arena + if err := vmem.arena_init_growing(&parser_arena); err != nil { + fmt.eprintln("failed to initialize parser arena:", err) + return 2 + } + defer vmem.arena_destroy(&parser_arena) + checker_arena: vmem.Arena + if err := vmem.arena_init_growing(&checker_arena); err != nil { + fmt.eprintln("failed to initialize checker arena:", err) + return 2 + } + defer vmem.arena_destroy(&checker_arena) + lower_arena: vmem.Arena + if err := vmem.arena_init_growing(&lower_arena); err != nil { + fmt.eprintln("failed to initialize lowering arena:", err) + return 2 + } + 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)) + vmem.arena_free_all(&lexer_arena) + hir_module := checker.check(&ast_module, &diagnostics, vmem.arena_allocator(&checker_arena)) + vmem.arena_free_all(&parser_arena) + ir_module := lower.lower(&hir_module, vmem.arena_allocator(&lower_arena)) + vmem.arena_free_all(&checker_arena) + opt.run(&ir_module) + + llvm_text := llvm.emit(&ir_module, &diagnostics) + defer delete(llvm_text) + vmem.arena_free_all(&lower_arena) + llvm_path := fmt.tprintf("%s.brolang-%d.ll", output_path, os2.get_pid()) + defer _ = os.remove(llvm_path) + if err := os.write_entire_file_or_err(llvm_path, transmute([]byte)llvm_text); err != nil { + fmt.eprintln("failed to write temporary LLVM IR:", err) + return 2 + } + + source.print_all(&diagnostics) + if !backend.compile(llvm_path, output_path) { + return 2 + } + if len(diagnostics.items) > 0 { + return 1 + } + return 0 +} diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin new file mode 100644 index 0000000..317e60c --- /dev/null +++ b/compiler/hir/hir.odin @@ -0,0 +1,121 @@ +package hir + +import "../source" +import "../types" +import "core:mem" + +INVALID_ID :: -1 + +Expr_Kind :: enum { + Invalid, + Integer, + Local, + Global, + Widen, + Add, + Call, +} + +Expr :: struct { + kind: Expr_Kind, + span: source.Span, + type: types.Type, + integer: i64, + target: int, + left: int, + right: int, + args: []int, + diagnostic: int, +} + +Local :: struct { + name: string, + type: types.Type, + mutable: bool, + parameter: bool, +} + +Stmt_Kind :: enum { + Declaration, + Assignment, + Return, + Expression, + Sink, + Trap, +} + +Stmt :: struct { + kind: Stmt_Kind, + span: source.Span, + local: int, + expr: int, + diagnostic: int, +} + +Function :: struct { + name: string, + link_name: string, + c_abi: bool, + is_main: bool, + params: []int, + result: types.Type, + locals: []Local, + body: []int, + direct_global_reads: []int, + calls: []int, + problematic: bool, + diagnostic: int, +} + +Global :: struct { + name: string, + type: types.Type, + expr: int, + static_value: i64, + is_static: bool, + dependencies: []int, + calls: []int, + direct_problem: bool, + problematic: bool, + diagnostic: int, +} + +Module :: struct { + exprs: [dynamic]Expr, + statements: [dynamic]Stmt, + functions: [dynamic]Function, + globals: [dynamic]Global, + allocator: mem.Allocator, +} + +init_module :: proc(allocator := context.allocator) -> Module { + module: Module + module.allocator = allocator + module.exprs.allocator = allocator + module.statements.allocator = allocator + module.functions.allocator = allocator + module.globals.allocator = allocator + return module +} + +destroy_module :: proc(module: ^Module) { + for expr in module.exprs { + delete(expr.args, module.allocator) + } + for function in module.functions { + delete(function.link_name, module.allocator) + delete(function.params, module.allocator) + delete(function.locals, module.allocator) + delete(function.body, module.allocator) + delete(function.direct_global_reads, module.allocator) + delete(function.calls, module.allocator) + } + for global in module.globals { + delete(global.dependencies, module.allocator) + delete(global.calls, module.allocator) + } + delete(module.exprs) + delete(module.statements) + delete(module.functions) + delete(module.globals) +} diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin new file mode 100644 index 0000000..928dd3c --- /dev/null +++ b/compiler/ir/ir.odin @@ -0,0 +1,91 @@ +package ir + +import "../source" +import "../types" +import "core:mem" + +INVALID_ID :: -1 + +Opcode :: enum { + Param, + Const, + Load_Global, + Alloca, + Load, + Store, + Widen, + Add_Checked, + Call, + Trap, + Return, + Return_Void, +} + +Instruction :: struct { + op: Opcode, + span: source.Span, + type: types.Type, + integer: i64, + target: int, + a: int, + b: int, + args: []int, + diagnostic: int, +} + +Function :: struct { + name: string, + link_name: string, + c_abi: bool, + is_main: bool, + param_types: []types.Type, + result: types.Type, + instructions: []Instruction, + problematic: bool, +} + +Global :: struct { + name: string, + type: types.Type, + is_static: bool, + static_value: i64, + initializer: []Instruction, + problematic: bool, + diagnostic: int, +} + +Module :: struct { + functions: [dynamic]Function, + globals: [dynamic]Global, + allocator: mem.Allocator, +} + +init_module :: proc(allocator := context.allocator) -> Module { + module: Module + module.functions.allocator = allocator + module.globals.allocator = allocator + module.allocator = allocator + return module +} + +destroy_instructions :: proc(instructions: []Instruction, allocator: mem.Allocator) { + for instruction in instructions { + delete(instruction.args, allocator) + } + delete(instructions, allocator) +} + +destroy_module :: proc(module: ^Module) { + for function in module.functions { + delete(function.name, module.allocator) + delete(function.link_name, module.allocator) + delete(function.param_types, module.allocator) + destroy_instructions(function.instructions, module.allocator) + } + for global in module.globals { + delete(global.name, module.allocator) + destroy_instructions(global.initializer, module.allocator) + } + delete(module.functions) + delete(module.globals) +} diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin new file mode 100644 index 0000000..193314b --- /dev/null +++ b/compiler/lexer/lexer.odin @@ -0,0 +1,135 @@ +package lexer + +import "../source" +import "../token" + +is_identifier_start :: proc(value: byte) -> bool { + return value == '_' || value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' +} + +is_identifier_continue :: proc(value: byte) -> bool { + return is_identifier_start(value) || value >= '0' && value <= '9' +} + +keyword_kind :: proc(text: string) -> token.Kind { + switch text { + case "c": return .Keyword_C + case "func": return .Keyword_Func + case "return": return .Keyword_Return + case "void": return .Keyword_Void + case "int": return .Keyword_Int + case "i8": return .Keyword_I8 + case "i16": return .Keyword_I16 + case "i32": return .Keyword_I32 + case "i64": return .Keyword_I64 + case "_": return .Underscore + } + return .Identifier +} + +append_token :: proc( + stream: ^token.Stream, + source_file: ^source.Source, + kind: token.Kind, + start, end: int, + diagnostic := -1, +) { + append(&stream.items, token.Token{ + kind=kind, + span=source.Span{start=start, end=end}, + text=source_file.text[start:end], + diagnostic=diagnostic, + }) +} + +lex :: proc( + source_file: ^source.Source, + diagnostics: ^source.Diagnostics, + allocator := context.allocator, +) -> token.Stream { + stream: token.Stream + stream.items.allocator = allocator + bytes := transmute([]byte)source_file.text + cursor := 0 + + for cursor < len(bytes) { + value := bytes[cursor] + switch value { + case ' ', '\t', '\r': + cursor += 1 + case '\n': + append_token(&stream, source_file, .Newline, cursor, cursor+1) + cursor += 1 + case '#': + for cursor < len(bytes) && bytes[cursor] != '\n' { + cursor += 1 + } + case ':': + start := cursor + cursor += 1 + if cursor < len(bytes) && bytes[cursor] == ':' { + 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 ':'") + append_token(&stream, source_file, .Invalid, start, cursor, id) + } + case '=': + append_token(&stream, source_file, .Equal, cursor, cursor+1) + cursor += 1 + case '+': + append_token(&stream, source_file, .Plus, cursor, cursor+1) + cursor += 1 + case '(': + append_token(&stream, source_file, .Left_Paren, cursor, cursor+1) + cursor += 1 + case ')': + append_token(&stream, source_file, .Right_Paren, cursor, cursor+1) + cursor += 1 + case '{': + append_token(&stream, source_file, .Left_Brace, cursor, cursor+1) + cursor += 1 + case '}': + append_token(&stream, source_file, .Right_Brace, cursor, cursor+1) + cursor += 1 + case ',': + append_token(&stream, source_file, .Comma, cursor, cursor+1) + cursor += 1 + case ';': + id := source.add( + diagnostics, + source.Span{start=cursor, end=cursor+1}, + "semicolons are invalid; terminate statements with a newline", + ) + append_token(&stream, source_file, .Invalid, cursor, cursor+1, id) + cursor += 1 + case: + if value >= '0' && value <= '9' { + start := cursor + for cursor < len(bytes) && bytes[cursor] >= '0' && bytes[cursor] <= '9' { + cursor += 1 + } + append_token(&stream, source_file, .Integer, start, cursor) + } else if is_identifier_start(value) { + start := cursor + for cursor < len(bytes) && is_identifier_continue(bytes[cursor]) { + cursor += 1 + } + text := source_file.text[start:cursor] + append_token(&stream, source_file, keyword_kind(text), start, cursor) + } else { + id := source.addf( + diagnostics, + source.Span{start=cursor, end=cursor+1}, + "invalid source byte 0x%02x", + value, + ) + append_token(&stream, source_file, .Invalid, cursor, cursor+1, id) + cursor += 1 + } + } + } + + append_token(&stream, source_file, .Eof, len(bytes), len(bytes)) + return stream +} diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin new file mode 100644 index 0000000..19b1e44 --- /dev/null +++ b/compiler/llvm/llvm.odin @@ -0,0 +1,389 @@ +package llvm + +import "../ir" +import "../source" +import "../types" +import "core:fmt" +import "core:mem" +import "core:strings" + +Trap_Message :: struct { + text: string, +} + +Emitter :: struct { + module: ^ir.Module, + diagnostics: ^source.Diagnostics, + builder: strings.Builder, + messages: [dynamic]Trap_Message, + allocator: mem.Allocator, +} + +llvm_type :: proc(value: types.Type) -> string { + if value.kind == .Void { + return "void" + } + switch value.bits { + case 8: return "i8" + case 16: return "i16" + case 32: return "i32" + case: return "i64" + } +} + +function_result_type :: proc(function: ir.Function) -> string { + if function.is_main { + return "i32" + } + return llvm_type(function.result) +} + +write_operand :: proc(builder: ^strings.Builder, instructions: []ir.Instruction, value_id: int) { + if value_id < 0 || value_id >= len(instructions) { + fmt.sbprintf(builder, "-6148914691236517206") + return + } + value := instructions[value_id] + if value.op == .Const { + fmt.sbprintf(builder, "%d", value.integer) + } else { + fmt.sbprintf(builder, "%%v%d", value_id) + } +} + +register_message :: proc(emitter: ^Emitter, text: string) -> int { + id := len(emitter.messages) + cloned := fmt.aprintf("%s\n", text, allocator=emitter.allocator) + append(&emitter.messages, Trap_Message{text=cloned}) + return id +} + +diagnostic_message :: proc(emitter: ^Emitter, diagnostic: int, span: source.Span, fallback: string) -> int { + if diagnostic >= 0 && diagnostic < len(emitter.diagnostics.items) { + message := source.format(emitter.diagnostics, diagnostic, emitter.allocator) + id := register_message(emitter, message) + delete(message, emitter.allocator) + return id + } + line, column := source.line_and_column(emitter.diagnostics.source, span.start) + message := fmt.aprintf( + "%s:%d:%d: runtime trap: %s", + emitter.diagnostics.source.path, + line, + column, + fallback, + allocator=emitter.allocator, + ) + id := register_message(emitter, message) + delete(message, emitter.allocator) + return id +} + +emit_trap_call :: proc(emitter: ^Emitter, message_id: int) { + message := emitter.messages[message_id] + fmt.sbprintf( + &emitter.builder, + " call void @bro.trap(ptr @bro.msg.%d, i64 %d)\n", + message_id, + len(message.text), + ) +} + +emit_call_args :: proc(builder: ^strings.Builder, instructions: []ir.Instruction, args: []int) { + for arg, index in args { + if index > 0 { + strings.write_string(builder, ", ") + } + fmt.sbprintf(builder, "%s ", llvm_type(instructions[arg].type)) + write_operand(builder, instructions, arg) + } +} + +emit_instruction_stream :: proc( + emitter: ^Emitter, + instructions: []ir.Instruction, + function: ir.Function, + global_initializer := false, +) -> int { + return_value := -1 + after_return := false + for instruction, instruction_id in instructions { + if after_return { + fmt.sbprintf(&emitter.builder, "recover_after_return_%d:\n", instruction_id) + after_return = false + } + switch instruction.op { + case .Param, .Const: + case .Load_Global: + if instruction.target < 0 || instruction.target >= len(emitter.module.globals) { + message := diagnostic_message(emitter, -1, instruction.span, "invalid global reference") + emit_trap_call(emitter, message) + continue + } + global := emitter.module.globals[instruction.target] + if global.is_static { + fmt.sbprintf( + &emitter.builder, + " %%v%d = load %s, ptr @bro.g.%d\n", + instruction_id, + llvm_type(global.type), + instruction.target, + ) + } else { + fmt.sbprintf( + &emitter.builder, + " %%v%d = call %s @bro.get.%d()\n", + instruction_id, + llvm_type(global.type), + instruction.target, + ) + } + case .Alloca: + fmt.sbprintf(&emitter.builder, " %%v%d = alloca %s\n", instruction_id, llvm_type(instruction.type)) + case .Load: + fmt.sbprintf( + &emitter.builder, + " %%v%d = load %s, ptr %%v%d\n", + instruction_id, + llvm_type(instruction.type), + instruction.a, + ) + case .Store: + fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(instruction.type)) + write_operand(&emitter.builder, instructions, instruction.b) + fmt.sbprintf(&emitter.builder, ", ptr %%v%d\n", instruction.a) + case .Widen: + from_type := instructions[instruction.a].type + fmt.sbprintf(&emitter.builder, " %%v%d = sext %s ", instruction_id, llvm_type(from_type)) + write_operand(&emitter.builder, instructions, instruction.a) + fmt.sbprintf(&emitter.builder, " to %s\n", llvm_type(instruction.type)) + case .Add_Checked: + type_name := llvm_type(instruction.type) + fmt.sbprintf(&emitter.builder, " %%pair%d = call ", instruction_id) + strings.write_string(&emitter.builder, "{ ") + fmt.sbprintf(&emitter.builder, "%s, i1 } @llvm.sadd.with.overflow.%s(%s ", type_name, type_name, type_name) + write_operand(&emitter.builder, instructions, instruction.a) + fmt.sbprintf(&emitter.builder, ", %s ", type_name) + write_operand(&emitter.builder, instructions, instruction.b) + fmt.sbprintf(&emitter.builder, ")\n") + fmt.sbprintf(&emitter.builder, " %%v%d = extractvalue ", instruction_id) + strings.write_string(&emitter.builder, "{ ") + fmt.sbprintf(&emitter.builder, "%s, i1 } %%pair%d, 0\n", type_name, instruction_id) + fmt.sbprintf(&emitter.builder, " %%overflow%d = extractvalue ", instruction_id) + strings.write_string(&emitter.builder, "{ ") + fmt.sbprintf(&emitter.builder, "%s, i1 } %%pair%d, 1\n", type_name, instruction_id) + fmt.sbprintf( + &emitter.builder, + " br i1 %%overflow%d, label %%overflow_trap%d, label %%overflow_continue%d\n", + instruction_id, + instruction_id, + instruction_id, + ) + fmt.sbprintf(&emitter.builder, "overflow_trap%d:\n", instruction_id) + message := diagnostic_message(emitter, -1, instruction.span, "signed integer addition overflow") + emit_trap_call(emitter, message) + fmt.sbprintf(&emitter.builder, " unreachable\noverflow_continue%d:\n", instruction_id) + case .Call: + if instruction.target < 0 || instruction.target >= len(emitter.module.functions) { + message := diagnostic_message(emitter, -1, instruction.span, "invalid function specialization") + emit_trap_call(emitter, message) + continue + } + target := emitter.module.functions[instruction.target] + if instruction.type.kind != .Void { + fmt.sbprintf(&emitter.builder, " %%v%d = ", instruction_id) + } else { + strings.write_string(&emitter.builder, " ") + } + strings.write_string(&emitter.builder, "call ") + if !target.c_abi { + strings.write_string(&emitter.builder, "fastcc ") + } + fmt.sbprintf(&emitter.builder, "%s @%s(", function_result_type(target), target.link_name) + emit_call_args(&emitter.builder, instructions, instruction.args) + strings.write_string(&emitter.builder, ")\n") + case .Trap: + message := diagnostic_message(emitter, instruction.diagnostic, instruction.span, "invalid recovered source") + emit_trap_call(emitter, message) + case .Return: + if global_initializer { + return_value = instruction.a + continue + } + fmt.sbprintf(&emitter.builder, " ret %s ", function_result_type(function)) + write_operand(&emitter.builder, instructions, instruction.a) + strings.write_string(&emitter.builder, "\n") + after_return = true + case .Return_Void: + if global_initializer { + continue + } + if function.is_main { + strings.write_string(&emitter.builder, " ret i32 0\n") + } else { + strings.write_string(&emitter.builder, " ret void\n") + } + after_return = true + } + } + return return_value +} + +emit_globals :: proc(emitter: ^Emitter) { + for global, global_id in emitter.module.globals { + if global.is_static { + fmt.sbprintf( + &emitter.builder, + "@bro.g.%d = internal constant %s %d\n", + global_id, + llvm_type(global.type), + global.static_value, + ) + } else { + fmt.sbprintf( + &emitter.builder, + "@bro.g.%d = internal global %s 0\n@bro.gstate.%d = internal global i8 0\n", + global_id, + llvm_type(global.type), + global_id, + ) + } + } + strings.write_string(&emitter.builder, "\n") +} + +emit_global_accessors :: proc(emitter: ^Emitter) { + placeholder_function := ir.Function{result=types.I64} + for global, global_id in emitter.module.globals { + if global.is_static { + continue + } + type_name := llvm_type(global.type) + fmt.sbprintf(&emitter.builder, "define internal %s @bro.get.%d() ", type_name, global_id) + strings.write_string(&emitter.builder, "{\nentry:\n") + fmt.sbprintf( + &emitter.builder, + " %%state = load i8, ptr @bro.gstate.%d\n %%done = icmp eq i8 %%state, 2\n br i1 %%done, label %%ready, label %%check\n", + global_id, + ) + strings.write_string(&emitter.builder, "check:\n %visiting = icmp eq i8 %state, 1\n br i1 %visiting, label %cycle, label %initialize\ncycle:\n") + message_text := fmt.aprintf("runtime trap: global initialization cycle involving '%s'", global.name, allocator=emitter.allocator) + message := register_message(emitter, message_text) + delete(message_text, emitter.allocator) + emit_trap_call(emitter, message) + strings.write_string(&emitter.builder, " unreachable\ninitialize:\n") + fmt.sbprintf(&emitter.builder, " store i8 1, ptr @bro.gstate.%d\n", global_id) + placeholder_function.result = global.type + value := emit_instruction_stream(emitter, global.initializer, placeholder_function, true) + fmt.sbprintf(&emitter.builder, " store %s ", type_name) + write_operand(&emitter.builder, global.initializer, value) + fmt.sbprintf(&emitter.builder, ", ptr @bro.g.%d\n", global_id) + fmt.sbprintf(&emitter.builder, " store i8 2, ptr @bro.gstate.%d\n", global_id) + fmt.sbprintf(&emitter.builder, " ret %s ", type_name) + write_operand(&emitter.builder, global.initializer, value) + strings.write_string(&emitter.builder, "\nready:\n") + fmt.sbprintf(&emitter.builder, " %%value = load %s, ptr @bro.g.%d\n ret %s %%value\n}\n\n", type_name, global_id, type_name) + } +} + +emit_constructor :: proc(emitter: ^Emitter) { + count := 0 + for global in emitter.module.globals { + if !global.is_static && !global.problematic { + count += 1 + } + } + if count == 0 { + return + } + strings.write_string( + &emitter.builder, + "@llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @bro.init, ptr null }]\n\n", + ) + strings.write_string(&emitter.builder, "define internal void @bro.init() {\nentry:\n") + for global, global_id in emitter.module.globals { + if !global.is_static && !global.problematic { + fmt.sbprintf(&emitter.builder, " %%g%d = call %s @bro.get.%d()\n", global_id, llvm_type(global.type), global_id) + } + } + strings.write_string(&emitter.builder, " ret void\n}\n\n") +} + +emit_functions :: proc(emitter: ^Emitter) { + for function in emitter.module.functions { + strings.write_string(&emitter.builder, "define ") + if !function.c_abi { + strings.write_string(&emitter.builder, "internal fastcc ") + } + fmt.sbprintf(&emitter.builder, "%s @%s(", function_result_type(function), function.link_name) + for param_type, index in function.param_types { + if index > 0 { + strings.write_string(&emitter.builder, ", ") + } + fmt.sbprintf(&emitter.builder, "%s %%v%d", llvm_type(param_type), index) + } + strings.write_string(&emitter.builder, ") {\nentry:\n") + _ = emit_instruction_stream(emitter, function.instructions, function) + strings.write_string(&emitter.builder, "}\n\n") + } +} + +emit_escaped_bytes :: proc(builder: ^strings.Builder, text: string) { + for value in transmute([]byte)text { + if value >= 32 && value <= 126 && value != '\\' && value != '"' { + strings.write_byte(builder, value) + } else { + fmt.sbprintf(builder, "\\%02X", value) + } + } +} + +emit_messages :: proc(emitter: ^Emitter) { + for message, message_id in emitter.messages { + fmt.sbprintf(&emitter.builder, "@bro.msg.%d = private unnamed_addr constant [%d x i8] c\"", message_id, len(message.text)) + emit_escaped_bytes(&emitter.builder, message.text) + strings.write_string(&emitter.builder, "\"\n") + } + strings.write_string(&emitter.builder, "\n") +} + +emit_declarations :: proc(emitter: ^Emitter) { + strings.write_string(&emitter.builder, "declare i64 @write(i32, ptr, i64)\ndeclare void @llvm.trap()\n") + widths := [?]int{8, 16, 32, 64} + for bits in widths { + strings.write_string(&emitter.builder, "declare { i") + fmt.sbprintf(&emitter.builder, "%d", bits) + strings.write_string(&emitter.builder, ", i1 } @llvm.sadd.with.overflow.i") + fmt.sbprintf(&emitter.builder, "%d(i%d, i%d)\n", bits, bits, bits) + } + strings.write_string( + &emitter.builder, + "\ndefine internal void @bro.trap(ptr %message, i64 %length) {\nentry:\n %written = call i64 @write(i32 2, ptr %message, i64 %length)\n call void @llvm.trap()\n unreachable\n}\n\n", + ) +} + +emit :: proc(module: ^ir.Module, diagnostics: ^source.Diagnostics, allocator := context.allocator) -> string { + emitter := Emitter{ + module=module, + diagnostics=diagnostics, + builder=strings.builder_make(allocator), + allocator=allocator, + } + emitter.messages.allocator = allocator + defer { + for message in emitter.messages { + delete(message.text, allocator) + } + delete(emitter.messages) + strings.builder_destroy(&emitter.builder) + } + + strings.write_string(&emitter.builder, "; generated by brolang\n\n") + emit_globals(&emitter) + emit_constructor(&emitter) + emit_global_accessors(&emitter) + emit_functions(&emitter) + emit_messages(&emitter) + emit_declarations(&emitter) + return fmt.aprintf("%s", strings.to_string(emitter.builder), allocator=allocator) +} diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin new file mode 100644 index 0000000..c780dfa --- /dev/null +++ b/compiler/lower/lower.odin @@ -0,0 +1,356 @@ +package lower + +import "../hir" +import "../ir" +import "../types" +import "core:fmt" +import "core:mem" + +State :: struct { + hir_module: ^hir.Module, + instructions: [dynamic]ir.Instruction, + local_values: []int, + local_slots: []int, + allocator: mem.Allocator, +} + +append_instruction :: proc(state: ^State, instruction: ir.Instruction) -> int { + id := len(state.instructions) + append(&state.instructions, instruction) + return id +} + +clone_args :: proc(values: []int, allocator: mem.Allocator) -> []int { + result := make([]int, len(values), allocator) + copy(result, values) + return result +} + +sentinel :: proc(value_type: types.Type) -> i64 { + switch value_type.bits { + case 8: return -86 + case 16: return -21846 + case 32: return -1431655766 + case: return -6148914691236517206 + } +} + +lower_expr :: proc(state: ^State, expr_id: int) -> int { + if expr_id < 0 || expr_id >= len(state.hir_module.exprs) { + trap := append_instruction(state, ir.Instruction{ + op=.Trap, + type=types.VOID, + target=-1, + a=-1, + b=-1, + diagnostic=-1, + }) + _ = trap + return append_instruction(state, ir.Instruction{ + op=.Const, + type=types.I64, + integer=sentinel(types.I64), + target=-1, + a=-1, + b=-1, + diagnostic=-1, + }) + } + expr := state.hir_module.exprs[expr_id] + switch expr.kind { + case .Invalid: + append_instruction(state, ir.Instruction{ + op=.Trap, + span=expr.span, + type=types.VOID, + target=-1, + a=-1, + b=-1, + diagnostic=expr.diagnostic, + }) + fallback := expr.type + if !types.is_concrete_integer(fallback) { + fallback = types.I64 + } + return append_instruction(state, ir.Instruction{ + op=.Const, + span=expr.span, + type=fallback, + integer=sentinel(fallback), + target=-1, + a=-1, + b=-1, + diagnostic=-1, + }) + case .Integer: + return append_instruction(state, ir.Instruction{ + op=.Const, + span=expr.span, + type=expr.type, + integer=expr.integer, + target=-1, + a=-1, + b=-1, + diagnostic=-1, + }) + case .Local: + if expr.target >= 0 && expr.target < len(state.local_slots) && state.local_slots[expr.target] >= 0 { + return append_instruction(state, ir.Instruction{ + op=.Load, + span=expr.span, + type=expr.type, + target=-1, + a=state.local_slots[expr.target], + b=-1, + diagnostic=-1, + }) + } + if expr.target >= 0 && expr.target < len(state.local_values) { + return state.local_values[expr.target] + } + case .Global: + return append_instruction(state, ir.Instruction{ + op=.Load_Global, + span=expr.span, + type=expr.type, + target=expr.target, + a=-1, + b=-1, + diagnostic=-1, + }) + case .Widen: + value := lower_expr(state, expr.left) + return append_instruction(state, ir.Instruction{ + op=.Widen, + span=expr.span, + type=expr.type, + target=-1, + a=value, + b=-1, + diagnostic=-1, + }) + case .Add: + left := lower_expr(state, expr.left) + right := lower_expr(state, expr.right) + return append_instruction(state, ir.Instruction{ + op=.Add_Checked, + span=expr.span, + type=expr.type, + target=-1, + a=left, + b=right, + diagnostic=-1, + }) + case .Call: + args := make([]int, len(expr.args), state.allocator) + for arg, index in expr.args { + args[index] = lower_expr(state, arg) + } + return append_instruction(state, ir.Instruction{ + op=.Call, + span=expr.span, + type=expr.type, + target=expr.target, + a=-1, + b=-1, + args=args, + diagnostic=-1, + }) + } + append_instruction(state, ir.Instruction{ + op=.Trap, + span=expr.span, + type=types.VOID, + target=-1, + a=-1, + b=-1, + diagnostic=expr.diagnostic, + }) + return append_instruction(state, ir.Instruction{ + op=.Const, + span=expr.span, + type=types.I64, + integer=sentinel(types.I64), + target=-1, + a=-1, + b=-1, + diagnostic=-1, + }) +} + +lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: mem.Allocator) -> []ir.Instruction { + state := State{ + hir_module=hir_module, + allocator=allocator, + local_values=make([]int, len(function.locals), allocator), + local_slots=make([]int, len(function.locals), allocator), + } + state.instructions.allocator = allocator + defer { + delete(state.local_values, allocator) + delete(state.local_slots, allocator) + } + for _, index in state.local_values { + state.local_values[index] = -1 + state.local_slots[index] = -1 + } + for local_id in function.params { + param := append_instruction(&state, ir.Instruction{ + op=.Param, + type=function.locals[local_id].type, + target=local_id, + a=-1, + b=-1, + diagnostic=-1, + }) + state.local_values[local_id] = param + } + + for statement_id in function.body { + statement := hir_module.statements[statement_id] + switch statement.kind { + case .Declaration: + value := lower_expr(&state, statement.expr) + local := function.locals[statement.local] + if local.mutable { + slot := append_instruction(&state, ir.Instruction{ + op=.Alloca, + span=statement.span, + type=local.type, + target=statement.local, + a=-1, + b=-1, + diagnostic=-1, + }) + state.local_slots[statement.local] = slot + append_instruction(&state, ir.Instruction{ + op=.Store, + span=statement.span, + type=local.type, + target=-1, + a=slot, + b=value, + diagnostic=-1, + }) + } else { + state.local_values[statement.local] = value + } + case .Assignment: + value := lower_expr(&state, statement.expr) + slot := -1 + if statement.local >= 0 && statement.local < len(state.local_slots) { + slot = state.local_slots[statement.local] + } + append_instruction(&state, ir.Instruction{ + op=.Store, + span=statement.span, + type=function.locals[statement.local].type, + target=-1, + a=slot, + b=value, + diagnostic=-1, + }) + case .Return: + if statement.expr < 0 { + append_instruction(&state, ir.Instruction{ + op=.Return_Void, + span=statement.span, + type=types.VOID, + target=-1, + a=-1, + b=-1, + diagnostic=-1, + }) + } else { + value := lower_expr(&state, statement.expr) + append_instruction(&state, ir.Instruction{ + op=.Return, + span=statement.span, + type=function.result, + target=-1, + a=value, + b=-1, + diagnostic=-1, + }) + } + case .Expression, .Sink: + _ = lower_expr(&state, statement.expr) + case .Trap: + append_instruction(&state, ir.Instruction{ + op=.Trap, + span=statement.span, + type=types.VOID, + target=-1, + a=-1, + b=-1, + diagnostic=statement.diagnostic, + }) + } + } + if len(state.instructions) == 0 || + (state.instructions[len(state.instructions)-1].op != .Return && + state.instructions[len(state.instructions)-1].op != .Return_Void) { + if function.result.kind == .Void { + append_instruction(&state, ir.Instruction{op=.Return_Void, type=types.VOID, target=-1, a=-1, b=-1, diagnostic=-1}) + } else { + value := append_instruction(&state, ir.Instruction{ + op=.Const, + type=function.result, + integer=sentinel(function.result), + target=-1, + a=-1, + b=-1, + diagnostic=-1, + }) + append_instruction(&state, ir.Instruction{op=.Return, type=function.result, target=-1, a=value, b=-1, diagnostic=-1}) + } + } + return state.instructions[:] +} + +lower_global_initializer :: proc(hir_module: ^hir.Module, global: hir.Global, allocator: mem.Allocator) -> []ir.Instruction { + state := State{hir_module=hir_module, allocator=allocator} + state.instructions.allocator = allocator + value := lower_expr(&state, global.expr) + append_instruction(&state, ir.Instruction{ + op=.Return, + type=global.type, + target=-1, + a=value, + b=-1, + diagnostic=-1, + }) + return state.instructions[:] +} + +lower :: proc(hir_module: ^hir.Module, allocator := context.allocator) -> ir.Module { + module := ir.init_module(allocator) + for global in hir_module.globals { + append(&module.globals, ir.Global{ + name=fmt.aprintf("%s", global.name, allocator=allocator), + type=global.type, + is_static=global.is_static, + static_value=global.static_value, + initializer=nil if global.is_static else lower_global_initializer(hir_module, global, allocator), + problematic=global.problematic, + diagnostic=global.diagnostic, + }) + } + for function in hir_module.functions { + param_types := make([]types.Type, len(function.params), allocator) + for local_id, index in function.params { + param_types[index] = function.locals[local_id].type + } + append(&module.functions, ir.Function{ + name=fmt.aprintf("%s", function.name, allocator=allocator), + link_name=fmt.aprintf("%s", function.link_name, allocator=allocator), + c_abi=function.c_abi, + is_main=function.is_main, + param_types=param_types, + result=function.result, + instructions=lower_body(hir_module, function, allocator), + problematic=function.problematic, + }) + } + return module +} diff --git a/compiler/opt/opt.odin b/compiler/opt/opt.odin new file mode 100644 index 0000000..2442dd7 --- /dev/null +++ b/compiler/opt/opt.odin @@ -0,0 +1,12 @@ +package opt + +import "../ir" + +Pass :: enum { + // Intentionally empty in v1. This enum is the stable optimization boundary. +} + +run :: proc(module: ^ir.Module, passes: []Pass = nil) { + _ = module + _ = passes +} diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin new file mode 100644 index 0000000..139f3de --- /dev/null +++ b/compiler/parser/parser.odin @@ -0,0 +1,459 @@ +package parser + +import "../ast" +import "../source" +import "../token" +import "core:strconv" + +Parser :: struct { + tokens: ^token.Stream, + diagnostics: ^source.Diagnostics, + module: ast.Module, + cursor: int, + delimiter_depth: int, +} + +current :: proc(parser: ^Parser) -> token.Token { + return parser.tokens.items[min(parser.cursor, len(parser.tokens.items)-1)] +} + +previous :: proc(parser: ^Parser) -> token.Token { + return parser.tokens.items[max(parser.cursor-1, 0)] +} + +advance :: proc(parser: ^Parser) -> token.Token { + result := current(parser) + if result.kind != .Eof { + parser.cursor += 1 + } + return result +} + +allow :: proc(parser: ^Parser, kind: token.Kind) -> (token.Token, bool) { + if current(parser).kind == kind { + return advance(parser), true + } + return current(parser), false +} + +skip_newlines :: proc(parser: ^Parser) { + for current(parser).kind == .Newline { + advance(parser) + } +} + +add_expr :: proc(parser: ^Parser, expr: ast.Expr) -> int { + id := len(parser.module.exprs) + append(&parser.module.exprs, expr) + return id +} + +invalid_expr :: proc(parser: ^Parser, span: source.Span, message: string) -> int { + id := source.add(parser.diagnostics, span, message) + return add_expr(parser, ast.Expr{ + kind=.Invalid, + span=span, + left=ast.INVALID_ID, + right=ast.INVALID_ID, + diagnostic=id, + }) +} + +is_type_token :: proc(kind: token.Kind) -> bool { + #partial switch kind { + case .Keyword_Int, .Keyword_I8, .Keyword_I16, .Keyword_I32, .Keyword_I64, .Keyword_Void: + return true + } + return false +} + +parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax { + tok := current(parser) + #partial switch tok.kind { + case .Keyword_Int: + advance(parser) + return .Int + case .Keyword_I8: + advance(parser) + return .I8 + case .Keyword_I16: + advance(parser) + return .I16 + case .Keyword_I32: + advance(parser) + return .I32 + case .Keyword_I64: + advance(parser) + return .I64 + case .Keyword_Void: + advance(parser) + return .Void + } + source.add(parser.diagnostics, tok.span, "expected a type") + return .Invalid +} + +parse_call :: proc(parser: ^Parser, name: token.Token) -> int { + left_paren := advance(parser) + parser.delimiter_depth += 1 + defer parser.delimiter_depth -= 1 + args: [dynamic]int + args.allocator = parser.module.allocator + skip_newlines(parser) + for current(parser).kind != .Right_Paren && current(parser).kind != .Eof { + append(&args, parse_expression(parser)) + skip_newlines(parser) + if _, ok := allow(parser, .Comma); ok { + skip_newlines(parser) + continue + } + break + } + right_paren, ok := allow(parser, .Right_Paren) + if !ok { + source.add(parser.diagnostics, current(parser).span, "expected ')' after call arguments") + right_paren = left_paren + } + return add_expr(parser, ast.Expr{ + kind=.Call, + span=source.Span{start=name.span.start, end=right_paren.span.end}, + text=name.text, + args=args[:], + left=ast.INVALID_ID, + right=ast.INVALID_ID, + diagnostic=-1, + }) +} + +parse_primary :: proc(parser: ^Parser) -> int { + tok := current(parser) + #partial switch tok.kind { + case .Integer: + advance(parser) + value, ok := strconv.parse_i64(tok.text) + if !ok { + return invalid_expr(parser, tok.span, "integer literal does not fit in i64") + } + return add_expr(parser, ast.Expr{ + kind=.Integer, + span=tok.span, + integer=value, + left=ast.INVALID_ID, + right=ast.INVALID_ID, + diagnostic=-1, + }) + case .Identifier: + advance(parser) + if current(parser).kind == .Left_Paren { + return parse_call(parser, tok) + } + return add_expr(parser, ast.Expr{ + kind=.Name, + span=tok.span, + text=tok.text, + left=ast.INVALID_ID, + right=ast.INVALID_ID, + diagnostic=-1, + }) + case .Underscore: + advance(parser) + return invalid_expr(parser, tok.span, "'_' is a write-only sink and cannot be read") + case .Left_Paren: + advance(parser) + parser.delimiter_depth += 1 + defer parser.delimiter_depth -= 1 + skip_newlines(parser) + expr := parse_expression(parser) + skip_newlines(parser) + if _, ok := allow(parser, .Right_Paren); !ok { + source.add(parser.diagnostics, current(parser).span, "expected ')'") + } + return expr + case .Invalid: + advance(parser) + return add_expr(parser, ast.Expr{ + kind=.Invalid, + span=tok.span, + left=ast.INVALID_ID, + right=ast.INVALID_ID, + diagnostic=tok.diagnostic, + }) + } + if tok.kind != .Newline && tok.kind != .Right_Brace && tok.kind != .Eof { + advance(parser) + } + return invalid_expr(parser, tok.span, "expected an expression") +} + +parse_expression :: proc(parser: ^Parser) -> int { + left := parse_primary(parser) + if parser.delimiter_depth > 0 { + skip_newlines(parser) + } + for current(parser).kind == .Plus { + advance(parser) + skip_newlines(parser) + right := parse_primary(parser) + left_expr := parser.module.exprs[left] + 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}, + left=left, + right=right, + diagnostic=-1, + }) + if parser.delimiter_depth > 0 { + skip_newlines(parser) + } + } + return left +} + +finish_statement :: proc(parser: ^Parser) -> int { + if current(parser).kind == .Newline { + skip_newlines(parser) + return -1 + } + if current(parser).kind == .Eof { + return -1 + } + diagnostic := source.add( + parser.diagnostics, + current(parser).span, + "completed statements must be followed by a newline", + ) + for current(parser).kind != .Newline && + current(parser).kind != .Right_Brace && + current(parser).kind != .Eof { + advance(parser) + } + skip_newlines(parser) + return diagnostic +} + +parse_return :: proc(parser: ^Parser) -> int { + start := advance(parser) + skip_newlines(parser) + if current(parser).kind == .Underscore { + end := advance(parser) + id := len(parser.module.statements) + append(&parser.module.statements, ast.Stmt{ + kind=.Return, + span=source.Span{start=start.span.start, end=end.span.end}, + name="_", + expr=ast.INVALID_ID, + diagnostic=-1, + }) + return id + } + expr := parse_expression(parser) + 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}, + expr=expr, + diagnostic=-1, + }) + return id +} + +parse_statement :: proc(parser: ^Parser) -> int { + if current(parser).kind == .Keyword_Return { + return parse_return(parser) + } + + if current(parser).kind == .Identifier || current(parser).kind == .Underscore { + start_cursor := parser.cursor + name := advance(parser) + type_syntax := ast.Type_Syntax.Invalid + had_type := false + if is_type_token(current(parser).kind) { + type_syntax = parse_type(parser) + had_type = true + } + operator := current(parser) + if operator.kind == .Colon_Colon || operator.kind == .Equal { + advance(parser) + skip_newlines(parser) + expr := parse_expression(parser) + kind := ast.Stmt_Kind.Assignment + immutable := false + if operator.kind == .Colon_Colon || had_type { + kind = .Declaration + immutable = operator.kind == .Colon_Colon + } + 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}, + name=name.text, + type=type_syntax, + immutable=immutable, + expr=expr, + diagnostic=-1, + }) + return id + } + parser.cursor = start_cursor + } + + expr := parse_expression(parser) + id := len(parser.module.statements) + append(&parser.module.statements, ast.Stmt{ + kind=.Expression, + span=parser.module.exprs[expr].span, + expr=expr, + diagnostic=-1, + }) + return id +} + +parse_params :: proc(parser: ^Parser) -> []ast.Param { + params: [dynamic]ast.Param + params.allocator = parser.module.allocator + skip_newlines(parser) + for current(parser).kind != .Right_Paren && current(parser).kind != .Eof { + names: [dynamic]token.Token + names.allocator = parser.module.allocator + for { + if current(parser).kind != .Identifier { + source.add(parser.diagnostics, current(parser).span, "expected parameter name") + break + } + append(&names, advance(parser)) + if is_type_token(current(parser).kind) { + break + } + if _, ok := allow(parser, .Comma); !ok { + source.add(parser.diagnostics, current(parser).span, "expected ',' or parameter type") + break + } + skip_newlines(parser) + } + type_syntax := parse_type(parser) + for name in names { + append(¶ms, ast.Param{name=name.text, span=name.span, type=type_syntax}) + } + delete(names) + skip_newlines(parser) + if _, ok := allow(parser, .Comma); ok { + skip_newlines(parser) + continue + } + break + } + return params[:] +} + +parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { + func_token := advance(parser) + if _, ok := allow(parser, .Left_Paren); !ok { + source.add(parser.diagnostics, current(parser).span, "expected '(' after 'func'") + } + params := parse_params(parser) + if _, ok := allow(parser, .Right_Paren); !ok { + source.add(parser.diagnostics, current(parser).span, "expected ')' after parameters") + } + skip_newlines(parser) + result := parse_type(parser) + skip_newlines(parser) + if _, ok := allow(parser, .Left_Brace); !ok { + source.add(parser.diagnostics, current(parser).span, "expected '{' before function body") + } + + body: [dynamic]int + body.allocator = parser.module.allocator + 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 { + statement_id := len(parser.module.statements) + append(&parser.module.statements, ast.Stmt{ + kind=.Invalid, + span=current(parser).span, + expr=ast.INVALID_ID, + diagnostic=diagnostic, + }) + append(&body, statement_id) + } + } + end := current(parser) + if _, ok := allow(parser, .Right_Brace); !ok { + source.add(parser.diagnostics, current(parser).span, "expected '}' after function body") + end = func_token + } + append(&parser.module.functions, ast.Function{ + span=source.Span{start=name.span.start, end=end.span.end}, + name=name.text, + c_abi=c_abi, + params=params, + result=result, + body=body[:], + diagnostic=-1, + }) +} + +parse_top_level :: proc(parser: ^Parser) { + 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 { + advance(parser) + } + _ = finish_statement(parser) + return + } + name := advance(parser) + type_syntax := ast.Type_Syntax.Invalid + if is_type_token(current(parser).kind) { + type_syntax = parse_type(parser) + } + operator := current(parser) + if operator.kind != .Colon_Colon && operator.kind != .Equal { + source.add(parser.diagnostics, operator.span, "expected '::' or '=' after top-level name") + _ = finish_statement(parser) + return + } + advance(parser) + skip_newlines(parser) + + c_abi := false + if operator.kind == .Colon_Colon && current(parser).kind == .Keyword_C { + c_abi = true + advance(parser) + skip_newlines(parser) + } + if operator.kind == .Colon_Colon && current(parser).kind == .Keyword_Func { + parse_function(parser, name, c_abi) + return + } + + expr := parse_expression(parser) + append(&parser.module.globals, ast.Global{ + span=source.Span{start=name.span.start, end=parser.module.exprs[expr].span.end}, + name=name.text, + type=type_syntax, + immutable=operator.kind == .Colon_Colon, + expr=expr, + diagnostic=-1, + }) + _ = finish_statement(parser) +} + +parse :: proc( + stream: ^token.Stream, + diagnostics: ^source.Diagnostics, + allocator := context.allocator, +) -> ast.Module { + parser := Parser{ + tokens=stream, + diagnostics=diagnostics, + module=ast.init_module(allocator), + } + skip_newlines(&parser) + for current(&parser).kind != .Eof { + parse_top_level(&parser) + skip_newlines(&parser) + } + return parser.module +} diff --git a/compiler/source/source.odin b/compiler/source/source.odin new file mode 100644 index 0000000..fb69ad7 --- /dev/null +++ b/compiler/source/source.odin @@ -0,0 +1,104 @@ +package source + +import "core:fmt" +import "core:mem" + +Span :: struct { + start: int, + end: int, +} + +Source :: struct { + path: string, + text: string, +} + +Diagnostic :: struct { + span: Span, + message: string, +} + +Diagnostics :: struct { + source: ^Source, + items: [dynamic]Diagnostic, + allocator: mem.Allocator, +} + +init_diagnostics :: proc(source_file: ^Source, allocator := context.allocator) -> Diagnostics { + result: Diagnostics + result.source = source_file + result.allocator = allocator + result.items.allocator = allocator + return result +} + +destroy_diagnostics :: proc(diagnostics: ^Diagnostics) { + for diagnostic in diagnostics.items { + delete(diagnostic.message, diagnostics.allocator) + } + delete(diagnostics.items) +} + +add :: proc(diagnostics: ^Diagnostics, span: Span, message: string) -> int { + for diagnostic, id in diagnostics.items { + if diagnostic.span == span && diagnostic.message == message { + return id + } + } + id := len(diagnostics.items) + cloned := fmt.aprintf("%s", message, allocator=diagnostics.allocator) + append(&diagnostics.items, Diagnostic{span=span, message=cloned}) + return id +} + +addf :: proc(diagnostics: ^Diagnostics, span: Span, format: string, args: ..any) -> int { + message := fmt.aprintf(format, ..args, allocator=diagnostics.allocator) + for diagnostic, id in diagnostics.items { + if diagnostic.span == span && diagnostic.message == message { + delete(message, diagnostics.allocator) + return id + } + } + id := len(diagnostics.items) + append(&diagnostics.items, Diagnostic{span=span, message=message}) + return id +} + +line_and_column :: proc(source_file: ^Source, offset: int) -> (line, column: int) { + line = 1 + column = 1 + limit := min(offset, len(source_file.text)) + for byte_value in transmute([]byte)source_file.text[:limit] { + if byte_value == '\n' { + line += 1 + column = 1 + } else { + column += 1 + } + } + return +} + +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) + } + diagnostic := diagnostics.items[id] + line, column := line_and_column(diagnostics.source, diagnostic.span.start) + return fmt.aprintf( + "%s:%d:%d: error: %s", + diagnostics.source.path, + line, + column, + diagnostic.message, + allocator=allocator, + ) +} + +print_all :: proc(diagnostics: ^Diagnostics) { + for _, id in diagnostics.items { + message := format(diagnostics, id) + fmt.eprintln(message) + delete(message) + } +} diff --git a/compiler/token/token.odin b/compiler/token/token.odin new file mode 100644 index 0000000..2f4f685 --- /dev/null +++ b/compiler/token/token.odin @@ -0,0 +1,40 @@ +package token + +import "../source" + +Kind :: enum { + Invalid, + Eof, + Newline, + Identifier, + Integer, + Underscore, + Colon_Colon, + Equal, + Plus, + Left_Paren, + Right_Paren, + Left_Brace, + Right_Brace, + Comma, + Keyword_C, + Keyword_Func, + Keyword_Return, + Keyword_Void, + Keyword_Int, + Keyword_I8, + Keyword_I16, + Keyword_I32, + Keyword_I64, +} + +Token :: struct { + kind: Kind, + span: source.Span, + text: string, + diagnostic: int, +} + +Stream :: struct { + items: [dynamic]Token, +} diff --git a/compiler/types/types.odin b/compiler/types/types.odin new file mode 100644 index 0000000..149270d --- /dev/null +++ b/compiler/types/types.odin @@ -0,0 +1,139 @@ +package types + +import "core:fmt" + +Numeric_Category :: enum { + None, + Signed_Integer, + Unsigned_Integer, + Float, +} + +Kind :: enum { + Invalid, + Void, + Int_Constraint, + Concrete, +} + +Type :: struct { + kind: Kind, + category: Numeric_Category, + bits: int, +} + +INVALID :: Type { + kind = .Invalid, +} +VOID :: Type { + kind = .Void, +} +INT :: Type { + kind = .Int_Constraint, + category = .Signed_Integer, +} +I8 :: Type { + kind = .Concrete, + category = .Signed_Integer, + bits = 8, +} +I16 :: Type { + kind = .Concrete, + category = .Signed_Integer, + bits = 16, +} +I32 :: Type { + kind = .Concrete, + category = .Signed_Integer, + bits = 32, +} +I64 :: Type { + kind = .Concrete, + category = .Signed_Integer, + bits = 64, +} + +is_valid :: proc(value: Type) -> bool { + return value.kind != .Invalid +} + +is_concrete_integer :: proc(value: Type) -> bool { + return( + value.kind == .Concrete && + (value.category == .Signed_Integer || value.category == .Unsigned_Integer) \ + ) +} + +is_signed :: proc(value: Type) -> bool { + return value.kind == .Concrete && value.category == .Signed_Integer +} + +equal :: proc(a, b: Type) -> bool { + return a.kind == b.kind && a.category == b.category && a.bits == b.bits +} + +can_widen :: proc(from, to: Type) -> bool { + if equal(from, to) { + return true + } + return( + from.kind == .Concrete && + to.kind == .Concrete && + from.category == to.category && + from.bits < to.bits \ + ) +} + +widest :: proc(a, b: Type) -> Type { + if a.kind != .Concrete || b.kind != .Concrete || a.category != b.category { + return INVALID + } + if a.bits >= b.bits { + return a + } + return b +} + +smallest_signed_for_literal :: proc(value: i64) -> Type { + if value >= -128 && value <= 127 { + return I8 + } + if value >= -32768 && value <= 32767 { + return I16 + } + if value >= -2147483648 && value <= 2147483647 { + return I32 + } + return I64 +} + +name :: proc(value: Type) -> string { + switch value.kind { + case .Invalid: + return "" + case .Void: + return "void" + case .Int_Constraint: + return "int" + case .Concrete: + switch value.category { + case .Signed_Integer: + switch value.bits { + case 8: + return "i8" + case 16: + return "i16" + case 32: + return "i32" + case 64: + return "i64" + } + case .Unsigned_Integer: + return fmt.tprintf("u%d", value.bits) + case .Float: + return fmt.tprintf("f%d", value.bits) + case .None: + } + } + return "" +} diff --git a/compiler_tests.odin b/compiler_tests.odin new file mode 100644 index 0000000..36c7991 --- /dev/null +++ b/compiler_tests.odin @@ -0,0 +1,509 @@ +package main + +import compiler_core "./compiler" +import "./compiler/ast" +import "./compiler/backend" +import "./compiler/checker" +import "./compiler/hir" +import "./compiler/ir" +import "./compiler/lexer" +import "./compiler/llvm" +import "./compiler/lower" +import "./compiler/parser" +import "./compiler/source" +import "./compiler/token" +import "./compiler/types" +import "core:os" +import "core:os/os2" +import "core:strings" +import "core:testing" + +@(test) +lexer_preserves_newlines_and_skips_comments :: proc(t: ^testing.T) { + source_file := source.Source{path="test.bro", text="# comment\nmain :: func() void {}\n"} + 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), 0) + testing.expect_value(t, stream.items[0].kind, token.Kind.Newline) + testing.expect_value(t, stream.items[1].kind, token.Kind.Identifier) +} + +@(test) +parser_accepts_grouped_params_and_multiline_statements :: proc(t: ^testing.T) { + text := `sum :: func(a, + b int) int { + return (a + + b) +} +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.functions), 2) + testing.expect_value(t, len(module.functions[0].params), 2) +} + +@(test) +pipeline_emits_specialized_calling_conventions_and_checked_add :: proc(t: ^testing.T) { + text := `sum_c :: c func(a, b int) int { + return a + b +} +sum_bro :: func(a, b int) int { + return a + b +} +main :: func() void { + _ = sum_c(1, 2) + _ = sum_bro(1, 2) +} +` + 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) + 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_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, "@llvm.sadd.with.overflow.i8")) +} + +@(test) +literal_addition_trees_fold_with_contextual_types :: proc(t: ^testing.T) { + text := `return_i16 :: func() i16 { + return 1 + 2 +} +take_i16 :: func(value i16) i16 { + return value +} +take_int :: func(value int) int { + return value +} +main :: func() void { + local i16 :: 1 + 2 + _ = 100 + (20 + 8) + _ = return_i16() + _ = take_i16(1 + 2) + _ = take_int(127 + 1) +} +` + 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) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics) + defer delete(llvm_text) + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, strings.contains(llvm_text, "@bro__take_int__i16")) + for function in ir_module.functions { + for instruction in function.instructions { + testing.expect(t, instruction.op != ir.Opcode.Add_Checked) + } + } + + for function in hir_module.functions { + for statement_id in function.body { + statement := hir_module.statements[statement_id] + if statement.expr < 0 { + continue + } + expr := hir_module.exprs[statement.expr] + if expr.kind == .Integer { + testing.expect(t, types.equal(expr.type, types.I16)) + } + if expr.kind == .Call && function.name == "main" && len(expr.args) > 0 { + arg := hir_module.exprs[expr.args[0]] + testing.expect_value(t, arg.kind, hir.Expr_Kind.Integer) + testing.expect(t, types.equal(arg.type, types.I16)) + } + } + } +} + +@(test) +runtime_arithmetic_does_not_inherit_result_context :: proc(t: ^testing.T) { + text := `widen_after_add :: func(value i8) i16 { + return value + 1 +} +main :: func() void { + _ = widen_after_add(1) +} +` + 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) + found := false + for function in hir_module.functions { + if function.name != "widen_after_add" { + continue + } + found = true + statement := hir_module.statements[function.body[0]] + widen := hir_module.exprs[statement.expr] + add := hir_module.exprs[widen.left] + testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen) + testing.expect(t, types.equal(widen.type, types.I16)) + testing.expect_value(t, add.kind, hir.Expr_Kind.Add) + testing.expect(t, types.equal(add.type, types.I8)) + } + testing.expect(t, found) +} + +@(test) +parser_recovers_after_invalid_tokens :: proc(t: ^testing.T) { + text := `broken @ declaration +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(t, len(diagnostics.items) > 0) + testing.expect_value(t, len(module.functions), 1) + testing.expect_value(t, module.functions[0].name, "main") +} + +@(test) +return_sink_and_unconsumed_values_have_distinct_hir :: proc(t: ^testing.T) { + text := `give :: func() i8 { + return 1 +} +done :: func() void { + return _ +} +main :: func() void { + done() + _ = give() + 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) + + done_id, main_id := -1, -1 + for function, id in hir_module.functions { + if function.name == "done" { + done_id = id + } else if function.name == "main" { + main_id = id + } + } + testing.expect(t, done_id >= 0) + testing.expect(t, main_id >= 0) + testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].kind, hir.Stmt_Kind.Return) + testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].expr, -1) + main := hir_module.functions[main_id] + testing.expect_value(t, hir_module.statements[main.body[0]].kind, hir.Stmt_Kind.Expression) + testing.expect_value(t, hir_module.statements[main.body[1]].kind, hir.Stmt_Kind.Sink) + testing.expect_value(t, hir_module.statements[main.body[2]].kind, hir.Stmt_Kind.Trap) +} + +@(test) +recursive_specialization_reaches_a_fixed_point :: proc(t: ^testing.T) { + text := `a :: func(value int) i32 { + return b(value) +} +b :: func(value int) i32 { + return a(value) +} +main :: func() void { + _ = a(1) +} +` + 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) +} + +run_executable :: proc(path: string) -> os2.Process_State { + state, stdout, stderr, _ := os2.process_exec( + os2.Process_Desc{command=[]string{path}}, + context.allocator, + ) + delete(stdout) + delete(stderr) + return state +} + +@(test) +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) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(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) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect(t, !state.success) +} + +@(test) +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) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 3) +} + +@(test) +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) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 4) +} + +@(test) +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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +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) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect(t, !state.success) +} + +@(test) +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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect(t, !state.success) +} + +@(test) +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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect(t, !state.success) +} + +@(test) +same_line_statements_are_diagnosed :: proc(t: ^testing.T) { + text := "main :: func() void { _ = 1 _ = 2\n}\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) + module := parser.parse(&stream, &diagnostics) + defer ast.destroy_module(&module) + testing.expect(t, len(diagnostics.items) > 0) +} + +@(test) +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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect(t, !state.success) +} + +@(test) +backend_failure_preserves_existing_output :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-preserved-output" + defer _ = os.remove(output) + previous := "previous artifact" + testing.expect(t, os.write_entire_file(output, transmute([]byte)previous)) + testing.expect(t, !backend.compile("/definitely/not/llvm.ll", output)) + data, ok := os.read_entire_file(output) + defer delete(data) + testing.expect(t, ok) + testing.expect_value(t, string(data), previous) +} + +@(test) +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) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 3) +} + +@(test) +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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect(t, !state.success) +} + +@(test) +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) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect(t, !state.success) +} + +@(test) +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) + 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) { + 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) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +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) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect(t, !state.success) +} diff --git a/examples/constant_context_error.bro b/examples/constant_context_error.bro new file mode 100644 index 0000000..08364f8 --- /dev/null +++ b/examples/constant_context_error.bro @@ -0,0 +1,4 @@ +main :: func() void { + value i8 :: 127 + 1 + _ = value +} diff --git a/examples/constant_fold.bro b/examples/constant_fold.bro new file mode 100644 index 0000000..d6fda21 --- /dev/null +++ b/examples/constant_fold.bro @@ -0,0 +1,3 @@ +main :: func() void { + _ = 127 + 1 +} diff --git a/examples/constant_i64_overflow.bro b/examples/constant_i64_overflow.bro new file mode 100644 index 0000000..0f8b820 --- /dev/null +++ b/examples/constant_i64_overflow.bro @@ -0,0 +1,3 @@ +main :: func() void { + _ = 9223372036854775807 + 1 +} diff --git a/examples/cycle_unused.bro b/examples/cycle_unused.bro new file mode 100644 index 0000000..b5718fa --- /dev/null +++ b/examples/cycle_unused.bro @@ -0,0 +1,6 @@ +a int :: b +b int :: a + +main :: func() void { + _ = 1 +} diff --git a/examples/cycle_used.bro b/examples/cycle_used.bro new file mode 100644 index 0000000..995f7b9 --- /dev/null +++ b/examples/cycle_used.bro @@ -0,0 +1,6 @@ +a int :: b +b int :: a + +main :: func() void { + _ = a +} diff --git a/examples/function_global_unused.bro b/examples/function_global_unused.bro new file mode 100644 index 0000000..a7dbea1 --- /dev/null +++ b/examples/function_global_unused.bro @@ -0,0 +1,9 @@ +bad int = 1 + +read_bad :: func() int { + return bad +} + +derived :: read_bad() + +main :: func() void {} diff --git a/examples/function_global_used.bro b/examples/function_global_used.bro new file mode 100644 index 0000000..1762f86 --- /dev/null +++ b/examples/function_global_used.bro @@ -0,0 +1,11 @@ +bad int = 1 + +read_bad :: func() int { + return bad +} + +derived :: read_bad() + +main :: func() void { + _ = derived +} diff --git a/examples/invalid_transitive_unused_global.bro b/examples/invalid_transitive_unused_global.bro new file mode 100644 index 0000000..a741a10 --- /dev/null +++ b/examples/invalid_transitive_unused_global.bro @@ -0,0 +1,11 @@ +bad int = 4 + +read_bad :: func() int { + return bad +} + +derived int :: read_bad() + +main :: func() void { + _ = 1 +} diff --git a/examples/invalid_transitive_used_global.bro b/examples/invalid_transitive_used_global.bro new file mode 100644 index 0000000..35f8717 --- /dev/null +++ b/examples/invalid_transitive_used_global.bro @@ -0,0 +1,11 @@ +bad int = 4 + +read_bad :: func() int { + return bad +} + +derived int :: read_bad() + +main :: func() void { + _ = derived +} diff --git a/examples/invalid_unused_function.bro b/examples/invalid_unused_function.bro new file mode 100644 index 0000000..550157b --- /dev/null +++ b/examples/invalid_unused_function.bro @@ -0,0 +1,7 @@ +broken :: func(value int) int { + return missing + value +} + +main :: func() void { + _ = 1 +} diff --git a/examples/invalid_unused_global.bro b/examples/invalid_unused_global.bro new file mode 100644 index 0000000..037dbd2 --- /dev/null +++ b/examples/invalid_unused_global.bro @@ -0,0 +1,5 @@ +bad int = 4 + +main :: func() void { + _ = 1 +} diff --git a/examples/invalid_used_global.bro b/examples/invalid_used_global.bro new file mode 100644 index 0000000..686559c --- /dev/null +++ b/examples/invalid_used_global.bro @@ -0,0 +1,5 @@ +bad int = 4 + +main :: func() void { + _ = bad +} diff --git a/examples/main_i32.bro b/examples/main_i32.bro new file mode 100644 index 0000000..e77354d --- /dev/null +++ b/examples/main_i32.bro @@ -0,0 +1,3 @@ +main :: func() i32 { + return 4 +} diff --git a/examples/main_int.bro b/examples/main_int.bro new file mode 100644 index 0000000..2e27c16 --- /dev/null +++ b/examples/main_int.bro @@ -0,0 +1,3 @@ +main :: func() int { + return 3 +} diff --git a/examples/malformed_typed_recovery.bro b/examples/malformed_typed_recovery.bro new file mode 100644 index 0000000..b8324ad --- /dev/null +++ b/examples/malformed_typed_recovery.bro @@ -0,0 +1,12 @@ +take :: func(value i8) i8 { + return value +} + +bad_return :: func() i8 { + return missing +} + +main :: func() void { + _ = take(missing) + _ = bad_return() +} diff --git a/examples/missing_main.bro b/examples/missing_main.bro new file mode 100644 index 0000000..c744ce2 --- /dev/null +++ b/examples/missing_main.bro @@ -0,0 +1 @@ +x int :: 2 diff --git a/examples/mutable_local.bro b/examples/mutable_local.bro new file mode 100644 index 0000000..e5c83c6 --- /dev/null +++ b/examples/mutable_local.bro @@ -0,0 +1,5 @@ +main :: func() i32 { + value i32 = 1 + value = value + 2 + return value +} diff --git a/examples/narrowing_error.bro b/examples/narrowing_error.bro new file mode 100644 index 0000000..df9ada3 --- /dev/null +++ b/examples/narrowing_error.bro @@ -0,0 +1,7 @@ +take :: func(value i8) i8 { + return value +} + +main :: func() void { + _ = take(128) +} diff --git a/examples/overflow.bro b/examples/overflow.bro new file mode 100644 index 0000000..af605dd --- /dev/null +++ b/examples/overflow.bro @@ -0,0 +1,4 @@ +main :: func() void { + value i8 = 127 + _ = value + 1 +} diff --git a/examples/prototype.bro b/examples/prototype.bro new file mode 100644 index 0000000..15bec4d --- /dev/null +++ b/examples/prototype.bro @@ -0,0 +1,20 @@ +# this is a comment + +x int :: 2 + +sum_c :: c func(a, b int) int { + return a + b +} + +sum_brolang :: func(a, b int) int { + return a + b +} + +main :: func() void { + y int = 4 + a_add_b_c :: sum_c(1, 2) + a_add_b_brolang :: sum_brolang(1, 2) + _ = y + _ = a_add_b_c + _ = a_add_b_brolang +} diff --git a/examples/runtime_global.bro b/examples/runtime_global.bro new file mode 100644 index 0000000..fb33fd2 --- /dev/null +++ b/examples/runtime_global.bro @@ -0,0 +1,10 @@ +make_value :: func() int { + return 40 + 2 +} + +answer int :: make_value() + +main :: func() i32 { + _ = answer + return 0 +} diff --git a/main.odin b/main.odin new file mode 100644 index 0000000..e0762b0 --- /dev/null +++ b/main.odin @@ -0,0 +1,16 @@ +package main + +import "./compiler" +import "core:fmt" +import "core:os/os2" + +main :: proc() { + if len(os2.args) != 4 || os2.args[2] != "-o" { + fmt.eprintln("usage: brolang -o ") + os2.exit(2) + } + status := compiler.compile_file(os2.args[1], os2.args[3]) + if status != 0 { + os2.exit(status) + } +}