feat: lower typed programs and interpret IR
This commit is contained in:
+26
-2
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user