feat: lower typed programs and interpret IR

This commit is contained in:
2026-10-09 22:47:05 +02:00
parent bc318a29ad
commit 98f3a04daf
20 changed files with 2328 additions and 85 deletions
+26 -1
View File
@@ -10,6 +10,25 @@ build Honey from this directory:
bro build bro build
``` ```
run a source file with the interpreter:
```sh
./build/honey run example.hon
```
```hon
add proc(left, right int) int {
return left + right
}
main proc() int {
answer :: add(19, 23)
return answer
}
```
this prints `42`. `main` must be a top-level function with no parameters; nonvoid results are printed, not used as process exit statuses. the current macOS bootstrap obtains arguments from the native startup environment.
the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. parsing accepts integer division syntax, but type checking requires float operands for `/`. the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. parsing accepts integer division syntax, but type checking requires float operands for `/`.
function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow a trailing comma; newlines remain statement boundaries. type checking validates callable values, argument counts and argument types. function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow a trailing comma; newlines remain statement boundaries. type checking validates callable values, argument counts and argument types.
@@ -22,4 +41,10 @@ type checking (`@source/sema/check`) runs after name resolution. `check.check` b
numeric typing follows contextual rules within Honey's current `int` and `float` families. integer literals can adopt either family; float literals remain float. numeric demands propagate through arithmetic and inferred bindings before undemanded integers default to `int`. conflicting demands are errors; fixed `int` values do not implicitly convert to `float`. `+`, `-`, `*` and unary `-` require compatible numeric operands; `/` requires and supplies float context. `3 / 2`, `3.0 / 2`, and `ratio float :: 3 / 2` are valid; division involving fixed `int` values requires explicit conversion. numeric typing follows contextual rules within Honey's current `int` and `float` families. integer literals can adopt either family; float literals remain float. numeric demands propagate through arithmetic and inferred bindings before undemanded integers default to `int`. conflicting demands are errors; fixed `int` values do not implicitly convert to `float`. `+`, `-`, `*` and unary `-` require compatible numeric operands; `/` requires and supplies float context. `3 / 2`, `3.0 / 2`, and `ratio float :: 3 / 2` are valid; division involving fixed `int` values requires explicit conversion.
type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions and arguments are still checked. inference cycles without a type anchor are diagnosed. read-before-initialization and runtime initialization-cycle checks remain separate, unimplemented work. function signatures are checked independently of their bodies; since return statements are not yet parsed, nonvoid functions currently report missing-return diagnostics. lowering and executable generation remain unimplemented. type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions and arguments are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare `return` and same-line `return expression` are supported inside function bodies; returns must match the declared result type, and nonvoid fallthrough is diagnosed. statements after a return are still checked but are not executed.
`lower.lower` turns the checked AST and binding table into an owned `@source/ir` module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in return or diagnostic-trap terminators. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with `ir.deinit`; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter.
`interpreter.run` consumes only IR and returns a value or a diagnostic trap; returned strings borrow the module. invocation frames are heap-backed and independent; parameters are initialized on entry and locals execute in statement order. premature local reads trap. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. integers use signed 64-bit storage with checked arithmetic; floats use 64-bit IEEE arithmetic. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes.
recoverable source errors do not stop execution of unaffected code. unused invalid functions and globals retain diagnostic traps; reaching invalid code reports its original diagnostic identity. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation and control-flow/assignment syntax remain unimplemented.
+2 -2
View File
@@ -1,3 +1,3 @@
# compile through recoverable source errors # execute through recoverable source errors
Honey produces an executable whenever source errors can be represented safely: structurally valid syntax trees may contain typed invalid nodes carrying source spans and diagnostic identities, and semantic checking or lowering turns demanded invalid code into diagnostic runtime traps. clean compilation returns status `0`, compilation that emits an executable with source diagnostics returns `1`, and unrecoverable infrastructure or backend failure returns `2`. this deliberately favors testing unaffected program paths over conventional fail-fast compilation; parser recovery must preserve valid surrounding syntax, while compact syntax-tree storage remains private behind typed views. Honey lowers recoverable source errors into typed, target-independent IR with source spans and diagnostic-bearing trap terminators; the interpreter executes that IR first, and future native backends reuse the same lowering rather than duplicating frontend semantics. invalid code traps only when execution reaches it, allowing unaffected program paths to run; parser recovery preserves surrounding syntax and compact syntax-tree storage remains private behind typed views. clean execution returns status `0`, source diagnostics or runtime traps return `1` even when an unaffected entry point finishes successfully, and usage or unrecoverable infrastructure failure returns `2`.
+6
View File
@@ -0,0 +1,6 @@
to_fahrenheit proc(celsius float) float { return celsius * 9 / 5 + 32 }
main proc() float {
temperature :: 20
return to_fahrenheit(temperature)
}
+26 -2
View File
@@ -5,7 +5,7 @@
#! * op ....... -> '+' | '-' | '*' | '/' #! * op ....... -> '+' | '-' | '*' | '/'
#! * expr ..... -> identifier | literal | '(' expr ')' | '-' expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' #! * expr ..... -> identifier | literal | '(' expr ')' | '-' expr | expr op expr | expr '(' [expr {',' expr} [',']] ')'
#! * decl ..... -> identifier [type] assignment expr #! * decl ..... -> identifier [type] assignment expr
#! * stmt ..... -> decl | expr #! * stmt ..... -> decl | expr | 'return' [expr]
import "@std/debug" import "@std/debug"
import "@std/mem" import "@std/mem"
@@ -41,7 +41,7 @@ DeclKind :: enum {
} }
StmtKind :: enum { StmtKind :: enum {
declaration, expression, invalid declaration, expression, return_stmt, invalid
} }
Mutability :: enum { Mutability :: enum {
@@ -65,6 +65,7 @@ NodeKind :: enum {
expr_call, expr_call,
stmt_expr, stmt_expr,
stmt_decl, stmt_decl,
stmt_return,
func_decl, func_decl,
param_decl, param_decl,
block, block,
@@ -151,6 +152,11 @@ CallExprView :: struct {
callee NodeId callee NodeId
} }
ReturnView :: struct {
span Span
value ?NodeId
}
InvalidView :: struct { InvalidView :: struct {
span Span span Span
diagnostic DiagnosticId diagnostic DiagnosticId
@@ -259,6 +265,14 @@ add_expression_stmt proc(builder @mut Builder, expression NodeId) NodeId ! mem.A
}, builder_span(builder, expression)) }, 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( add_statement_decl proc(
builder @mut Builder, builder @mut Builder,
span Span, span Span,
@@ -393,6 +407,7 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind {
return match get_node(&tree.storage, id).kind { return match get_node(&tree.storage, id).kind {
.stmt_decl: .declaration .stmt_decl: .declaration
.stmt_expr: .expression .stmt_expr: .expression
.stmt_return: .return_stmt
.invalid_stmt: .invalid .invalid_stmt: .invalid
else: unreachable else: unreachable
} }
@@ -404,6 +419,15 @@ statement_expr proc(tree @Tree, id NodeId) NodeId {
return node.main.node_id 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 { statement_decl proc(tree @Tree, id NodeId) StatementDeclView {
node :: get_node(&tree.storage, id) node :: get_node(&tree.storage, id)
debug.assert(node.kind == .stmt_decl) debug.assert(node.kind == .stmt_decl)
+8
View File
@@ -51,6 +51,14 @@ render_function proc(
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, expression))) try append_text(renderer, ast.source_text(tree, ast.node_span(tree, expression)))
try append_text(renderer, "\n") 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") .invalid: try append_text(renderer, " invalid statement\n")
} }
} }
-27
View File
@@ -1,27 +0,0 @@
import "@std/mem"
import "@std/testing"
import "@source/ast"
renders_semantic_debug_view test {
builder := ast.init_builder(mem.c_allocator, "value :: 42")
defer ast.deinit_builder(&builder)
value :: try ast.add_literal(&builder, .int, ast.Span{ start = 9, end = 11 })
declaration :: try ast.add_statement_decl(
&builder,
ast.Span{ start = 0, end = 11 },
ast.Span{ start = 0, end = 5 },
null,
.constant,
value,
)
declarations [1]ast.NodeId :: [declaration]
package :: try ast.add_package(&builder, ast.Span{ start = 0, end = 11 }, declarations[..])
tree := ast.finish(&builder, package)
defer ast.deinit(&tree)
renderer := init(mem.c_allocator)
defer deinit(&renderer)
try render(&renderer, &tree)
try testing.expect(mem.eql(u8, "package\n declaration value\n", renderer.bytes.items))
}
+37
View File
@@ -26,6 +26,14 @@ Code :: enum {
argument_count_mismatch, argument_count_mismatch,
cannot_infer_type, cannot_infer_type,
missing_return, missing_return,
return_type_mismatch,
missing_entry,
invalid_entry,
literal_out_of_range,
invalid_escape,
read_before_initialization,
initialization_cycle,
integer_overflow,
} }
Diagnostic :: struct { Diagnostic :: struct {
@@ -64,3 +72,32 @@ get proc(store @Store, id DiagnosticId) ?Diagnostic {
count proc(store @Store) usize { count proc(store @Store) usize {
return store.list.items.len return store.list.items.len
} }
message proc(code Code) []u8 {
return match code {
.invalid_character: "invalid character"
.float_must_end_with_digit: "float literal must end with a digit"
.unterminated_string: "unterminated string literal"
.unexpected_token: "unexpected token"
.duplicate_name: "duplicate name or forbidden shadowing"
.unknown_name: "unknown name"
.expected_type: "annotation must name a type"
.type_not_value: "type name cannot be used as a value"
.invalid_void_type: "void cannot be stored or used as a parameter type"
.type_mismatch: "incompatible value type"
.invalid_operand: "arithmetic requires compatible numeric operands"
.integer_division: "division requires float operands"
.not_callable: "value is not callable"
.argument_count_mismatch: "argument count does not match the function signature"
.cannot_infer_type: "cannot infer a type without a type anchor"
.missing_return: "nonvoid function can finish without returning a value"
.return_type_mismatch: "return does not match the function result type"
.missing_entry: "program requires a main function"
.invalid_entry: "main must be a function with no parameters"
.literal_out_of_range: "numeric literal is outside its runtime type's range"
.invalid_escape: "invalid string escape"
.read_before_initialization: "local value was read before initialization"
.initialization_cycle: "global initialization cycle"
.integer_overflow: "integer arithmetic overflow"
}
}
+247
View File
@@ -0,0 +1,247 @@
import "@std/mem"
import "@std/arraylist"
import "@source/ir"
import "@source/diagnostics"
Outcome :: struct {
value ir.Value = ir.Value{}
trap ?diagnostics.DiagnosticId = null
}
@hide:file
Slot :: struct {
initialized bool = false
value ir.Value = ir.Value{}
}
@hide:file
GlobalStatus :: enum { uninitialized, initializing, ready }
@hide:file
GlobalState :: struct {
status GlobalStatus = .uninitialized
value ir.Value = ir.Value{}
}
@hide:file
Frame :: struct {
function ir.FunctionId
next_instruction usize = 0
destination ir.ValueId = ir.no_value
global ?usize = null
locals []mut Slot
values []mut ir.Value
}
#! executes only typed IR; invocation and global state are temporary and released on every exit.
#! returned strings borrow `module`; runtime source errors are traps, not infrastructure errors.
run proc(
allocator mem.Allocator,
module @ir.Module,
diagnostic_store @mut diagnostics.Store,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
entry ir.FunctionId := ir.FunctionId(0)
if module.entry |function| {
entry = function
} else {
return Outcome{ trap = module.entry_diagnostic }
}
globals :: try mem.alloc(GlobalState, allocator, module.globals.items.len)
defer mem.free(allocator, globals)
mem.set(globals, GlobalState{})
frames := arraylist.init(Frame, allocator)
defer {
for (frames.items) |frame| free_frame(allocator, frame)
arraylist.deinit(&frames)
}
try push_frame(allocator, module, &frames, entry, ir.no_value, null)
while frames.items.len != 0 {
frame_index :: frames.items.len - 1
function :: &module.functions.items[usize(frames.items[frame_index].function)]
block :: &function.blocks.items[usize(function.entry)]
instruction_index :: frames.items[frame_index].next_instruction
if instruction_index == block.instructions.items.len {
terminator :: block.terminator
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
value := ir.Value{}
if terminator.value != ir.no_value {
value = frames.items[frame_index].values[usize(terminator.value)]
}
completed :: frames.items[frame_index]
_ = arraylist.pop(&frames)
free_frame(allocator, completed)
if completed.global |global_index| {
globals[global_index].value = value
globals[global_index].status = .ready
}
if (frames.items.len == 0) return Outcome{ value = value }
if completed.destination != ir.no_value {
frames.items[frames.items.len - 1].values[usize(completed.destination)] = value
}
continue
}
instruction :: &block.instructions.items[instruction_index]
frames.items[frame_index].next_instruction += 1
value := ir.Value{}
match instruction.op {
.constant: value = instruction.constant
.load_local: {
local :: &frames.items[frame_index].locals[usize(instruction.slot)]
if !local.initialized {
return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span)
}
value = local.value
}
.store_local: {
frames.items[frame_index].locals[usize(instruction.slot)] = Slot{
initialized = true,
value = frames.items[frame_index].values[usize(instruction.left)],
}
continue
}
.load_global: {
global_index :: usize(instruction.slot)
match globals[global_index].status {
.ready: value = globals[global_index].value
.initializing: {
return try runtime_trap(diagnostic_store, .initialization_cycle, instruction.span)
}
.uninitialized: {
try push_frame(
allocator,
module,
&frames,
module.globals.items[global_index].initializer,
instruction.destination,
global_index,
)
globals[global_index].status = .initializing
continue
}
}
}
.call: {
callee :: frames.items[frame_index].values[usize(instruction.left)].data.function
try push_frame(allocator, module, &frames, callee, instruction.destination, null)
# frame indices survive stack growth; pointers into frames.items do not.
for (0..instruction.arguments_count) |argument_index| {
argument :: block.arguments.items[instruction.arguments_start + argument_index]
frames.items[frame_index + 1].locals[argument_index] = Slot{
initialized = true,
value = frames.items[frame_index].values[usize(argument)],
}
}
continue
}
.negate: {
operand :: frames.items[frame_index].values[usize(instruction.left)]
if instruction.type == .float {
value = ir.Value{ type = .float, data = ir.ValueData{ real = -operand.data.real } }
} else {
if operand.data.integer == minval!(i64) {
return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
}
value = ir.Value{ type = .int, data = ir.ValueData{ integer = -operand.data.integer } }
}
}
.add, .subtract, .multiply, .divide: {
left :: frames.items[frame_index].values[usize(instruction.left)]
right :: frames.items[frame_index].values[usize(instruction.right)]
if instruction.type == .float {
result f64 := 0
match instruction.op {
.add: result = left.data.real + right.data.real
.subtract: result = left.data.real - right.data.real
.multiply: result = left.data.real * right.data.real
.divide: result = left.data.real / right.data.real
else: unreachable
}
value = ir.Value{ type = .float, data = ir.ValueData{ real = result } }
} else {
result :: checked_integer(instruction.op, left.data.integer, right.data.integer)
if result |result_integer| {
value = ir.Value{ type = .int, data = ir.ValueData{ integer = result_integer } }
} else {
return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
}
}
}
}
if instruction.destination != ir.no_value {
frames.items[frame_index].values[usize(instruction.destination)] = value
}
}
unreachable
}
@hide:file
push_frame proc(
allocator mem.Allocator,
module @ir.Module,
frames @mut arraylist.ArrayList(Frame),
function ir.FunctionId,
destination ir.ValueId,
global ?usize,
) void ! mem.AllocError {
signature :: &module.functions.items[usize(function)]
locals :: try mem.alloc(Slot, allocator, signature.locals.items.len)
errdefer mem.free(allocator, locals)
mem.set(locals, Slot{})
values :: try mem.alloc(ir.Value, allocator, signature.value_count)
errdefer mem.free(allocator, values)
# typed IR assigns each temporary before use; only local slots need initialization flags.
try arraylist.append(frames, Frame{
function = function,
destination = destination,
global = global,
locals = locals,
values = values,
})
}
@hide:file
free_frame proc(allocator mem.Allocator, frame Frame) void {
mem.free(allocator, frame.locals)
mem.free(allocator, frame.values)
}
@hide:file
runtime_trap proc(
diagnostic_store @mut diagnostics.Store,
code diagnostics.Code,
span diagnostics.Span,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
return Outcome{ trap = try diagnostics.add(diagnostic_store, code, span) }
}
@hide:file
checked_integer proc(op ir.Op, left i64, right i64) ?i64 {
minimum :: minval!(i64)
maximum :: maxval!(i64)
match op {
.add: {
if (right > 0 and left > maximum - right) return null
if (right < 0 and left < minimum - right) return null
return left + right
}
.subtract: {
if (right > 0 and left < minimum + right) return null
if (right < 0 and left > maximum + right) return null
return left - right
}
.multiply: {
if left > 0 {
if (right > 0 and left > divtrunc!(maximum, right)) return null
if (right < 0 and right < divtrunc!(minimum, left)) return null
} else if left < 0 {
if (right > 0 and left < divtrunc!(minimum, right)) return null
if (right < 0 and left < divtrunc!(maximum, right)) return null
}
return left * right
}
else: unreachable
}
}
+479
View File
@@ -0,0 +1,479 @@
import "@std/mem"
import "@std/arraylist"
import "@std/testing"
import "@source/ir"
import "@source/diagnostics"
@hide:file
add_function proc(
module @mut ir.Module,
locals []ir.Type,
parameter_count usize,
result ir.Type,
value_count usize,
) ir.FunctionId ! mem.AllocError {
function := ir.init_function(module.allocator)
errdefer {
arraylist.deinit(&function.parameters)
arraylist.deinit(&function.locals)
for (function.blocks.items) |block| {
owned := block
arraylist.deinit(&owned.instructions)
arraylist.deinit(&owned.arguments)
}
arraylist.deinit(&function.blocks)
}
for (locals) |type, index| {
try arraylist.append(&function.locals, type)
if (index < parameter_count) try arraylist.append(&function.parameters, type)
}
function.result = result
function.value_count = value_count
try arraylist.append(&function.blocks, ir.init_block(module.allocator))
id :: ir.FunctionId(module.functions.items.len)
try arraylist.append(&module.functions, function)
return id
}
@hide:file
emit proc(module @mut ir.Module, function ir.FunctionId, instruction ir.Instruction) void ! mem.AllocError {
try arraylist.append(&module.functions.items[usize(function)].blocks.items[0].instructions, instruction)
}
@hide:file
return_value proc(module @mut ir.Module, function ir.FunctionId, value ir.ValueId) void {
module.functions.items[usize(function)].blocks.items[0].terminator = ir.Terminator{ value = value }
}
@hide:file
integer proc(destination u32, value i64) ir.Instruction {
return ir.Instruction{
op = .constant,
type = .int,
destination = ir.ValueId(destination),
constant = ir.Value{ type = .int, data = ir.ValueData{ integer = value } },
}
}
@hide:file
function_value proc(destination u32, function ir.FunctionId) ir.Instruction {
return ir.Instruction{
op = .constant,
type = .function,
destination = ir.ValueId(destination),
constant = ir.Value{ type = .function, data = ir.ValueData{ function = function } },
}
}
@hide:file
expect_trap proc(
outcome Outcome,
store @diagnostics.Store,
code diagnostics.Code,
span diagnostics.Span,
) void ! testing.Error {
if outcome.trap |id| {
if diagnostics.get(store, id) |diagnostic| {
try testing.expect_equal(code, diagnostic.code)
try testing.expect_equal(span.start, diagnostic.span.start)
try testing.expect_equal(span.end, diagnostic.span.end)
return
}
}
return .expectation_failed
}
repeated_calls_have_fresh_parameters_locals_and_temporaries test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
types [2]ir.Type :: [.int, .int]
bump :: try add_function(&module, types[..], 1, .int, 4)
try emit(&module, bump, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
try emit(&module, bump, integer(1, 1))
try emit(&module, bump, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
try emit(&module, bump, ir.Instruction{ op = .store_local, left = ir.ValueId(2), slot = 1 })
try emit(&module, bump, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(3), slot = 1 })
return_value(&module, bump, ir.ValueId(3))
noop :: try add_function(&module, mem.empty(ir.Type), 0, .void, 0)
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 8)
module.entry = main
try emit(&module, main, function_value(0, bump))
try emit(&module, main, integer(1, 19))
try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(1))
try emit(&module, main, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), arguments_count = 1 })
try emit(&module, main, integer(3, 21))
try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(3))
try emit(&module, main, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(4), left = ir.ValueId(0), arguments_start = 1, arguments_count = 1 })
try emit(&module, main, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(5), left = ir.ValueId(2), right = ir.ValueId(4) })
try emit(&module, main, function_value(6, noop))
try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(7), left = ir.ValueId(6) })
return_value(&module, main, ir.ValueId(5))
outcome :: try run(mem.c_allocator, &module, &store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.int, outcome.value.type)
try testing.expect_equal(i64(42), outcome.value.data.integer)
try testing.expect_equal(usize(0), diagnostics.count(&store))
}
lazy_globals_follow_forward_dependencies_and_reset_between_runs test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
second :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
try emit(&module, first, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 1 })
try emit(&module, first, integer(1, 1))
try emit(&module, first, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
return_value(&module, first, ir.ValueId(2))
try emit(&module, second, integer(0, 20))
return_value(&module, second, ir.ValueId(0))
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } })
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } })
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(1), slot = 0 })
try emit(&module, main, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
return_value(&module, main, ir.ValueId(2))
initial :: try run(mem.c_allocator, &module, &store)
try testing.expect(initial.trap == null)
try testing.expect_equal(i64(42), initial.value.data.integer)
module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = integer(0, 30)
repeated :: try run(mem.c_allocator, &module, &store)
try testing.expect(repeated.trap == null)
try testing.expect_equal(i64(62), repeated.value.data.integer)
}
function_aliases_and_borrowed_string_values_execute test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
text :: "line\n\"quoted\"\x00"
target :: try add_function(&module, mem.empty(ir.Type), 0, .string, 1)
try emit(&module, target, ir.Instruction{
op = .constant,
type = .string,
destination = ir.ValueId(0),
constant = ir.Value{ type = .string, data = ir.ValueData{ string = text } },
})
return_value(&module, target, ir.ValueId(0))
initializer :: try add_function(&module, mem.empty(ir.Type), 0, .function, 1)
try emit(&module, initializer, function_value(0, target))
return_value(&module, initializer, ir.ValueId(0))
try arraylist.append(&module.globals, ir.Global{ type = .function, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
main :: try add_function(&module, mem.empty(ir.Type), 0, .string, 2)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_global, type = .function, destination = ir.ValueId(0), slot = 0 })
try emit(&module, main, ir.Instruction{ op = .call, type = .string, destination = ir.ValueId(1), left = ir.ValueId(0) })
return_value(&module, main, ir.ValueId(1))
outcome :: try run(mem.c_allocator, &module, &store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.string, outcome.value.type)
try testing.expect_equal([]u8, text, outcome.value.data.string)
}
premature_local_reads_and_global_cycles_report_instruction_spans test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
span :: diagnostics.Span{ start = 14, end = 19 }
types [1]ir.Type :: [.int]
main :: try add_function(&module, types[..], 0, .int, 1)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0, span = span })
return_value(&module, main, ir.ValueId(0))
premature :: try run(mem.c_allocator, &module, &store)
try expect_trap(premature, &store, .read_before_initialization, span)
first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
second :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
cycle_span :: diagnostics.Span{ start = 40, end = 44 }
try emit(&module, first, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 1 })
try emit(&module, second, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0, span = cycle_span })
return_value(&module, first, ir.ValueId(0))
return_value(&module, second, ir.ValueId(0))
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } })
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } })
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 }
cycle :: try run(mem.c_allocator, &module, &store)
try expect_trap(cycle, &store, .initialization_cycle, cycle_span)
retry :: try run(mem.c_allocator, &module, &store)
try expect_trap(retry, &store, .initialization_cycle, cycle_span)
}
existing_trap_identity_entry_failures_and_unused_errors_survive test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
entry_id :: try diagnostics.add(&store, .missing_entry, diagnostics.Span{ start = 0, end = 0 })
module.entry_diagnostic = entry_id
absent :: try run(mem.c_allocator, &module, &store)
try testing.expect_equal(entry_id, absent.trap?)
invalid_id :: try diagnostics.add(&store, .unknown_name, diagnostics.Span{ start = 12, end = 17 })
bad :: try add_function(&module, mem.empty(ir.Type), 0, .invalid, 0)
module.functions.items[usize(bad)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = invalid_id }
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 2)
module.entry = main
try emit(&module, main, integer(0, 42))
return_value(&module, main, ir.ValueId(0))
success :: try run(mem.c_allocator, &module, &store)
try testing.expect(success.trap == null)
try testing.expect_equal(i64(42), success.value.data.integer)
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = function_value(0, bad)
try emit(&module, main, ir.Instruction{ op = .call, type = .invalid, destination = ir.ValueId(1), left = ir.ValueId(0) })
trapped :: try run(mem.c_allocator, &module, &store)
try testing.expect_equal(invalid_id, trapped.trap?)
try testing.expect_equal(usize(2), diagnostics.count(&store))
}
@hide:file
IntegerCase :: struct {
op ir.Op
left i64
right i64
expected ?i64
}
integer_arithmetic_checks_boundaries_before_host_operations test {
cases [19]IntegerCase :: [
IntegerCase{ op = .add, left = 19, right = 23, expected = 42 },
IntegerCase{ op = .add, left = maxval!(i64), right = 0, expected = maxval!(i64) },
IntegerCase{ op = .add, left = maxval!(i64), right = 1, expected = null },
IntegerCase{ op = .add, left = minval!(i64), right = -1, expected = null },
IntegerCase{ op = .add, left = minval!(i64), right = maxval!(i64), expected = -1 },
IntegerCase{ op = .subtract, left = minval!(i64), right = 0, expected = minval!(i64) },
IntegerCase{ op = .subtract, left = minval!(i64), right = 1, expected = null },
IntegerCase{ op = .subtract, left = maxval!(i64), right = -1, expected = null },
IntegerCase{ op = .subtract, left = -1, right = minval!(i64), expected = maxval!(i64) },
IntegerCase{ op = .multiply, left = minval!(i64), right = 1, expected = minval!(i64) },
IntegerCase{ op = .multiply, left = minval!(i64), right = -1, expected = null },
IntegerCase{ op = .multiply, left = -1, right = minval!(i64), expected = null },
IntegerCase{ op = .multiply, left = maxval!(i64), right = 2, expected = null },
IntegerCase{ op = .multiply, left = minval!(i64), right = 2, expected = null },
IntegerCase{ op = .multiply, left = -3, right = -14, expected = 42 },
IntegerCase{ op = .multiply, left = -3, right = 14, expected = -42 },
IntegerCase{ op = .multiply, left = 3, right = -14, expected = -42 },
IntegerCase{ op = .multiply, left = minval!(i64), right = 0, expected = 0 },
IntegerCase{ op = .multiply, left = 0, right = minval!(i64), expected = 0 },
]
for (cases) |case| {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
span :: diagnostics.Span{ start = 8, end = 16 }
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
module.entry = main
try emit(&module, main, integer(0, case.left))
try emit(&module, main, integer(1, case.right))
try emit(&module, main, ir.Instruction{ op = case.op, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1), span = span })
return_value(&module, main, ir.ValueId(2))
outcome :: try run(mem.c_allocator, &module, &store)
if case.expected |expected| {
try testing.expect(outcome.trap == null)
try testing.expect_equal(expected, outcome.value.data.integer)
} else {
try expect_trap(outcome, &store, .integer_overflow, span)
}
}
}
integer_negation_traps_only_at_minimum test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
span :: diagnostics.Span{ start = 21, end = 27 }
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 2)
module.entry = main
try emit(&module, main, integer(0, minval!(i64)))
try emit(&module, main, ir.Instruction{ op = .negate, type = .int, destination = ir.ValueId(1), left = ir.ValueId(0), span = span })
return_value(&module, main, ir.ValueId(1))
trapped :: try run(mem.c_allocator, &module, &store)
try expect_trap(trapped, &store, .integer_overflow, span)
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = integer(0, maxval!(i64))
success :: try run(mem.c_allocator, &module, &store)
try testing.expect(success.trap == null)
try testing.expect_equal(-maxval!(i64), success.value.data.integer)
}
float_arithmetic_and_ieee_zero_division_execute test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
main :: try add_function(&module, mem.empty(ir.Type), 0, .float, 7)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(0), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 10.5 } } })
try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(1), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 2.0 } } })
try emit(&module, main, ir.Instruction{ op = .multiply, type = .float, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
try emit(&module, main, ir.Instruction{ op = .add, type = .float, destination = ir.ValueId(3), left = ir.ValueId(2), right = ir.ValueId(1) })
try emit(&module, main, ir.Instruction{ op = .subtract, type = .float, destination = ir.ValueId(4), left = ir.ValueId(3), right = ir.ValueId(1) })
try emit(&module, main, ir.Instruction{ op = .divide, type = .float, destination = ir.ValueId(5), left = ir.ValueId(4), right = ir.ValueId(1) })
try emit(&module, main, ir.Instruction{ op = .negate, type = .float, destination = ir.ValueId(6), left = ir.ValueId(5) })
return_value(&module, main, ir.ValueId(6))
result :: try run(mem.c_allocator, &module, &store)
try testing.expect(result.trap == null)
try testing.expect_equal(ir.Type.float, result.value.type)
try testing.expect_equal(f64(-10.5), result.value.data.real)
module.functions.items[usize(main)].blocks.items[0].instructions.items[0].constant.data.real = 0
module.functions.items[usize(main)].blocks.items[0].instructions.items[1].constant.data.real = 0
nan :: try run(mem.c_allocator, &module, &store)
try testing.expect(nan.trap == null)
try testing.expect(nan.value.data.real != nan.value.data.real)
module.functions.items[usize(main)].blocks.items[0].instructions.items[0].constant.data.real = 1
module.functions.items[usize(main)].blocks.items[0].instructions.items[2].op = .add
infinite :: try run(mem.c_allocator, &module, &store)
try testing.expect(infinite.trap == null)
try testing.expect(infinite.value.data.real < f64(-1000000000))
try testing.expect_equal(infinite.value.data.real, infinite.value.data.real * f64(2))
try testing.expect_equal(usize(0), diagnostics.count(&store))
}
@hide:file
AllocationState :: struct {
remaining usize
live usize = 0
}
@hide:file
limited_alloc proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
if context |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (state.remaining == 0) return null
state.remaining -= 1
memory :: mem.raw_alloc(mem.c_allocator, size, alignment)
if (memory) |_| state.live += 1
return memory
}
unreachable
}
@hide:file
limited_realloc proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
if context |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (state.remaining == 0) return null
state.remaining -= 1
resized :: mem.raw_realloc(mem.c_allocator, memory, old_size, new_size, alignment)
if resized |_| {
if (memory) |_| {} else state.live += 1
}
return resized
}
unreachable
}
@hide:file
limited_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
if context |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (memory) |_| state.live -= 1
mem.raw_free(mem.c_allocator, memory, size, alignment)
return
}
unreachable
}
@hide:file
limited_vtable :: mem.AllocatorVTable{ alloc = limited_alloc, realloc = limited_realloc, free = limited_free }
recursive_invocations_use_heap_frames_and_free_on_allocation_failure test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
main :: try add_function(&module, mem.empty(ir.Type), 0, .void, 2)
module.entry = main
try emit(&module, main, function_value(0, main))
try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) })
state := AllocationState{ remaining = 2048 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
_ = run(allocator, &module, &store) catch |error| {
match error {
.out_of_memory: {}
else: return .expectation_failed
}
try testing.expect_equal(usize(0), state.live)
try testing.expect_equal(usize(0), diagnostics.count(&store))
return
}
return .expectation_failed
}
frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
types [1]ir.Type :: [.int]
initializer :: try add_function(&module, types[..], 0, .int, 1)
try emit(&module, initializer, integer(0, 42))
return_value(&module, initializer, ir.ValueId(0))
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
main :: try add_function(&module, types[..], 0, .int, 1)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
return_value(&module, main, ir.ValueId(0))
succeeded := false
failures usize := 0
for (usize(0)..usize(16)) |limit| {
state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
outcome :: run(allocator, &module, &store) catch |error| {
match error {
.out_of_memory: {}
else: return .expectation_failed
}
failures += 1
try testing.expect_equal(usize(0), state.live)
continue
}
succeeded = true
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
try testing.expect_equal(usize(0), state.live)
}
try testing.expect(succeeded)
try testing.expect(failures > 0)
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 })
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
state := AllocationState{ remaining = 64 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
trapped :: try run(allocator, &module, &store)
try testing.expect_equal(trap_id, trapped.trap?)
try testing.expect_equal(usize(0), state.live)
}
runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
types [1]ir.Type :: [.int]
main :: try add_function(&module, types[..], 0, .int, 1)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
return_value(&module, main, ir.ValueId(0))
diagnostic_state := AllocationState{ remaining = 0 }
diagnostic_allocator :: mem.Allocator{ context = &diagnostic_state, vtable = &limited_vtable }
store := diagnostics.init(diagnostic_allocator)
defer diagnostics.deinit(&store)
state := AllocationState{ remaining = 64 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
_ = run(allocator, &module, &store) catch |error| {
match error {
.out_of_memory: {}
else: return .expectation_failed
}
try testing.expect_equal(usize(0), state.live)
try testing.expect_equal(usize(0), diagnostic_state.live)
try testing.expect_equal(usize(0), diagnostics.count(&store))
return
}
return .expectation_failed
}
+132
View File
@@ -0,0 +1,132 @@
import "@std/mem"
import "@std/arraylist"
import "@source/diagnostics"
FunctionId :: distinct u32
GlobalId :: distinct u32
LocalId :: distinct u32
ValueId :: distinct u32
BlockId :: distinct u32
no_value :: maxval!(ValueId)
Type :: enum { invalid, int, float, string, void, function }
ValueData :: union {
integer i64
real f64
string []u8
function FunctionId
}
Value :: struct {
type Type = .void
data ValueData = ValueData{ integer = 0 }
}
Op :: enum {
constant
load_local
store_local
load_global
negate
add
subtract
multiply
divide
call
}
Instruction :: struct {
op Op
type Type = .void
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
destination ValueId = no_value
left ValueId = no_value
right ValueId = no_value
slot u32 = 0
constant Value = Value{}
arguments_start usize = 0
arguments_count usize = 0
}
TerminatorKind :: enum { return_value, trap }
Terminator :: struct {
kind TerminatorKind = .return_value
value ValueId = no_value
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
}
Block :: struct {
instructions arraylist.ArrayList(Instruction)
arguments arraylist.ArrayList(ValueId)
terminator Terminator = Terminator{}
}
Function :: struct {
parameters arraylist.ArrayList(Type)
locals arraylist.ArrayList(Type)
result Type = .void
blocks arraylist.ArrayList(Block)
entry BlockId = BlockId(0)
value_count usize = 0
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
}
Global :: struct {
type Type
initializer FunctionId
span diagnostics.Span
}
#! owns executable storage, including decoded strings; no AST or binding table is retained.
#! spans and diagnostic IDs refer to the caller's source and diagnostic store.
Module :: struct {
allocator mem.Allocator
functions arraylist.ArrayList(Function)
globals arraylist.ArrayList(Global)
strings arraylist.ArrayList([]mut u8)
entry ?FunctionId = null
entry_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
}
init proc(allocator mem.Allocator) Module {
return Module{
allocator = allocator,
functions = arraylist.init(allocator),
globals = arraylist.init(allocator),
strings = arraylist.init(allocator),
}
}
init_function proc(allocator mem.Allocator) Function {
return Function{
parameters = arraylist.init(allocator),
locals = arraylist.init(allocator),
blocks = arraylist.init(allocator),
}
}
init_block proc(allocator mem.Allocator) Block {
return Block{
instructions = arraylist.init(allocator),
arguments = arraylist.init(allocator),
}
}
#! releases all executable storage using the retained allocator.
deinit proc(module @mut Module) void {
for (0..(module.functions.items.len)) |i| {
function :: &module.functions.items[i]
for (0..(function.blocks.items.len)) |j| {
block :: &function.blocks.items[j]
arraylist.deinit(&block.instructions)
arraylist.deinit(&block.arguments)
}
arraylist.deinit(&function.parameters)
arraylist.deinit(&function.locals)
arraylist.deinit(&function.blocks)
}
for (module.strings.items) |string| mem.free(module.allocator, string)
arraylist.deinit(&module.functions)
arraylist.deinit(&module.globals)
arraylist.deinit(&module.strings)
}
+468
View File
@@ -0,0 +1,468 @@
import "@std/mem"
import "@std/arraylist"
import "@ffi/c"
import "@source/ast"
import "@source/diagnostics"
import "@source/ir"
sema :: import "@source/sema"
typecheck :: import "@source/sema/check"
@hide:file
StorageKind :: enum { none, function, global, local }
@hide:file
Storage :: struct {
kind StorageKind = .none
index u32 = 0
}
@hide:file
State :: struct {
tree @ast.Tree
resolution @sema.Resolution
checked @typecheck.Result
diagnostics @mut diagnostics.Store
module @mut ir.Module
storage []mut Storage
function usize = 0
terminated bool = false
}
#! lowers checked source into caller-owned executable storage; release with ir.deinit.
#! no frontend storage or source bytes are retained by the returned module.
lower proc(
allocator mem.Allocator,
tree @ast.Tree,
resolution @sema.Resolution,
checked @typecheck.Result,
diagnostic_store @mut diagnostics.Store,
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
module := ir.init(allocator)
errdefer ir.deinit(&module)
storage :: try mem.alloc(Storage, allocator, ast.node_count(tree))
defer mem.free(allocator, storage)
mem.set(storage, Storage{})
state := State{
tree = tree,
resolution = resolution,
checked = checked,
diagnostics = diagnostic_store,
module = &module,
storage = storage,
}
# allocate every package identity before lowering any initializer or body.
for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) {
.function: {
function :: try add_function(&module, ast.node_span(tree, id))
storage[usize(id)] = Storage{ kind = .function, index = u32(function) }
}
.statement: {
initializer :: try add_function(&module, ast.node_span(tree, id))
global :: u32(module.globals.items.len)
try arraylist.append(&module.globals, ir.Global{
type = lower_type(typecheck.type_of(checked, id)),
initializer = initializer,
span = ast.node_span(tree, id),
})
storage[usize(id)] = Storage{ kind = .global, index = global }
}
.invalid: {}
}
}
try find_entry(&state)
for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) {
.function: try lower_function(&state, id)
.statement: try lower_global(&state, id)
.invalid: {}
}
}
return module
}
@hide:file
lower_type proc(type typecheck.Type) ir.Type {
return match type.kind {
.int: .int
.float: .float
.string: .string
.void: .void
.function: .function
.none, .invalid: .invalid
}
}
@hide:file
add_function proc(module @mut ir.Module, span diagnostics.Span) ir.FunctionId ! mem.AllocError {
id :: ir.FunctionId(module.functions.items.len)
try arraylist.append(&module.functions, ir.init_function(module.allocator))
function := &module.functions.items[usize(id)]
function.span = span
try arraylist.append(&function.blocks, ir.init_block(module.allocator))
return id
}
@hide:file
find_entry proc(state @mut State) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
name := diagnostics.Span{ start = 0, end = 0 }
match ast.decl_kind(tree, id) {
.function: name = ast.function_decl(tree, id).name
.statement: name = ast.statement_decl(tree, id).name
.invalid: continue
}
if (!mem.eql(ast.source_text(tree, name), "main")) continue
if (ast.decl_kind(tree, id) != .function) {
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name)
return
}
declaration :: ast.function_decl(tree, id)
result :: lower_type(typecheck.type_of(state.checked, declaration.return_type))
if (ast.function_param_count(tree, id) != 0 or result == .function) {
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name)
return
}
state.module.entry = ir.FunctionId(state.storage[usize(id)].index)
return
}
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .missing_entry, ast.node_span(tree, tree.root))
}
@hide:file
trap proc(state @mut State, diagnostic diagnostics.DiagnosticId, span diagnostics.Span) void {
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
kind = .trap,
diagnostic = diagnostic,
span = span,
}
state.terminated = true
}
@hide:file
trap_type proc(state @mut State, id ast.NodeId) bool {
type :: typecheck.type_of(state.checked, id)
if (type.kind != .invalid) return false
trap(state, type.diagnostic, ast.node_span(state.tree, id))
return true
}
@hide:file
trap_binding proc(state @mut State, id ast.NodeId) bool {
binding :: sema.binding(state.resolution, id)
if (binding.kind != .invalid) return false
trap(state, binding.data.diagnostic, ast.node_span(state.tree, id))
return true
}
@hide:file
finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void {
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
value = value,
span = span,
}
state.terminated = true
}
@hide:file
emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! mem.AllocError {
function := &state.module.functions.items[state.function]
operation := instruction
if (operation.op != .store_local) {
operation.destination = ir.ValueId(function.value_count)
function.value_count += 1
}
try arraylist.append(&function.blocks.items[0].instructions, operation)
return operation.destination
}
@hide:file
constant proc(state @mut State, value ir.Value, span diagnostics.Span) ir.ValueId ! mem.AllocError {
return try emit(state, ir.Instruction{ op = .constant, type = value.type, constant = value, span = span })
}
@hide:file
lower_global proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
global :: state.module.globals.items[usize(state.storage[usize(id)].index)]
state.function = usize(global.initializer)
state.terminated = false
state.module.functions.items[state.function].result = global.type
if (trap_binding(state, id)) return
declaration :: ast.statement_decl(state.tree, id)
if (declaration.type) |annotation| if (trap_type(state, annotation)) return
value :: try lower_expression(state, declaration.initializer)
if (state.terminated or trap_type(state, id)) return
finish(state, value, declaration.span)
}
@hide:file
lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
declaration :: ast.function_decl(tree, id)
state.function = usize(state.storage[usize(id)].index)
state.terminated = false
function := &state.module.functions.items[state.function]
function.result = lower_type(typecheck.type_of(state.checked, declaration.return_type))
for (0..ast.function_param_count(tree, id)) |i| {
parameter :: ast.function_param(tree, id, i)
type :: lower_type(typecheck.type_of(state.checked, parameter))
state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
try arraylist.append(&function.parameters, type)
try arraylist.append(&function.locals, type)
}
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
statement :: ast.block_statement(tree, declaration.body, i)
if (ast.stmt_kind(tree, statement) != .declaration) continue
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
try arraylist.append(&function.locals, lower_type(typecheck.type_of(state.checked, statement)))
}
# signature errors are entry traps; body errors are reached in statement order.
if (trap_binding(state, id) or trap_type(state, declaration.return_type)) return
for (0..ast.function_param_count(tree, id)) |i| {
if (trap_type(state, ast.function_param(tree, id, i))) return
}
if (trap_type(state, id)) return
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
if (state.terminated) return
try lower_statement(state, ast.block_statement(tree, declaration.body, i))
}
if (state.terminated or trap_type(state, declaration.body)) return
finish(state, ir.no_value, ast.node_span(tree, declaration.body))
}
@hide:file
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
match ast.stmt_kind(tree, id) {
.invalid: {
invalid :: ast.invalid(tree, id)
trap(state, invalid.diagnostic, invalid.span)
}
.expression: _ = try lower_expression(state, ast.statement_expr(tree, id))
.declaration: {
if (trap_binding(state, id)) return
declaration :: ast.statement_decl(tree, id)
if (declaration.type) |annotation| if (trap_type(state, annotation)) return
value :: try lower_expression(state, declaration.initializer)
if (state.terminated or trap_type(state, id)) return
_ = try emit(state, ir.Instruction{
op = .store_local,
type = lower_type(typecheck.type_of(state.checked, id)),
left = value,
slot = state.storage[usize(id)].index,
span = declaration.span,
})
}
.return_stmt: {
statement :: ast.return_stmt(tree, id)
value := ir.no_value
if (statement.value) |expression| value = try lower_expression(state, expression)
if (state.terminated or trap_type(state, id)) return
finish(state, value, statement.span)
}
}
}
@hide:file
lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
span :: ast.node_span(tree, id)
type :: lower_type(typecheck.type_of(state.checked, id))
match ast.expr_kind(tree, id) {
.invalid: {
invalid :: ast.invalid(tree, id)
trap(state, invalid.diagnostic, invalid.span)
}
.literal_int, .literal_float: {
if (trap_type(state, id)) return ir.no_value
return try lower_number(state, id)
}
.literal_string: {
if (trap_type(state, id)) return ir.no_value
return try lower_string(state, id)
}
.identifier: {
if (trap_binding(state, id) or trap_type(state, id)) return ir.no_value
binding :: sema.binding(state.resolution, id)
location :: state.storage[usize(binding.data.declaration)]
match location.kind {
.function: return try constant(state, ir.Value{
type = .function,
data = ir.ValueData{ function = ir.FunctionId(location.index) },
}, span)
.global: return try emit(state, ir.Instruction{ op = .load_global, type = type, slot = location.index, span = span })
.local: return try emit(state, ir.Instruction{ op = .load_local, type = type, slot = location.index, span = span })
.none: unreachable
}
}
.unary: {
expression :: ast.unary_expr(tree, id)
if (type == .int and ast.expr_kind(tree, expression.operand) == .literal_int) {
text :: ast.source_text(tree, ast.node_span(tree, expression.operand))
if (integer_magnitude(text, u64(maxval!(i64)) + 1)) |magnitude| {
if (magnitude == u64(maxval!(i64)) + 1) {
return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = minval!(i64) } }, span)
}
}
}
operand :: try lower_expression(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{ op = .negate, type = type, left = operand, span = span })
}
.binary: {
expression :: ast.binary_expr(tree, id)
left :: try lower_expression(state, expression.left)
if (state.terminated) return ir.no_value
right :: try lower_expression(state, expression.right)
if (state.terminated or trap_type(state, id)) return ir.no_value
op ir.Op :: match expression.operator {
.add: .add
.subtract: .subtract
.multiply: .multiply
.divide: .divide
}
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
}
.call: {
callee :: try lower_expression(state, ast.call_expr(tree, id).callee)
if (state.terminated) return ir.no_value
# reserve this call's packed slice before nested calls append theirs.
block := &state.module.functions.items[state.function].blocks.items[0]
start :: block.arguments.items.len
count :: ast.call_arg_count(tree, id)
for (0..count) |_| try arraylist.append(&block.arguments, ir.no_value)
for (0..count) |i| {
argument :: try lower_expression(state, ast.call_arg(tree, id, i))
if (state.terminated) return ir.no_value
block.arguments.items[start + i] = argument
}
if (trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{
op = .call,
type = type,
left = callee,
arguments_start = start,
arguments_count = count,
span = span,
})
}
}
return ir.no_value
}
@hide:file
integer_magnitude proc(text []u8, limit u64) ?u64 {
value := u64(0)
for (text) |byte| {
if (byte < '0' or byte > '9') return null
digit :: u64(byte - '0')
if (value > divtrunc!(limit - digit, u64(10))) return null
value = value * 10 + digit
}
return value
}
@hide:file
lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id)
text :: ast.source_text(state.tree, span)
type :: lower_type(typecheck.type_of(state.checked, id))
if (type == .int) {
if (integer_magnitude(text, u64(maxval!(i64)))) |magnitude| {
return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = i64(magnitude) } }, span)
}
} else {
buffer :: try mem.alloc(u8, state.module.allocator, text.len + 1)
defer mem.free(state.module.allocator, buffer)
for (text) |byte, i| buffer[i] = byte
buffer[text.len] = 0
c.__error()?^ = 0
value :: f64(c.strtod(ptrcast!(buffer.ptr), null))
# libc reports ERANGE for representable subnormals as well as zero/infinity.
if (c.__error()?^ != c.ERANGE or (value > 0 and value < 1)) {
return try constant(state, ir.Value{ type = .float, data = ir.ValueData{ real = value } }, span)
}
}
trap(state, try diagnostics.add(state.diagnostics, .literal_out_of_range, span), span)
return ir.no_value
}
@hide:file
hex_digit proc(byte u8) ?u8 {
if (byte >= '0' and byte <= '9') return byte - '0'
if (byte >= 'a' and byte <= 'f') return byte - 'a' + 10
if (byte >= 'A' and byte <= 'F') return byte - 'A' + 10
return null
}
@hide:file
lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id)
text :: ast.source_text(state.tree, span)
owned :: try mem.alloc(u8, state.module.allocator, text.len - 2)
transferred := false
defer if (!transferred) mem.free(state.module.allocator, owned)
length := usize(0)
cursor := usize(1)
end :: text.len - 1
valid := true
while (cursor < end) {
byte := text[cursor]
cursor += 1
if (byte == '\\') {
if (cursor >= end) {
valid = false
break
}
escape :: text[cursor]
cursor += 1
match escape {
'\\': byte = '\\'
'"': byte = '"'
'n': byte = '\n'
'r': byte = '\r'
't': byte = '\t'
'0': byte = 0
'x': {
if (end - cursor < 2) {
valid = false
break
}
high := u8(0)
low := u8(0)
if (hex_digit(text[cursor])) |digit| high = digit
else {
valid = false
break
}
if (hex_digit(text[cursor + 1])) |digit| low = digit
else {
valid = false
break
}
byte = high * 16 + low
cursor += 2
}
else: {
valid = false
break
}
}
}
owned[length] = byte
length += 1
}
if (!valid) {
trap(state, try diagnostics.add(state.diagnostics, .invalid_escape, span), span)
return ir.no_value
}
# retain the complete allocation for freeing, but expose only decoded bytes.
try arraylist.append(&state.module.strings, owned)
transferred = true
return try constant(state, ir.Value{ type = .string, data = ir.ValueData{ string = owned[..length] } }, span)
}
+452
View File
@@ -0,0 +1,452 @@
import "@std/mem"
import "@std/arraylist"
import "@std/testing"
import "@source/diagnostics"
import "@source/strpool"
import "@source/lexer"
import "@source/parser"
import "@source/ast"
import "@source/ir"
import "@source/interpreter"
sema :: import "@source/sema"
typecheck :: import "@source/sema/check"
@hide:file
CompileError :: alias mem.AllocError | diagnostics.CapacityError | strpool.InternError | lexer.SourceError
# every consumer test executes only after all source and frontend storage is gone.
@hide:file
compile_owned proc(text []u8, diagnostic_store @mut diagnostics.Store) ir.Module ! CompileError {
source :: try mem.alloc(u8, mem.c_allocator, text.len)
defer mem.free(mem.c_allocator, source)
for (text) |byte, i| source[i] = byte
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, diagnostic_store)
defer sema.deinit(&resolution)
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, diagnostic_store)
defer typecheck.deinit(&checked)
return try lower(mem.c_allocator, &tree, &resolution, &checked, diagnostic_store)
}
@hide:file
expect_trap proc(
outcome interpreter.Outcome,
diagnostic_store @diagnostics.Store,
code diagnostics.Code,
) void ! testing.Error {
if (outcome.trap) |id| {
if (diagnostics.get(diagnostic_store, id)) |diagnostic| {
try testing.expect_equal(code, diagnostic.code)
return
}
}
return .expectation_failed
}
executes_calls_locals_arithmetic_and_nested_argument_slices test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`add proc(left int, right int) int {
` result :: left + right
` return result
`}
`double proc(value int) int {
` return value * 2
`}
`main proc() int {
` a :: add(double(4), add(5, 6))
` b :: add(10, 13)
` return a + b
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.int, outcome.value.type)
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
owns_decoded_strings_after_source_and_frontend_are_freed test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`text :: "a\\\"\n\r\t\0\x41\x7a"
`identity proc(value string) string {
` return value
`}
`main proc() string {
` return identity(text)
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.string, outcome.value.type)
try testing.expect(mem.eql(outcome.value.data.string, "a\\\"\n\r\t\x00Az"))
}
honors_float_context_and_float_division test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() float {
` left :: 3
` return -(left / 2) + 2.0
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.float, outcome.value.type)
try testing.expect_equal(f64(0.5), outcome.value.data.real)
}
executes_forward_lazy_globals_and_function_aliases test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`forward :: later + 1
`alias :: add
`later :: 18
`add proc(left int, right int) int {
` local_alias :: alias
` return left + right
`}
`main proc() int {
` invoke :: alias
` return invoke(forward, 23)
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
again :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(again.trap == null)
try testing.expect_equal(i64(42), again.value.data.integer)
}
supports_bare_void_value_returns_and_fallthrough test {
sources [3][]u8 :: [
"main proc() void {\n return\n}\n",
"noop proc() void {}\nmain proc() void {\n return noop()\n}\n",
"noop proc() void {}\nmain proc() void {\n noop()\n}\n",
]
for (sources) |source| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.void, outcome.value.type)
}
}
preserves_demanded_diagnostic_identity_and_source_location test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`bad proc() int {
` return unknown
`}
`main proc() int {
` before :: 1 + 2
` return bad()
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(diagnostics.Code.unknown_name, diagnostic.code)
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "unknown"))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
skips_unreachable_and_unused_invalid_code test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`bad proc() int {
` return unknown
`}
`unused :: "\q"
`main proc() int {
` return 42
` 9223372036854775807 + 1
` missing
` huge :: 9223372036854775808
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect(diagnostics.count(&diagnostic_store) > 0)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
traps_local_forward_loads_and_global_initialization_cycles test {
sources [3][]u8 :: [
"main proc() int {\n before :: after\n after :: 42\n return before\n}\n",
"left int :: right\nright int :: left\nmain proc() int {\n return left\n}\n",
"self int :: self\nmain proc() int {\n return self\n}\n",
]
for (sources) |source, i| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
code diagnostics.Code :: if (i == 0) .read_before_initialization else .initialization_cycle
try expect_trap(outcome, &diagnostic_store, code)
}
}
handles_integer_boundaries_without_host_overflow test {
sources [3][]u8 :: [
"main proc() int {\n return 9223372036854775807\n}\n",
"main proc() int {\n return -9223372036854775808\n}\n",
"main proc() int {\n return -0009223372036854775808\n}\n",
]
expected [3]i64 :: [maxval!(i64), minval!(i64), minval!(i64)]
for (sources) |source, i| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(expected[i], outcome.value.data.integer)
}
}
traps_invalid_literals_escapes_and_checked_integer_arithmetic test {
sources [8][]u8 :: [
"main proc() int {\n return 9223372036854775808\n}\n",
"main proc() int {\n return -9223372036854775809\n}\n",
"main proc() int {\n return 9223372036854775807 + 1\n}\n",
"main proc() int {\n return -(-9223372036854775808)\n}\n",
"main proc() int {\n return -9223372036854775808 - 1\n}\n",
"main proc() int {\n return 9223372036854775807 * 2\n}\n",
"main proc() string {\n return \"\\q\"\n}\n",
"main proc() string {\n return \"\\x0g\"\n}\n",
]
for (sources) |source, i| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
code := diagnostics.Code.integer_overflow
if (i < 2) code = .literal_out_of_range
if (i >= 6) code = .invalid_escape
try expect_trap(outcome, &diagnostic_store, code)
}
}
validates_main_and_preserves_first_declaration test {
sources [4][]u8 :: [
"other proc() void {}\n",
"main :: 42\n",
"main proc(value int) int {\n return value\n}\n",
"main proc() int {\n return 42\n}\nmain proc() int {\n return 7\n}\n",
]
for (sources) |source, i| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
if (i == 3) {
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
} else {
code diagnostics.Code :: if (i == 0) .missing_entry else .invalid_entry
try expect_trap(outcome, &diagnostic_store, code)
try testing.expect_equal(module.entry_diagnostic, outcome.trap?)
}
}
}
invalid_signatures_trap_without_poisoning_unrelated_main test {
sources [2][]u8 :: [
"broken proc(value void) void {}\nmain proc() int {\n return 42\n}\n",
"main proc() Unknown {\n return 42\n}\n",
]
for (sources) |source, i| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect(diagnostics.count(&diagnostic_store) > 0)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
if (i == 0) {
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
} else {
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
try expect_trap(outcome, &diagnostic_store, .unknown_name)
}
}
}
parses_float_context_without_integer_range_limits_and_rejects_float_overflow test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(
"main proc() float {\n return 18446744073709551616\n}\n",
&diagnostic_store,
)
defer ir.deinit(&module)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.float, outcome.value.type)
try testing.expect_equal(f64(18446744073709551616.0), outcome.value.data.real)
source := arraylist.init(u8, mem.c_allocator)
defer arraylist.deinit(&source)
for ("main proc() float {\n return ") |byte| try arraylist.append(&source, byte)
for (usize(0)..usize(400)) |_| try arraylist.append(&source, '9')
for (".0\n}\n") |byte| try arraylist.append(&source, byte)
oversized := try compile_owned(source.items, &diagnostic_store)
defer ir.deinit(&oversized)
trapped :: try interpreter.run(mem.c_allocator, &oversized, &diagnostic_store)
try expect_trap(trapped, &diagnostic_store, .literal_out_of_range)
}
accepts_representable_subnormals_and_rejects_underflow_to_zero test {
zeros [2]usize :: [307, 400]
for (zeros) |count| {
source := arraylist.init(u8, mem.c_allocator)
defer arraylist.deinit(&source)
for ("main proc() float {\n return 0.") |byte| try arraylist.append(&source, byte)
for (usize(0)..count) |_| try arraylist.append(&source, '0')
for ("1\n}\n") |byte| try arraylist.append(&source, byte)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source.items, &diagnostic_store)
defer ir.deinit(&module)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
if (count == 307) {
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.float, outcome.value.type)
try testing.expect(outcome.value.data.real > 0 and outcome.value.data.real < 1)
} else {
try expect_trap(outcome, &diagnostic_store, .literal_out_of_range)
}
}
}
@hide:file
AllocationState :: struct {
remaining usize
live usize = 0
}
@hide:file
limited_alloc proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
if (context) |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (state.remaining == 0) return null
state.remaining -= 1
memory :: mem.raw_alloc(mem.c_allocator, size, alignment)
if (memory) |_| state.live += 1
return memory
}
unreachable
}
@hide:file
limited_realloc proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
if (context) |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (state.remaining == 0) return null
state.remaining -= 1
resized :: mem.raw_realloc(mem.c_allocator, memory, old_size, new_size, alignment)
if (resized) |_| {
if (memory) |_| {} else state.live += 1
}
return resized
}
unreachable
}
@hide:file
limited_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
if (context) |userdata| {
state @mut AllocationState :: ptrcast!(userdata)
if (memory) |_| state.live -= 1
mem.raw_free(mem.c_allocator, memory, size, alignment)
return
}
unreachable
}
@hide:file
limited_vtable :: mem.AllocatorVTable{ alloc = limited_alloc, realloc = limited_realloc, free = limited_free }
releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`text :: "owned\nstring"
`factor float :: 3.5
`identity proc(value string) string {
` local :: value
` return local
`}
`main proc() string {
` computed float :: factor * 2
` return identity(text)
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
defer sema.deinit(&resolution)
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
defer typecheck.deinit(&checked)
succeeded := false
failures := usize(0)
for (usize(0)..usize(128)) |limit| {
state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
module := lower(allocator, &tree, &resolution, &checked, &diagnostic_store) catch |error| {
try testing.expect(error == .out_of_memory)
failures += 1
try testing.expect_equal(usize(0), state.live)
continue
}
outcome :: interpreter.run(mem.c_allocator, &module, &diagnostic_store) catch |error| {
ir.deinit(&module)
return error
}
ir.deinit(&module)
try testing.expect(outcome.trap == null)
try testing.expect_equal(usize(0), state.live)
succeeded = true
break
}
try testing.expect(succeeded)
try testing.expect(failures > 0)
}
+195 -51
View File
@@ -1,5 +1,8 @@
import "@std/debug" import "@ffi/c"
import "@std/mem" import "@std/mem"
import "@std/arraylist"
import "@std/io"
import "@std/process"
import "@source/diagnostics" import "@source/diagnostics"
import "@source/strpool" import "@source/strpool"
@@ -7,8 +10,9 @@ import "@source/lexer"
import "@source/ast" import "@source/ast"
import "@source/parser" import "@source/parser"
import "@source/sema" import "@source/sema"
import "@source/ir"
ast_renderer :: import "@source/ast/renderer" import "@source/lower"
import "@source/interpreter"
typecheck :: import "@source/sema/check" typecheck :: import "@source/sema/check"
test import "@std/mem" test import "@std/mem"
@@ -21,58 +25,198 @@ test import "@source/diagnostics"
test import "@source/strpool" test import "@source/strpool"
test import "@source/lexer" test import "@source/lexer"
test import "@source/ast" test import "@source/ast"
test import "@source/ast/renderer"
test import "@source/parser" test import "@source/parser"
test import "@source/sema" test import "@source/sema"
test import "@source/sema/check" test import "@source/sema/check"
test import "@source/lower"
test import "@source/interpreter"
@hide:file @hide:file
program :: DriverError :: alias (
`# literals mem.AllocError
`x float :: 123.9 | diagnostics.CapacityError
`y int := 43 | lexer.SourceError
`z :: "hello" | strpool.InternError
` | io.OpenError
`a := 1 + 2 * 3 | io.CloseError
`b :: -3 / 2 + 1 | io.ReadError
` | io.WriteError
`grouped proc(left, right int, extra float) void { )
` value int :: later + left
` later int :: right
` empty()
` grouped(value, later, extra)
`}
`empty proc() void {}
main proc() void! { main proc(init process.Init) i32 {
strpool.strings = strpool.init(mem.c_allocator) return command(mem.c_allocator, init.io) catch |err| {
defer strpool.deinit(&strpool.strings) if (process.argument(2)) |path| {
io.print("{s}: infrastructure error: {s}\n", io.stderr(init.io), {
diagnostic_store := diagnostics.init(mem.c_allocator) argument_text(path), infrastructure_message(err),
defer diagnostics.deinit(&diagnostic_store) }) catch |_| {}
} else {
scan_state := lexer.init(mem.c_allocator) io.print("honey: infrastructure error: {s}\n", io.stderr(init.io), {
defer lexer.deinit(&scan_state) infrastructure_message(err),
try lexer.scan(&scan_state, program, &diagnostic_store) }) catch |_| {}
}
tree := try parser.parse( return 2
mem.c_allocator, }
scan_state.tokens.items, }
program,
&diagnostic_store, @hide:file
) infrastructure_message proc(error DriverError) []u8 {
defer ast.deinit(&tree) return match error {
.out_of_memory: "out of memory"
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) .diagnostic_limit_reached: "diagnostic limit reached"
defer sema.deinit(&resolution) .source_too_large: "source file is too large"
.out_of_space: "identifier storage is full"
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, &diagnostic_store) .open_failed: "cannot open source file"
defer typecheck.deinit(&checked) .close_failed: "cannot close source file"
.not_open_for_reading: "source file is not readable"
renderer := ast_renderer.init(mem.c_allocator) .read_failed: "cannot read source file"
defer ast_renderer.deinit(&renderer) .not_open_for_writing: "output is not writable"
try ast_renderer.render(&renderer, &tree) .write_failed: "cannot write output"
.no_progress: "output writer made no progress"
debug.print("{s}", {renderer.bytes.items}) }
debug.print("diagnostics: {}\n", {diagnostics.count(&diagnostic_store)}) }
@hide:file
command proc(allocator mem.Allocator, runtime io.Io) i32 ! DriverError {
if (process.argument_count() != 3) {
try io.print("usage: honey run <file>\n", io.stderr(runtime), {})
return 2
}
if (process.argument(1)) |operation| {
if (!mem.eql(argument_text(operation), "run")) {
try io.print("usage: honey run <file>\n", io.stderr(runtime), {})
return 2
}
} else {
return 2
}
if (process.argument(2)) |path| return try run_file(allocator, runtime, path)
return 2
}
@hide:file
argument_text proc(pointer [*;0]u8) []u8 {
length := usize(0)
while (pointer[length] != 0) length += 1
return pointer[..length]
}
@hide:file
read_source proc(allocator mem.Allocator, runtime io.Io, path [*;0]u8) arraylist.ArrayList(u8) ! DriverError {
# bootstrap cannot construct a dynamic sentinel slice from argv's sentinel pointer.
descriptor := c_int(-1)
while true {
descriptor = c.open(ptrcast!(path), c.O_RDONLY)
if (descriptor >= 0) break
if (c.__error()?^ != c.EINTR) return .open_failed
}
file :: io.File{ io = runtime, handle = io.Handle{ file_desc = descriptor } }
closing := false
defer {
if (!closing) io.close(file) catch |_| {}
}
bytes := arraylist.init(u8, allocator)
errdefer arraylist.deinit(&bytes)
input :: io.reader(file)
maximum :: usize(maxval!(u32))
while true {
length :: bytes.items.len
if (length == maximum) {
probe [1]mut u8 := undefined
if (try io.read(input, (&probe)[..]) != 0) return .source_too_large
break
}
if (length == bytes.capacity) {
growth := usize(4096)
if (growth > maximum - length) growth = maximum - length
try arraylist.reserve(&bytes, length + growth)
}
storage :: bytes.items.ptr[..bytes.capacity]
end := bytes.capacity
if (end > maximum) end = maximum
count :: try io.read(input, storage[length..end])
if (count == 0) break
bytes.items = storage[..length + count]
}
closing = true
try io.close(file)
return bytes
}
@hide:file
compile proc(allocator mem.Allocator, source []u8, diagnostic_store @mut diagnostics.Store) ir.Module ! DriverError {
strpool.strings = strpool.init(allocator)
defer strpool.deinit(&strpool.strings)
scan_state := lexer.init(allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, diagnostic_store)
tree := try parser.parse(allocator, scan_state.tokens.items, source, diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(allocator, &tree, diagnostic_store)
defer sema.deinit(&resolution)
checked := try typecheck.check(allocator, &tree, &resolution, diagnostic_store)
defer typecheck.deinit(&checked)
return try lower.lower(allocator, &tree, &resolution, &checked, diagnostic_store)
}
@hide:file
run_file proc(allocator mem.Allocator, runtime io.Io, path [*;0]u8) i32 ! DriverError {
source := try read_source(allocator, runtime, path)
defer arraylist.deinit(&source)
diagnostic_store := diagnostics.init(allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile(allocator, source.items, &diagnostic_store)
defer ir.deinit(&module)
outcome :: try interpreter.run(allocator, &module, &diagnostic_store)
if (outcome.trap == null) try print_result(io.stdout(runtime), outcome.value)
try report_diagnostics(allocator, io.stderr(runtime), argument_text(path), source.items, &diagnostic_store, outcome.trap)
if (outcome.trap != null or diagnostics.count(&diagnostic_store) != 0) return 1
return 0
}
@hide:file
print_result proc(output io.Writer, value ir.Value) void ! io.WriteError {
match value.type {
.int: try io.print("{}\n", output, {value.data.integer})
.float: try io.print("{d}\n", output, {value.data.real})
.string: try io.print("{s}\n", output, {value.data.string})
.void: {}
else: unreachable
}
}
@hide:file
report_diagnostics proc(
allocator mem.Allocator,
output io.Writer,
path []u8,
source []u8,
diagnostic_store @diagnostics.Store,
trap ?diagnostics.DiagnosticId,
) void ! (mem.AllocError | io.WriteError) {
if (diagnostics.count(diagnostic_store) == 0) return
starts := arraylist.init(usize, allocator)
defer arraylist.deinit(&starts)
try arraylist.append(&starts, usize(0))
for (source) |byte, index| {
if (byte == '\n') try arraylist.append(&starts, index + 1)
}
for (0..diagnostics.count(diagnostic_store)) |index| {
if (diagnostics.get(diagnostic_store, diagnostics.DiagnosticId(index))) |diagnostic| {
offset :: usize(diagnostic.span.start)
line := usize(0)
end := starts.items.len
while (line + 1 < end) {
middle :: line + divtrunc!(end - line, 2)
if (starts.items[middle] <= offset) line = middle
else end = middle
}
label []u8 := "error"
if (trap) |id| {
if (id == diagnostics.DiagnosticId(index)) label = "runtime trap"
}
try io.print("{s}:{}:{}: {s}: {s}\n", output, {
path, line + 1, offset - starts.items[line] + 1, label, diagnostics.message(diagnostic.code),
})
}
}
} }
+20
View File
@@ -179,6 +179,7 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
return try parse_statement_decl(state) return try parse_statement_decl(state)
} }
match current(state).kind { match current(state).kind {
.return: return try parse_return(state)
.ident, .int, .float, .string, .minus, .open_paren: { .ident, .int, .float, .string, .minus, .open_paren: {
expression :: try parse_expr(state) expression :: try parse_expr(state)
return try ast.add_expression_stmt(&state.builder, expression) 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 @hide:file
parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError { parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
name :: try consume(state, .ident) name :: try consume(state, .ident)
+45
View File
@@ -285,3 +285,48 @@ recovers_malformed_calls_and_preserves_siblings test {
try testing.expect_equal("tail", ast.source_text(&tree, tail.name)) try testing.expect_equal("tail", ast.source_text(&tree, tail.name))
try testing.expect_equal(0, ast.call_arg_count(&tree, tail.initializer)) try testing.expect_equal(0, ast.call_arg_count(&tree, tail.initializer))
} }
parses_return_boundaries_and_recovers_trailing_tokens test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`return 1
`main proc() void {
` return
` 2
` return (3 + 4)
` return 5 stray
` after :: 6
` return }
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(2, ast.package_decl_count(&tree))
try testing.expect_equal(ast.DeclKind.invalid, ast.decl_kind(&tree, ast.package_decl(&tree, 0)))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body
try testing.expect_equal(6, ast.block_statement_count(&tree, body))
first :: ast.block_statement(&tree, body, 0)
try testing.expect_equal(ast.StmtKind.return_stmt, ast.stmt_kind(&tree, first))
try testing.expect(ast.return_stmt(&tree, first).value == null)
try testing.expect_equal("return", ast.source_text(&tree, ast.return_stmt(&tree, first).span))
try testing.expect_equal(ast.StmtKind.expression, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 1)))
grouped :: ast.return_stmt(&tree, ast.block_statement(&tree, body, 2))
try testing.expect_equal("return (3 + 4)", ast.source_text(&tree, grouped.span))
if (grouped.value) |value| {
try testing.expect_equal(ast.ExprKind.binary, ast.expr_kind(&tree, value))
} else {
return .expectation_failed
}
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 3)))
after :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 4))
try testing.expect_equal("after", ast.source_text(&tree, after.name))
last :: ast.return_stmt(&tree, ast.block_statement(&tree, body, 5))
try testing.expect(last.value == null)
try testing.expect_equal("return", ast.source_text(&tree, last.span))
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
}
+40 -1
View File
@@ -27,7 +27,7 @@ Result :: struct { storage Storage }
no_index :: maxval!(usize) no_index :: maxval!(usize)
@hide:file @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 @hide:file
Cell :: struct { Cell :: struct {
@@ -40,6 +40,7 @@ Cell :: struct {
demand TypeKind = .none demand TypeKind = .none
fixed bool = false fixed bool = false
first_edge usize = no_index first_edge usize = no_index
return_annotation ast.NodeId = ast.NodeId(maxval!(u32))
} }
@hide:file @hide:file
@@ -194,6 +195,11 @@ check proc(
try demand(&state, declaration.initializer, types[usize(annotation)], .type_mismatch) 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 if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue
signature :: group_type(&state, ast.call_expr(tree, id).callee) signature :: group_type(&state, ast.call_expr(tree, id).callee)
if (signature.function == ast.NodeId(maxval!(u32))) continue 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) { match ast.stmt_kind(tree, statement) {
.declaration: try collect_declaration(state, statement) .declaration: try collect_declaration(state, statement)
.expression: try collect_expression(state, ast.statement_expr(tree, 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) .invalid: collect_invalid(state, statement)
} }
source :: if (ast.stmt_kind(tree, statement) == .expression) ast.statement_expr(tree, statement) else 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) function :: ast.function_decl(tree, id)
result :: state.types[usize(function.return_type)] result :: state.types[usize(function.return_type)]
if (result.kind == .void or result.kind == .invalid or state.types[usize(id)].kind == .invalid) return 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)) { if (syntactically_valid_body(tree, function.body)) {
diagnostic :: try diagnostics.add(state.diagnostics, .missing_return, ast.node_span(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) { 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: { .expression: {
match ast.expr_kind(tree, id) { match ast.expr_kind(tree, id) {
.unary: { .unary: {
@@ -644,6 +678,11 @@ syntactically_valid_body proc(tree @ast.Tree, id ast.NodeId) bool {
.expression: { .expression: {
if (!syntactically_valid_expression(tree, ast.statement_expr(tree, statement))) return false 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: { .declaration: {
if (!syntactically_valid_expression(tree, ast.statement_decl(tree, statement).initializer)) return false if (!syntactically_valid_expression(tree, ast.statement_decl(tree, statement).initializer)) return false
} }
+125
View File
@@ -606,3 +606,128 @@ preserves_existing_syntax_name_and_duplicate_ids_while_checking_siblings test {
try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand)) try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(previous_count + 3, diagnostics.count(&diagnostic_store)) try testing.expect_equal(previous_count + 3, diagnostics.count(&diagnostic_store))
} }
returns_supply_numeric_context_and_resolve_forward_local_and_parameter_names test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`fraction proc() float {
` return -later / 2
` later := seed
`}
`identity proc(value int) int { return value }
`wrapper proc() void { return noop() }
`noop proc() void { return }
`text proc() string { return "honey" }
`seed :: 3
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
defer sema.deinit(&resolution)
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
defer deinit(&result)
expected [5]TypeKind :: [.float, .int, .void, .void, .string]
for (expected) |kind, index| {
function :: ast.package_decl(&tree, index)
view :: ast.function_decl(&tree, function)
try expect_kind(&result, function, .function)
try expect_kind(&result, view.return_type, kind)
statement :: ast.block_statement(&tree, view.body, 0)
try expect_kind(&result, statement, .void)
if (ast.return_stmt(&tree, statement).value) |value| try expect_kind(&result, value, kind)
}
fraction :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
local :: ast.block_statement(&tree, fraction.body, 1)
try expect_kind(&result, local, .float)
try expect_kind(&result, ast.package_decl(&tree, 5), .float)
if (ast.return_stmt(&tree, ast.block_statement(&tree, fraction.body, 0)).value) |value| {
division :: ast.binary_expr(&tree, value)
negate :: ast.unary_expr(&tree, division.left)
try expect_kind(&result, division.left, .float)
try expect_kind(&result, division.right, .float)
try testing.expect_equal(local, sema.binding(&resolution, negate.operand).data.declaration)
} else {
return .expectation_failed
}
identity :: ast.package_decl(&tree, 1)
identity_body :: ast.function_decl(&tree, identity).body
if (ast.return_stmt(&tree, ast.block_statement(&tree, identity_body, 0)).value) |value| {
try testing.expect_equal(ast.function_param(&tree, identity, 0), sema.binding(&resolution, value).data.declaration)
} else {
return .expectation_failed
}
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
return_errors_preserve_identity_check_unreachable_code_and_suppress_missing_return_cascades test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`bare proc() int { return }
`wrong proc() int { return "text" }
`void_value proc() void { return 1 }
`unknown proc() int { return absent }
`syntax proc() int { return (1 }
`unreachable proc() int {
` return 7
` -"bad"
`}
`fallthrough proc() int {}
`recovered proc() int {
` return 1 stray
` valid :: 2
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
defer sema.deinit(&resolution)
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
defer deinit(&result)
for (usize(0)..usize(3)) |index| {
function :: ast.package_decl(&tree, index)
view :: ast.function_decl(&tree, function)
try expect_kind(&result, function, .function)
statement :: ast.block_statement(&tree, view.body, 0)
returned :: ast.return_stmt(&tree, statement)
span :: if (returned.value) |value| ast.node_span(&tree, value) else returned.span
diagnostic :: try expect_error(&result, &diagnostic_store, statement, .return_type_mismatch, span)
if (returned.value) |value| try testing.expect_equal(diagnostic, type_of(&result, value).diagnostic)
}
for (usize(3)..usize(5)) |index| {
view :: ast.function_decl(&tree, ast.package_decl(&tree, index))
statement :: ast.block_statement(&tree, view.body, 0)
if (ast.return_stmt(&tree, statement).value) |value| {
original :: type_of(&result, value).diagnostic
try testing.expect(original != diagnostics.no_diagnostic)
try testing.expect_equal(original, type_of(&result, statement).diagnostic)
try testing.expect_equal(original, type_of(&result, view.body).diagnostic)
} else {
return .expectation_failed
}
}
unreachable_body :: ast.function_decl(&tree, ast.package_decl(&tree, 5)).body
try expect_kind(&result, ast.block_statement(&tree, unreachable_body, 0), .void)
bad :: ast.statement_expr(&tree, ast.block_statement(&tree, unreachable_body, 1))
_ = try expect_error(&result, &diagnostic_store, bad, .invalid_operand, ast.node_span(&tree, bad))
missing_body :: ast.function_decl(&tree, ast.package_decl(&tree, 6)).body
_ = try expect_error(&result, &diagnostic_store, missing_body, .missing_return, ast.node_span(&tree, missing_body))
recovered_body :: ast.function_decl(&tree, ast.package_decl(&tree, 7)).body
try expect_kind(&result, ast.block_statement(&tree, recovered_body, 1), .int)
try testing.expect_equal(3, code_count(&diagnostic_store, .return_type_mismatch))
try testing.expect_equal(1, code_count(&diagnostic_store, .missing_return))
try testing.expect_equal(1, code_count(&diagnostic_store, .unknown_name))
try testing.expect_equal(2, code_count(&diagnostic_store, .unexpected_token))
try testing.expect_equal(8, diagnostics.count(&diagnostic_store))
}
+3
View File
@@ -187,6 +187,9 @@ resolve_function proc(
match ast.stmt_kind(tree, statement) { match ast.stmt_kind(tree, statement) {
.declaration: try resolve_statement_decl(state, &scope, statement) .declaration: try resolve_statement_decl(state, &scope, statement)
.expression: try resolve_expr(state, &scope, ast.statement_expr(tree, 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) .invalid: resolve_invalid(state, statement)
} }
} }
+1 -1
View File
@@ -392,7 +392,7 @@ write_float proc($T type, output Writer, value T, scientific bool) void ! WriteE
match typeinfo!(T) { match typeinfo!(T) {
.float: { .float: {
buffer [64]mut u8 := undefined buffer [64]mut u8 := undefined
count := 0 count c_int := 0
if sizeof!(T) == 4 { if sizeof!(T) == 4 {
if (scientific) count = c.snprintf(ptrcast!((&buffer).ptr), c_ulong(buffer.len), "%.8e", value) if (scientific) count = c.snprintf(ptrcast!((&buffer).ptr), c_ulong(buffer.len), "%.8e", value)
else count = c.snprintf(ptrcast!((&buffer).ptr), c_ulong(buffer.len), "%.9g", value) else count = c.snprintf(ptrcast!((&buffer).ptr), c_ulong(buffer.len), "%.9g", value)
+16
View File
@@ -0,0 +1,16 @@
# the bootstrap Init currently exposes only io; macOS provides process-lifetime argv storage.
@hide:file
_NSGetArgc c_func() @c_int
@hide:file
_NSGetArgv c_func() @*[*;0]u8
argument_count proc() usize {
return usize(_NSGetArgc()^)
}
#! returns a borrowed, zero-terminated argument; storage lasts for the process lifetime.
argument proc(index usize) ?[*;0]u8 {
if (index >= argument_count()) return null
return _NSGetArgv()^[index]
}