feat: emit LLVM IR and build native executables

This commit is contained in:
2026-10-25 02:05:26 +01:00
parent c01d935a7b
commit 45359c0cdf
10 changed files with 1267 additions and 9 deletions
+26 -2
View File
@@ -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 <executable>` 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. 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. 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`. 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. `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.
+2
View File
@@ -0,0 +1,2 @@
- [ ] milestone: self-hosted honey compiler.
- honey can compile itself
+16
View File
@@ -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
+221
View File
@@ -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)
}
+396
View File
@@ -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")
}
+139
View File
@@ -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
`}
+87 -6
View File
@@ -1,3 +1,4 @@
import "@ffi/c"
import "@std/mem" import "@std/mem"
import "@std/arraylist" import "@std/arraylist"
import "@std/enummap" import "@std/enummap"
@@ -13,6 +14,7 @@ import "@source/sema"
import "@source/ir" import "@source/ir"
import "@source/lower" import "@source/lower"
import "@source/interpreter" import "@source/interpreter"
import "@source/llvm"
typecheck :: import "@source/sema/check" typecheck :: import "@source/sema/check"
@@ -46,8 +48,13 @@ DriverError :: alias (
| io.CloseError | io.CloseError
| io.ReadError | io.ReadError
| io.WriteError | io.WriteError
| process.RunError
| NativeBuildError
) )
@hide:file
NativeBuildError :: enum { temporary_file_failed, compiler_failed }
main proc(init process.Init) i32 { main proc(init process.Init) i32 {
return command(mem.c_allocator, init.io, init.args) catch |err| { return command(mem.c_allocator, init.io, init.args) catch |err| {
if (init.args.values.len > 2) { if (init.args.values.len > 2) {
@@ -77,19 +84,93 @@ infrastructure_messages :: $enummap.init(DriverError, []u8, {
not_open_for_writing = "output is not writable", not_open_for_writing = "output is not writable",
write_failed = "cannot write output", write_failed = "cannot write output",
no_progress = "output writer made no progress", 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 @hide:file
command proc(allocator mem.Allocator, runtime io.Io, args process.Args) i32 ! DriverError { command proc(allocator mem.Allocator, runtime io.Io, args process.Args) i32 ! DriverError {
if (args.values.len != 3) { if (args.values.len == 3) {
try io.print("usage: honey run <file>\n", io.stderr(runtime), {}) 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 (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 2
} }
if (!mem.eql(args.values[1], "run")) {
try io.print("usage: honey run <file>\n", io.stderr(runtime), {})
return 2
} }
return try run_file(allocator, runtime, args.values[2]) return try build_file(allocator, runtime, args.values[2], args.values[4], optimization)
}
}
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 <file>\n")
try io.write_all(output, " honey emit-llvm <file>\n")
try io.write_all(output, " honey build <file> -o <executable> [-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 @hide:file
+55
View File
@@ -1,4 +1,6 @@
import "@std/io" import "@std/io"
import "@ffi/c"
import "@std/mem"
Init :: struct { Init :: struct {
io io.Io io io.Io
@@ -9,3 +11,56 @@ Init :: struct {
Args :: struct { Args :: struct {
values [][;0]u8 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.
}
}
+26
View File
@@ -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
}
+298
View File
@@ -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()