feat: lower typed programs and interpret IR

This commit is contained in:
2026-10-09 22:47:05 +02:00
parent bc318a29ad
commit 98f3a04daf
20 changed files with 2328 additions and 85 deletions
+26 -2
View File
@@ -5,7 +5,7 @@
#! * op ....... -> '+' | '-' | '*' | '/'
#! * expr ..... -> identifier | literal | '(' expr ')' | '-' expr | expr op expr | expr '(' [expr {',' expr} [',']] ')'
#! * decl ..... -> identifier [type] assignment expr
#! * stmt ..... -> decl | expr
#! * stmt ..... -> decl | expr | 'return' [expr]
import "@std/debug"
import "@std/mem"
@@ -41,7 +41,7 @@ DeclKind :: enum {
}
StmtKind :: enum {
declaration, expression, invalid
declaration, expression, return_stmt, invalid
}
Mutability :: enum {
@@ -65,6 +65,7 @@ NodeKind :: enum {
expr_call,
stmt_expr,
stmt_decl,
stmt_return,
func_decl,
param_decl,
block,
@@ -151,6 +152,11 @@ CallExprView :: struct {
callee NodeId
}
ReturnView :: struct {
span Span
value ?NodeId
}
InvalidView :: struct {
span Span
diagnostic DiagnosticId
@@ -259,6 +265,14 @@ add_expression_stmt proc(builder @mut Builder, expression NodeId) NodeId ! mem.A
}, builder_span(builder, expression))
}
add_return_stmt proc(builder @mut Builder, span Span, value ?NodeId) NodeId ! mem.AllocError {
raw_value :: if (value) |id| id else no_node
return try add_node(builder, Node{
kind = .stmt_return,
main = NodeData{ node_id = raw_value },
}, span)
}
add_statement_decl proc(
builder @mut Builder,
span Span,
@@ -393,6 +407,7 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind {
return match get_node(&tree.storage, id).kind {
.stmt_decl: .declaration
.stmt_expr: .expression
.stmt_return: .return_stmt
.invalid_stmt: .invalid
else: unreachable
}
@@ -404,6 +419,15 @@ statement_expr proc(tree @Tree, id NodeId) NodeId {
return node.main.node_id
}
return_stmt proc(tree @Tree, id NodeId) ReturnView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .stmt_return)
return ReturnView{
span = get_span(&tree.storage, id),
value = maybe_node_id(u32(node.main.node_id)),
}
}
statement_decl proc(tree @Tree, id NodeId) StatementDeclView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .stmt_decl)
+8
View File
@@ -51,6 +51,14 @@ render_function proc(
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, expression)))
try append_text(renderer, "\n")
}
.return_stmt: {
try append_text(renderer, " return")
if (ast.return_stmt(tree, statement).value) |value| {
try append_text(renderer, " ")
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, value)))
}
try append_text(renderer, "\n")
}
.invalid: try append_text(renderer, " invalid statement\n")
}
}
-27
View File
@@ -1,27 +0,0 @@
import "@std/mem"
import "@std/testing"
import "@source/ast"
renders_semantic_debug_view test {
builder := ast.init_builder(mem.c_allocator, "value :: 42")
defer ast.deinit_builder(&builder)
value :: try ast.add_literal(&builder, .int, ast.Span{ start = 9, end = 11 })
declaration :: try ast.add_statement_decl(
&builder,
ast.Span{ start = 0, end = 11 },
ast.Span{ start = 0, end = 5 },
null,
.constant,
value,
)
declarations [1]ast.NodeId :: [declaration]
package :: try ast.add_package(&builder, ast.Span{ start = 0, end = 11 }, declarations[..])
tree := ast.finish(&builder, package)
defer ast.deinit(&tree)
renderer := init(mem.c_allocator)
defer deinit(&renderer)
try render(&renderer, &tree)
try testing.expect(mem.eql(u8, "package\n declaration value\n", renderer.bytes.items))
}
+37
View File
@@ -26,6 +26,14 @@ Code :: enum {
argument_count_mismatch,
cannot_infer_type,
missing_return,
return_type_mismatch,
missing_entry,
invalid_entry,
literal_out_of_range,
invalid_escape,
read_before_initialization,
initialization_cycle,
integer_overflow,
}
Diagnostic :: struct {
@@ -64,3 +72,32 @@ get proc(store @Store, id DiagnosticId) ?Diagnostic {
count proc(store @Store) usize {
return store.list.items.len
}
message proc(code Code) []u8 {
return match code {
.invalid_character: "invalid character"
.float_must_end_with_digit: "float literal must end with a digit"
.unterminated_string: "unterminated string literal"
.unexpected_token: "unexpected token"
.duplicate_name: "duplicate name or forbidden shadowing"
.unknown_name: "unknown name"
.expected_type: "annotation must name a type"
.type_not_value: "type name cannot be used as a value"
.invalid_void_type: "void cannot be stored or used as a parameter type"
.type_mismatch: "incompatible value type"
.invalid_operand: "arithmetic requires compatible numeric operands"
.integer_division: "division requires float operands"
.not_callable: "value is not callable"
.argument_count_mismatch: "argument count does not match the function signature"
.cannot_infer_type: "cannot infer a type without a type anchor"
.missing_return: "nonvoid function can finish without returning a value"
.return_type_mismatch: "return does not match the function result type"
.missing_entry: "program requires a main function"
.invalid_entry: "main must be a function with no parameters"
.literal_out_of_range: "numeric literal is outside its runtime type's range"
.invalid_escape: "invalid string escape"
.read_before_initialization: "local value was read before initialization"
.initialization_cycle: "global initialization cycle"
.integer_overflow: "integer arithmetic overflow"
}
}
+247
View File
@@ -0,0 +1,247 @@
import "@std/mem"
import "@std/arraylist"
import "@source/ir"
import "@source/diagnostics"
Outcome :: struct {
value ir.Value = ir.Value{}
trap ?diagnostics.DiagnosticId = null
}
@hide:file
Slot :: struct {
initialized bool = false
value ir.Value = ir.Value{}
}
@hide:file
GlobalStatus :: enum { uninitialized, initializing, ready }
@hide:file
GlobalState :: struct {
status GlobalStatus = .uninitialized
value ir.Value = ir.Value{}
}
@hide:file
Frame :: struct {
function ir.FunctionId
next_instruction usize = 0
destination ir.ValueId = ir.no_value
global ?usize = null
locals []mut Slot
values []mut ir.Value
}
#! executes only typed IR; invocation and global state are temporary and released on every exit.
#! returned strings borrow `module`; runtime source errors are traps, not infrastructure errors.
run proc(
allocator mem.Allocator,
module @ir.Module,
diagnostic_store @mut diagnostics.Store,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
entry ir.FunctionId := ir.FunctionId(0)
if module.entry |function| {
entry = function
} else {
return Outcome{ trap = module.entry_diagnostic }
}
globals :: try mem.alloc(GlobalState, allocator, module.globals.items.len)
defer mem.free(allocator, globals)
mem.set(globals, GlobalState{})
frames := arraylist.init(Frame, allocator)
defer {
for (frames.items) |frame| free_frame(allocator, frame)
arraylist.deinit(&frames)
}
try push_frame(allocator, module, &frames, entry, ir.no_value, null)
while frames.items.len != 0 {
frame_index :: frames.items.len - 1
function :: &module.functions.items[usize(frames.items[frame_index].function)]
block :: &function.blocks.items[usize(function.entry)]
instruction_index :: frames.items[frame_index].next_instruction
if instruction_index == block.instructions.items.len {
terminator :: block.terminator
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
value := ir.Value{}
if terminator.value != ir.no_value {
value = frames.items[frame_index].values[usize(terminator.value)]
}
completed :: frames.items[frame_index]
_ = arraylist.pop(&frames)
free_frame(allocator, completed)
if completed.global |global_index| {
globals[global_index].value = value
globals[global_index].status = .ready
}
if (frames.items.len == 0) return Outcome{ value = value }
if completed.destination != ir.no_value {
frames.items[frames.items.len - 1].values[usize(completed.destination)] = value
}
continue
}
instruction :: &block.instructions.items[instruction_index]
frames.items[frame_index].next_instruction += 1
value := ir.Value{}
match instruction.op {
.constant: value = instruction.constant
.load_local: {
local :: &frames.items[frame_index].locals[usize(instruction.slot)]
if !local.initialized {
return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span)
}
value = local.value
}
.store_local: {
frames.items[frame_index].locals[usize(instruction.slot)] = Slot{
initialized = true,
value = frames.items[frame_index].values[usize(instruction.left)],
}
continue
}
.load_global: {
global_index :: usize(instruction.slot)
match globals[global_index].status {
.ready: value = globals[global_index].value
.initializing: {
return try runtime_trap(diagnostic_store, .initialization_cycle, instruction.span)
}
.uninitialized: {
try push_frame(
allocator,
module,
&frames,
module.globals.items[global_index].initializer,
instruction.destination,
global_index,
)
globals[global_index].status = .initializing
continue
}
}
}
.call: {
callee :: frames.items[frame_index].values[usize(instruction.left)].data.function
try push_frame(allocator, module, &frames, callee, instruction.destination, null)
# frame indices survive stack growth; pointers into frames.items do not.
for (0..instruction.arguments_count) |argument_index| {
argument :: block.arguments.items[instruction.arguments_start + argument_index]
frames.items[frame_index + 1].locals[argument_index] = Slot{
initialized = true,
value = frames.items[frame_index].values[usize(argument)],
}
}
continue
}
.negate: {
operand :: frames.items[frame_index].values[usize(instruction.left)]
if instruction.type == .float {
value = ir.Value{ type = .float, data = ir.ValueData{ real = -operand.data.real } }
} else {
if operand.data.integer == minval!(i64) {
return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
}
value = ir.Value{ type = .int, data = ir.ValueData{ integer = -operand.data.integer } }
}
}
.add, .subtract, .multiply, .divide: {
left :: frames.items[frame_index].values[usize(instruction.left)]
right :: frames.items[frame_index].values[usize(instruction.right)]
if instruction.type == .float {
result f64 := 0
match instruction.op {
.add: result = left.data.real + right.data.real
.subtract: result = left.data.real - right.data.real
.multiply: result = left.data.real * right.data.real
.divide: result = left.data.real / right.data.real
else: unreachable
}
value = ir.Value{ type = .float, data = ir.ValueData{ real = result } }
} else {
result :: checked_integer(instruction.op, left.data.integer, right.data.integer)
if result |result_integer| {
value = ir.Value{ type = .int, data = ir.ValueData{ integer = result_integer } }
} else {
return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
}
}
}
}
if instruction.destination != ir.no_value {
frames.items[frame_index].values[usize(instruction.destination)] = value
}
}
unreachable
}
@hide:file
push_frame proc(
allocator mem.Allocator,
module @ir.Module,
frames @mut arraylist.ArrayList(Frame),
function ir.FunctionId,
destination ir.ValueId,
global ?usize,
) void ! mem.AllocError {
signature :: &module.functions.items[usize(function)]
locals :: try mem.alloc(Slot, allocator, signature.locals.items.len)
errdefer mem.free(allocator, locals)
mem.set(locals, Slot{})
values :: try mem.alloc(ir.Value, allocator, signature.value_count)
errdefer mem.free(allocator, values)
# typed IR assigns each temporary before use; only local slots need initialization flags.
try arraylist.append(frames, Frame{
function = function,
destination = destination,
global = global,
locals = locals,
values = values,
})
}
@hide:file
free_frame proc(allocator mem.Allocator, frame Frame) void {
mem.free(allocator, frame.locals)
mem.free(allocator, frame.values)
}
@hide:file
runtime_trap proc(
diagnostic_store @mut diagnostics.Store,
code diagnostics.Code,
span diagnostics.Span,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
return Outcome{ trap = try diagnostics.add(diagnostic_store, code, span) }
}
@hide:file
checked_integer proc(op ir.Op, left i64, right i64) ?i64 {
minimum :: minval!(i64)
maximum :: maxval!(i64)
match op {
.add: {
if (right > 0 and left > maximum - right) return null
if (right < 0 and left < minimum - right) return null
return left + right
}
.subtract: {
if (right > 0 and left < minimum + right) return null
if (right < 0 and left > maximum + right) return null
return left - right
}
.multiply: {
if left > 0 {
if (right > 0 and left > divtrunc!(maximum, right)) return null
if (right < 0 and right < divtrunc!(minimum, left)) return null
} else if left < 0 {
if (right > 0 and left < divtrunc!(minimum, right)) return null
if (right < 0 and left < divtrunc!(maximum, right)) return null
}
return left * right
}
else: unreachable
}
}
+479
View File
@@ -0,0 +1,479 @@
import "@std/mem"
import "@std/arraylist"
import "@std/testing"
import "@source/ir"
import "@source/diagnostics"
@hide:file
add_function proc(
module @mut ir.Module,
locals []ir.Type,
parameter_count usize,
result ir.Type,
value_count usize,
) ir.FunctionId ! mem.AllocError {
function := ir.init_function(module.allocator)
errdefer {
arraylist.deinit(&function.parameters)
arraylist.deinit(&function.locals)
for (function.blocks.items) |block| {
owned := block
arraylist.deinit(&owned.instructions)
arraylist.deinit(&owned.arguments)
}
arraylist.deinit(&function.blocks)
}
for (locals) |type, index| {
try arraylist.append(&function.locals, type)
if (index < parameter_count) try arraylist.append(&function.parameters, type)
}
function.result = result
function.value_count = value_count
try arraylist.append(&function.blocks, ir.init_block(module.allocator))
id :: ir.FunctionId(module.functions.items.len)
try arraylist.append(&module.functions, function)
return id
}
@hide:file
emit proc(module @mut ir.Module, function ir.FunctionId, instruction ir.Instruction) void ! mem.AllocError {
try arraylist.append(&module.functions.items[usize(function)].blocks.items[0].instructions, instruction)
}
@hide:file
return_value proc(module @mut ir.Module, function ir.FunctionId, value ir.ValueId) void {
module.functions.items[usize(function)].blocks.items[0].terminator = ir.Terminator{ value = value }
}
@hide:file
integer proc(destination u32, value i64) ir.Instruction {
return ir.Instruction{
op = .constant,
type = .int,
destination = ir.ValueId(destination),
constant = ir.Value{ type = .int, data = ir.ValueData{ integer = value } },
}
}
@hide:file
function_value proc(destination u32, function ir.FunctionId) ir.Instruction {
return ir.Instruction{
op = .constant,
type = .function,
destination = ir.ValueId(destination),
constant = ir.Value{ type = .function, data = ir.ValueData{ function = function } },
}
}
@hide:file
expect_trap proc(
outcome Outcome,
store @diagnostics.Store,
code diagnostics.Code,
span diagnostics.Span,
) void ! testing.Error {
if outcome.trap |id| {
if diagnostics.get(store, id) |diagnostic| {
try testing.expect_equal(code, diagnostic.code)
try testing.expect_equal(span.start, diagnostic.span.start)
try testing.expect_equal(span.end, diagnostic.span.end)
return
}
}
return .expectation_failed
}
repeated_calls_have_fresh_parameters_locals_and_temporaries test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
types [2]ir.Type :: [.int, .int]
bump :: try add_function(&module, types[..], 1, .int, 4)
try emit(&module, bump, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
try emit(&module, bump, integer(1, 1))
try emit(&module, bump, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
try emit(&module, bump, ir.Instruction{ op = .store_local, left = ir.ValueId(2), slot = 1 })
try emit(&module, bump, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(3), slot = 1 })
return_value(&module, bump, ir.ValueId(3))
noop :: try add_function(&module, mem.empty(ir.Type), 0, .void, 0)
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 8)
module.entry = main
try emit(&module, main, function_value(0, bump))
try emit(&module, main, integer(1, 19))
try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(1))
try emit(&module, main, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), arguments_count = 1 })
try emit(&module, main, integer(3, 21))
try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(3))
try emit(&module, main, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(4), left = ir.ValueId(0), arguments_start = 1, arguments_count = 1 })
try emit(&module, main, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(5), left = ir.ValueId(2), right = ir.ValueId(4) })
try emit(&module, main, function_value(6, noop))
try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(7), left = ir.ValueId(6) })
return_value(&module, main, ir.ValueId(5))
outcome :: try run(mem.c_allocator, &module, &store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.int, outcome.value.type)
try testing.expect_equal(i64(42), outcome.value.data.integer)
try testing.expect_equal(usize(0), diagnostics.count(&store))
}
lazy_globals_follow_forward_dependencies_and_reset_between_runs test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
second :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
try emit(&module, first, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 1 })
try emit(&module, first, integer(1, 1))
try emit(&module, first, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
return_value(&module, first, ir.ValueId(2))
try emit(&module, second, integer(0, 20))
return_value(&module, second, ir.ValueId(0))
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } })
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } })
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(1), slot = 0 })
try emit(&module, main, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
return_value(&module, main, ir.ValueId(2))
initial :: try run(mem.c_allocator, &module, &store)
try testing.expect(initial.trap == null)
try testing.expect_equal(i64(42), initial.value.data.integer)
module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = integer(0, 30)
repeated :: try run(mem.c_allocator, &module, &store)
try testing.expect(repeated.trap == null)
try testing.expect_equal(i64(62), repeated.value.data.integer)
}
function_aliases_and_borrowed_string_values_execute test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
text :: "line\n\"quoted\"\x00"
target :: try add_function(&module, mem.empty(ir.Type), 0, .string, 1)
try emit(&module, target, ir.Instruction{
op = .constant,
type = .string,
destination = ir.ValueId(0),
constant = ir.Value{ type = .string, data = ir.ValueData{ string = text } },
})
return_value(&module, target, ir.ValueId(0))
initializer :: try add_function(&module, mem.empty(ir.Type), 0, .function, 1)
try emit(&module, initializer, function_value(0, target))
return_value(&module, initializer, ir.ValueId(0))
try arraylist.append(&module.globals, ir.Global{ type = .function, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
main :: try add_function(&module, mem.empty(ir.Type), 0, .string, 2)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_global, type = .function, destination = ir.ValueId(0), slot = 0 })
try emit(&module, main, ir.Instruction{ op = .call, type = .string, destination = ir.ValueId(1), left = ir.ValueId(0) })
return_value(&module, main, ir.ValueId(1))
outcome :: try run(mem.c_allocator, &module, &store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.string, outcome.value.type)
try testing.expect_equal([]u8, text, outcome.value.data.string)
}
premature_local_reads_and_global_cycles_report_instruction_spans test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
span :: diagnostics.Span{ start = 14, end = 19 }
types [1]ir.Type :: [.int]
main :: try add_function(&module, types[..], 0, .int, 1)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0, span = span })
return_value(&module, main, ir.ValueId(0))
premature :: try run(mem.c_allocator, &module, &store)
try expect_trap(premature, &store, .read_before_initialization, span)
first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
second :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
cycle_span :: diagnostics.Span{ start = 40, end = 44 }
try emit(&module, first, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 1 })
try emit(&module, second, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0, span = cycle_span })
return_value(&module, first, ir.ValueId(0))
return_value(&module, second, ir.ValueId(0))
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } })
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } })
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 }
cycle :: try run(mem.c_allocator, &module, &store)
try expect_trap(cycle, &store, .initialization_cycle, cycle_span)
retry :: try run(mem.c_allocator, &module, &store)
try expect_trap(retry, &store, .initialization_cycle, cycle_span)
}
existing_trap_identity_entry_failures_and_unused_errors_survive test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
entry_id :: try diagnostics.add(&store, .missing_entry, diagnostics.Span{ start = 0, end = 0 })
module.entry_diagnostic = entry_id
absent :: try run(mem.c_allocator, &module, &store)
try testing.expect_equal(entry_id, absent.trap?)
invalid_id :: try diagnostics.add(&store, .unknown_name, diagnostics.Span{ start = 12, end = 17 })
bad :: try add_function(&module, mem.empty(ir.Type), 0, .invalid, 0)
module.functions.items[usize(bad)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = invalid_id }
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 2)
module.entry = main
try emit(&module, main, integer(0, 42))
return_value(&module, main, ir.ValueId(0))
success :: try run(mem.c_allocator, &module, &store)
try testing.expect(success.trap == null)
try testing.expect_equal(i64(42), success.value.data.integer)
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = function_value(0, bad)
try emit(&module, main, ir.Instruction{ op = .call, type = .invalid, destination = ir.ValueId(1), left = ir.ValueId(0) })
trapped :: try run(mem.c_allocator, &module, &store)
try testing.expect_equal(invalid_id, trapped.trap?)
try testing.expect_equal(usize(2), diagnostics.count(&store))
}
@hide:file
IntegerCase :: struct {
op ir.Op
left i64
right i64
expected ?i64
}
integer_arithmetic_checks_boundaries_before_host_operations test {
cases [19]IntegerCase :: [
IntegerCase{ op = .add, left = 19, right = 23, expected = 42 },
IntegerCase{ op = .add, left = maxval!(i64), right = 0, expected = maxval!(i64) },
IntegerCase{ op = .add, left = maxval!(i64), right = 1, expected = null },
IntegerCase{ op = .add, left = minval!(i64), right = -1, expected = null },
IntegerCase{ op = .add, left = minval!(i64), right = maxval!(i64), expected = -1 },
IntegerCase{ op = .subtract, left = minval!(i64), right = 0, expected = minval!(i64) },
IntegerCase{ op = .subtract, left = minval!(i64), right = 1, expected = null },
IntegerCase{ op = .subtract, left = maxval!(i64), right = -1, expected = null },
IntegerCase{ op = .subtract, left = -1, right = minval!(i64), expected = maxval!(i64) },
IntegerCase{ op = .multiply, left = minval!(i64), right = 1, expected = minval!(i64) },
IntegerCase{ op = .multiply, left = minval!(i64), right = -1, expected = null },
IntegerCase{ op = .multiply, left = -1, right = minval!(i64), expected = null },
IntegerCase{ op = .multiply, left = maxval!(i64), right = 2, expected = null },
IntegerCase{ op = .multiply, left = minval!(i64), right = 2, expected = null },
IntegerCase{ op = .multiply, left = -3, right = -14, expected = 42 },
IntegerCase{ op = .multiply, left = -3, right = 14, expected = -42 },
IntegerCase{ op = .multiply, left = 3, right = -14, expected = -42 },
IntegerCase{ op = .multiply, left = minval!(i64), right = 0, expected = 0 },
IntegerCase{ op = .multiply, left = 0, right = minval!(i64), expected = 0 },
]
for (cases) |case| {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
span :: diagnostics.Span{ start = 8, end = 16 }
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
module.entry = main
try emit(&module, main, integer(0, case.left))
try emit(&module, main, integer(1, case.right))
try emit(&module, main, ir.Instruction{ op = case.op, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1), span = span })
return_value(&module, main, ir.ValueId(2))
outcome :: try run(mem.c_allocator, &module, &store)
if case.expected |expected| {
try testing.expect(outcome.trap == null)
try testing.expect_equal(expected, outcome.value.data.integer)
} else {
try expect_trap(outcome, &store, .integer_overflow, span)
}
}
}
integer_negation_traps_only_at_minimum test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
span :: diagnostics.Span{ start = 21, end = 27 }
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 2)
module.entry = main
try emit(&module, main, integer(0, minval!(i64)))
try emit(&module, main, ir.Instruction{ op = .negate, type = .int, destination = ir.ValueId(1), left = ir.ValueId(0), span = span })
return_value(&module, main, ir.ValueId(1))
trapped :: try run(mem.c_allocator, &module, &store)
try expect_trap(trapped, &store, .integer_overflow, span)
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = integer(0, maxval!(i64))
success :: try run(mem.c_allocator, &module, &store)
try testing.expect(success.trap == null)
try testing.expect_equal(-maxval!(i64), success.value.data.integer)
}
float_arithmetic_and_ieee_zero_division_execute test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
main :: try add_function(&module, mem.empty(ir.Type), 0, .float, 7)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(0), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 10.5 } } })
try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(1), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 2.0 } } })
try emit(&module, main, ir.Instruction{ op = .multiply, type = .float, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
try emit(&module, main, ir.Instruction{ op = .add, type = .float, destination = ir.ValueId(3), left = ir.ValueId(2), right = ir.ValueId(1) })
try emit(&module, main, ir.Instruction{ op = .subtract, type = .float, destination = ir.ValueId(4), left = ir.ValueId(3), right = ir.ValueId(1) })
try emit(&module, main, ir.Instruction{ op = .divide, type = .float, destination = ir.ValueId(5), left = ir.ValueId(4), right = ir.ValueId(1) })
try emit(&module, main, ir.Instruction{ op = .negate, type = .float, destination = ir.ValueId(6), left = ir.ValueId(5) })
return_value(&module, main, ir.ValueId(6))
result :: try run(mem.c_allocator, &module, &store)
try testing.expect(result.trap == null)
try testing.expect_equal(ir.Type.float, result.value.type)
try testing.expect_equal(f64(-10.5), result.value.data.real)
module.functions.items[usize(main)].blocks.items[0].instructions.items[0].constant.data.real = 0
module.functions.items[usize(main)].blocks.items[0].instructions.items[1].constant.data.real = 0
nan :: try run(mem.c_allocator, &module, &store)
try testing.expect(nan.trap == null)
try testing.expect(nan.value.data.real != nan.value.data.real)
module.functions.items[usize(main)].blocks.items[0].instructions.items[0].constant.data.real = 1
module.functions.items[usize(main)].blocks.items[0].instructions.items[2].op = .add
infinite :: try run(mem.c_allocator, &module, &store)
try testing.expect(infinite.trap == null)
try testing.expect(infinite.value.data.real < f64(-1000000000))
try testing.expect_equal(infinite.value.data.real, infinite.value.data.real * f64(2))
try testing.expect_equal(usize(0), diagnostics.count(&store))
}
@hide:file
AllocationState :: struct {
remaining usize
live usize = 0
}
@hide:file
limited_alloc proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
if context |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (state.remaining == 0) return null
state.remaining -= 1
memory :: mem.raw_alloc(mem.c_allocator, size, alignment)
if (memory) |_| state.live += 1
return memory
}
unreachable
}
@hide:file
limited_realloc proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
if context |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (state.remaining == 0) return null
state.remaining -= 1
resized :: mem.raw_realloc(mem.c_allocator, memory, old_size, new_size, alignment)
if resized |_| {
if (memory) |_| {} else state.live += 1
}
return resized
}
unreachable
}
@hide:file
limited_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
if context |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (memory) |_| state.live -= 1
mem.raw_free(mem.c_allocator, memory, size, alignment)
return
}
unreachable
}
@hide:file
limited_vtable :: mem.AllocatorVTable{ alloc = limited_alloc, realloc = limited_realloc, free = limited_free }
recursive_invocations_use_heap_frames_and_free_on_allocation_failure test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
main :: try add_function(&module, mem.empty(ir.Type), 0, .void, 2)
module.entry = main
try emit(&module, main, function_value(0, main))
try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) })
state := AllocationState{ remaining = 2048 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
_ = run(allocator, &module, &store) catch |error| {
match error {
.out_of_memory: {}
else: return .expectation_failed
}
try testing.expect_equal(usize(0), state.live)
try testing.expect_equal(usize(0), diagnostics.count(&store))
return
}
return .expectation_failed
}
frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
types [1]ir.Type :: [.int]
initializer :: try add_function(&module, types[..], 0, .int, 1)
try emit(&module, initializer, integer(0, 42))
return_value(&module, initializer, ir.ValueId(0))
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
main :: try add_function(&module, types[..], 0, .int, 1)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
return_value(&module, main, ir.ValueId(0))
succeeded := false
failures usize := 0
for (usize(0)..usize(16)) |limit| {
state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
outcome :: run(allocator, &module, &store) catch |error| {
match error {
.out_of_memory: {}
else: return .expectation_failed
}
failures += 1
try testing.expect_equal(usize(0), state.live)
continue
}
succeeded = true
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
try testing.expect_equal(usize(0), state.live)
}
try testing.expect(succeeded)
try testing.expect(failures > 0)
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 })
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
state := AllocationState{ remaining = 64 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
trapped :: try run(allocator, &module, &store)
try testing.expect_equal(trap_id, trapped.trap?)
try testing.expect_equal(usize(0), state.live)
}
runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
types [1]ir.Type :: [.int]
main :: try add_function(&module, types[..], 0, .int, 1)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
return_value(&module, main, ir.ValueId(0))
diagnostic_state := AllocationState{ remaining = 0 }
diagnostic_allocator :: mem.Allocator{ context = &diagnostic_state, vtable = &limited_vtable }
store := diagnostics.init(diagnostic_allocator)
defer diagnostics.deinit(&store)
state := AllocationState{ remaining = 64 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
_ = run(allocator, &module, &store) catch |error| {
match error {
.out_of_memory: {}
else: return .expectation_failed
}
try testing.expect_equal(usize(0), state.live)
try testing.expect_equal(usize(0), diagnostic_state.live)
try testing.expect_equal(usize(0), diagnostics.count(&store))
return
}
return .expectation_failed
}
+132
View File
@@ -0,0 +1,132 @@
import "@std/mem"
import "@std/arraylist"
import "@source/diagnostics"
FunctionId :: distinct u32
GlobalId :: distinct u32
LocalId :: distinct u32
ValueId :: distinct u32
BlockId :: distinct u32
no_value :: maxval!(ValueId)
Type :: enum { invalid, int, float, string, void, function }
ValueData :: union {
integer i64
real f64
string []u8
function FunctionId
}
Value :: struct {
type Type = .void
data ValueData = ValueData{ integer = 0 }
}
Op :: enum {
constant
load_local
store_local
load_global
negate
add
subtract
multiply
divide
call
}
Instruction :: struct {
op Op
type Type = .void
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
destination ValueId = no_value
left ValueId = no_value
right ValueId = no_value
slot u32 = 0
constant Value = Value{}
arguments_start usize = 0
arguments_count usize = 0
}
TerminatorKind :: enum { return_value, trap }
Terminator :: struct {
kind TerminatorKind = .return_value
value ValueId = no_value
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
}
Block :: struct {
instructions arraylist.ArrayList(Instruction)
arguments arraylist.ArrayList(ValueId)
terminator Terminator = Terminator{}
}
Function :: struct {
parameters arraylist.ArrayList(Type)
locals arraylist.ArrayList(Type)
result Type = .void
blocks arraylist.ArrayList(Block)
entry BlockId = BlockId(0)
value_count usize = 0
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
}
Global :: struct {
type Type
initializer FunctionId
span diagnostics.Span
}
#! owns executable storage, including decoded strings; no AST or binding table is retained.
#! spans and diagnostic IDs refer to the caller's source and diagnostic store.
Module :: struct {
allocator mem.Allocator
functions arraylist.ArrayList(Function)
globals arraylist.ArrayList(Global)
strings arraylist.ArrayList([]mut u8)
entry ?FunctionId = null
entry_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
}
init proc(allocator mem.Allocator) Module {
return Module{
allocator = allocator,
functions = arraylist.init(allocator),
globals = arraylist.init(allocator),
strings = arraylist.init(allocator),
}
}
init_function proc(allocator mem.Allocator) Function {
return Function{
parameters = arraylist.init(allocator),
locals = arraylist.init(allocator),
blocks = arraylist.init(allocator),
}
}
init_block proc(allocator mem.Allocator) Block {
return Block{
instructions = arraylist.init(allocator),
arguments = arraylist.init(allocator),
}
}
#! releases all executable storage using the retained allocator.
deinit proc(module @mut Module) void {
for (0..(module.functions.items.len)) |i| {
function :: &module.functions.items[i]
for (0..(function.blocks.items.len)) |j| {
block :: &function.blocks.items[j]
arraylist.deinit(&block.instructions)
arraylist.deinit(&block.arguments)
}
arraylist.deinit(&function.parameters)
arraylist.deinit(&function.locals)
arraylist.deinit(&function.blocks)
}
for (module.strings.items) |string| mem.free(module.allocator, string)
arraylist.deinit(&module.functions)
arraylist.deinit(&module.globals)
arraylist.deinit(&module.strings)
}
+468
View File
@@ -0,0 +1,468 @@
import "@std/mem"
import "@std/arraylist"
import "@ffi/c"
import "@source/ast"
import "@source/diagnostics"
import "@source/ir"
sema :: import "@source/sema"
typecheck :: import "@source/sema/check"
@hide:file
StorageKind :: enum { none, function, global, local }
@hide:file
Storage :: struct {
kind StorageKind = .none
index u32 = 0
}
@hide:file
State :: struct {
tree @ast.Tree
resolution @sema.Resolution
checked @typecheck.Result
diagnostics @mut diagnostics.Store
module @mut ir.Module
storage []mut Storage
function usize = 0
terminated bool = false
}
#! lowers checked source into caller-owned executable storage; release with ir.deinit.
#! no frontend storage or source bytes are retained by the returned module.
lower proc(
allocator mem.Allocator,
tree @ast.Tree,
resolution @sema.Resolution,
checked @typecheck.Result,
diagnostic_store @mut diagnostics.Store,
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
module := ir.init(allocator)
errdefer ir.deinit(&module)
storage :: try mem.alloc(Storage, allocator, ast.node_count(tree))
defer mem.free(allocator, storage)
mem.set(storage, Storage{})
state := State{
tree = tree,
resolution = resolution,
checked = checked,
diagnostics = diagnostic_store,
module = &module,
storage = storage,
}
# allocate every package identity before lowering any initializer or body.
for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) {
.function: {
function :: try add_function(&module, ast.node_span(tree, id))
storage[usize(id)] = Storage{ kind = .function, index = u32(function) }
}
.statement: {
initializer :: try add_function(&module, ast.node_span(tree, id))
global :: u32(module.globals.items.len)
try arraylist.append(&module.globals, ir.Global{
type = lower_type(typecheck.type_of(checked, id)),
initializer = initializer,
span = ast.node_span(tree, id),
})
storage[usize(id)] = Storage{ kind = .global, index = global }
}
.invalid: {}
}
}
try find_entry(&state)
for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) {
.function: try lower_function(&state, id)
.statement: try lower_global(&state, id)
.invalid: {}
}
}
return module
}
@hide:file
lower_type proc(type typecheck.Type) ir.Type {
return match type.kind {
.int: .int
.float: .float
.string: .string
.void: .void
.function: .function
.none, .invalid: .invalid
}
}
@hide:file
add_function proc(module @mut ir.Module, span diagnostics.Span) ir.FunctionId ! mem.AllocError {
id :: ir.FunctionId(module.functions.items.len)
try arraylist.append(&module.functions, ir.init_function(module.allocator))
function := &module.functions.items[usize(id)]
function.span = span
try arraylist.append(&function.blocks, ir.init_block(module.allocator))
return id
}
@hide:file
find_entry proc(state @mut State) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
name := diagnostics.Span{ start = 0, end = 0 }
match ast.decl_kind(tree, id) {
.function: name = ast.function_decl(tree, id).name
.statement: name = ast.statement_decl(tree, id).name
.invalid: continue
}
if (!mem.eql(ast.source_text(tree, name), "main")) continue
if (ast.decl_kind(tree, id) != .function) {
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name)
return
}
declaration :: ast.function_decl(tree, id)
result :: lower_type(typecheck.type_of(state.checked, declaration.return_type))
if (ast.function_param_count(tree, id) != 0 or result == .function) {
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name)
return
}
state.module.entry = ir.FunctionId(state.storage[usize(id)].index)
return
}
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .missing_entry, ast.node_span(tree, tree.root))
}
@hide:file
trap proc(state @mut State, diagnostic diagnostics.DiagnosticId, span diagnostics.Span) void {
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
kind = .trap,
diagnostic = diagnostic,
span = span,
}
state.terminated = true
}
@hide:file
trap_type proc(state @mut State, id ast.NodeId) bool {
type :: typecheck.type_of(state.checked, id)
if (type.kind != .invalid) return false
trap(state, type.diagnostic, ast.node_span(state.tree, id))
return true
}
@hide:file
trap_binding proc(state @mut State, id ast.NodeId) bool {
binding :: sema.binding(state.resolution, id)
if (binding.kind != .invalid) return false
trap(state, binding.data.diagnostic, ast.node_span(state.tree, id))
return true
}
@hide:file
finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void {
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
value = value,
span = span,
}
state.terminated = true
}
@hide:file
emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! mem.AllocError {
function := &state.module.functions.items[state.function]
operation := instruction
if (operation.op != .store_local) {
operation.destination = ir.ValueId(function.value_count)
function.value_count += 1
}
try arraylist.append(&function.blocks.items[0].instructions, operation)
return operation.destination
}
@hide:file
constant proc(state @mut State, value ir.Value, span diagnostics.Span) ir.ValueId ! mem.AllocError {
return try emit(state, ir.Instruction{ op = .constant, type = value.type, constant = value, span = span })
}
@hide:file
lower_global proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
global :: state.module.globals.items[usize(state.storage[usize(id)].index)]
state.function = usize(global.initializer)
state.terminated = false
state.module.functions.items[state.function].result = global.type
if (trap_binding(state, id)) return
declaration :: ast.statement_decl(state.tree, id)
if (declaration.type) |annotation| if (trap_type(state, annotation)) return
value :: try lower_expression(state, declaration.initializer)
if (state.terminated or trap_type(state, id)) return
finish(state, value, declaration.span)
}
@hide:file
lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
declaration :: ast.function_decl(tree, id)
state.function = usize(state.storage[usize(id)].index)
state.terminated = false
function := &state.module.functions.items[state.function]
function.result = lower_type(typecheck.type_of(state.checked, declaration.return_type))
for (0..ast.function_param_count(tree, id)) |i| {
parameter :: ast.function_param(tree, id, i)
type :: lower_type(typecheck.type_of(state.checked, parameter))
state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
try arraylist.append(&function.parameters, type)
try arraylist.append(&function.locals, type)
}
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
statement :: ast.block_statement(tree, declaration.body, i)
if (ast.stmt_kind(tree, statement) != .declaration) continue
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
try arraylist.append(&function.locals, lower_type(typecheck.type_of(state.checked, statement)))
}
# signature errors are entry traps; body errors are reached in statement order.
if (trap_binding(state, id) or trap_type(state, declaration.return_type)) return
for (0..ast.function_param_count(tree, id)) |i| {
if (trap_type(state, ast.function_param(tree, id, i))) return
}
if (trap_type(state, id)) return
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
if (state.terminated) return
try lower_statement(state, ast.block_statement(tree, declaration.body, i))
}
if (state.terminated or trap_type(state, declaration.body)) return
finish(state, ir.no_value, ast.node_span(tree, declaration.body))
}
@hide:file
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
match ast.stmt_kind(tree, id) {
.invalid: {
invalid :: ast.invalid(tree, id)
trap(state, invalid.diagnostic, invalid.span)
}
.expression: _ = try lower_expression(state, ast.statement_expr(tree, id))
.declaration: {
if (trap_binding(state, id)) return
declaration :: ast.statement_decl(tree, id)
if (declaration.type) |annotation| if (trap_type(state, annotation)) return
value :: try lower_expression(state, declaration.initializer)
if (state.terminated or trap_type(state, id)) return
_ = try emit(state, ir.Instruction{
op = .store_local,
type = lower_type(typecheck.type_of(state.checked, id)),
left = value,
slot = state.storage[usize(id)].index,
span = declaration.span,
})
}
.return_stmt: {
statement :: ast.return_stmt(tree, id)
value := ir.no_value
if (statement.value) |expression| value = try lower_expression(state, expression)
if (state.terminated or trap_type(state, id)) return
finish(state, value, statement.span)
}
}
}
@hide:file
lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
span :: ast.node_span(tree, id)
type :: lower_type(typecheck.type_of(state.checked, id))
match ast.expr_kind(tree, id) {
.invalid: {
invalid :: ast.invalid(tree, id)
trap(state, invalid.diagnostic, invalid.span)
}
.literal_int, .literal_float: {
if (trap_type(state, id)) return ir.no_value
return try lower_number(state, id)
}
.literal_string: {
if (trap_type(state, id)) return ir.no_value
return try lower_string(state, id)
}
.identifier: {
if (trap_binding(state, id) or trap_type(state, id)) return ir.no_value
binding :: sema.binding(state.resolution, id)
location :: state.storage[usize(binding.data.declaration)]
match location.kind {
.function: return try constant(state, ir.Value{
type = .function,
data = ir.ValueData{ function = ir.FunctionId(location.index) },
}, span)
.global: return try emit(state, ir.Instruction{ op = .load_global, type = type, slot = location.index, span = span })
.local: return try emit(state, ir.Instruction{ op = .load_local, type = type, slot = location.index, span = span })
.none: unreachable
}
}
.unary: {
expression :: ast.unary_expr(tree, id)
if (type == .int and ast.expr_kind(tree, expression.operand) == .literal_int) {
text :: ast.source_text(tree, ast.node_span(tree, expression.operand))
if (integer_magnitude(text, u64(maxval!(i64)) + 1)) |magnitude| {
if (magnitude == u64(maxval!(i64)) + 1) {
return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = minval!(i64) } }, span)
}
}
}
operand :: try lower_expression(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{ op = .negate, type = type, left = operand, span = span })
}
.binary: {
expression :: ast.binary_expr(tree, id)
left :: try lower_expression(state, expression.left)
if (state.terminated) return ir.no_value
right :: try lower_expression(state, expression.right)
if (state.terminated or trap_type(state, id)) return ir.no_value
op ir.Op :: match expression.operator {
.add: .add
.subtract: .subtract
.multiply: .multiply
.divide: .divide
}
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
}
.call: {
callee :: try lower_expression(state, ast.call_expr(tree, id).callee)
if (state.terminated) return ir.no_value
# reserve this call's packed slice before nested calls append theirs.
block := &state.module.functions.items[state.function].blocks.items[0]
start :: block.arguments.items.len
count :: ast.call_arg_count(tree, id)
for (0..count) |_| try arraylist.append(&block.arguments, ir.no_value)
for (0..count) |i| {
argument :: try lower_expression(state, ast.call_arg(tree, id, i))
if (state.terminated) return ir.no_value
block.arguments.items[start + i] = argument
}
if (trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{
op = .call,
type = type,
left = callee,
arguments_start = start,
arguments_count = count,
span = span,
})
}
}
return ir.no_value
}
@hide:file
integer_magnitude proc(text []u8, limit u64) ?u64 {
value := u64(0)
for (text) |byte| {
if (byte < '0' or byte > '9') return null
digit :: u64(byte - '0')
if (value > divtrunc!(limit - digit, u64(10))) return null
value = value * 10 + digit
}
return value
}
@hide:file
lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id)
text :: ast.source_text(state.tree, span)
type :: lower_type(typecheck.type_of(state.checked, id))
if (type == .int) {
if (integer_magnitude(text, u64(maxval!(i64)))) |magnitude| {
return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = i64(magnitude) } }, span)
}
} else {
buffer :: try mem.alloc(u8, state.module.allocator, text.len + 1)
defer mem.free(state.module.allocator, buffer)
for (text) |byte, i| buffer[i] = byte
buffer[text.len] = 0
c.__error()?^ = 0
value :: f64(c.strtod(ptrcast!(buffer.ptr), null))
# libc reports ERANGE for representable subnormals as well as zero/infinity.
if (c.__error()?^ != c.ERANGE or (value > 0 and value < 1)) {
return try constant(state, ir.Value{ type = .float, data = ir.ValueData{ real = value } }, span)
}
}
trap(state, try diagnostics.add(state.diagnostics, .literal_out_of_range, span), span)
return ir.no_value
}
@hide:file
hex_digit proc(byte u8) ?u8 {
if (byte >= '0' and byte <= '9') return byte - '0'
if (byte >= 'a' and byte <= 'f') return byte - 'a' + 10
if (byte >= 'A' and byte <= 'F') return byte - 'A' + 10
return null
}
@hide:file
lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id)
text :: ast.source_text(state.tree, span)
owned :: try mem.alloc(u8, state.module.allocator, text.len - 2)
transferred := false
defer if (!transferred) mem.free(state.module.allocator, owned)
length := usize(0)
cursor := usize(1)
end :: text.len - 1
valid := true
while (cursor < end) {
byte := text[cursor]
cursor += 1
if (byte == '\\') {
if (cursor >= end) {
valid = false
break
}
escape :: text[cursor]
cursor += 1
match escape {
'\\': byte = '\\'
'"': byte = '"'
'n': byte = '\n'
'r': byte = '\r'
't': byte = '\t'
'0': byte = 0
'x': {
if (end - cursor < 2) {
valid = false
break
}
high := u8(0)
low := u8(0)
if (hex_digit(text[cursor])) |digit| high = digit
else {
valid = false
break
}
if (hex_digit(text[cursor + 1])) |digit| low = digit
else {
valid = false
break
}
byte = high * 16 + low
cursor += 2
}
else: {
valid = false
break
}
}
}
owned[length] = byte
length += 1
}
if (!valid) {
trap(state, try diagnostics.add(state.diagnostics, .invalid_escape, span), span)
return ir.no_value
}
# retain the complete allocation for freeing, but expose only decoded bytes.
try arraylist.append(&state.module.strings, owned)
transferred = true
return try constant(state, ir.Value{ type = .string, data = ir.ValueData{ string = owned[..length] } }, span)
}
+452
View File
@@ -0,0 +1,452 @@
import "@std/mem"
import "@std/arraylist"
import "@std/testing"
import "@source/diagnostics"
import "@source/strpool"
import "@source/lexer"
import "@source/parser"
import "@source/ast"
import "@source/ir"
import "@source/interpreter"
sema :: import "@source/sema"
typecheck :: import "@source/sema/check"
@hide:file
CompileError :: alias mem.AllocError | diagnostics.CapacityError | strpool.InternError | lexer.SourceError
# every consumer test executes only after all source and frontend storage is gone.
@hide:file
compile_owned proc(text []u8, diagnostic_store @mut diagnostics.Store) ir.Module ! CompileError {
source :: try mem.alloc(u8, mem.c_allocator, text.len)
defer mem.free(mem.c_allocator, source)
for (text) |byte, i| source[i] = byte
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, diagnostic_store)
defer sema.deinit(&resolution)
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, diagnostic_store)
defer typecheck.deinit(&checked)
return try lower(mem.c_allocator, &tree, &resolution, &checked, diagnostic_store)
}
@hide:file
expect_trap proc(
outcome interpreter.Outcome,
diagnostic_store @diagnostics.Store,
code diagnostics.Code,
) void ! testing.Error {
if (outcome.trap) |id| {
if (diagnostics.get(diagnostic_store, id)) |diagnostic| {
try testing.expect_equal(code, diagnostic.code)
return
}
}
return .expectation_failed
}
executes_calls_locals_arithmetic_and_nested_argument_slices test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`add proc(left int, right int) int {
` result :: left + right
` return result
`}
`double proc(value int) int {
` return value * 2
`}
`main proc() int {
` a :: add(double(4), add(5, 6))
` b :: add(10, 13)
` return a + b
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.int, outcome.value.type)
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
owns_decoded_strings_after_source_and_frontend_are_freed test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`text :: "a\\\"\n\r\t\0\x41\x7a"
`identity proc(value string) string {
` return value
`}
`main proc() string {
` return identity(text)
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.string, outcome.value.type)
try testing.expect(mem.eql(outcome.value.data.string, "a\\\"\n\r\t\x00Az"))
}
honors_float_context_and_float_division test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() float {
` left :: 3
` return -(left / 2) + 2.0
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.float, outcome.value.type)
try testing.expect_equal(f64(0.5), outcome.value.data.real)
}
executes_forward_lazy_globals_and_function_aliases test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`forward :: later + 1
`alias :: add
`later :: 18
`add proc(left int, right int) int {
` local_alias :: alias
` return left + right
`}
`main proc() int {
` invoke :: alias
` return invoke(forward, 23)
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
again :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(again.trap == null)
try testing.expect_equal(i64(42), again.value.data.integer)
}
supports_bare_void_value_returns_and_fallthrough test {
sources [3][]u8 :: [
"main proc() void {\n return\n}\n",
"noop proc() void {}\nmain proc() void {\n return noop()\n}\n",
"noop proc() void {}\nmain proc() void {\n noop()\n}\n",
]
for (sources) |source| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.void, outcome.value.type)
}
}
preserves_demanded_diagnostic_identity_and_source_location test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`bad proc() int {
` return unknown
`}
`main proc() int {
` before :: 1 + 2
` return bad()
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(diagnostics.Code.unknown_name, diagnostic.code)
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "unknown"))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
skips_unreachable_and_unused_invalid_code test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`bad proc() int {
` return unknown
`}
`unused :: "\q"
`main proc() int {
` return 42
` 9223372036854775807 + 1
` missing
` huge :: 9223372036854775808
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect(diagnostics.count(&diagnostic_store) > 0)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
traps_local_forward_loads_and_global_initialization_cycles test {
sources [3][]u8 :: [
"main proc() int {\n before :: after\n after :: 42\n return before\n}\n",
"left int :: right\nright int :: left\nmain proc() int {\n return left\n}\n",
"self int :: self\nmain proc() int {\n return self\n}\n",
]
for (sources) |source, i| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
code diagnostics.Code :: if (i == 0) .read_before_initialization else .initialization_cycle
try expect_trap(outcome, &diagnostic_store, code)
}
}
handles_integer_boundaries_without_host_overflow test {
sources [3][]u8 :: [
"main proc() int {\n return 9223372036854775807\n}\n",
"main proc() int {\n return -9223372036854775808\n}\n",
"main proc() int {\n return -0009223372036854775808\n}\n",
]
expected [3]i64 :: [maxval!(i64), minval!(i64), minval!(i64)]
for (sources) |source, i| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(expected[i], outcome.value.data.integer)
}
}
traps_invalid_literals_escapes_and_checked_integer_arithmetic test {
sources [8][]u8 :: [
"main proc() int {\n return 9223372036854775808\n}\n",
"main proc() int {\n return -9223372036854775809\n}\n",
"main proc() int {\n return 9223372036854775807 + 1\n}\n",
"main proc() int {\n return -(-9223372036854775808)\n}\n",
"main proc() int {\n return -9223372036854775808 - 1\n}\n",
"main proc() int {\n return 9223372036854775807 * 2\n}\n",
"main proc() string {\n return \"\\q\"\n}\n",
"main proc() string {\n return \"\\x0g\"\n}\n",
]
for (sources) |source, i| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
code := diagnostics.Code.integer_overflow
if (i < 2) code = .literal_out_of_range
if (i >= 6) code = .invalid_escape
try expect_trap(outcome, &diagnostic_store, code)
}
}
validates_main_and_preserves_first_declaration test {
sources [4][]u8 :: [
"other proc() void {}\n",
"main :: 42\n",
"main proc(value int) int {\n return value\n}\n",
"main proc() int {\n return 42\n}\nmain proc() int {\n return 7\n}\n",
]
for (sources) |source, i| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
if (i == 3) {
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
} else {
code diagnostics.Code :: if (i == 0) .missing_entry else .invalid_entry
try expect_trap(outcome, &diagnostic_store, code)
try testing.expect_equal(module.entry_diagnostic, outcome.trap?)
}
}
}
invalid_signatures_trap_without_poisoning_unrelated_main test {
sources [2][]u8 :: [
"broken proc(value void) void {}\nmain proc() int {\n return 42\n}\n",
"main proc() Unknown {\n return 42\n}\n",
]
for (sources) |source, i| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect(diagnostics.count(&diagnostic_store) > 0)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
if (i == 0) {
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
} else {
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
try expect_trap(outcome, &diagnostic_store, .unknown_name)
}
}
}
parses_float_context_without_integer_range_limits_and_rejects_float_overflow test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(
"main proc() float {\n return 18446744073709551616\n}\n",
&diagnostic_store,
)
defer ir.deinit(&module)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.float, outcome.value.type)
try testing.expect_equal(f64(18446744073709551616.0), outcome.value.data.real)
source := arraylist.init(u8, mem.c_allocator)
defer arraylist.deinit(&source)
for ("main proc() float {\n return ") |byte| try arraylist.append(&source, byte)
for (usize(0)..usize(400)) |_| try arraylist.append(&source, '9')
for (".0\n}\n") |byte| try arraylist.append(&source, byte)
oversized := try compile_owned(source.items, &diagnostic_store)
defer ir.deinit(&oversized)
trapped :: try interpreter.run(mem.c_allocator, &oversized, &diagnostic_store)
try expect_trap(trapped, &diagnostic_store, .literal_out_of_range)
}
accepts_representable_subnormals_and_rejects_underflow_to_zero test {
zeros [2]usize :: [307, 400]
for (zeros) |count| {
source := arraylist.init(u8, mem.c_allocator)
defer arraylist.deinit(&source)
for ("main proc() float {\n return 0.") |byte| try arraylist.append(&source, byte)
for (usize(0)..count) |_| try arraylist.append(&source, '0')
for ("1\n}\n") |byte| try arraylist.append(&source, byte)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source.items, &diagnostic_store)
defer ir.deinit(&module)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
if (count == 307) {
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.float, outcome.value.type)
try testing.expect(outcome.value.data.real > 0 and outcome.value.data.real < 1)
} else {
try expect_trap(outcome, &diagnostic_store, .literal_out_of_range)
}
}
}
@hide:file
AllocationState :: struct {
remaining usize
live usize = 0
}
@hide:file
limited_alloc proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
if (context) |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (state.remaining == 0) return null
state.remaining -= 1
memory :: mem.raw_alloc(mem.c_allocator, size, alignment)
if (memory) |_| state.live += 1
return memory
}
unreachable
}
@hide:file
limited_realloc proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
if (context) |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (state.remaining == 0) return null
state.remaining -= 1
resized :: mem.raw_realloc(mem.c_allocator, memory, old_size, new_size, alignment)
if (resized) |_| {
if (memory) |_| {} else state.live += 1
}
return resized
}
unreachable
}
@hide:file
limited_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
if (context) |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (memory) |_| state.live -= 1
mem.raw_free(mem.c_allocator, memory, size, alignment)
return
}
unreachable
}
@hide:file
limited_vtable :: mem.AllocatorVTable{ alloc = limited_alloc, realloc = limited_realloc, free = limited_free }
releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`text :: "owned\nstring"
`factor float :: 3.5
`identity proc(value string) string {
` local :: value
` return local
`}
`main proc() string {
` computed float :: factor * 2
` return identity(text)
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
defer sema.deinit(&resolution)
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
defer typecheck.deinit(&checked)
succeeded := false
failures := usize(0)
for (usize(0)..usize(128)) |limit| {
state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
module := lower(allocator, &tree, &resolution, &checked, &diagnostic_store) catch |error| {
try testing.expect(error == .out_of_memory)
failures += 1
try testing.expect_equal(usize(0), state.live)
continue
}
outcome :: interpreter.run(mem.c_allocator, &module, &diagnostic_store) catch |error| {
ir.deinit(&module)
return error
}
ir.deinit(&module)
try testing.expect(outcome.trap == null)
try testing.expect_equal(usize(0), state.live)
succeeded = true
break
}
try testing.expect(succeeded)
try testing.expect(failures > 0)
}
+195 -51
View File
@@ -1,5 +1,8 @@
import "@std/debug"
import "@ffi/c"
import "@std/mem"
import "@std/arraylist"
import "@std/io"
import "@std/process"
import "@source/diagnostics"
import "@source/strpool"
@@ -7,8 +10,9 @@ import "@source/lexer"
import "@source/ast"
import "@source/parser"
import "@source/sema"
ast_renderer :: import "@source/ast/renderer"
import "@source/ir"
import "@source/lower"
import "@source/interpreter"
typecheck :: import "@source/sema/check"
test import "@std/mem"
@@ -21,58 +25,198 @@ test import "@source/diagnostics"
test import "@source/strpool"
test import "@source/lexer"
test import "@source/ast"
test import "@source/ast/renderer"
test import "@source/parser"
test import "@source/sema"
test import "@source/sema/check"
test import "@source/lower"
test import "@source/interpreter"
@hide:file
program ::
`# literals
`x float :: 123.9
`y int := 43
`z :: "hello"
`
`a := 1 + 2 * 3
`b :: -3 / 2 + 1
`
`grouped proc(left, right int, extra float) void {
` value int :: later + left
` later int :: right
` empty()
` grouped(value, later, extra)
`}
`empty proc() void {}
DriverError :: alias (
mem.AllocError
| diagnostics.CapacityError
| lexer.SourceError
| strpool.InternError
| io.OpenError
| io.CloseError
| io.ReadError
| io.WriteError
)
main proc() void! {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, program, &diagnostic_store)
tree := try parser.parse(
mem.c_allocator,
scan_state.tokens.items,
program,
&diagnostic_store,
)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
defer sema.deinit(&resolution)
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
defer typecheck.deinit(&checked)
renderer := ast_renderer.init(mem.c_allocator)
defer ast_renderer.deinit(&renderer)
try ast_renderer.render(&renderer, &tree)
debug.print("{s}", {renderer.bytes.items})
debug.print("diagnostics: {}\n", {diagnostics.count(&diagnostic_store)})
main proc(init process.Init) i32 {
return command(mem.c_allocator, init.io) catch |err| {
if (process.argument(2)) |path| {
io.print("{s}: infrastructure error: {s}\n", io.stderr(init.io), {
argument_text(path), infrastructure_message(err),
}) catch |_| {}
} else {
io.print("honey: infrastructure error: {s}\n", io.stderr(init.io), {
infrastructure_message(err),
}) catch |_| {}
}
return 2
}
}
@hide:file
infrastructure_message proc(error DriverError) []u8 {
return match error {
.out_of_memory: "out of memory"
.diagnostic_limit_reached: "diagnostic limit reached"
.source_too_large: "source file is too large"
.out_of_space: "identifier storage is full"
.open_failed: "cannot open source file"
.close_failed: "cannot close source file"
.not_open_for_reading: "source file is not readable"
.read_failed: "cannot read source file"
.not_open_for_writing: "output is not writable"
.write_failed: "cannot write output"
.no_progress: "output writer made no progress"
}
}
@hide:file
command proc(allocator mem.Allocator, runtime io.Io) i32 ! DriverError {
if (process.argument_count() != 3) {
try io.print("usage: honey run <file>\n", io.stderr(runtime), {})
return 2
}
if (process.argument(1)) |operation| {
if (!mem.eql(argument_text(operation), "run")) {
try io.print("usage: honey run <file>\n", io.stderr(runtime), {})
return 2
}
} else {
return 2
}
if (process.argument(2)) |path| return try run_file(allocator, runtime, path)
return 2
}
@hide:file
argument_text proc(pointer [*;0]u8) []u8 {
length := usize(0)
while (pointer[length] != 0) length += 1
return pointer[..length]
}
@hide:file
read_source proc(allocator mem.Allocator, runtime io.Io, path [*;0]u8) arraylist.ArrayList(u8) ! DriverError {
# bootstrap cannot construct a dynamic sentinel slice from argv's sentinel pointer.
descriptor := c_int(-1)
while true {
descriptor = c.open(ptrcast!(path), c.O_RDONLY)
if (descriptor >= 0) break
if (c.__error()?^ != c.EINTR) return .open_failed
}
file :: io.File{ io = runtime, handle = io.Handle{ file_desc = descriptor } }
closing := false
defer {
if (!closing) io.close(file) catch |_| {}
}
bytes := arraylist.init(u8, allocator)
errdefer arraylist.deinit(&bytes)
input :: io.reader(file)
maximum :: usize(maxval!(u32))
while true {
length :: bytes.items.len
if (length == maximum) {
probe [1]mut u8 := undefined
if (try io.read(input, (&probe)[..]) != 0) return .source_too_large
break
}
if (length == bytes.capacity) {
growth := usize(4096)
if (growth > maximum - length) growth = maximum - length
try arraylist.reserve(&bytes, length + growth)
}
storage :: bytes.items.ptr[..bytes.capacity]
end := bytes.capacity
if (end > maximum) end = maximum
count :: try io.read(input, storage[length..end])
if (count == 0) break
bytes.items = storage[..length + count]
}
closing = true
try io.close(file)
return bytes
}
@hide:file
compile proc(allocator mem.Allocator, source []u8, diagnostic_store @mut diagnostics.Store) ir.Module ! DriverError {
strpool.strings = strpool.init(allocator)
defer strpool.deinit(&strpool.strings)
scan_state := lexer.init(allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, diagnostic_store)
tree := try parser.parse(allocator, scan_state.tokens.items, source, diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(allocator, &tree, diagnostic_store)
defer sema.deinit(&resolution)
checked := try typecheck.check(allocator, &tree, &resolution, diagnostic_store)
defer typecheck.deinit(&checked)
return try lower.lower(allocator, &tree, &resolution, &checked, diagnostic_store)
}
@hide:file
run_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)
outcome :: try interpreter.run(allocator, &module, &diagnostic_store)
if (outcome.trap == null) try print_result(io.stdout(runtime), outcome.value)
try report_diagnostics(allocator, io.stderr(runtime), argument_text(path), source.items, &diagnostic_store, outcome.trap)
if (outcome.trap != null or diagnostics.count(&diagnostic_store) != 0) return 1
return 0
}
@hide:file
print_result proc(output io.Writer, value ir.Value) void ! io.WriteError {
match value.type {
.int: try io.print("{}\n", output, {value.data.integer})
.float: try io.print("{d}\n", output, {value.data.real})
.string: try io.print("{s}\n", output, {value.data.string})
.void: {}
else: unreachable
}
}
@hide:file
report_diagnostics proc(
allocator mem.Allocator,
output io.Writer,
path []u8,
source []u8,
diagnostic_store @diagnostics.Store,
trap ?diagnostics.DiagnosticId,
) void ! (mem.AllocError | io.WriteError) {
if (diagnostics.count(diagnostic_store) == 0) return
starts := arraylist.init(usize, allocator)
defer arraylist.deinit(&starts)
try arraylist.append(&starts, usize(0))
for (source) |byte, index| {
if (byte == '\n') try arraylist.append(&starts, index + 1)
}
for (0..diagnostics.count(diagnostic_store)) |index| {
if (diagnostics.get(diagnostic_store, diagnostics.DiagnosticId(index))) |diagnostic| {
offset :: usize(diagnostic.span.start)
line := usize(0)
end := starts.items.len
while (line + 1 < end) {
middle :: line + divtrunc!(end - line, 2)
if (starts.items[middle] <= offset) line = middle
else end = middle
}
label []u8 := "error"
if (trap) |id| {
if (id == diagnostics.DiagnosticId(index)) label = "runtime trap"
}
try io.print("{s}:{}:{}: {s}: {s}\n", output, {
path, line + 1, offset - starts.items[line] + 1, label, diagnostics.message(diagnostic.code),
})
}
}
}
+20
View File
@@ -179,6 +179,7 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
return try parse_statement_decl(state)
}
match current(state).kind {
.return: return try parse_return(state)
.ident, .int, .float, .string, .minus, .open_paren: {
expression :: try parse_expr(state)
return try ast.add_expression_stmt(&state.builder, expression)
@@ -187,6 +188,25 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
}
}
@hide:file
parse_return proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .return)
if (next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)) {
return try ast.add_return_stmt(&state.builder, keyword.span, null)
}
value :: try parse_expr(state)
if (!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)) {
return .unexpected_token
}
# include consumed grouping punctuation, which has no separate expression node.
end :: state.tokens[state.next_token - 1].span.end
return try ast.add_return_stmt(
&state.builder,
ast.Span{ start = keyword.span.start, end = end },
value,
)
}
@hide:file
parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
name :: try consume(state, .ident)
+45
View File
@@ -285,3 +285,48 @@ recovers_malformed_calls_and_preserves_siblings test {
try testing.expect_equal("tail", ast.source_text(&tree, tail.name))
try testing.expect_equal(0, ast.call_arg_count(&tree, tail.initializer))
}
parses_return_boundaries_and_recovers_trailing_tokens test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`return 1
`main proc() void {
` return
` 2
` return (3 + 4)
` return 5 stray
` after :: 6
` return }
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(2, ast.package_decl_count(&tree))
try testing.expect_equal(ast.DeclKind.invalid, ast.decl_kind(&tree, ast.package_decl(&tree, 0)))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body
try testing.expect_equal(6, ast.block_statement_count(&tree, body))
first :: ast.block_statement(&tree, body, 0)
try testing.expect_equal(ast.StmtKind.return_stmt, ast.stmt_kind(&tree, first))
try testing.expect(ast.return_stmt(&tree, first).value == null)
try testing.expect_equal("return", ast.source_text(&tree, ast.return_stmt(&tree, first).span))
try testing.expect_equal(ast.StmtKind.expression, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 1)))
grouped :: ast.return_stmt(&tree, ast.block_statement(&tree, body, 2))
try testing.expect_equal("return (3 + 4)", ast.source_text(&tree, grouped.span))
if (grouped.value) |value| {
try testing.expect_equal(ast.ExprKind.binary, ast.expr_kind(&tree, value))
} else {
return .expectation_failed
}
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 3)))
after :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 4))
try testing.expect_equal("after", ast.source_text(&tree, after.name))
last :: ast.return_stmt(&tree, ast.block_statement(&tree, body, 5))
try testing.expect(last.value == null)
try testing.expect_equal("return", ast.source_text(&tree, last.span))
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
}
+40 -1
View File
@@ -27,7 +27,7 @@ Result :: struct { storage Storage }
no_index :: maxval!(usize)
@hide:file
Role :: enum { none, expression, declaration, parameter, function, annotation, invalid, body }
Role :: enum { none, expression, declaration, parameter, function, annotation, invalid, body, return_stmt }
@hide:file
Cell :: struct {
@@ -40,6 +40,7 @@ Cell :: struct {
demand TypeKind = .none
fixed bool = false
first_edge usize = no_index
return_annotation ast.NodeId = ast.NodeId(maxval!(u32))
}
@hide:file
@@ -194,6 +195,11 @@ check proc(
try demand(&state, declaration.initializer, types[usize(annotation)], .type_mismatch)
}
}
if (cells[i].role == .return_stmt) {
if (ast.return_stmt(tree, id).value) |value| {
try demand(&state, value, types[usize(cells[i].return_annotation)], .return_type_mismatch)
}
}
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue
signature :: group_type(&state, ast.call_expr(tree, id).callee)
if (signature.function == ast.NodeId(maxval!(u32))) continue
@@ -427,6 +433,15 @@ collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError |
match ast.stmt_kind(tree, statement) {
.declaration: try collect_declaration(state, statement)
.expression: try collect_expression(state, ast.statement_expr(tree, statement))
.return_stmt: {
state.cells[usize(statement)].role = .return_stmt
state.cells[usize(statement)].type = Type{ kind = .void }
state.cells[usize(statement)].return_annotation = function.return_type
if (ast.return_stmt(tree, statement).value) |value| {
try collect_expression(state, value)
try depend(state, value, statement)
}
}
.invalid: collect_invalid(state, statement)
}
source :: if (ast.stmt_kind(tree, statement) == .expression) ast.statement_expr(tree, statement) else statement
@@ -557,6 +572,9 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
function :: ast.function_decl(tree, id)
result :: state.types[usize(function.return_type)]
if (result.kind == .void or result.kind == .invalid or state.types[usize(id)].kind == .invalid) return
for (0..ast.block_statement_count(tree, function.body)) |i| {
if (ast.stmt_kind(tree, ast.block_statement(tree, function.body, i)) == .return_stmt) return
}
if (syntactically_valid_body(tree, function.body)) {
diagnostic :: try diagnostics.add(state.diagnostics, .missing_return, ast.node_span(tree, function.body))
if (state.types[usize(function.body)].kind != .invalid) {
@@ -564,6 +582,22 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
}
}
}
.return_stmt: {
statement :: ast.return_stmt(tree, id)
expected :: state.types[usize(state.cells[usize(id)].return_annotation)]
if (expected.kind == .invalid) return
if (statement.value) |value| {
actual :: state.types[usize(value)]
if (actual.kind == .invalid) {
state.types[usize(id)] = actual
} else if (actual.kind != expected.kind) {
try fail(state, value, .return_type_mismatch, ast.node_span(tree, value))
state.types[usize(id)] = state.types[usize(value)]
}
} else if (expected.kind != .void) {
try fail(state, id, .return_type_mismatch, statement.span)
}
}
.expression: {
match ast.expr_kind(tree, id) {
.unary: {
@@ -644,6 +678,11 @@ syntactically_valid_body proc(tree @ast.Tree, id ast.NodeId) bool {
.expression: {
if (!syntactically_valid_expression(tree, ast.statement_expr(tree, statement))) return false
}
.return_stmt: {
if (ast.return_stmt(tree, statement).value) |value| {
if (!syntactically_valid_expression(tree, value)) return false
}
}
.declaration: {
if (!syntactically_valid_expression(tree, ast.statement_decl(tree, statement).initializer)) return false
}
+125
View File
@@ -606,3 +606,128 @@ preserves_existing_syntax_name_and_duplicate_ids_while_checking_siblings test {
try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(previous_count + 3, diagnostics.count(&diagnostic_store))
}
returns_supply_numeric_context_and_resolve_forward_local_and_parameter_names test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`fraction proc() float {
` return -later / 2
` later := seed
`}
`identity proc(value int) int { return value }
`wrapper proc() void { return noop() }
`noop proc() void { return }
`text proc() string { return "honey" }
`seed :: 3
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
defer sema.deinit(&resolution)
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
defer deinit(&result)
expected [5]TypeKind :: [.float, .int, .void, .void, .string]
for (expected) |kind, index| {
function :: ast.package_decl(&tree, index)
view :: ast.function_decl(&tree, function)
try expect_kind(&result, function, .function)
try expect_kind(&result, view.return_type, kind)
statement :: ast.block_statement(&tree, view.body, 0)
try expect_kind(&result, statement, .void)
if (ast.return_stmt(&tree, statement).value) |value| try expect_kind(&result, value, kind)
}
fraction :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
local :: ast.block_statement(&tree, fraction.body, 1)
try expect_kind(&result, local, .float)
try expect_kind(&result, ast.package_decl(&tree, 5), .float)
if (ast.return_stmt(&tree, ast.block_statement(&tree, fraction.body, 0)).value) |value| {
division :: ast.binary_expr(&tree, value)
negate :: ast.unary_expr(&tree, division.left)
try expect_kind(&result, division.left, .float)
try expect_kind(&result, division.right, .float)
try testing.expect_equal(local, sema.binding(&resolution, negate.operand).data.declaration)
} else {
return .expectation_failed
}
identity :: ast.package_decl(&tree, 1)
identity_body :: ast.function_decl(&tree, identity).body
if (ast.return_stmt(&tree, ast.block_statement(&tree, identity_body, 0)).value) |value| {
try testing.expect_equal(ast.function_param(&tree, identity, 0), sema.binding(&resolution, value).data.declaration)
} else {
return .expectation_failed
}
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
return_errors_preserve_identity_check_unreachable_code_and_suppress_missing_return_cascades test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`bare proc() int { return }
`wrong proc() int { return "text" }
`void_value proc() void { return 1 }
`unknown proc() int { return absent }
`syntax proc() int { return (1 }
`unreachable proc() int {
` return 7
` -"bad"
`}
`fallthrough proc() int {}
`recovered proc() int {
` return 1 stray
` valid :: 2
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
defer sema.deinit(&resolution)
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
defer deinit(&result)
for (usize(0)..usize(3)) |index| {
function :: ast.package_decl(&tree, index)
view :: ast.function_decl(&tree, function)
try expect_kind(&result, function, .function)
statement :: ast.block_statement(&tree, view.body, 0)
returned :: ast.return_stmt(&tree, statement)
span :: if (returned.value) |value| ast.node_span(&tree, value) else returned.span
diagnostic :: try expect_error(&result, &diagnostic_store, statement, .return_type_mismatch, span)
if (returned.value) |value| try testing.expect_equal(diagnostic, type_of(&result, value).diagnostic)
}
for (usize(3)..usize(5)) |index| {
view :: ast.function_decl(&tree, ast.package_decl(&tree, index))
statement :: ast.block_statement(&tree, view.body, 0)
if (ast.return_stmt(&tree, statement).value) |value| {
original :: type_of(&result, value).diagnostic
try testing.expect(original != diagnostics.no_diagnostic)
try testing.expect_equal(original, type_of(&result, statement).diagnostic)
try testing.expect_equal(original, type_of(&result, view.body).diagnostic)
} else {
return .expectation_failed
}
}
unreachable_body :: ast.function_decl(&tree, ast.package_decl(&tree, 5)).body
try expect_kind(&result, ast.block_statement(&tree, unreachable_body, 0), .void)
bad :: ast.statement_expr(&tree, ast.block_statement(&tree, unreachable_body, 1))
_ = try expect_error(&result, &diagnostic_store, bad, .invalid_operand, ast.node_span(&tree, bad))
missing_body :: ast.function_decl(&tree, ast.package_decl(&tree, 6)).body
_ = try expect_error(&result, &diagnostic_store, missing_body, .missing_return, ast.node_span(&tree, missing_body))
recovered_body :: ast.function_decl(&tree, ast.package_decl(&tree, 7)).body
try expect_kind(&result, ast.block_statement(&tree, recovered_body, 1), .int)
try testing.expect_equal(3, code_count(&diagnostic_store, .return_type_mismatch))
try testing.expect_equal(1, code_count(&diagnostic_store, .missing_return))
try testing.expect_equal(1, code_count(&diagnostic_store, .unknown_name))
try testing.expect_equal(2, code_count(&diagnostic_store, .unexpected_token))
try testing.expect_equal(8, diagnostics.count(&diagnostic_store))
}
+3
View File
@@ -187,6 +187,9 @@ resolve_function proc(
match ast.stmt_kind(tree, statement) {
.declaration: try resolve_statement_decl(state, &scope, statement)
.expression: try resolve_expr(state, &scope, ast.statement_expr(tree, statement))
.return_stmt: {
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, &scope, value)
}
.invalid: resolve_invalid(state, statement)
}
}