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