feat: lower typed programs and interpret IR
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import "@std/mem"
|
|
||||||
import "@std/testing"
|
|
||||||
import "@source/ast"
|
|
||||||
|
|
||||||
renders_semantic_debug_view test {
|
|
||||||
builder := ast.init_builder(mem.c_allocator, "value :: 42")
|
|
||||||
defer ast.deinit_builder(&builder)
|
|
||||||
value :: try ast.add_literal(&builder, .int, ast.Span{ start = 9, end = 11 })
|
|
||||||
declaration :: try ast.add_statement_decl(
|
|
||||||
&builder,
|
|
||||||
ast.Span{ start = 0, end = 11 },
|
|
||||||
ast.Span{ start = 0, end = 5 },
|
|
||||||
null,
|
|
||||||
.constant,
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
declarations [1]ast.NodeId :: [declaration]
|
|
||||||
package :: try ast.add_package(&builder, ast.Span{ start = 0, end = 11 }, declarations[..])
|
|
||||||
tree := ast.finish(&builder, package)
|
|
||||||
defer ast.deinit(&tree)
|
|
||||||
renderer := init(mem.c_allocator)
|
|
||||||
defer deinit(&renderer)
|
|
||||||
|
|
||||||
try render(&renderer, &tree)
|
|
||||||
|
|
||||||
try testing.expect(mem.eql(u8, "package\n declaration value\n", renderer.bytes.items))
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,14 @@ Code :: enum {
|
|||||||
argument_count_mismatch,
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import "@std/mem"
|
||||||
|
import "@std/arraylist"
|
||||||
|
import "@source/ir"
|
||||||
|
import "@source/diagnostics"
|
||||||
|
|
||||||
|
Outcome :: struct {
|
||||||
|
value ir.Value = ir.Value{}
|
||||||
|
trap ?diagnostics.DiagnosticId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
Slot :: struct {
|
||||||
|
initialized bool = false
|
||||||
|
value ir.Value = ir.Value{}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
GlobalStatus :: enum { uninitialized, initializing, ready }
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
GlobalState :: struct {
|
||||||
|
status GlobalStatus = .uninitialized
|
||||||
|
value ir.Value = ir.Value{}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
Frame :: struct {
|
||||||
|
function ir.FunctionId
|
||||||
|
next_instruction usize = 0
|
||||||
|
destination ir.ValueId = ir.no_value
|
||||||
|
global ?usize = null
|
||||||
|
locals []mut Slot
|
||||||
|
values []mut ir.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
#! executes only typed IR; invocation and global state are temporary and released on every exit.
|
||||||
|
#! returned strings borrow `module`; runtime source errors are traps, not infrastructure errors.
|
||||||
|
run proc(
|
||||||
|
allocator mem.Allocator,
|
||||||
|
module @ir.Module,
|
||||||
|
diagnostic_store @mut diagnostics.Store,
|
||||||
|
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
entry ir.FunctionId := ir.FunctionId(0)
|
||||||
|
if module.entry |function| {
|
||||||
|
entry = function
|
||||||
|
} else {
|
||||||
|
return Outcome{ trap = module.entry_diagnostic }
|
||||||
|
}
|
||||||
|
|
||||||
|
globals :: try mem.alloc(GlobalState, allocator, module.globals.items.len)
|
||||||
|
defer mem.free(allocator, globals)
|
||||||
|
mem.set(globals, GlobalState{})
|
||||||
|
frames := arraylist.init(Frame, allocator)
|
||||||
|
defer {
|
||||||
|
for (frames.items) |frame| free_frame(allocator, frame)
|
||||||
|
arraylist.deinit(&frames)
|
||||||
|
}
|
||||||
|
try push_frame(allocator, module, &frames, entry, ir.no_value, null)
|
||||||
|
|
||||||
|
while frames.items.len != 0 {
|
||||||
|
frame_index :: frames.items.len - 1
|
||||||
|
function :: &module.functions.items[usize(frames.items[frame_index].function)]
|
||||||
|
block :: &function.blocks.items[usize(function.entry)]
|
||||||
|
instruction_index :: frames.items[frame_index].next_instruction
|
||||||
|
if instruction_index == block.instructions.items.len {
|
||||||
|
terminator :: block.terminator
|
||||||
|
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
|
||||||
|
value := ir.Value{}
|
||||||
|
if terminator.value != ir.no_value {
|
||||||
|
value = frames.items[frame_index].values[usize(terminator.value)]
|
||||||
|
}
|
||||||
|
completed :: frames.items[frame_index]
|
||||||
|
_ = arraylist.pop(&frames)
|
||||||
|
free_frame(allocator, completed)
|
||||||
|
if completed.global |global_index| {
|
||||||
|
globals[global_index].value = value
|
||||||
|
globals[global_index].status = .ready
|
||||||
|
}
|
||||||
|
if (frames.items.len == 0) return Outcome{ value = value }
|
||||||
|
if completed.destination != ir.no_value {
|
||||||
|
frames.items[frames.items.len - 1].values[usize(completed.destination)] = value
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
instruction :: &block.instructions.items[instruction_index]
|
||||||
|
frames.items[frame_index].next_instruction += 1
|
||||||
|
value := ir.Value{}
|
||||||
|
match instruction.op {
|
||||||
|
.constant: value = instruction.constant
|
||||||
|
.load_local: {
|
||||||
|
local :: &frames.items[frame_index].locals[usize(instruction.slot)]
|
||||||
|
if !local.initialized {
|
||||||
|
return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span)
|
||||||
|
}
|
||||||
|
value = local.value
|
||||||
|
}
|
||||||
|
.store_local: {
|
||||||
|
frames.items[frame_index].locals[usize(instruction.slot)] = Slot{
|
||||||
|
initialized = true,
|
||||||
|
value = frames.items[frame_index].values[usize(instruction.left)],
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
.load_global: {
|
||||||
|
global_index :: usize(instruction.slot)
|
||||||
|
match globals[global_index].status {
|
||||||
|
.ready: value = globals[global_index].value
|
||||||
|
.initializing: {
|
||||||
|
return try runtime_trap(diagnostic_store, .initialization_cycle, instruction.span)
|
||||||
|
}
|
||||||
|
.uninitialized: {
|
||||||
|
try push_frame(
|
||||||
|
allocator,
|
||||||
|
module,
|
||||||
|
&frames,
|
||||||
|
module.globals.items[global_index].initializer,
|
||||||
|
instruction.destination,
|
||||||
|
global_index,
|
||||||
|
)
|
||||||
|
globals[global_index].status = .initializing
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.call: {
|
||||||
|
callee :: frames.items[frame_index].values[usize(instruction.left)].data.function
|
||||||
|
try push_frame(allocator, module, &frames, callee, instruction.destination, null)
|
||||||
|
# frame indices survive stack growth; pointers into frames.items do not.
|
||||||
|
for (0..instruction.arguments_count) |argument_index| {
|
||||||
|
argument :: block.arguments.items[instruction.arguments_start + argument_index]
|
||||||
|
frames.items[frame_index + 1].locals[argument_index] = Slot{
|
||||||
|
initialized = true,
|
||||||
|
value = frames.items[frame_index].values[usize(argument)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
.negate: {
|
||||||
|
operand :: frames.items[frame_index].values[usize(instruction.left)]
|
||||||
|
if instruction.type == .float {
|
||||||
|
value = ir.Value{ type = .float, data = ir.ValueData{ real = -operand.data.real } }
|
||||||
|
} else {
|
||||||
|
if operand.data.integer == minval!(i64) {
|
||||||
|
return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
|
||||||
|
}
|
||||||
|
value = ir.Value{ type = .int, data = ir.ValueData{ integer = -operand.data.integer } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.add, .subtract, .multiply, .divide: {
|
||||||
|
left :: frames.items[frame_index].values[usize(instruction.left)]
|
||||||
|
right :: frames.items[frame_index].values[usize(instruction.right)]
|
||||||
|
if instruction.type == .float {
|
||||||
|
result f64 := 0
|
||||||
|
match instruction.op {
|
||||||
|
.add: result = left.data.real + right.data.real
|
||||||
|
.subtract: result = left.data.real - right.data.real
|
||||||
|
.multiply: result = left.data.real * right.data.real
|
||||||
|
.divide: result = left.data.real / right.data.real
|
||||||
|
else: unreachable
|
||||||
|
}
|
||||||
|
value = ir.Value{ type = .float, data = ir.ValueData{ real = result } }
|
||||||
|
} else {
|
||||||
|
result :: checked_integer(instruction.op, left.data.integer, right.data.integer)
|
||||||
|
if result |result_integer| {
|
||||||
|
value = ir.Value{ type = .int, data = ir.ValueData{ integer = result_integer } }
|
||||||
|
} else {
|
||||||
|
return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if instruction.destination != ir.no_value {
|
||||||
|
frames.items[frame_index].values[usize(instruction.destination)] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
push_frame proc(
|
||||||
|
allocator mem.Allocator,
|
||||||
|
module @ir.Module,
|
||||||
|
frames @mut arraylist.ArrayList(Frame),
|
||||||
|
function ir.FunctionId,
|
||||||
|
destination ir.ValueId,
|
||||||
|
global ?usize,
|
||||||
|
) void ! mem.AllocError {
|
||||||
|
signature :: &module.functions.items[usize(function)]
|
||||||
|
locals :: try mem.alloc(Slot, allocator, signature.locals.items.len)
|
||||||
|
errdefer mem.free(allocator, locals)
|
||||||
|
mem.set(locals, Slot{})
|
||||||
|
values :: try mem.alloc(ir.Value, allocator, signature.value_count)
|
||||||
|
errdefer mem.free(allocator, values)
|
||||||
|
# typed IR assigns each temporary before use; only local slots need initialization flags.
|
||||||
|
try arraylist.append(frames, Frame{
|
||||||
|
function = function,
|
||||||
|
destination = destination,
|
||||||
|
global = global,
|
||||||
|
locals = locals,
|
||||||
|
values = values,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
free_frame proc(allocator mem.Allocator, frame Frame) void {
|
||||||
|
mem.free(allocator, frame.locals)
|
||||||
|
mem.free(allocator, frame.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
runtime_trap proc(
|
||||||
|
diagnostic_store @mut diagnostics.Store,
|
||||||
|
code diagnostics.Code,
|
||||||
|
span diagnostics.Span,
|
||||||
|
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
return Outcome{ trap = try diagnostics.add(diagnostic_store, code, span) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
checked_integer proc(op ir.Op, left i64, right i64) ?i64 {
|
||||||
|
minimum :: minval!(i64)
|
||||||
|
maximum :: maxval!(i64)
|
||||||
|
match op {
|
||||||
|
.add: {
|
||||||
|
if (right > 0 and left > maximum - right) return null
|
||||||
|
if (right < 0 and left < minimum - right) return null
|
||||||
|
return left + right
|
||||||
|
}
|
||||||
|
.subtract: {
|
||||||
|
if (right > 0 and left < minimum + right) return null
|
||||||
|
if (right < 0 and left > maximum + right) return null
|
||||||
|
return left - right
|
||||||
|
}
|
||||||
|
.multiply: {
|
||||||
|
if left > 0 {
|
||||||
|
if (right > 0 and left > divtrunc!(maximum, right)) return null
|
||||||
|
if (right < 0 and right < divtrunc!(minimum, left)) return null
|
||||||
|
} else if left < 0 {
|
||||||
|
if (right > 0 and left < divtrunc!(minimum, right)) return null
|
||||||
|
if (right < 0 and left < divtrunc!(maximum, right)) return null
|
||||||
|
}
|
||||||
|
return left * right
|
||||||
|
}
|
||||||
|
else: unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
import "@std/mem"
|
||||||
|
import "@std/arraylist"
|
||||||
|
import "@std/testing"
|
||||||
|
import "@source/ir"
|
||||||
|
import "@source/diagnostics"
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
add_function proc(
|
||||||
|
module @mut ir.Module,
|
||||||
|
locals []ir.Type,
|
||||||
|
parameter_count usize,
|
||||||
|
result ir.Type,
|
||||||
|
value_count usize,
|
||||||
|
) ir.FunctionId ! mem.AllocError {
|
||||||
|
function := ir.init_function(module.allocator)
|
||||||
|
errdefer {
|
||||||
|
arraylist.deinit(&function.parameters)
|
||||||
|
arraylist.deinit(&function.locals)
|
||||||
|
for (function.blocks.items) |block| {
|
||||||
|
owned := block
|
||||||
|
arraylist.deinit(&owned.instructions)
|
||||||
|
arraylist.deinit(&owned.arguments)
|
||||||
|
}
|
||||||
|
arraylist.deinit(&function.blocks)
|
||||||
|
}
|
||||||
|
for (locals) |type, index| {
|
||||||
|
try arraylist.append(&function.locals, type)
|
||||||
|
if (index < parameter_count) try arraylist.append(&function.parameters, type)
|
||||||
|
}
|
||||||
|
function.result = result
|
||||||
|
function.value_count = value_count
|
||||||
|
try arraylist.append(&function.blocks, ir.init_block(module.allocator))
|
||||||
|
id :: ir.FunctionId(module.functions.items.len)
|
||||||
|
try arraylist.append(&module.functions, function)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
emit proc(module @mut ir.Module, function ir.FunctionId, instruction ir.Instruction) void ! mem.AllocError {
|
||||||
|
try arraylist.append(&module.functions.items[usize(function)].blocks.items[0].instructions, instruction)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
return_value proc(module @mut ir.Module, function ir.FunctionId, value ir.ValueId) void {
|
||||||
|
module.functions.items[usize(function)].blocks.items[0].terminator = ir.Terminator{ value = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
integer proc(destination u32, value i64) ir.Instruction {
|
||||||
|
return ir.Instruction{
|
||||||
|
op = .constant,
|
||||||
|
type = .int,
|
||||||
|
destination = ir.ValueId(destination),
|
||||||
|
constant = ir.Value{ type = .int, data = ir.ValueData{ integer = value } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
function_value proc(destination u32, function ir.FunctionId) ir.Instruction {
|
||||||
|
return ir.Instruction{
|
||||||
|
op = .constant,
|
||||||
|
type = .function,
|
||||||
|
destination = ir.ValueId(destination),
|
||||||
|
constant = ir.Value{ type = .function, data = ir.ValueData{ function = function } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
expect_trap proc(
|
||||||
|
outcome Outcome,
|
||||||
|
store @diagnostics.Store,
|
||||||
|
code diagnostics.Code,
|
||||||
|
span diagnostics.Span,
|
||||||
|
) void ! testing.Error {
|
||||||
|
if outcome.trap |id| {
|
||||||
|
if diagnostics.get(store, id) |diagnostic| {
|
||||||
|
try testing.expect_equal(code, diagnostic.code)
|
||||||
|
try testing.expect_equal(span.start, diagnostic.span.start)
|
||||||
|
try testing.expect_equal(span.end, diagnostic.span.end)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .expectation_failed
|
||||||
|
}
|
||||||
|
|
||||||
|
repeated_calls_have_fresh_parameters_locals_and_temporaries test {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
types [2]ir.Type :: [.int, .int]
|
||||||
|
bump :: try add_function(&module, types[..], 1, .int, 4)
|
||||||
|
try emit(&module, bump, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||||
|
try emit(&module, bump, integer(1, 1))
|
||||||
|
try emit(&module, bump, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||||
|
try emit(&module, bump, ir.Instruction{ op = .store_local, left = ir.ValueId(2), slot = 1 })
|
||||||
|
try emit(&module, bump, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(3), slot = 1 })
|
||||||
|
return_value(&module, bump, ir.ValueId(3))
|
||||||
|
noop :: try add_function(&module, mem.empty(ir.Type), 0, .void, 0)
|
||||||
|
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 8)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, function_value(0, bump))
|
||||||
|
try emit(&module, main, integer(1, 19))
|
||||||
|
try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(1))
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), arguments_count = 1 })
|
||||||
|
try emit(&module, main, integer(3, 21))
|
||||||
|
try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(3))
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(4), left = ir.ValueId(0), arguments_start = 1, arguments_count = 1 })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(5), left = ir.ValueId(2), right = ir.ValueId(4) })
|
||||||
|
try emit(&module, main, function_value(6, noop))
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(7), left = ir.ValueId(6) })
|
||||||
|
return_value(&module, main, ir.ValueId(5))
|
||||||
|
outcome :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.int, outcome.value.type)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
try testing.expect_equal(usize(0), diagnostics.count(&store))
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_globals_follow_forward_dependencies_and_reset_between_runs test {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
|
||||||
|
second :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
|
||||||
|
try emit(&module, first, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 1 })
|
||||||
|
try emit(&module, first, integer(1, 1))
|
||||||
|
try emit(&module, first, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||||
|
return_value(&module, first, ir.ValueId(2))
|
||||||
|
try emit(&module, second, integer(0, 20))
|
||||||
|
return_value(&module, second, ir.ValueId(0))
|
||||||
|
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||||
|
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||||
|
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(1), slot = 0 })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||||
|
return_value(&module, main, ir.ValueId(2))
|
||||||
|
initial :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(initial.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), initial.value.data.integer)
|
||||||
|
module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = integer(0, 30)
|
||||||
|
repeated :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(repeated.trap == null)
|
||||||
|
try testing.expect_equal(i64(62), repeated.value.data.integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
function_aliases_and_borrowed_string_values_execute test {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
text :: "line\n\"quoted\"\x00"
|
||||||
|
target :: try add_function(&module, mem.empty(ir.Type), 0, .string, 1)
|
||||||
|
try emit(&module, target, ir.Instruction{
|
||||||
|
op = .constant,
|
||||||
|
type = .string,
|
||||||
|
destination = ir.ValueId(0),
|
||||||
|
constant = ir.Value{ type = .string, data = ir.ValueData{ string = text } },
|
||||||
|
})
|
||||||
|
return_value(&module, target, ir.ValueId(0))
|
||||||
|
initializer :: try add_function(&module, mem.empty(ir.Type), 0, .function, 1)
|
||||||
|
try emit(&module, initializer, function_value(0, target))
|
||||||
|
return_value(&module, initializer, ir.ValueId(0))
|
||||||
|
try arraylist.append(&module.globals, ir.Global{ type = .function, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||||
|
main :: try add_function(&module, mem.empty(ir.Type), 0, .string, 2)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .load_global, type = .function, destination = ir.ValueId(0), slot = 0 })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .call, type = .string, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||||
|
return_value(&module, main, ir.ValueId(1))
|
||||||
|
outcome :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.string, outcome.value.type)
|
||||||
|
try testing.expect_equal([]u8, text, outcome.value.data.string)
|
||||||
|
}
|
||||||
|
|
||||||
|
premature_local_reads_and_global_cycles_report_instruction_spans test {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
span :: diagnostics.Span{ start = 14, end = 19 }
|
||||||
|
types [1]ir.Type :: [.int]
|
||||||
|
main :: try add_function(&module, types[..], 0, .int, 1)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0, span = span })
|
||||||
|
return_value(&module, main, ir.ValueId(0))
|
||||||
|
premature :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try expect_trap(premature, &store, .read_before_initialization, span)
|
||||||
|
|
||||||
|
first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
|
||||||
|
second :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
|
||||||
|
cycle_span :: diagnostics.Span{ start = 40, end = 44 }
|
||||||
|
try emit(&module, first, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 1 })
|
||||||
|
try emit(&module, second, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0, span = cycle_span })
|
||||||
|
return_value(&module, first, ir.ValueId(0))
|
||||||
|
return_value(&module, second, ir.ValueId(0))
|
||||||
|
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||||
|
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||||
|
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 }
|
||||||
|
cycle :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try expect_trap(cycle, &store, .initialization_cycle, cycle_span)
|
||||||
|
retry :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try expect_trap(retry, &store, .initialization_cycle, cycle_span)
|
||||||
|
}
|
||||||
|
|
||||||
|
existing_trap_identity_entry_failures_and_unused_errors_survive test {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
entry_id :: try diagnostics.add(&store, .missing_entry, diagnostics.Span{ start = 0, end = 0 })
|
||||||
|
module.entry_diagnostic = entry_id
|
||||||
|
absent :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect_equal(entry_id, absent.trap?)
|
||||||
|
invalid_id :: try diagnostics.add(&store, .unknown_name, diagnostics.Span{ start = 12, end = 17 })
|
||||||
|
bad :: try add_function(&module, mem.empty(ir.Type), 0, .invalid, 0)
|
||||||
|
module.functions.items[usize(bad)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = invalid_id }
|
||||||
|
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 2)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, integer(0, 42))
|
||||||
|
return_value(&module, main, ir.ValueId(0))
|
||||||
|
success :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(success.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), success.value.data.integer)
|
||||||
|
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = function_value(0, bad)
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .call, type = .invalid, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||||
|
trapped :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect_equal(invalid_id, trapped.trap?)
|
||||||
|
try testing.expect_equal(usize(2), diagnostics.count(&store))
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
IntegerCase :: struct {
|
||||||
|
op ir.Op
|
||||||
|
left i64
|
||||||
|
right i64
|
||||||
|
expected ?i64
|
||||||
|
}
|
||||||
|
|
||||||
|
integer_arithmetic_checks_boundaries_before_host_operations test {
|
||||||
|
cases [19]IntegerCase :: [
|
||||||
|
IntegerCase{ op = .add, left = 19, right = 23, expected = 42 },
|
||||||
|
IntegerCase{ op = .add, left = maxval!(i64), right = 0, expected = maxval!(i64) },
|
||||||
|
IntegerCase{ op = .add, left = maxval!(i64), right = 1, expected = null },
|
||||||
|
IntegerCase{ op = .add, left = minval!(i64), right = -1, expected = null },
|
||||||
|
IntegerCase{ op = .add, left = minval!(i64), right = maxval!(i64), expected = -1 },
|
||||||
|
IntegerCase{ op = .subtract, left = minval!(i64), right = 0, expected = minval!(i64) },
|
||||||
|
IntegerCase{ op = .subtract, left = minval!(i64), right = 1, expected = null },
|
||||||
|
IntegerCase{ op = .subtract, left = maxval!(i64), right = -1, expected = null },
|
||||||
|
IntegerCase{ op = .subtract, left = -1, right = minval!(i64), expected = maxval!(i64) },
|
||||||
|
IntegerCase{ op = .multiply, left = minval!(i64), right = 1, expected = minval!(i64) },
|
||||||
|
IntegerCase{ op = .multiply, left = minval!(i64), right = -1, expected = null },
|
||||||
|
IntegerCase{ op = .multiply, left = -1, right = minval!(i64), expected = null },
|
||||||
|
IntegerCase{ op = .multiply, left = maxval!(i64), right = 2, expected = null },
|
||||||
|
IntegerCase{ op = .multiply, left = minval!(i64), right = 2, expected = null },
|
||||||
|
IntegerCase{ op = .multiply, left = -3, right = -14, expected = 42 },
|
||||||
|
IntegerCase{ op = .multiply, left = -3, right = 14, expected = -42 },
|
||||||
|
IntegerCase{ op = .multiply, left = 3, right = -14, expected = -42 },
|
||||||
|
IntegerCase{ op = .multiply, left = minval!(i64), right = 0, expected = 0 },
|
||||||
|
IntegerCase{ op = .multiply, left = 0, right = minval!(i64), expected = 0 },
|
||||||
|
]
|
||||||
|
for (cases) |case| {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
span :: diagnostics.Span{ start = 8, end = 16 }
|
||||||
|
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, integer(0, case.left))
|
||||||
|
try emit(&module, main, integer(1, case.right))
|
||||||
|
try emit(&module, main, ir.Instruction{ op = case.op, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1), span = span })
|
||||||
|
return_value(&module, main, ir.ValueId(2))
|
||||||
|
outcome :: try run(mem.c_allocator, &module, &store)
|
||||||
|
if case.expected |expected| {
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(expected, outcome.value.data.integer)
|
||||||
|
} else {
|
||||||
|
try expect_trap(outcome, &store, .integer_overflow, span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
integer_negation_traps_only_at_minimum test {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
span :: diagnostics.Span{ start = 21, end = 27 }
|
||||||
|
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 2)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, integer(0, minval!(i64)))
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .negate, type = .int, destination = ir.ValueId(1), left = ir.ValueId(0), span = span })
|
||||||
|
return_value(&module, main, ir.ValueId(1))
|
||||||
|
trapped :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try expect_trap(trapped, &store, .integer_overflow, span)
|
||||||
|
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = integer(0, maxval!(i64))
|
||||||
|
success :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(success.trap == null)
|
||||||
|
try testing.expect_equal(-maxval!(i64), success.value.data.integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
float_arithmetic_and_ieee_zero_division_execute test {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
main :: try add_function(&module, mem.empty(ir.Type), 0, .float, 7)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(0), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 10.5 } } })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(1), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 2.0 } } })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .multiply, type = .float, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .add, type = .float, destination = ir.ValueId(3), left = ir.ValueId(2), right = ir.ValueId(1) })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .subtract, type = .float, destination = ir.ValueId(4), left = ir.ValueId(3), right = ir.ValueId(1) })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .divide, type = .float, destination = ir.ValueId(5), left = ir.ValueId(4), right = ir.ValueId(1) })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .negate, type = .float, destination = ir.ValueId(6), left = ir.ValueId(5) })
|
||||||
|
return_value(&module, main, ir.ValueId(6))
|
||||||
|
result :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(result.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.float, result.value.type)
|
||||||
|
try testing.expect_equal(f64(-10.5), result.value.data.real)
|
||||||
|
module.functions.items[usize(main)].blocks.items[0].instructions.items[0].constant.data.real = 0
|
||||||
|
module.functions.items[usize(main)].blocks.items[0].instructions.items[1].constant.data.real = 0
|
||||||
|
nan :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(nan.trap == null)
|
||||||
|
try testing.expect(nan.value.data.real != nan.value.data.real)
|
||||||
|
module.functions.items[usize(main)].blocks.items[0].instructions.items[0].constant.data.real = 1
|
||||||
|
module.functions.items[usize(main)].blocks.items[0].instructions.items[2].op = .add
|
||||||
|
infinite :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(infinite.trap == null)
|
||||||
|
try testing.expect(infinite.value.data.real < f64(-1000000000))
|
||||||
|
try testing.expect_equal(infinite.value.data.real, infinite.value.data.real * f64(2))
|
||||||
|
try testing.expect_equal(usize(0), diagnostics.count(&store))
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
AllocationState :: struct {
|
||||||
|
remaining usize
|
||||||
|
live usize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
limited_alloc proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
|
||||||
|
if context |userdata| {
|
||||||
|
state @mut AllocationState :: ptrcast!(userdata)
|
||||||
|
if (state.remaining == 0) return null
|
||||||
|
state.remaining -= 1
|
||||||
|
memory :: mem.raw_alloc(mem.c_allocator, size, alignment)
|
||||||
|
if (memory) |_| state.live += 1
|
||||||
|
return memory
|
||||||
|
}
|
||||||
|
unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
limited_realloc proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
|
||||||
|
if context |userdata| {
|
||||||
|
state @mut AllocationState :: ptrcast!(userdata)
|
||||||
|
if (state.remaining == 0) return null
|
||||||
|
state.remaining -= 1
|
||||||
|
resized :: mem.raw_realloc(mem.c_allocator, memory, old_size, new_size, alignment)
|
||||||
|
if resized |_| {
|
||||||
|
if (memory) |_| {} else state.live += 1
|
||||||
|
}
|
||||||
|
return resized
|
||||||
|
}
|
||||||
|
unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
limited_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
|
||||||
|
if context |userdata| {
|
||||||
|
state @mut AllocationState :: ptrcast!(userdata)
|
||||||
|
if (memory) |_| state.live -= 1
|
||||||
|
mem.raw_free(mem.c_allocator, memory, size, alignment)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
limited_vtable :: mem.AllocatorVTable{ alloc = limited_alloc, realloc = limited_realloc, free = limited_free }
|
||||||
|
|
||||||
|
recursive_invocations_use_heap_frames_and_free_on_allocation_failure test {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
main :: try add_function(&module, mem.empty(ir.Type), 0, .void, 2)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, function_value(0, main))
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||||
|
state := AllocationState{ remaining = 2048 }
|
||||||
|
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||||
|
_ = run(allocator, &module, &store) catch |error| {
|
||||||
|
match error {
|
||||||
|
.out_of_memory: {}
|
||||||
|
else: return .expectation_failed
|
||||||
|
}
|
||||||
|
try testing.expect_equal(usize(0), state.live)
|
||||||
|
try testing.expect_equal(usize(0), diagnostics.count(&store))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return .expectation_failed
|
||||||
|
}
|
||||||
|
|
||||||
|
frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
types [1]ir.Type :: [.int]
|
||||||
|
initializer :: try add_function(&module, types[..], 0, .int, 1)
|
||||||
|
try emit(&module, initializer, integer(0, 42))
|
||||||
|
return_value(&module, initializer, ir.ValueId(0))
|
||||||
|
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||||
|
main :: try add_function(&module, types[..], 0, .int, 1)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||||
|
return_value(&module, main, ir.ValueId(0))
|
||||||
|
succeeded := false
|
||||||
|
failures usize := 0
|
||||||
|
for (usize(0)..usize(16)) |limit| {
|
||||||
|
state := AllocationState{ remaining = limit }
|
||||||
|
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||||
|
outcome :: run(allocator, &module, &store) catch |error| {
|
||||||
|
match error {
|
||||||
|
.out_of_memory: {}
|
||||||
|
else: return .expectation_failed
|
||||||
|
}
|
||||||
|
failures += 1
|
||||||
|
try testing.expect_equal(usize(0), state.live)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
succeeded = true
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
try testing.expect_equal(usize(0), state.live)
|
||||||
|
}
|
||||||
|
try testing.expect(succeeded)
|
||||||
|
try testing.expect(failures > 0)
|
||||||
|
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 })
|
||||||
|
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||||
|
state := AllocationState{ remaining = 64 }
|
||||||
|
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||||
|
trapped :: try run(allocator, &module, &store)
|
||||||
|
try testing.expect_equal(trap_id, trapped.trap?)
|
||||||
|
try testing.expect_equal(usize(0), state.live)
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames test {
|
||||||
|
module := ir.init(mem.c_allocator)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
types [1]ir.Type :: [.int]
|
||||||
|
main :: try add_function(&module, types[..], 0, .int, 1)
|
||||||
|
module.entry = main
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||||
|
return_value(&module, main, ir.ValueId(0))
|
||||||
|
diagnostic_state := AllocationState{ remaining = 0 }
|
||||||
|
diagnostic_allocator :: mem.Allocator{ context = &diagnostic_state, vtable = &limited_vtable }
|
||||||
|
store := diagnostics.init(diagnostic_allocator)
|
||||||
|
defer diagnostics.deinit(&store)
|
||||||
|
state := AllocationState{ remaining = 64 }
|
||||||
|
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||||
|
_ = run(allocator, &module, &store) catch |error| {
|
||||||
|
match error {
|
||||||
|
.out_of_memory: {}
|
||||||
|
else: return .expectation_failed
|
||||||
|
}
|
||||||
|
try testing.expect_equal(usize(0), state.live)
|
||||||
|
try testing.expect_equal(usize(0), diagnostic_state.live)
|
||||||
|
try testing.expect_equal(usize(0), diagnostics.count(&store))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return .expectation_failed
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import "@std/mem"
|
||||||
|
import "@std/arraylist"
|
||||||
|
import "@source/diagnostics"
|
||||||
|
|
||||||
|
FunctionId :: distinct u32
|
||||||
|
GlobalId :: distinct u32
|
||||||
|
LocalId :: distinct u32
|
||||||
|
ValueId :: distinct u32
|
||||||
|
BlockId :: distinct u32
|
||||||
|
no_value :: maxval!(ValueId)
|
||||||
|
|
||||||
|
Type :: enum { invalid, int, float, string, void, function }
|
||||||
|
ValueData :: union {
|
||||||
|
integer i64
|
||||||
|
real f64
|
||||||
|
string []u8
|
||||||
|
function FunctionId
|
||||||
|
}
|
||||||
|
Value :: struct {
|
||||||
|
type Type = .void
|
||||||
|
data ValueData = ValueData{ integer = 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Op :: enum {
|
||||||
|
constant
|
||||||
|
load_local
|
||||||
|
store_local
|
||||||
|
load_global
|
||||||
|
negate
|
||||||
|
add
|
||||||
|
subtract
|
||||||
|
multiply
|
||||||
|
divide
|
||||||
|
call
|
||||||
|
}
|
||||||
|
|
||||||
|
Instruction :: struct {
|
||||||
|
op Op
|
||||||
|
type Type = .void
|
||||||
|
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
|
||||||
|
destination ValueId = no_value
|
||||||
|
left ValueId = no_value
|
||||||
|
right ValueId = no_value
|
||||||
|
slot u32 = 0
|
||||||
|
constant Value = Value{}
|
||||||
|
arguments_start usize = 0
|
||||||
|
arguments_count usize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminatorKind :: enum { return_value, trap }
|
||||||
|
Terminator :: struct {
|
||||||
|
kind TerminatorKind = .return_value
|
||||||
|
value ValueId = no_value
|
||||||
|
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
|
||||||
|
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Block :: struct {
|
||||||
|
instructions arraylist.ArrayList(Instruction)
|
||||||
|
arguments arraylist.ArrayList(ValueId)
|
||||||
|
terminator Terminator = Terminator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function :: struct {
|
||||||
|
parameters arraylist.ArrayList(Type)
|
||||||
|
locals arraylist.ArrayList(Type)
|
||||||
|
result Type = .void
|
||||||
|
blocks arraylist.ArrayList(Block)
|
||||||
|
entry BlockId = BlockId(0)
|
||||||
|
value_count usize = 0
|
||||||
|
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Global :: struct {
|
||||||
|
type Type
|
||||||
|
initializer FunctionId
|
||||||
|
span diagnostics.Span
|
||||||
|
}
|
||||||
|
|
||||||
|
#! owns executable storage, including decoded strings; no AST or binding table is retained.
|
||||||
|
#! spans and diagnostic IDs refer to the caller's source and diagnostic store.
|
||||||
|
Module :: struct {
|
||||||
|
allocator mem.Allocator
|
||||||
|
functions arraylist.ArrayList(Function)
|
||||||
|
globals arraylist.ArrayList(Global)
|
||||||
|
strings arraylist.ArrayList([]mut u8)
|
||||||
|
entry ?FunctionId = null
|
||||||
|
entry_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
|
||||||
|
}
|
||||||
|
|
||||||
|
init proc(allocator mem.Allocator) Module {
|
||||||
|
return Module{
|
||||||
|
allocator = allocator,
|
||||||
|
functions = arraylist.init(allocator),
|
||||||
|
globals = arraylist.init(allocator),
|
||||||
|
strings = arraylist.init(allocator),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init_function proc(allocator mem.Allocator) Function {
|
||||||
|
return Function{
|
||||||
|
parameters = arraylist.init(allocator),
|
||||||
|
locals = arraylist.init(allocator),
|
||||||
|
blocks = arraylist.init(allocator),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init_block proc(allocator mem.Allocator) Block {
|
||||||
|
return Block{
|
||||||
|
instructions = arraylist.init(allocator),
|
||||||
|
arguments = arraylist.init(allocator),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#! releases all executable storage using the retained allocator.
|
||||||
|
deinit proc(module @mut Module) void {
|
||||||
|
for (0..(module.functions.items.len)) |i| {
|
||||||
|
function :: &module.functions.items[i]
|
||||||
|
for (0..(function.blocks.items.len)) |j| {
|
||||||
|
block :: &function.blocks.items[j]
|
||||||
|
arraylist.deinit(&block.instructions)
|
||||||
|
arraylist.deinit(&block.arguments)
|
||||||
|
}
|
||||||
|
arraylist.deinit(&function.parameters)
|
||||||
|
arraylist.deinit(&function.locals)
|
||||||
|
arraylist.deinit(&function.blocks)
|
||||||
|
}
|
||||||
|
for (module.strings.items) |string| mem.free(module.allocator, string)
|
||||||
|
arraylist.deinit(&module.functions)
|
||||||
|
arraylist.deinit(&module.globals)
|
||||||
|
arraylist.deinit(&module.strings)
|
||||||
|
}
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
import "@std/mem"
|
||||||
|
import "@std/arraylist"
|
||||||
|
import "@ffi/c"
|
||||||
|
import "@source/ast"
|
||||||
|
import "@source/diagnostics"
|
||||||
|
import "@source/ir"
|
||||||
|
sema :: import "@source/sema"
|
||||||
|
typecheck :: import "@source/sema/check"
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
StorageKind :: enum { none, function, global, local }
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
Storage :: struct {
|
||||||
|
kind StorageKind = .none
|
||||||
|
index u32 = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
State :: struct {
|
||||||
|
tree @ast.Tree
|
||||||
|
resolution @sema.Resolution
|
||||||
|
checked @typecheck.Result
|
||||||
|
diagnostics @mut diagnostics.Store
|
||||||
|
module @mut ir.Module
|
||||||
|
storage []mut Storage
|
||||||
|
function usize = 0
|
||||||
|
terminated bool = false
|
||||||
|
}
|
||||||
|
|
||||||
|
#! lowers checked source into caller-owned executable storage; release with ir.deinit.
|
||||||
|
#! no frontend storage or source bytes are retained by the returned module.
|
||||||
|
lower proc(
|
||||||
|
allocator mem.Allocator,
|
||||||
|
tree @ast.Tree,
|
||||||
|
resolution @sema.Resolution,
|
||||||
|
checked @typecheck.Result,
|
||||||
|
diagnostic_store @mut diagnostics.Store,
|
||||||
|
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
module := ir.init(allocator)
|
||||||
|
errdefer ir.deinit(&module)
|
||||||
|
storage :: try mem.alloc(Storage, allocator, ast.node_count(tree))
|
||||||
|
defer mem.free(allocator, storage)
|
||||||
|
mem.set(storage, Storage{})
|
||||||
|
state := State{
|
||||||
|
tree = tree,
|
||||||
|
resolution = resolution,
|
||||||
|
checked = checked,
|
||||||
|
diagnostics = diagnostic_store,
|
||||||
|
module = &module,
|
||||||
|
storage = storage,
|
||||||
|
}
|
||||||
|
|
||||||
|
# allocate every package identity before lowering any initializer or body.
|
||||||
|
for (0..ast.package_decl_count(tree)) |i| {
|
||||||
|
id :: ast.package_decl(tree, i)
|
||||||
|
match ast.decl_kind(tree, id) {
|
||||||
|
.function: {
|
||||||
|
function :: try add_function(&module, ast.node_span(tree, id))
|
||||||
|
storage[usize(id)] = Storage{ kind = .function, index = u32(function) }
|
||||||
|
}
|
||||||
|
.statement: {
|
||||||
|
initializer :: try add_function(&module, ast.node_span(tree, id))
|
||||||
|
global :: u32(module.globals.items.len)
|
||||||
|
try arraylist.append(&module.globals, ir.Global{
|
||||||
|
type = lower_type(typecheck.type_of(checked, id)),
|
||||||
|
initializer = initializer,
|
||||||
|
span = ast.node_span(tree, id),
|
||||||
|
})
|
||||||
|
storage[usize(id)] = Storage{ kind = .global, index = global }
|
||||||
|
}
|
||||||
|
.invalid: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try find_entry(&state)
|
||||||
|
for (0..ast.package_decl_count(tree)) |i| {
|
||||||
|
id :: ast.package_decl(tree, i)
|
||||||
|
match ast.decl_kind(tree, id) {
|
||||||
|
.function: try lower_function(&state, id)
|
||||||
|
.statement: try lower_global(&state, id)
|
||||||
|
.invalid: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return module
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
lower_type proc(type typecheck.Type) ir.Type {
|
||||||
|
return match type.kind {
|
||||||
|
.int: .int
|
||||||
|
.float: .float
|
||||||
|
.string: .string
|
||||||
|
.void: .void
|
||||||
|
.function: .function
|
||||||
|
.none, .invalid: .invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
add_function proc(module @mut ir.Module, span diagnostics.Span) ir.FunctionId ! mem.AllocError {
|
||||||
|
id :: ir.FunctionId(module.functions.items.len)
|
||||||
|
try arraylist.append(&module.functions, ir.init_function(module.allocator))
|
||||||
|
function := &module.functions.items[usize(id)]
|
||||||
|
function.span = span
|
||||||
|
try arraylist.append(&function.blocks, ir.init_block(module.allocator))
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
find_entry proc(state @mut State) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
tree :: state.tree
|
||||||
|
for (0..ast.package_decl_count(tree)) |i| {
|
||||||
|
id :: ast.package_decl(tree, i)
|
||||||
|
name := diagnostics.Span{ start = 0, end = 0 }
|
||||||
|
match ast.decl_kind(tree, id) {
|
||||||
|
.function: name = ast.function_decl(tree, id).name
|
||||||
|
.statement: name = ast.statement_decl(tree, id).name
|
||||||
|
.invalid: continue
|
||||||
|
}
|
||||||
|
if (!mem.eql(ast.source_text(tree, name), "main")) continue
|
||||||
|
if (ast.decl_kind(tree, id) != .function) {
|
||||||
|
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
declaration :: ast.function_decl(tree, id)
|
||||||
|
result :: lower_type(typecheck.type_of(state.checked, declaration.return_type))
|
||||||
|
if (ast.function_param_count(tree, id) != 0 or result == .function) {
|
||||||
|
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.module.entry = ir.FunctionId(state.storage[usize(id)].index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .missing_entry, ast.node_span(tree, tree.root))
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
trap proc(state @mut State, diagnostic diagnostics.DiagnosticId, span diagnostics.Span) void {
|
||||||
|
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
|
||||||
|
kind = .trap,
|
||||||
|
diagnostic = diagnostic,
|
||||||
|
span = span,
|
||||||
|
}
|
||||||
|
state.terminated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
trap_type proc(state @mut State, id ast.NodeId) bool {
|
||||||
|
type :: typecheck.type_of(state.checked, id)
|
||||||
|
if (type.kind != .invalid) return false
|
||||||
|
trap(state, type.diagnostic, ast.node_span(state.tree, id))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
trap_binding proc(state @mut State, id ast.NodeId) bool {
|
||||||
|
binding :: sema.binding(state.resolution, id)
|
||||||
|
if (binding.kind != .invalid) return false
|
||||||
|
trap(state, binding.data.diagnostic, ast.node_span(state.tree, id))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void {
|
||||||
|
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
|
||||||
|
value = value,
|
||||||
|
span = span,
|
||||||
|
}
|
||||||
|
state.terminated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! mem.AllocError {
|
||||||
|
function := &state.module.functions.items[state.function]
|
||||||
|
operation := instruction
|
||||||
|
if (operation.op != .store_local) {
|
||||||
|
operation.destination = ir.ValueId(function.value_count)
|
||||||
|
function.value_count += 1
|
||||||
|
}
|
||||||
|
try arraylist.append(&function.blocks.items[0].instructions, operation)
|
||||||
|
return operation.destination
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
constant proc(state @mut State, value ir.Value, span diagnostics.Span) ir.ValueId ! mem.AllocError {
|
||||||
|
return try emit(state, ir.Instruction{ op = .constant, type = value.type, constant = value, span = span })
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
lower_global proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
global :: state.module.globals.items[usize(state.storage[usize(id)].index)]
|
||||||
|
state.function = usize(global.initializer)
|
||||||
|
state.terminated = false
|
||||||
|
state.module.functions.items[state.function].result = global.type
|
||||||
|
if (trap_binding(state, id)) return
|
||||||
|
declaration :: ast.statement_decl(state.tree, id)
|
||||||
|
if (declaration.type) |annotation| if (trap_type(state, annotation)) return
|
||||||
|
value :: try lower_expression(state, declaration.initializer)
|
||||||
|
if (state.terminated or trap_type(state, id)) return
|
||||||
|
finish(state, value, declaration.span)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
tree :: state.tree
|
||||||
|
declaration :: ast.function_decl(tree, id)
|
||||||
|
state.function = usize(state.storage[usize(id)].index)
|
||||||
|
state.terminated = false
|
||||||
|
function := &state.module.functions.items[state.function]
|
||||||
|
function.result = lower_type(typecheck.type_of(state.checked, declaration.return_type))
|
||||||
|
for (0..ast.function_param_count(tree, id)) |i| {
|
||||||
|
parameter :: ast.function_param(tree, id, i)
|
||||||
|
type :: lower_type(typecheck.type_of(state.checked, parameter))
|
||||||
|
state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
|
||||||
|
try arraylist.append(&function.parameters, type)
|
||||||
|
try arraylist.append(&function.locals, type)
|
||||||
|
}
|
||||||
|
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
|
||||||
|
statement :: ast.block_statement(tree, declaration.body, i)
|
||||||
|
if (ast.stmt_kind(tree, statement) != .declaration) continue
|
||||||
|
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
|
||||||
|
try arraylist.append(&function.locals, lower_type(typecheck.type_of(state.checked, statement)))
|
||||||
|
}
|
||||||
|
# signature errors are entry traps; body errors are reached in statement order.
|
||||||
|
if (trap_binding(state, id) or trap_type(state, declaration.return_type)) return
|
||||||
|
for (0..ast.function_param_count(tree, id)) |i| {
|
||||||
|
if (trap_type(state, ast.function_param(tree, id, i))) return
|
||||||
|
}
|
||||||
|
if (trap_type(state, id)) return
|
||||||
|
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
|
||||||
|
if (state.terminated) return
|
||||||
|
try lower_statement(state, ast.block_statement(tree, declaration.body, i))
|
||||||
|
}
|
||||||
|
if (state.terminated or trap_type(state, declaration.body)) return
|
||||||
|
finish(state, ir.no_value, ast.node_span(tree, declaration.body))
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
tree :: state.tree
|
||||||
|
match ast.stmt_kind(tree, id) {
|
||||||
|
.invalid: {
|
||||||
|
invalid :: ast.invalid(tree, id)
|
||||||
|
trap(state, invalid.diagnostic, invalid.span)
|
||||||
|
}
|
||||||
|
.expression: _ = try lower_expression(state, ast.statement_expr(tree, id))
|
||||||
|
.declaration: {
|
||||||
|
if (trap_binding(state, id)) return
|
||||||
|
declaration :: ast.statement_decl(tree, id)
|
||||||
|
if (declaration.type) |annotation| if (trap_type(state, annotation)) return
|
||||||
|
value :: try lower_expression(state, declaration.initializer)
|
||||||
|
if (state.terminated or trap_type(state, id)) return
|
||||||
|
_ = try emit(state, ir.Instruction{
|
||||||
|
op = .store_local,
|
||||||
|
type = lower_type(typecheck.type_of(state.checked, id)),
|
||||||
|
left = value,
|
||||||
|
slot = state.storage[usize(id)].index,
|
||||||
|
span = declaration.span,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.return_stmt: {
|
||||||
|
statement :: ast.return_stmt(tree, id)
|
||||||
|
value := ir.no_value
|
||||||
|
if (statement.value) |expression| value = try lower_expression(state, expression)
|
||||||
|
if (state.terminated or trap_type(state, id)) return
|
||||||
|
finish(state, value, statement.span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
tree :: state.tree
|
||||||
|
span :: ast.node_span(tree, id)
|
||||||
|
type :: lower_type(typecheck.type_of(state.checked, id))
|
||||||
|
match ast.expr_kind(tree, id) {
|
||||||
|
.invalid: {
|
||||||
|
invalid :: ast.invalid(tree, id)
|
||||||
|
trap(state, invalid.diagnostic, invalid.span)
|
||||||
|
}
|
||||||
|
.literal_int, .literal_float: {
|
||||||
|
if (trap_type(state, id)) return ir.no_value
|
||||||
|
return try lower_number(state, id)
|
||||||
|
}
|
||||||
|
.literal_string: {
|
||||||
|
if (trap_type(state, id)) return ir.no_value
|
||||||
|
return try lower_string(state, id)
|
||||||
|
}
|
||||||
|
.identifier: {
|
||||||
|
if (trap_binding(state, id) or trap_type(state, id)) return ir.no_value
|
||||||
|
binding :: sema.binding(state.resolution, id)
|
||||||
|
location :: state.storage[usize(binding.data.declaration)]
|
||||||
|
match location.kind {
|
||||||
|
.function: return try constant(state, ir.Value{
|
||||||
|
type = .function,
|
||||||
|
data = ir.ValueData{ function = ir.FunctionId(location.index) },
|
||||||
|
}, span)
|
||||||
|
.global: return try emit(state, ir.Instruction{ op = .load_global, type = type, slot = location.index, span = span })
|
||||||
|
.local: return try emit(state, ir.Instruction{ op = .load_local, type = type, slot = location.index, span = span })
|
||||||
|
.none: unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unary: {
|
||||||
|
expression :: ast.unary_expr(tree, id)
|
||||||
|
if (type == .int and ast.expr_kind(tree, expression.operand) == .literal_int) {
|
||||||
|
text :: ast.source_text(tree, ast.node_span(tree, expression.operand))
|
||||||
|
if (integer_magnitude(text, u64(maxval!(i64)) + 1)) |magnitude| {
|
||||||
|
if (magnitude == u64(maxval!(i64)) + 1) {
|
||||||
|
return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = minval!(i64) } }, span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operand :: try lower_expression(state, expression.operand)
|
||||||
|
if (state.terminated or trap_type(state, id)) return ir.no_value
|
||||||
|
return try emit(state, ir.Instruction{ op = .negate, type = type, left = operand, span = span })
|
||||||
|
}
|
||||||
|
.binary: {
|
||||||
|
expression :: ast.binary_expr(tree, id)
|
||||||
|
left :: try lower_expression(state, expression.left)
|
||||||
|
if (state.terminated) return ir.no_value
|
||||||
|
right :: try lower_expression(state, expression.right)
|
||||||
|
if (state.terminated or trap_type(state, id)) return ir.no_value
|
||||||
|
op ir.Op :: match expression.operator {
|
||||||
|
.add: .add
|
||||||
|
.subtract: .subtract
|
||||||
|
.multiply: .multiply
|
||||||
|
.divide: .divide
|
||||||
|
}
|
||||||
|
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
|
||||||
|
}
|
||||||
|
.call: {
|
||||||
|
callee :: try lower_expression(state, ast.call_expr(tree, id).callee)
|
||||||
|
if (state.terminated) return ir.no_value
|
||||||
|
# reserve this call's packed slice before nested calls append theirs.
|
||||||
|
block := &state.module.functions.items[state.function].blocks.items[0]
|
||||||
|
start :: block.arguments.items.len
|
||||||
|
count :: ast.call_arg_count(tree, id)
|
||||||
|
for (0..count) |_| try arraylist.append(&block.arguments, ir.no_value)
|
||||||
|
for (0..count) |i| {
|
||||||
|
argument :: try lower_expression(state, ast.call_arg(tree, id, i))
|
||||||
|
if (state.terminated) return ir.no_value
|
||||||
|
block.arguments.items[start + i] = argument
|
||||||
|
}
|
||||||
|
if (trap_type(state, id)) return ir.no_value
|
||||||
|
return try emit(state, ir.Instruction{
|
||||||
|
op = .call,
|
||||||
|
type = type,
|
||||||
|
left = callee,
|
||||||
|
arguments_start = start,
|
||||||
|
arguments_count = count,
|
||||||
|
span = span,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ir.no_value
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
integer_magnitude proc(text []u8, limit u64) ?u64 {
|
||||||
|
value := u64(0)
|
||||||
|
for (text) |byte| {
|
||||||
|
if (byte < '0' or byte > '9') return null
|
||||||
|
digit :: u64(byte - '0')
|
||||||
|
if (value > divtrunc!(limit - digit, u64(10))) return null
|
||||||
|
value = value * 10 + digit
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
span :: ast.node_span(state.tree, id)
|
||||||
|
text :: ast.source_text(state.tree, span)
|
||||||
|
type :: lower_type(typecheck.type_of(state.checked, id))
|
||||||
|
if (type == .int) {
|
||||||
|
if (integer_magnitude(text, u64(maxval!(i64)))) |magnitude| {
|
||||||
|
return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = i64(magnitude) } }, span)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer :: try mem.alloc(u8, state.module.allocator, text.len + 1)
|
||||||
|
defer mem.free(state.module.allocator, buffer)
|
||||||
|
for (text) |byte, i| buffer[i] = byte
|
||||||
|
buffer[text.len] = 0
|
||||||
|
c.__error()?^ = 0
|
||||||
|
value :: f64(c.strtod(ptrcast!(buffer.ptr), null))
|
||||||
|
# libc reports ERANGE for representable subnormals as well as zero/infinity.
|
||||||
|
if (c.__error()?^ != c.ERANGE or (value > 0 and value < 1)) {
|
||||||
|
return try constant(state, ir.Value{ type = .float, data = ir.ValueData{ real = value } }, span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trap(state, try diagnostics.add(state.diagnostics, .literal_out_of_range, span), span)
|
||||||
|
return ir.no_value
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
hex_digit proc(byte u8) ?u8 {
|
||||||
|
if (byte >= '0' and byte <= '9') return byte - '0'
|
||||||
|
if (byte >= 'a' and byte <= 'f') return byte - 'a' + 10
|
||||||
|
if (byte >= 'A' and byte <= 'F') return byte - 'A' + 10
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
span :: ast.node_span(state.tree, id)
|
||||||
|
text :: ast.source_text(state.tree, span)
|
||||||
|
owned :: try mem.alloc(u8, state.module.allocator, text.len - 2)
|
||||||
|
transferred := false
|
||||||
|
defer if (!transferred) mem.free(state.module.allocator, owned)
|
||||||
|
length := usize(0)
|
||||||
|
cursor := usize(1)
|
||||||
|
end :: text.len - 1
|
||||||
|
valid := true
|
||||||
|
while (cursor < end) {
|
||||||
|
byte := text[cursor]
|
||||||
|
cursor += 1
|
||||||
|
if (byte == '\\') {
|
||||||
|
if (cursor >= end) {
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
escape :: text[cursor]
|
||||||
|
cursor += 1
|
||||||
|
match escape {
|
||||||
|
'\\': byte = '\\'
|
||||||
|
'"': byte = '"'
|
||||||
|
'n': byte = '\n'
|
||||||
|
'r': byte = '\r'
|
||||||
|
't': byte = '\t'
|
||||||
|
'0': byte = 0
|
||||||
|
'x': {
|
||||||
|
if (end - cursor < 2) {
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
high := u8(0)
|
||||||
|
low := u8(0)
|
||||||
|
if (hex_digit(text[cursor])) |digit| high = digit
|
||||||
|
else {
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (hex_digit(text[cursor + 1])) |digit| low = digit
|
||||||
|
else {
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
byte = high * 16 + low
|
||||||
|
cursor += 2
|
||||||
|
}
|
||||||
|
else: {
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
owned[length] = byte
|
||||||
|
length += 1
|
||||||
|
}
|
||||||
|
if (!valid) {
|
||||||
|
trap(state, try diagnostics.add(state.diagnostics, .invalid_escape, span), span)
|
||||||
|
return ir.no_value
|
||||||
|
}
|
||||||
|
# retain the complete allocation for freeing, but expose only decoded bytes.
|
||||||
|
try arraylist.append(&state.module.strings, owned)
|
||||||
|
transferred = true
|
||||||
|
return try constant(state, ir.Value{ type = .string, data = ir.ValueData{ string = owned[..length] } }, span)
|
||||||
|
}
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
import "@std/mem"
|
||||||
|
import "@std/arraylist"
|
||||||
|
import "@std/testing"
|
||||||
|
import "@source/diagnostics"
|
||||||
|
import "@source/strpool"
|
||||||
|
import "@source/lexer"
|
||||||
|
import "@source/parser"
|
||||||
|
import "@source/ast"
|
||||||
|
import "@source/ir"
|
||||||
|
import "@source/interpreter"
|
||||||
|
sema :: import "@source/sema"
|
||||||
|
typecheck :: import "@source/sema/check"
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
CompileError :: alias mem.AllocError | diagnostics.CapacityError | strpool.InternError | lexer.SourceError
|
||||||
|
|
||||||
|
# every consumer test executes only after all source and frontend storage is gone.
|
||||||
|
@hide:file
|
||||||
|
compile_owned proc(text []u8, diagnostic_store @mut diagnostics.Store) ir.Module ! CompileError {
|
||||||
|
source :: try mem.alloc(u8, mem.c_allocator, text.len)
|
||||||
|
defer mem.free(mem.c_allocator, source)
|
||||||
|
for (text) |byte, i| source[i] = byte
|
||||||
|
strpool.strings = strpool.init(mem.c_allocator)
|
||||||
|
defer strpool.deinit(&strpool.strings)
|
||||||
|
scan_state := lexer.init(mem.c_allocator)
|
||||||
|
defer lexer.deinit(&scan_state)
|
||||||
|
try lexer.scan(&scan_state, source, diagnostic_store)
|
||||||
|
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, diagnostic_store)
|
||||||
|
defer ast.deinit(&tree)
|
||||||
|
resolution := try sema.resolve(mem.c_allocator, &tree, diagnostic_store)
|
||||||
|
defer sema.deinit(&resolution)
|
||||||
|
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, diagnostic_store)
|
||||||
|
defer typecheck.deinit(&checked)
|
||||||
|
return try lower(mem.c_allocator, &tree, &resolution, &checked, diagnostic_store)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
expect_trap proc(
|
||||||
|
outcome interpreter.Outcome,
|
||||||
|
diagnostic_store @diagnostics.Store,
|
||||||
|
code diagnostics.Code,
|
||||||
|
) void ! testing.Error {
|
||||||
|
if (outcome.trap) |id| {
|
||||||
|
if (diagnostics.get(diagnostic_store, id)) |diagnostic| {
|
||||||
|
try testing.expect_equal(code, diagnostic.code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .expectation_failed
|
||||||
|
}
|
||||||
|
|
||||||
|
executes_calls_locals_arithmetic_and_nested_argument_slices test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`add proc(left int, right int) int {
|
||||||
|
` result :: left + right
|
||||||
|
` return result
|
||||||
|
`}
|
||||||
|
`double proc(value int) int {
|
||||||
|
` return value * 2
|
||||||
|
`}
|
||||||
|
`main proc() int {
|
||||||
|
` a :: add(double(4), add(5, 6))
|
||||||
|
` b :: add(10, 13)
|
||||||
|
` return a + b
|
||||||
|
`}
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.int, outcome.value.type)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
owns_decoded_strings_after_source_and_frontend_are_freed test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`text :: "a\\\"\n\r\t\0\x41\x7a"
|
||||||
|
`identity proc(value string) string {
|
||||||
|
` return value
|
||||||
|
`}
|
||||||
|
`main proc() string {
|
||||||
|
` return identity(text)
|
||||||
|
`}
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.string, outcome.value.type)
|
||||||
|
try testing.expect(mem.eql(outcome.value.data.string, "a\\\"\n\r\t\x00Az"))
|
||||||
|
}
|
||||||
|
|
||||||
|
honors_float_context_and_float_division test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`main proc() float {
|
||||||
|
` left :: 3
|
||||||
|
` return -(left / 2) + 2.0
|
||||||
|
`}
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.float, outcome.value.type)
|
||||||
|
try testing.expect_equal(f64(0.5), outcome.value.data.real)
|
||||||
|
}
|
||||||
|
|
||||||
|
executes_forward_lazy_globals_and_function_aliases test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`forward :: later + 1
|
||||||
|
`alias :: add
|
||||||
|
`later :: 18
|
||||||
|
`add proc(left int, right int) int {
|
||||||
|
` local_alias :: alias
|
||||||
|
` return left + right
|
||||||
|
`}
|
||||||
|
`main proc() int {
|
||||||
|
` invoke :: alias
|
||||||
|
` return invoke(forward, 23)
|
||||||
|
`}
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
again :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect(again.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), again.value.data.integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
supports_bare_void_value_returns_and_fallthrough test {
|
||||||
|
sources [3][]u8 :: [
|
||||||
|
"main proc() void {\n return\n}\n",
|
||||||
|
"noop proc() void {}\nmain proc() void {\n return noop()\n}\n",
|
||||||
|
"noop proc() void {}\nmain proc() void {\n noop()\n}\n",
|
||||||
|
]
|
||||||
|
for (sources) |source| {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.void, outcome.value.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preserves_demanded_diagnostic_identity_and_source_location test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`bad proc() int {
|
||||||
|
` return unknown
|
||||||
|
`}
|
||||||
|
`main proc() int {
|
||||||
|
` before :: 1 + 2
|
||||||
|
` return bad()
|
||||||
|
`}
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||||
|
diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
|
||||||
|
try testing.expect_equal(diagnostics.Code.unknown_name, diagnostic.code)
|
||||||
|
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "unknown"))
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
|
||||||
|
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|
||||||
|
skips_unreachable_and_unused_invalid_code test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`bad proc() int {
|
||||||
|
` return unknown
|
||||||
|
`}
|
||||||
|
`unused :: "\q"
|
||||||
|
`main proc() int {
|
||||||
|
` return 42
|
||||||
|
` 9223372036854775807 + 1
|
||||||
|
` missing
|
||||||
|
` huge :: 9223372036854775808
|
||||||
|
`}
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
try testing.expect(diagnostics.count(&diagnostic_store) > 0)
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
traps_local_forward_loads_and_global_initialization_cycles test {
|
||||||
|
sources [3][]u8 :: [
|
||||||
|
"main proc() int {\n before :: after\n after :: 42\n return before\n}\n",
|
||||||
|
"left int :: right\nright int :: left\nmain proc() int {\n return left\n}\n",
|
||||||
|
"self int :: self\nmain proc() int {\n return self\n}\n",
|
||||||
|
]
|
||||||
|
for (sources) |source, i| {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
code diagnostics.Code :: if (i == 0) .read_before_initialization else .initialization_cycle
|
||||||
|
try expect_trap(outcome, &diagnostic_store, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handles_integer_boundaries_without_host_overflow test {
|
||||||
|
sources [3][]u8 :: [
|
||||||
|
"main proc() int {\n return 9223372036854775807\n}\n",
|
||||||
|
"main proc() int {\n return -9223372036854775808\n}\n",
|
||||||
|
"main proc() int {\n return -0009223372036854775808\n}\n",
|
||||||
|
]
|
||||||
|
expected [3]i64 :: [maxval!(i64), minval!(i64), minval!(i64)]
|
||||||
|
for (sources) |source, i| {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(expected[i], outcome.value.data.integer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traps_invalid_literals_escapes_and_checked_integer_arithmetic test {
|
||||||
|
sources [8][]u8 :: [
|
||||||
|
"main proc() int {\n return 9223372036854775808\n}\n",
|
||||||
|
"main proc() int {\n return -9223372036854775809\n}\n",
|
||||||
|
"main proc() int {\n return 9223372036854775807 + 1\n}\n",
|
||||||
|
"main proc() int {\n return -(-9223372036854775808)\n}\n",
|
||||||
|
"main proc() int {\n return -9223372036854775808 - 1\n}\n",
|
||||||
|
"main proc() int {\n return 9223372036854775807 * 2\n}\n",
|
||||||
|
"main proc() string {\n return \"\\q\"\n}\n",
|
||||||
|
"main proc() string {\n return \"\\x0g\"\n}\n",
|
||||||
|
]
|
||||||
|
for (sources) |source, i| {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
code := diagnostics.Code.integer_overflow
|
||||||
|
if (i < 2) code = .literal_out_of_range
|
||||||
|
if (i >= 6) code = .invalid_escape
|
||||||
|
try expect_trap(outcome, &diagnostic_store, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validates_main_and_preserves_first_declaration test {
|
||||||
|
sources [4][]u8 :: [
|
||||||
|
"other proc() void {}\n",
|
||||||
|
"main :: 42\n",
|
||||||
|
"main proc(value int) int {\n return value\n}\n",
|
||||||
|
"main proc() int {\n return 42\n}\nmain proc() int {\n return 7\n}\n",
|
||||||
|
]
|
||||||
|
for (sources) |source, i| {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
if (i == 3) {
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
} else {
|
||||||
|
code diagnostics.Code :: if (i == 0) .missing_entry else .invalid_entry
|
||||||
|
try expect_trap(outcome, &diagnostic_store, code)
|
||||||
|
try testing.expect_equal(module.entry_diagnostic, outcome.trap?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid_signatures_trap_without_poisoning_unrelated_main test {
|
||||||
|
sources [2][]u8 :: [
|
||||||
|
"broken proc(value void) void {}\nmain proc() int {\n return 42\n}\n",
|
||||||
|
"main proc() Unknown {\n return 42\n}\n",
|
||||||
|
]
|
||||||
|
for (sources) |source, i| {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
try testing.expect(diagnostics.count(&diagnostic_store) > 0)
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
if (i == 0) {
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
} else {
|
||||||
|
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
|
||||||
|
try expect_trap(outcome, &diagnostic_store, .unknown_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parses_float_context_without_integer_range_limits_and_rejects_float_overflow test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile_owned(
|
||||||
|
"main proc() float {\n return 18446744073709551616\n}\n",
|
||||||
|
&diagnostic_store,
|
||||||
|
)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.float, outcome.value.type)
|
||||||
|
try testing.expect_equal(f64(18446744073709551616.0), outcome.value.data.real)
|
||||||
|
source := arraylist.init(u8, mem.c_allocator)
|
||||||
|
defer arraylist.deinit(&source)
|
||||||
|
for ("main proc() float {\n return ") |byte| try arraylist.append(&source, byte)
|
||||||
|
for (usize(0)..usize(400)) |_| try arraylist.append(&source, '9')
|
||||||
|
for (".0\n}\n") |byte| try arraylist.append(&source, byte)
|
||||||
|
oversized := try compile_owned(source.items, &diagnostic_store)
|
||||||
|
defer ir.deinit(&oversized)
|
||||||
|
trapped :: try interpreter.run(mem.c_allocator, &oversized, &diagnostic_store)
|
||||||
|
try expect_trap(trapped, &diagnostic_store, .literal_out_of_range)
|
||||||
|
}
|
||||||
|
|
||||||
|
accepts_representable_subnormals_and_rejects_underflow_to_zero test {
|
||||||
|
zeros [2]usize :: [307, 400]
|
||||||
|
for (zeros) |count| {
|
||||||
|
source := arraylist.init(u8, mem.c_allocator)
|
||||||
|
defer arraylist.deinit(&source)
|
||||||
|
for ("main proc() float {\n return 0.") |byte| try arraylist.append(&source, byte)
|
||||||
|
for (usize(0)..count) |_| try arraylist.append(&source, '0')
|
||||||
|
for ("1\n}\n") |byte| try arraylist.append(&source, byte)
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile_owned(source.items, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
if (count == 307) {
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.float, outcome.value.type)
|
||||||
|
try testing.expect(outcome.value.data.real > 0 and outcome.value.data.real < 1)
|
||||||
|
} else {
|
||||||
|
try expect_trap(outcome, &diagnostic_store, .literal_out_of_range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
AllocationState :: struct {
|
||||||
|
remaining usize
|
||||||
|
live usize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
limited_alloc proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
|
||||||
|
if (context) |userdata| {
|
||||||
|
state @mut AllocationState :: ptrcast!(userdata)
|
||||||
|
if (state.remaining == 0) return null
|
||||||
|
state.remaining -= 1
|
||||||
|
memory :: mem.raw_alloc(mem.c_allocator, size, alignment)
|
||||||
|
if (memory) |_| state.live += 1
|
||||||
|
return memory
|
||||||
|
}
|
||||||
|
unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
limited_realloc proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
|
||||||
|
if (context) |userdata| {
|
||||||
|
state @mut AllocationState :: ptrcast!(userdata)
|
||||||
|
if (state.remaining == 0) return null
|
||||||
|
state.remaining -= 1
|
||||||
|
resized :: mem.raw_realloc(mem.c_allocator, memory, old_size, new_size, alignment)
|
||||||
|
if (resized) |_| {
|
||||||
|
if (memory) |_| {} else state.live += 1
|
||||||
|
}
|
||||||
|
return resized
|
||||||
|
}
|
||||||
|
unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
limited_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
|
||||||
|
if (context) |userdata| {
|
||||||
|
state @mut AllocationState :: ptrcast!(userdata)
|
||||||
|
if (memory) |_| state.live -= 1
|
||||||
|
mem.raw_free(mem.c_allocator, memory, size, alignment)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
limited_vtable :: mem.AllocatorVTable{ alloc = limited_alloc, realloc = limited_realloc, free = limited_free }
|
||||||
|
|
||||||
|
releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
|
||||||
|
strpool.strings = strpool.init(mem.c_allocator)
|
||||||
|
defer strpool.deinit(&strpool.strings)
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`text :: "owned\nstring"
|
||||||
|
`factor float :: 3.5
|
||||||
|
`identity proc(value string) string {
|
||||||
|
` local :: value
|
||||||
|
` return local
|
||||||
|
`}
|
||||||
|
`main proc() string {
|
||||||
|
` computed float :: factor * 2
|
||||||
|
` return identity(text)
|
||||||
|
`}
|
||||||
|
scan_state := lexer.init(mem.c_allocator)
|
||||||
|
defer lexer.deinit(&scan_state)
|
||||||
|
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||||
|
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||||
|
defer ast.deinit(&tree)
|
||||||
|
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||||
|
defer sema.deinit(&resolution)
|
||||||
|
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||||
|
defer typecheck.deinit(&checked)
|
||||||
|
succeeded := false
|
||||||
|
failures := usize(0)
|
||||||
|
for (usize(0)..usize(128)) |limit| {
|
||||||
|
state := AllocationState{ remaining = limit }
|
||||||
|
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||||
|
module := lower(allocator, &tree, &resolution, &checked, &diagnostic_store) catch |error| {
|
||||||
|
try testing.expect(error == .out_of_memory)
|
||||||
|
failures += 1
|
||||||
|
try testing.expect_equal(usize(0), state.live)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
outcome :: interpreter.run(mem.c_allocator, &module, &diagnostic_store) catch |error| {
|
||||||
|
ir.deinit(&module)
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
ir.deinit(&module)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(usize(0), state.live)
|
||||||
|
succeeded = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
try testing.expect(succeeded)
|
||||||
|
try testing.expect(failures > 0)
|
||||||
|
}
|
||||||
+194
-50
@@ -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! {
|
|
||||||
strpool.strings = strpool.init(mem.c_allocator)
|
|
||||||
defer strpool.deinit(&strpool.strings)
|
|
||||||
|
|
||||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
|
||||||
defer diagnostics.deinit(&diagnostic_store)
|
|
||||||
|
|
||||||
scan_state := lexer.init(mem.c_allocator)
|
|
||||||
defer lexer.deinit(&scan_state)
|
|
||||||
try lexer.scan(&scan_state, program, &diagnostic_store)
|
|
||||||
|
|
||||||
tree := try parser.parse(
|
|
||||||
mem.c_allocator,
|
|
||||||
scan_state.tokens.items,
|
|
||||||
program,
|
|
||||||
&diagnostic_store,
|
|
||||||
)
|
)
|
||||||
defer ast.deinit(&tree)
|
|
||||||
|
|
||||||
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
main proc(init process.Init) i32 {
|
||||||
defer sema.deinit(&resolution)
|
return command(mem.c_allocator, init.io) catch |err| {
|
||||||
|
if (process.argument(2)) |path| {
|
||||||
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
io.print("{s}: infrastructure error: {s}\n", io.stderr(init.io), {
|
||||||
defer typecheck.deinit(&checked)
|
argument_text(path), infrastructure_message(err),
|
||||||
|
}) catch |_| {}
|
||||||
renderer := ast_renderer.init(mem.c_allocator)
|
} else {
|
||||||
defer ast_renderer.deinit(&renderer)
|
io.print("honey: infrastructure error: {s}\n", io.stderr(init.io), {
|
||||||
try ast_renderer.render(&renderer, &tree)
|
infrastructure_message(err),
|
||||||
|
}) catch |_| {}
|
||||||
debug.print("{s}", {renderer.bytes.items})
|
}
|
||||||
debug.print("diagnostics: {}\n", {diagnostics.count(&diagnostic_store)})
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
infrastructure_message proc(error DriverError) []u8 {
|
||||||
|
return match error {
|
||||||
|
.out_of_memory: "out of memory"
|
||||||
|
.diagnostic_limit_reached: "diagnostic limit reached"
|
||||||
|
.source_too_large: "source file is too large"
|
||||||
|
.out_of_space: "identifier storage is full"
|
||||||
|
.open_failed: "cannot open source file"
|
||||||
|
.close_failed: "cannot close source file"
|
||||||
|
.not_open_for_reading: "source file is not readable"
|
||||||
|
.read_failed: "cannot read source file"
|
||||||
|
.not_open_for_writing: "output is not writable"
|
||||||
|
.write_failed: "cannot write output"
|
||||||
|
.no_progress: "output writer made no progress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
command proc(allocator mem.Allocator, runtime io.Io) i32 ! DriverError {
|
||||||
|
if (process.argument_count() != 3) {
|
||||||
|
try io.print("usage: honey run <file>\n", io.stderr(runtime), {})
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if (process.argument(1)) |operation| {
|
||||||
|
if (!mem.eql(argument_text(operation), "run")) {
|
||||||
|
try io.print("usage: honey run <file>\n", io.stderr(runtime), {})
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if (process.argument(2)) |path| return try run_file(allocator, runtime, path)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
argument_text proc(pointer [*;0]u8) []u8 {
|
||||||
|
length := usize(0)
|
||||||
|
while (pointer[length] != 0) length += 1
|
||||||
|
return pointer[..length]
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
read_source proc(allocator mem.Allocator, runtime io.Io, path [*;0]u8) arraylist.ArrayList(u8) ! DriverError {
|
||||||
|
# bootstrap cannot construct a dynamic sentinel slice from argv's sentinel pointer.
|
||||||
|
descriptor := c_int(-1)
|
||||||
|
while true {
|
||||||
|
descriptor = c.open(ptrcast!(path), c.O_RDONLY)
|
||||||
|
if (descriptor >= 0) break
|
||||||
|
if (c.__error()?^ != c.EINTR) return .open_failed
|
||||||
|
}
|
||||||
|
file :: io.File{ io = runtime, handle = io.Handle{ file_desc = descriptor } }
|
||||||
|
closing := false
|
||||||
|
defer {
|
||||||
|
if (!closing) io.close(file) catch |_| {}
|
||||||
|
}
|
||||||
|
bytes := arraylist.init(u8, allocator)
|
||||||
|
errdefer arraylist.deinit(&bytes)
|
||||||
|
input :: io.reader(file)
|
||||||
|
maximum :: usize(maxval!(u32))
|
||||||
|
while true {
|
||||||
|
length :: bytes.items.len
|
||||||
|
if (length == maximum) {
|
||||||
|
probe [1]mut u8 := undefined
|
||||||
|
if (try io.read(input, (&probe)[..]) != 0) return .source_too_large
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (length == bytes.capacity) {
|
||||||
|
growth := usize(4096)
|
||||||
|
if (growth > maximum - length) growth = maximum - length
|
||||||
|
try arraylist.reserve(&bytes, length + growth)
|
||||||
|
}
|
||||||
|
storage :: bytes.items.ptr[..bytes.capacity]
|
||||||
|
end := bytes.capacity
|
||||||
|
if (end > maximum) end = maximum
|
||||||
|
count :: try io.read(input, storage[length..end])
|
||||||
|
if (count == 0) break
|
||||||
|
bytes.items = storage[..length + count]
|
||||||
|
}
|
||||||
|
closing = true
|
||||||
|
try io.close(file)
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
compile proc(allocator mem.Allocator, source []u8, diagnostic_store @mut diagnostics.Store) ir.Module ! DriverError {
|
||||||
|
strpool.strings = strpool.init(allocator)
|
||||||
|
defer strpool.deinit(&strpool.strings)
|
||||||
|
scan_state := lexer.init(allocator)
|
||||||
|
defer lexer.deinit(&scan_state)
|
||||||
|
try lexer.scan(&scan_state, source, diagnostic_store)
|
||||||
|
tree := try parser.parse(allocator, scan_state.tokens.items, source, diagnostic_store)
|
||||||
|
defer ast.deinit(&tree)
|
||||||
|
resolution := try sema.resolve(allocator, &tree, diagnostic_store)
|
||||||
|
defer sema.deinit(&resolution)
|
||||||
|
checked := try typecheck.check(allocator, &tree, &resolution, diagnostic_store)
|
||||||
|
defer typecheck.deinit(&checked)
|
||||||
|
return try lower.lower(allocator, &tree, &resolution, &checked, diagnostic_store)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
run_file proc(allocator mem.Allocator, runtime io.Io, path [*;0]u8) i32 ! DriverError {
|
||||||
|
source := try read_source(allocator, runtime, path)
|
||||||
|
defer arraylist.deinit(&source)
|
||||||
|
diagnostic_store := diagnostics.init(allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile(allocator, source.items, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
outcome :: try interpreter.run(allocator, &module, &diagnostic_store)
|
||||||
|
if (outcome.trap == null) try print_result(io.stdout(runtime), outcome.value)
|
||||||
|
try report_diagnostics(allocator, io.stderr(runtime), argument_text(path), source.items, &diagnostic_store, outcome.trap)
|
||||||
|
if (outcome.trap != null or diagnostics.count(&diagnostic_store) != 0) return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
print_result proc(output io.Writer, value ir.Value) void ! io.WriteError {
|
||||||
|
match value.type {
|
||||||
|
.int: try io.print("{}\n", output, {value.data.integer})
|
||||||
|
.float: try io.print("{d}\n", output, {value.data.real})
|
||||||
|
.string: try io.print("{s}\n", output, {value.data.string})
|
||||||
|
.void: {}
|
||||||
|
else: unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
report_diagnostics proc(
|
||||||
|
allocator mem.Allocator,
|
||||||
|
output io.Writer,
|
||||||
|
path []u8,
|
||||||
|
source []u8,
|
||||||
|
diagnostic_store @diagnostics.Store,
|
||||||
|
trap ?diagnostics.DiagnosticId,
|
||||||
|
) void ! (mem.AllocError | io.WriteError) {
|
||||||
|
if (diagnostics.count(diagnostic_store) == 0) return
|
||||||
|
starts := arraylist.init(usize, allocator)
|
||||||
|
defer arraylist.deinit(&starts)
|
||||||
|
try arraylist.append(&starts, usize(0))
|
||||||
|
for (source) |byte, index| {
|
||||||
|
if (byte == '\n') try arraylist.append(&starts, index + 1)
|
||||||
|
}
|
||||||
|
for (0..diagnostics.count(diagnostic_store)) |index| {
|
||||||
|
if (diagnostics.get(diagnostic_store, diagnostics.DiagnosticId(index))) |diagnostic| {
|
||||||
|
offset :: usize(diagnostic.span.start)
|
||||||
|
line := usize(0)
|
||||||
|
end := starts.items.len
|
||||||
|
while (line + 1 < end) {
|
||||||
|
middle :: line + divtrunc!(end - line, 2)
|
||||||
|
if (starts.items[middle] <= offset) line = middle
|
||||||
|
else end = middle
|
||||||
|
}
|
||||||
|
label []u8 := "error"
|
||||||
|
if (trap) |id| {
|
||||||
|
if (id == diagnostics.DiagnosticId(index)) label = "runtime trap"
|
||||||
|
}
|
||||||
|
try io.print("{s}:{}:{}: {s}: {s}\n", output, {
|
||||||
|
path, line + 1, offset - starts.items[line] + 1, label, diagnostics.message(diagnostic.code),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
return try parse_statement_decl(state)
|
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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user