From 45359c0cdf69f97fa26a46eb4093577ec3c2f599 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 25 Oct 2026 02:05:26 +0100 Subject: [PATCH] feat: emit LLVM IR and build native executables --- README.md | 28 ++- TODO.md | 2 + ffi/c/spawn.hon | 16 ++ source/llvm/instructions.hon | 221 +++++++++++++++++++ source/llvm/llvm.hon | 396 +++++++++++++++++++++++++++++++++++ source/llvm/runtime.hon | 139 ++++++++++++ source/main.hon | 95 ++++++++- std/process/process.hon | 55 +++++ std/process/process.test.hon | 26 +++ tests/native.py | 298 ++++++++++++++++++++++++++ 10 files changed, 1267 insertions(+), 9 deletions(-) create mode 100644 ffi/c/spawn.hon create mode 100644 source/llvm/instructions.hon create mode 100644 source/llvm/llvm.hon create mode 100644 source/llvm/runtime.hon create mode 100644 std/process/process.test.hon create mode 100644 tests/native.py diff --git a/README.md b/README.md index d123d82..f9a63da 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,34 @@ main proc() int { } ``` -this prints `42`. interpreted `main` must be a top-level function with no parameters; nonvoid results are printed, not used as process exit statuses. +this prints `42`. both interpreted and Honey-compiled `main` must be a top-level function with no parameters; nonvoid results are printed, not used as process exit statuses. + +compile the same supported language subset to a standalone Apple Silicon macOS executable: + +```sh +./build/honey build programs/factorial.hon -o build/factorial -O2 +./build/factorial +``` + +this prints `120`. `build` requires `-o ` and accepts `-O0` (the default) or `-O2` as its final option. Honey emits textual LLVM IR and invokes `clang` from `PATH` without a shell, targeting `arm64-apple-macosx11.0.0`. Apple Clang 21 is the verified toolchain; LLVM libraries, LLVM bindings and a C emission stage are not required. the temporary IR file is private and removed after compilation, including on failure. + +inspect or compile the LLVM text separately: + +```sh +./build/honey emit-llvm programs/factorial.hon > build/factorial.ll +clang -target arm64-apple-macosx11.0.0 -O2 build/factorial.ll -o build/factorial +``` + +`emit-llvm` writes LLVM text to stdout and source diagnostics to stderr. both emission commands return `1` for recoverable source diagnostics while still producing their output when emission and linking succeed. the resulting executable preserves demand-driven traps, reports all source diagnostics, and returns `1` even if unaffected code completes. runtime-generated diagnostics retain source locations; the original source file and compiler are not needed to run the executable. invocation or Clang failures return `2`. + +the backend (`@source/llvm`) consumes the existing typed IR, emits native functions and control flow, and uses entry-block stack slots for locals and instruction results, including non-SSA result IDs. LLVM can promote eligible slots during optimization. arrays use typed native aggregates with by-value copies; loop storage is allocated once per invocation. signed and unsigned arithmetic use checked overflow intrinsics, array accesses retain bounds checks, local initialization flags reset on scope re-entry, globals initialize lazily with cycle checks, and floating operations preserve IEEE behavior without fast-math flags. native calls use the native stack rather than the interpreter's heap-backed frames. + +run the bootstrap suite with `bro test`. after `bro build`, run `python3 tests/native.py` for executable-level interpreter comparisons at both `-O0` and `-O2`, including traps, source diagnostics, array ownership, calling conventions, and CLI error handling. the native bootstrap accepts either a no-argument `main` or `main proc(init process.Init)`. it supplies `init.io` and `init.args` from native startup `argc`/`argv`. startup scans each argument once into a heap-allocated array of borrowed zero-terminated slices (`process.Args.values: [][;0]u8`), without copying strings. the descriptor array and borrowed strings remain valid until `main` returns; startup then frees the descriptors, including on a fallible return. allocation failure exits with status `2`. `init.args.values.len` includes the executable name, and `init.args.values[index]` directly accesses an argument after a length guard. callers can supply their own `process.Args { values = slices[..] }` for testing. +`process.run(allocator, arguments)` starts a child synchronously using Darwin `posix_spawnp`, searches `PATH`, and inherits the environment and standard streams. argument strings are borrowed zero-terminated slices; no shell parsing occurs. it releases temporary argument descriptors on every return, retries interrupted waits, and returns the exit code or `128 + signal` for signal termination. + source loading uses the supplied `io.Io` for opening, reading, and closing files. `io.open` accepts a borrowed zero-terminated slice (`[;0]u8`), including `init.args.values[index]` without copying its storage. the bootstrap supports checked sentinel-slice construction with `pointer[..length;0]`. `io.read_all(allocator, reader, maximum)` reads to EOF into a caller-owned `ArrayList(u8)`; release it with `arraylist.deinit`. it frees partial storage on error and consumes one extra byte to distinguish an exact-size input from a limit violation. the driver maps `limit_exceeded` to `source_too_large` at its `u32` source-size limit. a recursive example is in [programs/factorial.hon](programs/factorial.hon); `./build/honey run programs/factorial.hon` prints `120`. @@ -95,4 +119,4 @@ IR construction owns derived layout metadata. `ir.add_array` checks shape capaci `interpreter.run` consumes only IR and returns an `Outcome` containing a value or diagnostic trap. returned arrays own their escaped cell storage; release every outcome with `interpreter.deinit` before releasing its module. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. array descriptors also borrow the module. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. `int`/`i64` use signed 64-bit storage, `usize` uses unsigned 64-bit storage, both with checked arithmetic; `float`/`f64` use 64-bit IEEE arithmetic and comparisons, including NaN behavior. `usize` supports `+`, `-`, `*` and comparisons, but not unary negation or division. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes. -recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation and compound assignment remain unimplemented. +recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. compound assignment remains unimplemented. diff --git a/TODO.md b/TODO.md index e69de29..c0c3050 100644 --- a/TODO.md +++ b/TODO.md @@ -0,0 +1,2 @@ +- [ ] milestone: self-hosted honey compiler. + - honey can compile itself \ No newline at end of file diff --git a/ffi/c/spawn.hon b/ffi/c/spawn.hon new file mode 100644 index 0000000..19e52c0 --- /dev/null +++ b/ffi/c/spawn.hon @@ -0,0 +1,16 @@ +# hand-written Darwin ABI declarations; pid_t is c_int in the existing bindings. +# Darwin's spawn action and attribute handles are pointers to opaque records. +posix_spawn_file_actions_t :: alias ?*mut anyopaque +posix_spawnattr_t :: alias ?*mut anyopaque + +posix_spawnp c_func( + pid ?*mut c_int, + file ?*c_char, + actions ?*posix_spawn_file_actions_t, + attributes ?*posix_spawnattr_t, + arguments ?*?*mut c_char, + environment ?*?*mut c_char, +) c_int + +# borrowed process environment; neither the vector nor its strings may be freed. +_NSGetEnviron c_func() ?*mut ?*mut ?*mut c_char diff --git a/source/llvm/instructions.hon b/source/llvm/instructions.hon new file mode 100644 index 0000000..089e917 --- /dev/null +++ b/source/llvm/instructions.hon @@ -0,0 +1,221 @@ +import "@std/io" +import "@source/ir" + +@hide +emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruction) void ! io.WriteError { + output :: state.output + type :: Type{ kind = instruction.type, shape = instruction.array_type } + result := fresh(state) + match instruction.op { + .constant: { + if (instruction.type == .void) return + try io.write_all(output, " store ") + try write_type(output, state.module, type) + try io.write_all(output, " ") + try write_constant(output, state.module, instruction.constant) + try io.print(", ptr %v{}\n", output, {u32(instruction.destination)}) + return + } + .load_local: { + try check_local(state, instruction.slot, true, instruction.span) + try io.print(" %r{} = load ", output, {result}) + try write_type(output, state.module, type) + try io.print(", ptr %l{}\n", output, {instruction.slot}) + } + .store_local, .assign_local: { + if (instruction.op == .assign_local) try check_local(state, instruction.slot, false, instruction.span) + value :: try load(state, instruction.left) + try io.write_all(output, " store ") + try write_type(output, state.module, state.values[usize(instruction.left)]) + try io.print(" %r{}, ptr %l{}\n", output, {value, instruction.slot}) + if (instruction.op == .store_local) try io.print(" store i1 true, ptr %i{}\n", output, {instruction.slot}) + return + } + .reset_local: { + try io.print(" store i1 false, ptr %i{}\n", output, {instruction.slot}) + return + } + .load_global: { + pointer :: fresh(state) + position :: location(state.line_starts, instruction.span) + try io.print(" %r{} = call ptr @global{}(i64 {}, i64 {})\n", output, {pointer, instruction.slot, position.line, position.column}) + try io.print(" %r{} = load ", output, {result}) + try write_type(output, state.module, type) + try io.print(", ptr %r{}\n", output, {pointer}) + } + .array_literal: { + for (0..instruction.arguments_count) |index| { + operand :: block.arguments.items[instruction.arguments_start + index] + value :: try load(state, operand) + pointer :: fresh(state) + try io.print(" %r{} = getelementptr ", output, {pointer}) + try write_type(output, state.module, type) + try io.print(", ptr %v{}, i64 0, i64 {}\n store ", output, {u32(instruction.destination), index}) + try write_type(output, state.module, state.values[usize(operand)]) + try io.print(" %r{}, ptr %r{}\n", output, {value, pointer}) + } + return + } + .index, .index_address: { + index :: try load(state, instruction.right) + base_type :: state.values[usize(instruction.left)] + shape :: state.module.arrays.items[usize(base_type.shape)] + failed :: fresh(state) + try io.print(" %r{} = icmp uge i64 %r{}, {}\n", output, {failed, index, shape.len}) + try check_failure(state, failed, .index_out_of_bounds, instruction.span) + pointer :: fresh(state) + if (instruction.op == .index_address) { + base :: try load(state, instruction.left) + data :: fresh(state) + flag :: fresh(state) + address :: fresh(state) + try io.print(" %r{} = extractvalue %address %r{}, 0\n", output, {data, base}) + try io.print(" %r{} = extractvalue %address %r{}, 1\n", output, {flag, base}) + try io.print(" %r{} = getelementptr ", output, {pointer}) + try write_type(output, state.module, Type{ kind = .array, shape = base_type.shape }) + try io.print(", ptr %r{}, i64 0, i64 %r{}\n", output, {data, index}) + try io.print(" %r{} = insertvalue %address poison, ptr %r{}, 0\n", output, {address, pointer}) + try io.print(" %r{} = insertvalue %address %r{}, ptr %r{}, 1\n", output, {result, address, flag}) + } else { + try io.print(" %r{} = getelementptr ", output, {pointer}) + try write_type(output, state.module, base_type) + try io.print(", ptr %v{}, i64 0, i64 %r{}\n %r{} = load ", output, {u32(instruction.left), index, result}) + try write_type(output, state.module, type) + try io.print(", ptr %r{}\n", output, {pointer}) + } + } + .address_local: { + address :: fresh(state) + try io.print(" %r{} = insertvalue %address poison, ptr %l{}, 0\n", output, {address, instruction.slot}) + try io.print(" %r{} = insertvalue %address %r{}, ptr %i{}, 1\n", output, {result, address, instruction.slot}) + } + .address_global: { + pointer :: fresh(state) + address :: fresh(state) + position :: location(state.line_starts, instruction.span) + try io.print(" %r{} = call ptr @global{}(i64 {}, i64 {})\n", output, {pointer, instruction.slot, position.line, position.column}) + try io.print(" %r{} = insertvalue %address poison, ptr %r{}, 0\n", output, {address, pointer}) + try io.print(" %r{} = insertvalue %address %r{}, ptr null, 1\n", output, {result, address}) + } + .load_address, .store_address: { + address :: try load(state, instruction.left) + pointer :: fresh(state) + flag :: fresh(state) + position :: location(state.line_starts, instruction.span) + try io.print(" %r{} = extractvalue %address %r{}, 0\n", output, {pointer, address}) + try io.print(" %r{} = extractvalue %address %r{}, 1\n", output, {flag, address}) + try io.print(" call void @check_init(ptr %r{}, i1 {}, i64 {}, i64 {})\n", output, {flag, instruction.op == .load_address, position.line, position.column}) + if (instruction.op == .store_address) { + value :: try load(state, instruction.right) + try io.write_all(output, " store ") + try write_type(output, state.module, state.values[usize(instruction.right)]) + try io.print(" %r{}, ptr %r{}\n", output, {value, pointer}) + return + } + try io.print(" %r{} = load ", output, {result}) + try write_type(output, state.module, type) + try io.print(", ptr %r{}\n", output, {pointer}) + } + .call: { + callee :: try load(state, instruction.left) + # operands are frame snapshots; loading them before the call preserves evaluation order. + # argument registers are consecutive because load emits exactly one fresh register. + first_argument :: state.next_register + for (0..instruction.arguments_count) |index| { + _ = try load(state, block.arguments.items[instruction.arguments_start + index]) + } + if (instruction.type != .void) try io.print(" %r{} = call ", output, {result}) + else try io.write_all(output, " call ") + try write_type(output, state.module, type) + try io.print(" %r{}(", output, {callee}) + for (0..instruction.arguments_count) |index| { + if (index != 0) try io.write_all(output, ", ") + operand :: block.arguments.items[instruction.arguments_start + index] + try write_type(output, state.module, state.values[usize(operand)]) + try io.print(" %r{}", output, {first_argument + index}) + } + try io.write_all(output, ")\n") + } + .negate: { + operand :: try load(state, instruction.left) + if (instruction.type == .float) { + try io.print(" %r{} = fneg double %r{}\n", output, {result, operand}) + } else { + pair :: fresh(state) + overflow :: fresh(state) + try io.print(" %r{} = call {{ i64, i1 }} @llvm.ssub.with.overflow.i64(i64 0, i64 %r{})\n", output, {pair, operand}) + try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 1\n", output, {overflow, pair}) + try check_failure(state, overflow, .integer_overflow, instruction.span) + try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 0\n", output, {result, pair}) + } + } + .logical_not: { + operand :: try load(state, instruction.left) + try io.print(" %r{} = xor i1 %r{}, true\n", output, {result, operand}) + } + .add, .subtract, .multiply, .divide: { + left :: try load(state, instruction.left) + right :: try load(state, instruction.right) + if (instruction.type == .float) { + opcode []u8 :: match instruction.op { + .add: "fadd" + .subtract: "fsub" + .multiply: "fmul" + .divide: "fdiv" + else: unreachable + } + try io.print(" %r{} = {s} double %r{}, %r{}\n", output, {result, opcode, left, right}) + } else { + opcode []u8 :: match instruction.op { + .add: "add" + .subtract: "sub" + .multiply: "mul" + else: unreachable + } + sign []u8 :: if (instruction.type == .usize) "u" else "s" + pair :: fresh(state) + overflow :: fresh(state) + try io.print(" %r{} = call {{ i64, i1 }} @llvm.{s}{s}.with.overflow.i64(i64 %r{}, i64 %r{})\n", output, {pair, sign, opcode, left, right}) + try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 1\n", output, {overflow, pair}) + try check_failure(state, overflow, .integer_overflow, instruction.span) + try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 0\n", output, {result, pair}) + } + } + .equal, .not_equal, .less, .less_equal, .greater, .greater_equal: { + left :: try load(state, instruction.left) + right :: try load(state, instruction.right) + operand_type :: state.values[usize(instruction.left)] + if (operand_type.kind == .float) { + predicate []u8 :: match instruction.op { + .equal: "oeq" + .not_equal: "une" + .less: "olt" + .less_equal: "ole" + .greater: "ogt" + .greater_equal: "oge" + else: unreachable + } + try io.print(" %r{} = fcmp {s} double %r{}, %r{}\n", output, {result, predicate, left, right}) + } else { + prefix []u8 := "" + if (instruction.op != .equal and instruction.op != .not_equal) { + if (operand_type.kind == .usize) prefix = "u" + else prefix = "s" + } + predicate []u8 :: match instruction.op { + .equal: "eq" + .not_equal: "ne" + .less: "lt" + .less_equal: "le" + .greater: "gt" + .greater_equal: "ge" + else: unreachable + } + try io.print(" %r{} = icmp {s}{s} ", output, {result, prefix, predicate}) + try write_type(output, state.module, operand_type) + try io.print(" %r{}, %r{}\n", output, {left, right}) + } + } + } + try store(state, instruction.destination, result) +} diff --git a/source/llvm/llvm.hon b/source/llvm/llvm.hon new file mode 100644 index 0000000..5ab2db2 --- /dev/null +++ b/source/llvm/llvm.hon @@ -0,0 +1,396 @@ +import "@std/mem" +import "@std/io" +import "@std/enummap" +import "@std/arraylist" +import "@source/ir" +import "@source/diagnostics" + +@hide +Type :: struct { + kind ir.Type = .void + shape ir.ArrayId = ir.no_array +} + +@hide +State :: struct { + output io.Writer + module @ir.Module + line_starts []usize + values []mut Type + next_register usize = 0 +} + +#! writes a standalone native LLVM module; all temporary emitter storage is released before returning. +emit proc( + allocator mem.Allocator, + output io.Writer, + module @ir.Module, + diagnostic_store @diagnostics.Store, + source []u8, + path []u8, +) void ! (mem.AllocError | io.WriteError) { + line_starts := arraylist.init(usize, allocator) + defer arraylist.deinit(&line_starts) + try arraylist.append(&line_starts, usize(0)) + for (source) |byte, index| { + if (byte == '\n') try arraylist.append(&line_starts, index + 1) + } + try io.write_all(output, runtime) + try io.write_all(output, "\n@path = private constant ") + try bytes_constant(output, path) + try io.write_all(output, "\n") + for (module.strings.items) |_, index| { + try io.print("@s{} = private constant ", output, {index}) + try bytes_constant(output, ir.string_bytes(module, ir.StringId(index))) + try io.write_all(output, "\n") + } + for (diagnostic_store.list.items) |diagnostic, index| { + try io.print("@d{} = private constant ", output, {index}) + try bytes_constant(output, enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)?) + try io.write_all(output, "\n") + } + try emit_reporting(output, diagnostic_store, line_starts.items, path.len) + for (module.globals.items) |global, index| { + try io.print("@g{} = internal global ", output, {index}) + storage_kind :: if (global.type == .void) ir.Type.invalid else global.type + try write_type(output, module, Type{ kind = storage_kind, shape = global.array_type }) + try io.print(" zeroinitializer\n@gs{} = internal global i8 0\n", output, {index}) + try emit_global(output, module, global, index) + } + for (module.arrays.items) |_, index| try emit_array_printer(output, module, ir.ArrayId(index)) + for (module.functions.items) |function, index| { + values :: try mem.alloc(Type, allocator, function.value_count) + defer mem.free(allocator, values) + mem.set(values, Type{}) + for (function.blocks.items) |block| { + for (block.instructions.items) |instruction| { + if (instruction.destination != ir.no_value) { + values[usize(instruction.destination)] = Type{ kind = instruction.type, shape = instruction.array_type } + } + } + } + state := State{ output = output, module = module, line_starts = line_starts.items, values = values } + try emit_function(&state, &function, index) + } + try io.write_all(output, "define i32 @main() {\nentry:\n") + if (module.entry) |entry| { + function :: module.functions.items[usize(entry)] + result :: Type{ kind = function.result, shape = function.result_array_type } + if (result.kind != .void) try io.write_all(output, " %result = ") + else try io.write_all(output, " ") + try io.write_all(output, "call ") + try write_type(output, module, result) + try io.print(" @f{}()\n", output, {u32(entry)}) + if (result.kind != .void and result.kind != .invalid) { + try print_call(output, module, result, "%result") + try io.write_all(output, " call void @out(i32 1, ptr @newline, i64 1)\n") + } + try io.write_all(output, " call void @report(i64 -1)\n") + status i32 :: if (diagnostics.count(diagnostic_store) == 0) 0 else 1 + try io.print(" ret i32 {}\n", output, {status}) + } else { + try io.print(" call void @trap(i64 {}, i64 0, i64 0, ptr null, i64 0)\n unreachable\n", output, {u32(module.entry_diagnostic)}) + } + try io.write_all(output, "}\n") +} + +@hide:file +bytes_constant proc(output io.Writer, bytes []u8) void ! io.WriteError { + try io.print("[{} x i8] c\"", output, {bytes.len}) + digits :: "0123456789ABCDEF" + for (bytes) |byte| { + escaped [3]u8 :: ['\\', digits[usize(divtrunc!(byte, 16))], digits[usize(rem!(byte, 16))]] + try io.write_all(output, (&escaped)[..]) + } + try io.write_all(output, "\"") +} + +@hide +write_type proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteError { + match type.kind { + .int, .usize: try io.write_all(output, "i64") + .float: try io.write_all(output, "double") + .bool: try io.write_all(output, "i1") + .string: try io.write_all(output, "%string") + .function: try io.write_all(output, "ptr") + .address: try io.write_all(output, "%address") + .array: { + shape :: module.arrays.items[usize(type.shape)] + try io.print("[{} x ", output, {shape.len}) + try write_type(output, module, Type{ kind = shape.element, shape = shape.element_array }) + try io.write_all(output, "]") + } + .void: try io.write_all(output, "void") + # invalid storage only belongs to recovery paths that trap before reading it. + .invalid: try io.write_all(output, "i8") + } +} + +@hide:file +local_type proc(function @ir.Function, index usize) Type { + type := Type{ kind = function.locals.items[index] } + if (type.kind == .void) type.kind = .invalid + if (type.kind == .array) { + for (function.array_storage.items) |storage| { + if (storage.local and usize(storage.index) == index) { + type.shape = storage.array_type + break + } + } + } + return type +} + +@hide +Location :: struct { + line usize = 1 + column usize = 1 +} + +@hide +location proc(line_starts []usize, span diagnostics.Span) Location { + offset :: usize(span.start) + line := usize(0) + end := line_starts.len + while (line + 1 < end) { + middle :: line + divtrunc!(end - line, 2) + if (line_starts[middle] <= offset) line = middle + else end = middle + } + return Location{ line = line + 1, column = offset - line_starts[line] + 1 } +} + +@hide:file +emit_reporting proc(output io.Writer, diagnostic_store @diagnostics.Store, line_starts []usize, path_length usize) void ! io.WriteError { + try io.write_all(output, "define internal void @io_failure() noreturn {\nentry:\n") + try io.print(" %path_result = call i64 @write(i32 2, ptr @path, i64 {})\n", output, {path_length}) + try io.write_all(output, " %message_result = call i64 @write(i32 2, ptr @write_error, i64 44)\n call void @exit(i32 2)\n unreachable\n}\n") + try io.write_all(output, "define internal void @diagnose(i64 %line, i64 %column, ptr %message, i64 %length, i1 %trap) {\nentry:\n") + try io.print(" call void @out(i32 2, ptr @path, i64 {})\n", output, {path_length}) + try io.write_all(output, + ` call void @out(i32 2, ptr @colon, i64 1) + ` call void @print_integer(i32 2, i64 %line, i1 true) + ` call void @out(i32 2, ptr @colon, i64 1) + ` call void @print_integer(i32 2, i64 %column, i1 true) + ` %label = select i1 %trap, ptr @runtime_trap, ptr @error + ` %label_length = select i1 %trap, i64 16, i64 9 + ` call void @out(i32 2, ptr %label, i64 %label_length) + ` call void @out(i32 2, ptr %message, i64 %length) + ` call void @out(i32 2, ptr @newline, i64 1) + ` ret void + `} + ) + try io.write_all(output, "define internal void @report(i64 %trap) {\nentry:\n") + for (diagnostic_store.list.items) |diagnostic, index| { + position :: location(line_starts, diagnostic.span) + message :: enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)? + try io.print(" %t{} = icmp eq i64 %trap, {}\n", output, {index, index}) + try io.print(" call void @diagnose(i64 {}, i64 {}, ptr @d{}, i64 {}, i1 %t{})\n", output, { + position.line, position.column, index, message.len, index, + }) + } + try io.write_all(output, " ret void\n}\n") +} + +@hide:file +emit_global proc(output io.Writer, module @ir.Module, global ir.Global, index usize) void ! io.WriteError { + type :: Type{ kind = global.type, shape = global.array_type } + try io.print("define internal ptr @global{}(i64 %line, i64 %column) {{\nentry:\n", output, {index}) + try io.print(" %state = load i8, ptr @gs{}\n", output, {index}) + try io.write_all(output, + ` switch i8 %state, label %initialize [ i8 1, label %cycle i8 2, label %ready ] + `cycle: + ` call void @trap(i64 -1, i64 %line, i64 %column, ptr @cycle, i64 27) + ` unreachable + `initialize: + ) + try io.print(" store i8 1, ptr @gs{}\n", output, {index}) + if (type.kind == .void) { + try io.print(" call void @f{}()\n", output, {u32(global.initializer)}) + } else { + try io.write_all(output, " %value = call ") + try write_type(output, module, type) + try io.print(" @f{}()\n store ", output, {u32(global.initializer)}) + try write_type(output, module, type) + try io.print(" %value, ptr @g{}\n", output, {index}) + } + try io.print(" store i8 2, ptr @gs{}\n br label %ready\nready:\n ret ptr @g{}\n}}\n", output, {index, index}) +} + +@hide:file +print_call proc(output io.Writer, module @ir.Module, type Type, operand []u8) void ! io.WriteError { + match type.kind { + .int, .usize: try io.print(" call void @print_integer(i32 1, i64 {s}, i1 {})\n", output, {operand, type.kind == .usize}) + .float: try io.print(" call void @print_float(double {s})\n", output, {operand}) + .bool: try io.print(" call void @print_bool(i1 {s})\n", output, {operand}) + .string: try io.print(" call void @print_string(%string {s})\n", output, {operand}) + .array: { + try io.print(" call void @print_array{}(", output, {u32(type.shape)}) + try write_type(output, module, type) + try io.print(" {s})\n", output, {operand}) + } + else: unreachable + } +} + +@hide:file +emit_array_printer proc(output io.Writer, module @ir.Module, id ir.ArrayId) void ! io.WriteError { + shape :: module.arrays.items[usize(id)] + type :: Type{ kind = .array, shape = id } + element :: Type{ kind = shape.element, shape = shape.element_array } + try io.print("define internal void @print_array{}(", output, {u32(id)}) + try write_type(output, module, type) + try io.write_all(output, " %value) {\nentry:\n %storage = alloca ") + try write_type(output, module, type) + try io.write_all(output, "\n store ") + try write_type(output, module, type) + try io.write_all(output, " %value, ptr %storage\n call void @out(i32 1, ptr @open, i64 1)\n br label %loop\nloop:\n %index = phi i64 [ 0, %entry ], [ %next, %element ]\n") + try io.print(" %done = icmp eq i64 %index, {}\n", output, {shape.len}) + try io.write_all(output, " br i1 %done, label %end, label %separator\nseparator:\n %first = icmp eq i64 %index, 0\n br i1 %first, label %element, label %comma\ncomma:\n call void @out(i32 1, ptr @separator, i64 2)\n br label %element\nelement:\n %pointer = getelementptr ") + try write_type(output, module, type) + try io.write_all(output, ", ptr %storage, i64 0, i64 %index\n %item = load ") + try write_type(output, module, element) + try io.write_all(output, ", ptr %pointer\n") + # functions may occur inside arrays without being printable source results. + if (element.kind != .function) try print_call(output, module, element, "%item") + else try io.write_all(output, " call void @exit(i32 2)\n") + try io.write_all(output, " %next = add i64 %index, 1\n br label %loop\nend:\n call void @out(i32 1, ptr @close, i64 1)\n ret void\n}\n") +} + +@hide +fresh proc(state @mut State) usize { + value :: state.next_register + state.next_register += 1 + return value +} + +@hide +load proc(state @mut State, value ir.ValueId) usize ! io.WriteError { + result :: fresh(state) + try io.print(" %r{} = load ", state.output, {result}) + try write_type(state.output, state.module, state.values[usize(value)]) + try io.print(", ptr %v{}\n", state.output, {u32(value)}) + return result +} + +@hide +store proc(state @State, destination ir.ValueId, register usize) void ! io.WriteError { + if (destination == ir.no_value or state.values[usize(destination)].kind == .void) return + try io.write_all(state.output, " store ") + try write_type(state.output, state.module, state.values[usize(destination)]) + try io.print(" %r{}, ptr %v{}\n", state.output, {register, u32(destination)}) +} + +@hide +check_local proc(state @State, slot u32, read bool, span diagnostics.Span) void ! io.WriteError { + position :: location(state.line_starts, span) + try io.print(" call void @check_init(ptr %i{}, i1 {}, i64 {}, i64 {})\n", state.output, {slot, read, position.line, position.column}) +} + +@hide:file +trap proc(state @State, code diagnostics.Code, span diagnostics.Span) void ! io.WriteError { + position :: location(state.line_starts, span) + symbol []u8 :: match code { + .integer_overflow: "overflow" + .index_out_of_bounds: "bounds" + else: unreachable + } + message :: enummap.get(diagnostics.Code, []u8, &diagnostics.messages, code)? + length :: message.len + try io.print(" call void @trap(i64 -1, i64 {}, i64 {}, ptr @{s}, i64 {})\n unreachable\n", state.output, {position.line, position.column, symbol, length}) +} + +@hide +check_failure proc(state @mut State, condition usize, code diagnostics.Code, span diagnostics.Span) void ! io.WriteError { + label :: fresh(state) + try io.print(" br i1 %r{}, label %fail{}, label %ok{}\nfail{}:\n", state.output, {condition, label, label, label}) + try trap(state, code, span) + try io.print("ok{}:\n", state.output, {label}) +} + +@hide +write_constant proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError { + match value.type { + .int: try io.print("{}", output, {value.data.integer}) + .usize: try io.print("{}", output, {value.data.unsigned}) + .float: { + bits :: ptrcast!(@u64, &value.data.real)^ + try io.print("bitcast (i64 {} to double)", output, {bits}) + } + .bool: try io.print("{}", output, {value.data.boolean}) + .string: try io.print("{{ ptr @s{}, i64 {} }}", output, {u32(value.data.string), ir.string_bytes(module, value.data.string).len}) + .function: try io.print("@f{}", output, {u32(value.data.function)}) + .array: { + shape :: module.arrays.items[usize(value.array_type)] + width :: if (shape.element == .array) module.arrays.items[usize(shape.element_array)].cells else 1 + try io.write_all(output, "[") + for (0..shape.len) |index| { + if (index != 0) try io.write_all(output, ", ") + element := ir.Value{} + if (shape.element == .array) { + element = ir.Value{ type = .array, array_type = shape.element_array, data = ir.ValueData{ elements = value.data.elements[index * width..(index + 1) * width] } } + } else element = value.data.elements[index] + try write_type(output, module, Type{ kind = element.type, shape = element.array_type }) + try io.write_all(output, " ") + try write_constant(output, module, element) + } + try io.write_all(output, "]") + } + else: unreachable + } +} + +@hide:file +emit_function proc(state @mut State, function @ir.Function, id usize) void ! io.WriteError { + output :: state.output + try io.write_all(output, "define internal ") + try write_type(output, state.module, Type{ kind = function.result, shape = function.result_array_type }) + try io.print(" @f{}(", output, {id}) + for (function.parameters.items) |_, index| { + if (index != 0) try io.write_all(output, ", ") + try write_type(output, state.module, local_type(function, index)) + try io.print(" %p{}", output, {index}) + } + try io.write_all(output, ") {\nentry:\n") + for (function.locals.items) |_, index| { + type :: local_type(function, index) + try io.print(" %l{} = alloca ", output, {index}) + try write_type(output, state.module, type) + try io.print("\n %i{} = alloca i1\n store i1 {}, ptr %i{}\n", output, {index, index < function.parameters.items.len, index}) + if (index < function.parameters.items.len) { + try io.write_all(output, " store ") + try write_type(output, state.module, type) + try io.print(" %p{}, ptr %l{}\n", output, {index, index}) + } + } + for (state.values) |type, index| { + if (type.kind == .void) continue + try io.print(" %v{} = alloca ", output, {index}) + try write_type(output, state.module, type) + try io.write_all(output, "\n") + } + try io.print(" br label %b{}\n", output, {u32(function.entry)}) + for (function.blocks.items) |block, index| { + try io.print("b{}:\n", output, {index}) + for (block.instructions.items) |instruction| try emit_instruction(state, &block, instruction) + terminator :: block.terminator + match terminator.kind { + .return_value: { + if (terminator.value == ir.no_value or state.values[usize(terminator.value)].kind == .void) try io.write_all(output, " ret void\n") + else { + result :: try load(state, terminator.value) + try io.write_all(output, " ret ") + try write_type(output, state.module, state.values[usize(terminator.value)]) + try io.print(" %r{}\n", output, {result}) + } + } + .trap: try io.print(" call void @trap(i64 {}, i64 0, i64 0, ptr null, i64 0)\n unreachable\n", output, {u32(terminator.diagnostic)}) + .jump: try io.print(" br label %b{}\n", output, {u32(terminator.target)}) + .branch: { + condition :: try load(state, terminator.value) + try io.print(" br i1 %r{}, label %b{}, label %b{}\n", output, {condition, u32(terminator.target), u32(terminator.alternative)}) + } + } + } + try io.write_all(output, "}\n") +} diff --git a/source/llvm/runtime.hon b/source/llvm/runtime.hon new file mode 100644 index 0000000..23c9c67 --- /dev/null +++ b/source/llvm/runtime.hon @@ -0,0 +1,139 @@ +@hide +runtime :: + `target triple = "arm64-apple-macosx11.0.0" + `%string = type { ptr, i64 } + `%address = type { ptr, ptr } + `declare i64 @write(i32, ptr, i64) + `declare ptr @__error() + `declare i32 @snprintf(ptr, i64, ptr, ...) + `declare void @exit(i32) noreturn + `declare { i64, i1 } @llvm.sadd.with.overflow.i64(i64, i64) + `declare { i64, i1 } @llvm.ssub.with.overflow.i64(i64, i64) + `declare { i64, i1 } @llvm.smul.with.overflow.i64(i64, i64) + `declare { i64, i1 } @llvm.uadd.with.overflow.i64(i64, i64) + `declare { i64, i1 } @llvm.usub.with.overflow.i64(i64, i64) + `declare { i64, i1 } @llvm.umul.with.overflow.i64(i64, i64) + `@fmt_int = private constant [5 x i8] c"%lld\00" + `@fmt_uint = private constant [5 x i8] c"%llu\00" + `@fmt_float = private constant [6 x i8] c"%.17g\00" + `@true = private constant [4 x i8] c"true" + `@false = private constant [5 x i8] c"false" + `@colon = private constant [1 x i8] c":" + `@error = private constant [9 x i8] c": error: " + `@runtime_trap = private constant [16 x i8] c": runtime trap: " + `@newline = private constant [1 x i8] c"\0A" + `@open = private constant [1 x i8] c"[" + `@close = private constant [1 x i8] c"]" + `@separator = private constant [2 x i8] c", " + `@write_error = private constant [44 x i8] c": infrastructure error: cannot write output\0A" + `@read = private constant [42 x i8] c"local value was read before initialization" + `@assign = private constant [46 x i8] c"local value was assigned before initialization" + `@cycle = private constant [27 x i8] c"global initialization cycle" + `@overflow = private constant [27 x i8] c"integer arithmetic overflow" + `@bounds = private constant [19 x i8] c"index out of bounds" + `define internal void @out(i32 %fd, ptr %bytes, i64 %len) { + `entry: + ` br label %loop + `loop: + ` %offset = phi i64 [ 0, %entry ], [ %next, %progress ], [ %offset, %retry ] + ` %done = icmp eq i64 %offset, %len + ` br i1 %done, label %end, label %send + `send: + ` %remaining = sub i64 %len, %offset + ` %pointer = getelementptr i8, ptr %bytes, i64 %offset + ` %count = call i64 @write(i32 %fd, ptr %pointer, i64 %remaining) + ` %positive = icmp sgt i64 %count, 0 + ` br i1 %positive, label %progress, label %check + `progress: + ` %next = add i64 %offset, %count + ` br label %loop + `check: + ` %negative = icmp slt i64 %count, 0 + ` br i1 %negative, label %errno, label %fail + `errno: + ` %error_pointer = call ptr @__error() + ` %error_value = load i32, ptr %error_pointer + ` %interrupted = icmp eq i32 %error_value, 4 + ` br i1 %interrupted, label %retry, label %fail + `retry: + ` br label %loop + `fail: + ` call void @io_failure() + ` unreachable + `end: + ` ret void + `} + `define internal void @print_integer(i32 %fd, i64 %value, i1 %unsigned) { + `entry: + ` %buffer = alloca [64 x i8], align 16 + ` %format = select i1 %unsigned, ptr @fmt_uint, ptr @fmt_int + ` %count = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buffer, i64 64, ptr %format, i64 %value) + ` %valid_low = icmp sge i32 %count, 0 + ` %valid_high = icmp slt i32 %count, 64 + ` %valid = and i1 %valid_low, %valid_high + ` br i1 %valid, label %output, label %fail + `output: + ` %length = zext i32 %count to i64 + ` call void @out(i32 %fd, ptr %buffer, i64 %length) + ` ret void + `fail: + ` call void @io_failure() + ` unreachable + `} + `define internal void @print_float(double %value) { + `entry: + ` %buffer = alloca [64 x i8], align 16 + ` %count = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buffer, i64 64, ptr @fmt_float, double %value) + ` %valid_low = icmp sge i32 %count, 0 + ` %valid_high = icmp slt i32 %count, 64 + ` %valid = and i1 %valid_low, %valid_high + ` br i1 %valid, label %output, label %fail + `output: + ` %length = zext i32 %count to i64 + ` call void @out(i32 1, ptr %buffer, i64 %length) + ` ret void + `fail: + ` call void @io_failure() + ` unreachable + `} + `define internal void @print_bool(i1 %value) { + `entry: + ` %bytes = select i1 %value, ptr @true, ptr @false + ` %length = select i1 %value, i64 4, i64 5 + ` call void @out(i32 1, ptr %bytes, i64 %length) + ` ret void + `} + `define internal void @print_string(%string %value) { + `entry: + ` %bytes = extractvalue %string %value, 0 + ` %length = extractvalue %string %value, 1 + ` call void @out(i32 1, ptr %bytes, i64 %length) + ` ret void + `} + `define internal void @trap(i64 %id, i64 %line, i64 %column, ptr %message, i64 %length) noreturn { + `entry: + ` call void @report(i64 %id) + ` %generated = icmp eq i64 %id, -1 + ` br i1 %generated, label %new, label %end + `new: + ` call void @diagnose(i64 %line, i64 %column, ptr %message, i64 %length, i1 true) + ` br label %end + `end: + ` call void @exit(i32 1) + ` unreachable + `} + `define internal void @check_init(ptr %flag, i1 %read, i64 %line, i64 %column) { + `entry: + ` %global = icmp eq ptr %flag, null + ` br i1 %global, label %end, label %check + `check: + ` %ready = load i1, ptr %flag + ` br i1 %ready, label %end, label %fail + `fail: + ` %message = select i1 %read, ptr @read, ptr @assign + ` %length = select i1 %read, i64 42, i64 46 + ` call void @trap(i64 -1, i64 %line, i64 %column, ptr %message, i64 %length) + ` unreachable + `end: + ` ret void + `} diff --git a/source/main.hon b/source/main.hon index a6004a0..e995557 100644 --- a/source/main.hon +++ b/source/main.hon @@ -1,3 +1,4 @@ +import "@ffi/c" import "@std/mem" import "@std/arraylist" import "@std/enummap" @@ -13,6 +14,7 @@ import "@source/sema" import "@source/ir" import "@source/lower" import "@source/interpreter" +import "@source/llvm" typecheck :: import "@source/sema/check" @@ -46,8 +48,13 @@ DriverError :: alias ( | io.CloseError | io.ReadError | io.WriteError + | process.RunError + | NativeBuildError ) +@hide:file +NativeBuildError :: enum { temporary_file_failed, compiler_failed } + main proc(init process.Init) i32 { return command(mem.c_allocator, init.io, init.args) catch |err| { if (init.args.values.len > 2) { @@ -77,19 +84,93 @@ infrastructure_messages :: $enummap.init(DriverError, []u8, { not_open_for_writing = "output is not writable", write_failed = "cannot write output", no_progress = "output writer made no progress", + spawn_failed = "cannot start native compiler", + wait_failed = "cannot wait for native compiler", + invalid_arguments = "invalid native compiler arguments", + temporary_file_failed = "cannot create temporary LLVM file", + compiler_failed = "native compiler failed", }) @hide:file command proc(allocator mem.Allocator, runtime io.Io, args process.Args) i32 ! DriverError { - if (args.values.len != 3) { - try io.print("usage: honey run \n", io.stderr(runtime), {}) - return 2 + if (args.values.len == 3) { + if (mem.eql(args.values[1], "run")) return try run_file(allocator, runtime, args.values[2]) + if (mem.eql(args.values[1], "emit-llvm")) return try emit_file(allocator, runtime, args.values[2]) } - if (!mem.eql(args.values[1], "run")) { - try io.print("usage: honey run \n", io.stderr(runtime), {}) - return 2 + if (args.values.len == 5 or args.values.len == 6) { + if (mem.eql(args.values[1], "build") and mem.eql(args.values[3], "-o") and args.values[4].len != 0) { + optimization [;0]u8 := "-O0" + if (args.values.len == 6) { + optimization = args.values[5] + if (!mem.eql(optimization, "-O0") and !mem.eql(optimization, "-O2")) { + try usage(io.stderr(runtime)) + return 2 + } + } + return try build_file(allocator, runtime, args.values[2], args.values[4], optimization) + } } - return try run_file(allocator, runtime, args.values[2]) + try usage(io.stderr(runtime)) + return 2 +} + +@hide:file +usage proc(output io.Writer) void ! io.WriteError { + try io.write_all(output, "usage: honey run \n") + try io.write_all(output, " honey emit-llvm \n") + try io.write_all(output, " honey build -o [-O0|-O2]\n") +} + +@hide:file +emit_file proc(allocator mem.Allocator, runtime io.Io, path [;0]u8) i32 ! DriverError { + source := try read_source(allocator, runtime, path) + defer arraylist.deinit(&source) + diagnostic_store := diagnostics.init(allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile(allocator, source.items, &diagnostic_store) + defer ir.deinit(&module) + try llvm.emit(allocator, io.stdout(runtime), &module, &diagnostic_store, source.items, path) + try report_diagnostics(allocator, io.stderr(runtime), path, source.items, &diagnostic_store, null) + return if (diagnostics.count(&diagnostic_store) == 0) 0 else 1 +} + +@hide:file +build_file proc( + allocator mem.Allocator, + runtime io.Io, + path [;0]u8, + output [;0]u8, + optimization [;0]u8, +) i32 ! DriverError { + source := try read_source(allocator, runtime, path) + defer arraylist.deinit(&source) + diagnostic_store := diagnostics.init(allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile(allocator, source.items, &diagnostic_store) + defer ir.deinit(&module) + + template [18]mut u8 := undefined + memcopy!(template[..], "/tmp/honey-XXXXXX\0") + descriptor :: c.mkstemp(ptrcast!(template[..].ptr)) + if (descriptor < 0) return .temporary_file_failed + defer _ = c.unlink(ptrcast!(template[..].ptr)) + file :: io.File{ io = runtime, handle = io.Handle{ file_desc = descriptor } } + closing := false + defer { + if (!closing) io.close(file) catch |_| {} + } + try llvm.emit(allocator, io.writer(file), &module, &diagnostic_store, source.items, path) + closing = true + try io.close(file) + temporary [;0]u8 :: template[..].ptr[..17;0] + arguments [9][;0]u8 :: [ + "clang", "-target", "arm64-apple-macosx11.0.0", + "-x", "ir", optimization, temporary, "-o", output, + ] + status :: try process.run(allocator, arguments[..]) + if (status != 0) return .compiler_failed + try report_diagnostics(allocator, io.stderr(runtime), path, source.items, &diagnostic_store, null) + return if (diagnostics.count(&diagnostic_store) == 0) 0 else 1 } @hide:file diff --git a/std/process/process.hon b/std/process/process.hon index 443c441..0150a1b 100644 --- a/std/process/process.hon +++ b/std/process/process.hon @@ -1,4 +1,6 @@ import "@std/io" +import "@ffi/c" +import "@std/mem" Init :: struct { io io.Io @@ -9,3 +11,56 @@ Init :: struct { Args :: struct { values [][;0]u8 } + +RunError :: enum { + invalid_arguments + spawn_failed + wait_failed +} + +#! run a program synchronously without a shell, searching PATH for `arguments[0]`. +#! arguments and zero-terminated strings are borrowed until this call returns. +#! an empty argument vector, empty executable name, or embedded NUL is invalid. +#! the child inherits the current environment and stdin/stdout/stderr. +#! temporary argument descriptors use `allocator` and are freed on every return. +#! return the exit code, or 128 plus the terminating signal number. +run proc(allocator mem.Allocator, arguments [][;0]u8) i32 ! (mem.AllocError | RunError) { + if (arguments.len == 0) return .invalid_arguments + if (arguments[0].len == 0) return .invalid_arguments + for arguments |argument| { + for (argument) |byte| if (byte == 0) return .invalid_arguments + } + if (arguments.len == maxval!(usize)) return .out_of_memory + + descriptors :: try mem.alloc(?*mut c_char, allocator, arguments.len + 1) + defer mem.free(allocator, descriptors) + for (0..arguments.len) |index| { + descriptors[index] = ptrcast!(constcast!(arguments[index]).ptr) + } + descriptors[arguments.len] = null + + pid [1]mut c_int := [0] + result :: c.posix_spawnp( + pid[..].ptr, + ptrcast!(arguments[0].ptr), + null, + null, + descriptors.ptr, + c._NSGetEnviron()?^, + ) + if (result != 0) return .spawn_failed + + status [1]mut c_int := [0] + while true { + waited :: c.waitpid(pid[0], status[..].ptr, 0) + if (waited < 0) { + if (c.__error()?^ == c.EINTR) continue + return .wait_failed + } + # Darwin wait status: low seven bits hold the signal, next byte the exit code. + signal :: status[0] & 127 + if (signal == 0) return i32(divtrunc!(status[0], 256) & 255) + if (signal != 127) return i32(128 + signal) + # A traced child can report a stop even without WUNTRACED; wait for termination. + } +} diff --git a/std/process/process.test.hon b/std/process/process.test.hon new file mode 100644 index 0000000..733a8ed --- /dev/null +++ b/std/process/process.test.hon @@ -0,0 +1,26 @@ +import "@std/testing" +import "@std/mem" + +runs_without_shell_and_preserves_exit_status test { + arguments [4][;0]u8 :: ["/bin/test", "a b;$(exit 99)", "=", "a b;$(exit 99)"] + status :: run(mem.c_allocator, arguments[..]) catch |_| { return .expectation_failed } + try testing.expect_equal(i32(0), status) + + failing [2][;0]u8 :: ["/bin/test", ""] + failure :: run(mem.c_allocator, failing[..]) catch |_| { return .expectation_failed } + try testing.expect_equal(i32(1), failure) +} + +rejects_invalid_arguments_and_reports_spawn_failure test { + empty [0][;0]u8 :: [] + _ = run(mem.c_allocator, empty[..]) catch |err| { + try testing.expect(err == .invalid_arguments) + missing [1][;0]u8 :: ["/nonexistent/honey-process-test"] + _ = run(mem.c_allocator, missing[..]) catch |spawn_error| { + try testing.expect(spawn_error == .spawn_failed) + return + } + return .expectation_failed + } + return .expectation_failed +} diff --git a/tests/native.py b/tests/native.py new file mode 100644 index 0000000..8e52ebe --- /dev/null +++ b/tests/native.py @@ -0,0 +1,298 @@ +"""Build Honey first with `bro build`, then run `python3 tests/native.py`.""" + +import os +from pathlib import Path +import subprocess +import tempfile +import textwrap + +ROOT = Path(__file__).resolve().parents[1] +HONEY = ROOT / "build/honey" + +# Explicit expectations guard against frontend bugs shared by both execution paths. +CASES = { + "void": ("main proc() void {}", b"", None), + "signed_limits": ( + "main proc() [2]i64 { return [-9223372036854775808, 9223372036854775807] }", + b"[-9223372036854775808, 9223372036854775807]\n", None, + ), + "float_context": ("main proc() float { return 3 / 2 }", b"1.5\n", None), + "float_ieee": ( + """ + main proc() bool { + nan :: 0.0 / 0.0 + inf :: 1.0 / 0.0 + return nan != nan and !(nan == nan) and !(nan < 0.0) and !(nan >= 0.0) and inf > 1.0 + } + """, b"true\n", None, + ), + "float_rounding": ( + "main proc() f64 { return 9007199254740993.0 - 9007199254740992 }", b"0\n", None, + ), + "strings": (r'main proc() string { return "a\0b\n\"\\" }', b'a\0b\n"\\\n', None), + "scalar_call_types": ( + """ + nothing proc() void {} + twice proc(x f64) f64 { return x * 2 } + select proc(flag bool, left string, right string) string { + nothing() + if flag { return left } + return right + } + main proc() string { return select(twice(1.5) == 3.0, "yes", "no") } + """, b"yes\n", None, + ), + "mutual_recursion": ( + """ + even proc(n int) bool { if n == 0 { return true } return odd(n - 1) } + odd proc(n int) bool { if n == 0 { return false } return even(n - 1) } + main proc() bool { return even(20) and odd(19) } + """, b"true\n", None, + ), + "function_values": ( + """ + plus proc(n int) int { return n + 1 } + minus proc(n int) int { return n - 1 } + main proc() bool { + f := plus + f = minus + a :: [plus, f] + return a[0](41) == 42 and a[1](43) == 42 + } + """, b"true\n", None, + ), + "short_circuit": ( + """ + cycle bool :: cycle + bad proc() bool { return 9223372036854775807 + 1 == 0 } + main proc() bool { return !(false and bad()) and (true or cycle) } + """, b"true\n", None, + ), + "lazy_globals": ( + """ + first :: identity(later) + later [2][2]string :: [["one", "two"], ["three", "four"]] + identity proc(a [2][2]string) [2][2]string { return a } + main proc() [2][2]string { return identity(identity(first)) } + """, b"[[one, two], [three, four]]\n", None, + ), + "array_copies": ( + """ + seed [2]mut [2]mut i64 :: [[1, 2], [3, 4]] + identity proc(a [2]mut [2]mut i64) [2]mut [2]mut i64 { return a } + main proc() [2]mut [2]mut i64 { + a := identity(seed) + b := a + (a[0])[1] = 99 + a[1] = a[0] + return b + } + """, b"[[1, 2], [3, 4]]\n", None, + ), + "zero_leaf_arrays": ( + """ + identity proc(a [2][0]bool) [2][0]bool { return a } + main proc() [2][0]bool { return identity([[], []]) } + """, b"[[], []]\n", None, + ), + "array_branchful_arguments": ( + """ + identity proc(a [2]bool) [2]bool { return a } + pair proc(a [2]bool, b [2]bool) [2][2]bool { return [a, b] } + main proc() [2][2]bool { + return pair(identity([true or false, false and true]), identity([false or true, !false])) + } + """, b"[[true, false], [true, true]]\n", None, + ), + "unsigned_max_range": ( + """ + main proc() int { + end usize :: 18446744073709551615 + start usize :: 18446744073709551614 + count := 0 + for start..=end |i| { + if (i < start) return 0 + count = count + 1 + continue + } + return count + } + """, b"2\n", None, + ), + "unsigned_comparisons": ( + """ + main proc() bool { + a usize :: 9223372036854775808 + return a > 9223372036854775807 and a * 1 + 1 - 1 == a + } + """, b"true\n", None, + ), + "signed_range_limits": ( + """ + main proc() int { + maximum :: 9223372036854775807 + count := 0 + for maximum..=maximum |_| { count = count + 1 } + for 2..1 |_| { count = 0 } + for (-2)..0 |_| { count = count + 1 } + return count + } + """, b"3\n", None, + ), + "loop_array_storage": ( + """ + main proc() int { + n := 0 + a [1]mut i64 := [0] + while n < 10000 { + row [1]mut i64 := [a[0]] + row[0] = row[0] + 1 + a = row + n = n + 1 + } + return a[0] + } + """, b"10000\n", None, + ), + "forward_local": ( + "main proc() int {\n before :: after\n after :: 42\n return before\n}", + b"", b"local value was read before initialization", + ), + "forward_assignment": ( + "main proc() int {\n after = 1\n after := 42\n return after\n}", + b"", b"local value was assigned before initialization", + ), + "loop_local_reset": ( + """ + main proc() int { + n := 0 + while n < 2 { + n = n + 1 + if (n == 2) return later[0] + later [1]i64 :: [42] + } + return 0 + } + """, b"", b"local value was read before initialization", + ), + "global_cycle": ( + "a [1]i64 :: b\nb [1]i64 :: a\nmain proc() int { return a[0] }", + b"", b"global initialization cycle", + ), + "bounds": ( + "main proc() int {\n a :: [42]\n return a[1]\n}", b"", b"index out of bounds", + ), + "empty_bounds": ( + "main proc() int {\n a [0]i64 :: []\n return a[0]\n}", b"", b"index out of bounds", + ), + "unsigned_bounds": ( + "main proc() int {\n a [1]mut i64 := [1]\n a[18446744073709551615] = 2\n return 0\n}", + b"", b"index out of bounds", + ), + "assignment_error_order": ( + "main proc() int {\n a [0]mut i64 := []\n a[0] = 9223372036854775807 + 1\n return 0\n}", + b"", b"integer arithmetic overflow", + ), + "signed_add_overflow": ( + "main proc() int { return 9223372036854775807 + 1 }", b"", b"integer arithmetic overflow", + ), + "signed_subtract_overflow": ( + "main proc() int { return -9223372036854775808 - 1 }", b"", b"integer arithmetic overflow", + ), + "signed_multiply_overflow": ( + "main proc() int { return 9223372036854775807 * 2 }", b"", b"integer arithmetic overflow", + ), + "signed_negate_overflow": ( + "main proc() int { return -(-9223372036854775808) }", b"", b"integer arithmetic overflow", + ), + "unsigned_subtract_overflow": ( + "main proc() usize {\n zero usize :: 0\n return zero - 1\n}", b"", b"integer arithmetic overflow", + ), + "unsigned_add_overflow": ( + "main proc() usize { return 18446744073709551615 + 1 }", b"", b"integer arithmetic overflow", + ), + "unsigned_multiply_overflow": ( + "main proc() usize {\n max usize :: 18446744073709551615\n return max * 2\n}", + b"", b"integer arithmetic overflow", + ), + "unused_errors": ( + "bad proc() int { return absent }\nmain proc() int { return 42 }", b"42\n", b"unknown name", + ), + "reached_source_error": ( + "main proc() int { return absent }", b"", b"unknown name", + ), + "missing_return": ( + "main proc() int { if false { return 42 } }", b"", b"nonvoid function can finish", + ), + "missing_entry": ("other proc() void {}", b"", b"program requires a main function"), + "invalid_unused_signature": ( + "bad proc(a void) void {}\nmain proc() int { return 42 }", b"42\n", b"void cannot be stored", + ), +} + + +def run(*args, env=None): + return subprocess.run(args, cwd=ROOT, capture_output=True, timeout=30, env=env) + + +def outcome(result): + return result.returncode, result.stdout, result.stderr + + +def check(source, executable, expected=None): + interpreted = run(str(HONEY), "run", str(source)) + assert interpreted.returncode in (0, 1), outcome(interpreted) + if expected is not None: + stdout, error = expected + assert interpreted.stdout == stdout, (source.name, outcome(interpreted), stdout) + assert interpreted.returncode == (1 if error else 0), outcome(interpreted) + if error: + assert error in interpreted.stderr, outcome(interpreted) + for optimization in ("-O0", "-O2"): + built = run(str(HONEY), "build", str(source), "-o", str(executable), optimization) + assert built.returncode in (0, 1), (source.name, optimization, outcome(built)) + assert built.stdout == b"", outcome(built) + assert executable.is_file(), (source.name, outcome(built)) + compiled = run(str(executable)) + assert outcome(compiled) == outcome(interpreted), ( + source.name, optimization, outcome(interpreted), outcome(compiled) + ) + executable.unlink() + + +def main(): + with tempfile.TemporaryDirectory(prefix="honey native ' ; $") as directory: + temporary = Path(directory) + executable = temporary / "program ' ; $" + count = 0 + for source in sorted((ROOT / "programs").glob("*.hon")): + check(source, executable) + count += 1 + for name, (text, stdout, error) in CASES.items(): + source = temporary / f"{name}.hon" + source.write_text(textwrap.dedent(text).strip() + "\n") + check(source, executable, (stdout, error)) + count += 1 + source = ROOT / "programs/factorial.hon" + emitted = run(str(HONEY), "emit-llvm", str(source)) + assert emitted.returncode == 0 and emitted.stderr == b"", outcome(emitted) + ir = temporary / "factorial.ll" + ir.write_bytes(emitted.stdout) + assembled = run("clang", str(ir), "-o", str(executable)) + assert assembled.returncode == 0, outcome(assembled) + assert outcome(run(str(executable))) == (0, b"120\n", b"") + executable.unlink() + missing_compiler = run( + str(HONEY), "build", str(source), "-o", str(executable), + env={**os.environ, "PATH": str(temporary)}, + ) + assert missing_compiler.returncode == 2, outcome(missing_compiler) + assert b"cannot start native compiler" in missing_compiler.stderr, outcome(missing_compiler) + assert not executable.exists() + invalid = run(str(HONEY), "build", str(source), "-o", str(executable), "-O3") + assert invalid.returncode == 2 and b"usage:" in invalid.stderr, outcome(invalid) + print(f"{count} programs matched interpreter at -O0 and -O2; LLVM output and CLI errors passed") + + +if __name__ == "__main__": + main()