feat: emit LLVM IR and build native executables
This commit is contained in:
+298
@@ -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()
|
||||
Reference in New Issue
Block a user