feat: execute conditionals through typed IR
Use typed singleton arrays for braceless arm blocks: &[statement] compiled but trapped under the Bro bootstrap.
This commit is contained in:
@@ -33,11 +33,17 @@ the native bootstrap accepts either a no-argument `main` or `main proc(init proc
|
|||||||
|
|
||||||
source loading uses the supplied `io.Io` for opening, reading, and closing files. `io.open` accepts a borrowed zero-terminated slice (`[;0]u8`), including `init.args.values[index]` without copying its storage. the bootstrap supports checked sentinel-slice construction with `pointer[..length;0]`. `io.read_all(allocator, reader, maximum)` reads to EOF into a caller-owned `ArrayList(u8)`; release it with `arraylist.deinit`. it frees partial storage on error and consumes one extra byte to distinguish an exact-size input from a limit violation. the driver maps `limit_exceeded` to `source_too_large` at its `u32` source-size limit.
|
source loading uses the supplied `io.Io` for opening, reading, and closing files. `io.open` accepts a borrowed zero-terminated slice (`[;0]u8`), including `init.args.values[index]` without copying its storage. the bootstrap supports checked sentinel-slice construction with `pointer[..length;0]`. `io.read_all(allocator, reader, maximum)` reads to EOF into a caller-owned `ArrayList(u8)`; release it with `arraylist.deinit`. it frees partial storage on error and consumes one extra byte to distinguish an exact-size input from a limit violation. the driver maps `limit_exceeded` to `source_too_large` at its `u32` source-size limit.
|
||||||
|
|
||||||
|
a recursive example is in [programs/factorial.hon](programs/factorial.hon); `./build/honey run programs/factorial.hon` prints `120`.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
name resolution (`@source/sema`) runs after parsing. it predeclares package names and function-body locals, allowing forward references and mutually recursive functions. parameters and locals share a function scope; signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `string`, `void`); sibling function scopes may independently reuse names.
|
`true` and `false` are boolean literals; `bool` is available for parameters, storage and results. `==`, `!=`, `<`, `<=`, `>` and `>=` compare compatible numeric operands and produce `bool`; `==` and `!=` also accept boolean operands. comparisons preserve contextual numeric inference without making their operands boolean. arithmetic binds tighter than relational comparisons, which bind tighter than equality; comparisons associate left to right. boolean arithmetic/ordering and string/function/void comparisons are invalid; there is no implicit numeric-to-boolean conversion.
|
||||||
|
|
||||||
|
`if`/`else` supports braced arms, single statements and `else if` chains. conditions must have type `bool`. a braceless guard requires parentheses unless it is a function call: `if (n <= 1) return 1` or `if ready() return`. `else` binds to the nearest `if`; after a braced arm it may start on the next line. branches are represented as AST blocks even when braceless.
|
||||||
|
|
||||||
|
name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `bool`, `string`, `void`); sibling function and arm scopes may independently reuse names.
|
||||||
|
|
||||||
`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax retains its existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration.
|
`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax retains its existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration.
|
||||||
|
|
||||||
@@ -45,10 +51,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. 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.
|
type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and conditional arms 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 and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing `else` permits fallthrough, even for a literal `true` condition; a later return can close that path. statements after a return and untaken branches are still type-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.
|
`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 jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; local slots and temporaries are function-wide, without SSA or phi nodes. 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.
|
`interpreter.run` consumes only IR and returns a value or a diagnostic trap; returned strings borrow the module. heap-backed invocation frames track their current block and resume it after calls or global initialization. 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 and comparisons, including NaN behavior. 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.
|
recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and conditional arms retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not arms that already return or trap. 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, loops, assignment and boolean logical operators remain unimplemented.
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
factorial proc(n int) int {
|
||||||
|
if (n <= 1) return 1
|
||||||
|
return n * factorial(n - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
main proc() int {
|
||||||
|
return factorial(5)
|
||||||
|
}
|
||||||
+65
-7
@@ -1,11 +1,14 @@
|
|||||||
#! grammar:
|
#! grammar:
|
||||||
#! * type ..... -> identifier
|
#! * type ..... -> identifier
|
||||||
#! * assignment -> :: | :=
|
#! * assignment -> :: | :=
|
||||||
#! * literal .. -> int | float | string
|
#! * literal .. -> int | float | string | 'true' | 'false'
|
||||||
#! * 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 | 'return' [expr]
|
#! * block .... -> '{' {stmt} '}'
|
||||||
|
#! * arm ...... -> block | stmt (single statements become canonical blocks)
|
||||||
|
#! * stmt ..... -> decl | expr | 'return' [expr] | 'if' expr arm ['else' arm]
|
||||||
|
#! braceless guards require parentheses unless the condition is a call; else binds to the nearest if.
|
||||||
|
|
||||||
import "@std/debug"
|
import "@std/debug"
|
||||||
import "@std/mem"
|
import "@std/mem"
|
||||||
@@ -22,13 +25,14 @@ NodeId :: distinct u32
|
|||||||
no_node :: maxval!(NodeId)
|
no_node :: maxval!(NodeId)
|
||||||
|
|
||||||
LiteralKind :: enum {
|
LiteralKind :: enum {
|
||||||
int, float, string
|
int, float, string, bool
|
||||||
}
|
}
|
||||||
|
|
||||||
ExprKind :: enum {
|
ExprKind :: enum {
|
||||||
literal_int
|
literal_int
|
||||||
literal_float
|
literal_float
|
||||||
literal_string
|
literal_string
|
||||||
|
literal_bool
|
||||||
identifier
|
identifier
|
||||||
unary
|
unary
|
||||||
binary
|
binary
|
||||||
@@ -41,7 +45,9 @@ DeclKind :: enum {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StmtKind :: enum {
|
StmtKind :: enum {
|
||||||
declaration, expression, return_stmt, invalid
|
declaration,
|
||||||
|
expression,
|
||||||
|
return_stmt, if_stmt, invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
Mutability :: enum {
|
Mutability :: enum {
|
||||||
@@ -51,7 +57,11 @@ Mutability :: enum {
|
|||||||
UnaryOperator :: enum { negate }
|
UnaryOperator :: enum { negate }
|
||||||
|
|
||||||
BinaryOperator :: enum {
|
BinaryOperator :: enum {
|
||||||
add, subtract, multiply, divide
|
add, subtract,
|
||||||
|
multiply, divide
|
||||||
|
equal, not_equal,
|
||||||
|
less, less_equal,
|
||||||
|
greater, greater_equal
|
||||||
}
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
@@ -59,6 +69,7 @@ NodeKind :: enum {
|
|||||||
literal_int,
|
literal_int,
|
||||||
literal_float,
|
literal_float,
|
||||||
literal_string,
|
literal_string,
|
||||||
|
literal_bool,
|
||||||
expr_identifier,
|
expr_identifier,
|
||||||
expr_unary,
|
expr_unary,
|
||||||
expr_binary,
|
expr_binary,
|
||||||
@@ -66,6 +77,7 @@ NodeKind :: enum {
|
|||||||
stmt_expr,
|
stmt_expr,
|
||||||
stmt_decl,
|
stmt_decl,
|
||||||
stmt_return,
|
stmt_return,
|
||||||
|
stmt_if,
|
||||||
func_decl,
|
func_decl,
|
||||||
param_decl,
|
param_decl,
|
||||||
block,
|
block,
|
||||||
@@ -157,6 +169,13 @@ ReturnView :: struct {
|
|||||||
value ?NodeId
|
value ?NodeId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IfView :: struct {
|
||||||
|
span Span
|
||||||
|
condition NodeId
|
||||||
|
then_block NodeId
|
||||||
|
else_block ?NodeId
|
||||||
|
}
|
||||||
|
|
||||||
InvalidView :: struct {
|
InvalidView :: struct {
|
||||||
span Span
|
span Span
|
||||||
diagnostic DiagnosticId
|
diagnostic DiagnosticId
|
||||||
@@ -206,6 +225,7 @@ add_literal proc(builder @mut Builder, kind LiteralKind, span Span) NodeId ! mem
|
|||||||
.int: .literal_int
|
.int: .literal_int
|
||||||
.float: .literal_float
|
.float: .literal_float
|
||||||
.string: .literal_string
|
.string: .literal_string
|
||||||
|
.bool: .literal_bool
|
||||||
}
|
}
|
||||||
return try add_node(builder, Node{ kind = node_kind }, span)
|
return try add_node(builder, Node{ kind = node_kind }, span)
|
||||||
}
|
}
|
||||||
@@ -273,6 +293,22 @@ add_return_stmt proc(builder @mut Builder, span Span, value ?NodeId) NodeId ! me
|
|||||||
}, span)
|
}, span)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
add_if_stmt proc(
|
||||||
|
builder @mut Builder,
|
||||||
|
span Span,
|
||||||
|
condition NodeId,
|
||||||
|
then_block NodeId,
|
||||||
|
else_block ?NodeId,
|
||||||
|
) NodeId ! mem.AllocError {
|
||||||
|
alternative :: if (else_block) |id| id else no_node
|
||||||
|
return try add_node(builder, Node{
|
||||||
|
kind = .stmt_if,
|
||||||
|
main = NodeData{ node_id = condition },
|
||||||
|
data0 = NodeData{ node_id = then_block },
|
||||||
|
data1 = NodeData{ node_id = alternative },
|
||||||
|
}, span)
|
||||||
|
}
|
||||||
|
|
||||||
add_statement_decl proc(
|
add_statement_decl proc(
|
||||||
builder @mut Builder,
|
builder @mut Builder,
|
||||||
span Span,
|
span Span,
|
||||||
@@ -408,6 +444,7 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind {
|
|||||||
.stmt_decl: .declaration
|
.stmt_decl: .declaration
|
||||||
.stmt_expr: .expression
|
.stmt_expr: .expression
|
||||||
.stmt_return: .return_stmt
|
.stmt_return: .return_stmt
|
||||||
|
.stmt_if: .if_stmt
|
||||||
.invalid_stmt: .invalid
|
.invalid_stmt: .invalid
|
||||||
else: unreachable
|
else: unreachable
|
||||||
}
|
}
|
||||||
@@ -428,6 +465,17 @@ return_stmt proc(tree @Tree, id NodeId) ReturnView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if_stmt proc(tree @Tree, id NodeId) IfView {
|
||||||
|
node :: get_node(&tree.storage, id)
|
||||||
|
debug.assert(node.kind == .stmt_if)
|
||||||
|
return IfView{
|
||||||
|
span = get_span(&tree.storage, id),
|
||||||
|
condition = node.main.node_id,
|
||||||
|
then_block = node.data0.node_id,
|
||||||
|
else_block = maybe_node_id(u32(node.data1.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)
|
||||||
@@ -491,10 +539,20 @@ block_statement proc(tree @Tree, id NodeId, index usize) NodeId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expr_kind proc(tree @Tree, id NodeId) ExprKind {
|
expr_kind proc(tree @Tree, id NodeId) ExprKind {
|
||||||
return match get_node(&tree.storage, id).kind {
|
return expression_kind(get_node(&tree.storage, id).kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder_expr_kind proc(builder @Builder, id NodeId) ExprKind {
|
||||||
|
return expression_kind(get_node(&builder.storage, id).kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
expression_kind proc(kind NodeKind) ExprKind {
|
||||||
|
return match kind {
|
||||||
.literal_int: .literal_int
|
.literal_int: .literal_int
|
||||||
.literal_float: .literal_float
|
.literal_float: .literal_float
|
||||||
.literal_string: .literal_string
|
.literal_string: .literal_string
|
||||||
|
.literal_bool: .literal_bool
|
||||||
.expr_identifier: .identifier
|
.expr_identifier: .identifier
|
||||||
.expr_unary: .unary
|
.expr_unary: .unary
|
||||||
.expr_binary: .binary
|
.expr_binary: .binary
|
||||||
|
|||||||
@@ -41,8 +41,14 @@ render_function proc(
|
|||||||
try append_text(renderer, " function ")
|
try append_text(renderer, " function ")
|
||||||
try append_text(renderer, ast.source_text(tree, function.name))
|
try append_text(renderer, ast.source_text(tree, function.name))
|
||||||
try append_text(renderer, "\n")
|
try append_text(renderer, "\n")
|
||||||
for 0..ast.block_statement_count(tree, function.body) |index| {
|
try render_block(renderer, tree, function.body, 2)
|
||||||
statement :: ast.block_statement(tree, function.body, index)
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, depth usize) void ! mem.AllocError {
|
||||||
|
for 0..ast.block_statement_count(tree, block) |index| {
|
||||||
|
statement :: ast.block_statement(tree, block, index)
|
||||||
|
for 0..depth |_| try append_text(renderer, " ")
|
||||||
match ast.stmt_kind(tree, statement) {
|
match ast.stmt_kind(tree, statement) {
|
||||||
.declaration: try render_statement(renderer, tree, statement, "")
|
.declaration: try render_statement(renderer, tree, statement, "")
|
||||||
.expression: {
|
.expression: {
|
||||||
@@ -59,6 +65,18 @@ render_function proc(
|
|||||||
}
|
}
|
||||||
try append_text(renderer, "\n")
|
try append_text(renderer, "\n")
|
||||||
}
|
}
|
||||||
|
.if_stmt: {
|
||||||
|
conditional :: ast.if_stmt(tree, statement)
|
||||||
|
try append_text(renderer, "if ")
|
||||||
|
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, conditional.condition)))
|
||||||
|
try append_text(renderer, "\n")
|
||||||
|
try render_block(renderer, tree, conditional.then_block, depth + 1)
|
||||||
|
if (conditional.else_block) |alternative| {
|
||||||
|
for 0..depth |_| try append_text(renderer, " ")
|
||||||
|
try append_text(renderer, "else\n")
|
||||||
|
try render_block(renderer, tree, alternative, depth + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
.invalid: try append_text(renderer, "invalid statement\n")
|
.invalid: try append_text(renderer, "invalid statement\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ Code :: enum {
|
|||||||
cannot_infer_type,
|
cannot_infer_type,
|
||||||
missing_return,
|
missing_return,
|
||||||
return_type_mismatch,
|
return_type_mismatch,
|
||||||
|
condition_not_bool,
|
||||||
missing_entry,
|
missing_entry,
|
||||||
invalid_entry,
|
invalid_entry,
|
||||||
literal_out_of_range,
|
literal_out_of_range,
|
||||||
@@ -85,13 +86,14 @@ message proc(code Code) []u8 {
|
|||||||
.type_not_value: "type name cannot be used as a value"
|
.type_not_value: "type name cannot be used as a value"
|
||||||
.invalid_void_type: "void cannot be stored or used as a parameter type"
|
.invalid_void_type: "void cannot be stored or used as a parameter type"
|
||||||
.type_mismatch: "incompatible value type"
|
.type_mismatch: "incompatible value type"
|
||||||
.invalid_operand: "arithmetic requires compatible numeric operands"
|
.invalid_operand: "operator requires compatible operand types"
|
||||||
.integer_division: "division requires float operands"
|
.integer_division: "division requires float operands"
|
||||||
.not_callable: "value is not callable"
|
.not_callable: "value is not callable"
|
||||||
.argument_count_mismatch: "argument count does not match the function signature"
|
.argument_count_mismatch: "argument count does not match the function signature"
|
||||||
.cannot_infer_type: "cannot infer a type without a type anchor"
|
.cannot_infer_type: "cannot infer a type without a type anchor"
|
||||||
.missing_return: "nonvoid function can finish without returning a value"
|
.missing_return: "nonvoid function can finish without returning a value"
|
||||||
.return_type_mismatch: "return does not match the function result type"
|
.return_type_mismatch: "return does not match the function result type"
|
||||||
|
.condition_not_bool: "if condition must have type bool"
|
||||||
.missing_entry: "program requires a main function"
|
.missing_entry: "program requires a main function"
|
||||||
.invalid_entry: "main must be a function with no parameters"
|
.invalid_entry: "main must be a function with no parameters"
|
||||||
.literal_out_of_range: "numeric literal is outside its runtime type's range"
|
.literal_out_of_range: "numeric literal is outside its runtime type's range"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ GlobalState :: struct {
|
|||||||
@hide:file
|
@hide:file
|
||||||
Frame :: struct {
|
Frame :: struct {
|
||||||
function ir.FunctionId
|
function ir.FunctionId
|
||||||
|
block ir.BlockId
|
||||||
next_instruction usize = 0
|
next_instruction usize = 0
|
||||||
destination ir.ValueId = ir.no_value
|
destination ir.ValueId = ir.no_value
|
||||||
global ?usize = null
|
global ?usize = null
|
||||||
@@ -60,11 +61,21 @@ run proc(
|
|||||||
while frames.items.len != 0 {
|
while frames.items.len != 0 {
|
||||||
frame_index :: frames.items.len - 1
|
frame_index :: frames.items.len - 1
|
||||||
function :: &module.functions.items[usize(frames.items[frame_index].function)]
|
function :: &module.functions.items[usize(frames.items[frame_index].function)]
|
||||||
block :: &function.blocks.items[usize(function.entry)]
|
block :: &function.blocks.items[usize(frames.items[frame_index].block)]
|
||||||
instruction_index :: frames.items[frame_index].next_instruction
|
instruction_index :: frames.items[frame_index].next_instruction
|
||||||
if instruction_index == block.instructions.items.len {
|
if instruction_index == block.instructions.items.len {
|
||||||
terminator :: block.terminator
|
terminator :: block.terminator
|
||||||
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
|
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
|
||||||
|
if (terminator.kind == .jump or terminator.kind == .branch) {
|
||||||
|
target := terminator.target
|
||||||
|
if (terminator.kind == .branch) {
|
||||||
|
condition :: frames.items[frame_index].values[usize(terminator.value)]
|
||||||
|
if (!condition.data.boolean) target = terminator.alternative
|
||||||
|
}
|
||||||
|
frames.items[frame_index].block = target
|
||||||
|
frames.items[frame_index].next_instruction = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
value := ir.Value{}
|
value := ir.Value{}
|
||||||
if terminator.value != ir.no_value {
|
if terminator.value != ir.no_value {
|
||||||
value = frames.items[frame_index].values[usize(terminator.value)]
|
value = frames.items[frame_index].values[usize(terminator.value)]
|
||||||
@@ -169,6 +180,38 @@ run proc(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.equal, .not_equal, .less, .less_equal, .greater, .greater_equal: {
|
||||||
|
left :: frames.items[frame_index].values[usize(instruction.left)]
|
||||||
|
right :: frames.items[frame_index].values[usize(instruction.right)]
|
||||||
|
result := false
|
||||||
|
match left.type {
|
||||||
|
.int: result = match instruction.op {
|
||||||
|
.equal: left.data.integer == right.data.integer
|
||||||
|
.not_equal: left.data.integer != right.data.integer
|
||||||
|
.less: left.data.integer < right.data.integer
|
||||||
|
.less_equal: left.data.integer <= right.data.integer
|
||||||
|
.greater: left.data.integer > right.data.integer
|
||||||
|
.greater_equal: left.data.integer >= right.data.integer
|
||||||
|
else: unreachable
|
||||||
|
}
|
||||||
|
.float: result = match instruction.op {
|
||||||
|
.equal: left.data.real == right.data.real
|
||||||
|
.not_equal: left.data.real != right.data.real
|
||||||
|
.less: left.data.real < right.data.real
|
||||||
|
.less_equal: left.data.real <= right.data.real
|
||||||
|
.greater: left.data.real > right.data.real
|
||||||
|
.greater_equal: left.data.real >= right.data.real
|
||||||
|
else: unreachable
|
||||||
|
}
|
||||||
|
.bool: result = match instruction.op {
|
||||||
|
.equal: left.data.boolean == right.data.boolean
|
||||||
|
.not_equal: left.data.boolean != right.data.boolean
|
||||||
|
else: unreachable
|
||||||
|
}
|
||||||
|
else: unreachable
|
||||||
|
}
|
||||||
|
value = ir.Value{ type = .bool, data = ir.ValueData{ boolean = result } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if instruction.destination != ir.no_value {
|
if instruction.destination != ir.no_value {
|
||||||
frames.items[frame_index].values[usize(instruction.destination)] = value
|
frames.items[frame_index].values[usize(instruction.destination)] = value
|
||||||
@@ -195,6 +238,7 @@ push_frame proc(
|
|||||||
# typed IR assigns each temporary before use; only local slots need initialization flags.
|
# typed IR assigns each temporary before use; only local slots need initialization flags.
|
||||||
try arraylist.append(frames, Frame{
|
try arraylist.append(frames, Frame{
|
||||||
function = function,
|
function = function,
|
||||||
|
block = signature.entry,
|
||||||
destination = destination,
|
destination = destination,
|
||||||
global = global,
|
global = global,
|
||||||
locals = locals,
|
locals = locals,
|
||||||
|
|||||||
@@ -391,8 +391,10 @@ recursive_invocations_use_heap_frames_and_free_on_allocation_failure test {
|
|||||||
defer diagnostics.deinit(&store)
|
defer diagnostics.deinit(&store)
|
||||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .void, 2)
|
main :: try add_function(&module, mem.empty(ir.Type), 0, .void, 2)
|
||||||
module.entry = main
|
module.entry = main
|
||||||
try emit(&module, main, function_value(0, main))
|
recursive_block :: try add_block(&module, main)
|
||||||
try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = recursive_block }
|
||||||
|
try emit_at(&module, main, recursive_block, function_value(0, main))
|
||||||
|
try emit_at(&module, main, recursive_block, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||||
state := AllocationState{ remaining = 2048 }
|
state := AllocationState{ remaining = 2048 }
|
||||||
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||||
_ = run(allocator, &module, &store) catch |error| {
|
_ = run(allocator, &module, &store) catch |error| {
|
||||||
@@ -414,13 +416,17 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
|
|||||||
defer diagnostics.deinit(&store)
|
defer diagnostics.deinit(&store)
|
||||||
types [1]ir.Type :: [.int]
|
types [1]ir.Type :: [.int]
|
||||||
initializer :: try add_function(&module, types[..], 0, .int, 1)
|
initializer :: try add_function(&module, types[..], 0, .int, 1)
|
||||||
try emit(&module, initializer, integer(0, 42))
|
init_block :: try add_block(&module, initializer)
|
||||||
return_value(&module, initializer, ir.ValueId(0))
|
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = init_block }
|
||||||
|
try emit_at(&module, initializer, init_block, integer(0, 42))
|
||||||
|
module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ value = ir.ValueId(0) }
|
||||||
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 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)
|
main :: try add_function(&module, types[..], 0, .int, 1)
|
||||||
module.entry = main
|
module.entry = main
|
||||||
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
|
work :: try add_block(&module, main)
|
||||||
return_value(&module, main, ir.ValueId(0))
|
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = work }
|
||||||
|
try emit_at(&module, main, work, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||||
|
module.functions.items[usize(main)].blocks.items[usize(work)].terminator = ir.Terminator{ value = ir.ValueId(0) }
|
||||||
succeeded := false
|
succeeded := false
|
||||||
failures usize := 0
|
failures usize := 0
|
||||||
for (usize(0)..usize(16)) |limit| {
|
for (usize(0)..usize(16)) |limit| {
|
||||||
@@ -443,7 +449,7 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
|
|||||||
try testing.expect(succeeded)
|
try testing.expect(succeeded)
|
||||||
try testing.expect(failures > 0)
|
try testing.expect(failures > 0)
|
||||||
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 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 }
|
module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||||
state := AllocationState{ remaining = 64 }
|
state := AllocationState{ remaining = 64 }
|
||||||
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||||
trapped :: try run(allocator, &module, &store)
|
trapped :: try run(allocator, &module, &store)
|
||||||
@@ -477,3 +483,185 @@ runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames t
|
|||||||
}
|
}
|
||||||
return .expectation_failed
|
return .expectation_failed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
add_block proc(module @mut ir.Module, function ir.FunctionId) ir.BlockId ! mem.AllocError {
|
||||||
|
body := &module.functions.items[usize(function)]
|
||||||
|
id :: ir.BlockId(body.blocks.items.len)
|
||||||
|
try arraylist.append(&body.blocks, ir.init_block(module.allocator))
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
emit_at proc(module @mut ir.Module, function ir.FunctionId, block ir.BlockId, instruction ir.Instruction) void ! mem.AllocError {
|
||||||
|
try arraylist.append(&module.functions.items[usize(function)].blocks.items[usize(block)].instructions, instruction)
|
||||||
|
}
|
||||||
|
|
||||||
|
branching_recursive_calls_and_lazy_initializers_resume_the_current_block 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]
|
||||||
|
count :: try add_function(&module, types[..], 1, .int, 9)
|
||||||
|
done :: try add_block(&module, count)
|
||||||
|
recurse :: try add_block(&module, count)
|
||||||
|
try emit(&module, count, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||||
|
try emit(&module, count, integer(1, 0))
|
||||||
|
try emit(&module, count, ir.Instruction{ op = .less_equal, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||||
|
module.functions.items[usize(count)].blocks.items[0].terminator = ir.Terminator{
|
||||||
|
kind = .branch, value = ir.ValueId(2), target = done, alternative = recurse,
|
||||||
|
}
|
||||||
|
module.functions.items[usize(count)].blocks.items[usize(done)].terminator = ir.Terminator{ value = ir.ValueId(1) }
|
||||||
|
try emit_at(&module, count, recurse, function_value(3, count))
|
||||||
|
try emit_at(&module, count, recurse, integer(4, 1))
|
||||||
|
try emit_at(&module, count, recurse, ir.Instruction{ op = .subtract, type = .int, destination = ir.ValueId(5), left = ir.ValueId(0), right = ir.ValueId(4) })
|
||||||
|
try arraylist.append(&module.functions.items[usize(count)].blocks.items[usize(recurse)].arguments, ir.ValueId(5))
|
||||||
|
try emit_at(&module, count, recurse, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(6), left = ir.ValueId(3), arguments_count = 1 })
|
||||||
|
try emit_at(&module, count, recurse, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(7), left = ir.ValueId(6), right = ir.ValueId(4) })
|
||||||
|
module.functions.items[usize(count)].blocks.items[usize(recurse)].terminator = ir.Terminator{ value = ir.ValueId(7) }
|
||||||
|
|
||||||
|
initializer :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
|
||||||
|
init_entry :: try add_block(&module, initializer)
|
||||||
|
init_exit :: try add_block(&module, initializer)
|
||||||
|
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 }
|
||||||
|
module.functions.items[usize(initializer)].entry = init_entry
|
||||||
|
try emit_at(&module, initializer, init_entry, function_value(0, count))
|
||||||
|
try emit_at(&module, initializer, init_entry, integer(1, 128))
|
||||||
|
try arraylist.append(&module.functions.items[usize(initializer)].blocks.items[usize(init_entry)].arguments, ir.ValueId(1))
|
||||||
|
try emit_at(&module, initializer, init_entry, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), arguments_count = 1 })
|
||||||
|
module.functions.items[usize(initializer)].blocks.items[usize(init_entry)].terminator = ir.Terminator{ kind = .jump, target = init_exit }
|
||||||
|
module.functions.items[usize(initializer)].blocks.items[usize(init_exit)].terminator = ir.Terminator{ value = ir.ValueId(2) }
|
||||||
|
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||||
|
|
||||||
|
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 5)
|
||||||
|
main_entry :: try add_block(&module, main)
|
||||||
|
main_exit :: try add_block(&module, main)
|
||||||
|
module.entry = main
|
||||||
|
module.functions.items[usize(main)].entry = main_entry
|
||||||
|
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||||
|
try emit_at(&module, main, main_entry, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||||
|
try emit_at(&module, main, main_entry, function_value(1, count))
|
||||||
|
try emit_at(&module, main, main_entry, integer(2, 2))
|
||||||
|
try arraylist.append(&module.functions.items[usize(main)].blocks.items[usize(main_entry)].arguments, ir.ValueId(2))
|
||||||
|
try emit_at(&module, main, main_entry, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(3), left = ir.ValueId(1), arguments_count = 1 })
|
||||||
|
module.functions.items[usize(main)].blocks.items[usize(main_entry)].terminator = ir.Terminator{ kind = .jump, target = main_exit }
|
||||||
|
try emit_at(&module, main, main_exit, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(4), left = ir.ValueId(0), right = ir.ValueId(3) })
|
||||||
|
module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ value = ir.ValueId(4) }
|
||||||
|
state := AllocationState{ remaining = 2048 }
|
||||||
|
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||||
|
outcome :: try run(allocator, &module, &store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(i64(130), outcome.value.data.integer)
|
||||||
|
try testing.expect_equal(usize(0), state.live)
|
||||||
|
try testing.expect_equal(usize(1), diagnostics.count(&store))
|
||||||
|
# trapping after returning through many heap frames also releases every live frame.
|
||||||
|
module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||||
|
trapped :: try run(allocator, &module, &store)
|
||||||
|
try testing.expect_equal(trap_id, trapped.trap?)
|
||||||
|
try testing.expect_equal(usize(0), state.live)
|
||||||
|
}
|
||||||
|
|
||||||
|
all_numeric_comparisons_dispatch_on_operand_type_not_bool_result_type test {
|
||||||
|
operations [6]ir.Op :: [.equal, .not_equal, .less, .less_equal, .greater, .greater_equal]
|
||||||
|
lefts [3]i64 :: [-2, 2, 3]
|
||||||
|
rights [3]i64 :: [3, 2, -2]
|
||||||
|
operand_types [2]ir.Type :: [.int, .float]
|
||||||
|
for (operand_types) |type| {
|
||||||
|
for (lefts) |left, pair| {
|
||||||
|
right :: rights[pair]
|
||||||
|
for (operations) |op| {
|
||||||
|
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, .bool, 3)
|
||||||
|
module.entry = main
|
||||||
|
left_value := ir.Value{ type = type, data = ir.ValueData{ integer = left } }
|
||||||
|
right_value := ir.Value{ type = type, data = ir.ValueData{ integer = right } }
|
||||||
|
if (type == .float) {
|
||||||
|
left_value.data = ir.ValueData{ real = f64(left) }
|
||||||
|
right_value.data = ir.ValueData{ real = f64(right) }
|
||||||
|
}
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .constant, type = type, destination = ir.ValueId(0), constant = left_value })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .constant, type = type, destination = ir.ValueId(1), constant = right_value })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||||
|
return_value(&module, main, ir.ValueId(2))
|
||||||
|
expected :: match op {
|
||||||
|
.equal: left == right
|
||||||
|
.not_equal: left != right
|
||||||
|
.less: left < right
|
||||||
|
.less_equal: left <= right
|
||||||
|
.greater: left > right
|
||||||
|
.greater_equal: left >= right
|
||||||
|
else: unreachable
|
||||||
|
}
|
||||||
|
outcome :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.bool, outcome.value.type)
|
||||||
|
try testing.expect_equal(expected, outcome.value.data.boolean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nan_comparisons_obey_ieee_rules_in_either_operand_position test {
|
||||||
|
operations [6]ir.Op :: [.equal, .not_equal, .less, .less_equal, .greater, .greater_equal]
|
||||||
|
for (operations) |op| {
|
||||||
|
for (usize(0)..usize(3)) |position| {
|
||||||
|
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, .bool, 3)
|
||||||
|
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 = 0 } },
|
||||||
|
})
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .divide, type = .float, destination = ir.ValueId(1), left = ir.ValueId(0), right = ir.ValueId(0) })
|
||||||
|
left ir.ValueId :: if (position == 1) ir.ValueId(0) else ir.ValueId(1)
|
||||||
|
right ir.ValueId :: if (position == 0) ir.ValueId(0) else ir.ValueId(1)
|
||||||
|
try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = left, right = right })
|
||||||
|
return_value(&module, main, ir.ValueId(2))
|
||||||
|
outcome :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.bool, outcome.value.type)
|
||||||
|
try testing.expect_equal(op == .not_equal, outcome.value.data.boolean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean_equality_and_branching_use_boolean_payloads test {
|
||||||
|
booleans [2]bool :: [false, true]
|
||||||
|
operations [2]ir.Op :: [.equal, .not_equal]
|
||||||
|
for (booleans) |left| {
|
||||||
|
for (booleans) |right| {
|
||||||
|
for (operations) |op| {
|
||||||
|
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, .int, 5)
|
||||||
|
module.entry = main
|
||||||
|
yes :: try add_block(&module, main)
|
||||||
|
no :: try add_block(&module, main)
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .constant, type = .bool, destination = ir.ValueId(0), constant = ir.Value{ type = .bool, data = ir.ValueData{ boolean = left } } })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = .constant, type = .bool, destination = ir.ValueId(1), constant = ir.Value{ type = .bool, data = ir.ValueData{ boolean = right } } })
|
||||||
|
try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||||
|
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .branch, value = ir.ValueId(2), target = yes, alternative = no }
|
||||||
|
try emit_at(&module, main, yes, integer(3, 42))
|
||||||
|
module.functions.items[usize(main)].blocks.items[usize(yes)].terminator = ir.Terminator{ value = ir.ValueId(3) }
|
||||||
|
try emit_at(&module, main, no, integer(4, 7))
|
||||||
|
module.functions.items[usize(main)].blocks.items[usize(no)].terminator = ir.Terminator{ value = ir.ValueId(4) }
|
||||||
|
equal :: left == right
|
||||||
|
expected_true :: if (op == .equal) equal else !equal
|
||||||
|
expected i64 :: if (expected_true) 42 else 7
|
||||||
|
outcome :: try run(mem.c_allocator, &module, &store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(expected, outcome.value.data.integer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+26
-2
@@ -9,10 +9,20 @@ ValueId :: distinct u32
|
|||||||
BlockId :: distinct u32
|
BlockId :: distinct u32
|
||||||
no_value :: maxval!(ValueId)
|
no_value :: maxval!(ValueId)
|
||||||
|
|
||||||
Type :: enum { invalid, int, float, string, void, function }
|
Type :: enum {
|
||||||
|
invalid,
|
||||||
|
int,
|
||||||
|
float,
|
||||||
|
bool,
|
||||||
|
string,
|
||||||
|
void,
|
||||||
|
function,
|
||||||
|
}
|
||||||
|
|
||||||
ValueData :: union {
|
ValueData :: union {
|
||||||
integer i64
|
integer i64
|
||||||
real f64
|
real f64
|
||||||
|
boolean bool
|
||||||
string []u8
|
string []u8
|
||||||
function FunctionId
|
function FunctionId
|
||||||
}
|
}
|
||||||
@@ -32,6 +42,12 @@ Op :: enum {
|
|||||||
multiply
|
multiply
|
||||||
divide
|
divide
|
||||||
call
|
call
|
||||||
|
equal
|
||||||
|
not_equal
|
||||||
|
less
|
||||||
|
less_equal
|
||||||
|
greater
|
||||||
|
greater_equal
|
||||||
}
|
}
|
||||||
|
|
||||||
Instruction :: struct {
|
Instruction :: struct {
|
||||||
@@ -47,10 +63,18 @@ Instruction :: struct {
|
|||||||
arguments_count usize = 0
|
arguments_count usize = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminatorKind :: enum { return_value, trap }
|
TerminatorKind :: enum {
|
||||||
|
return_value,
|
||||||
|
trap,
|
||||||
|
jump,
|
||||||
|
branch,
|
||||||
|
}
|
||||||
|
|
||||||
Terminator :: struct {
|
Terminator :: struct {
|
||||||
kind TerminatorKind = .return_value
|
kind TerminatorKind = .return_value
|
||||||
value ValueId = no_value
|
value ValueId = no_value
|
||||||
|
target BlockId = BlockId(0)
|
||||||
|
alternative BlockId = BlockId(0)
|
||||||
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
|
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
|
||||||
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
|
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
|
|||||||
{ "for", .for },
|
{ "for", .for },
|
||||||
{ "else", .else },
|
{ "else", .else },
|
||||||
{ "while", .while },
|
{ "while", .while },
|
||||||
|
{ "true", .true },
|
||||||
|
{ "false", .false },
|
||||||
])
|
])
|
||||||
|
|
||||||
#! owns token storage and retains its allocator; release with deinit.
|
#! owns token storage and retains its allocator; release with deinit.
|
||||||
@@ -203,8 +205,28 @@ scan proc(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cursor + 1 < program.len and program[cursor + 1] == '=') {
|
||||||
|
comparison ?TokenKind :: match char {
|
||||||
|
'=': .equal_equal
|
||||||
|
'!': .not_equal
|
||||||
|
'<': .less_equal
|
||||||
|
'>': .greater_equal
|
||||||
|
else: null
|
||||||
|
}
|
||||||
|
if (comparison) |value| {
|
||||||
|
try add_token(state, Token{
|
||||||
|
span = make_span(cursor, cursor + 2),
|
||||||
|
kind = value,
|
||||||
|
})
|
||||||
|
cursor += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
kind ?TokenKind :: match char {
|
kind ?TokenKind :: match char {
|
||||||
'=': .equal
|
'=': .equal
|
||||||
|
'<': .less
|
||||||
|
'>': .greater
|
||||||
',': .comma
|
',': .comma
|
||||||
'(': .open_paren
|
'(': .open_paren
|
||||||
')': .close_paren
|
')': .close_paren
|
||||||
|
|||||||
@@ -30,3 +30,26 @@ reports_invalid_input_and_keeps_scanning test {
|
|||||||
try testing.expect_equal(u32(3), state.tokens.items[1].span.start)
|
try testing.expect_equal(u32(3), state.tokens.items[1].span.start)
|
||||||
try testing.expect_equal(u32(7), state.tokens.items[1].span.end)
|
try testing.expect_equal(u32(7), state.tokens.items[1].span.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reserves_boolean_keywords_and_scans_comparisons 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)
|
||||||
|
state := init(mem.c_allocator)
|
||||||
|
defer deinit(&state)
|
||||||
|
source :: "true false bool trueish == != < <= > >="
|
||||||
|
try scan(&state, source, &diagnostic_store)
|
||||||
|
expected [11]TokenKind :: [
|
||||||
|
TokenKind.true, .false, .ident, .ident,
|
||||||
|
.equal_equal, .not_equal, .less, .less_equal, .greater, .greater_equal, .eof,
|
||||||
|
]
|
||||||
|
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||||
|
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||||
|
try testing.expect_equal(u32(0), state.tokens.items[0].span.start)
|
||||||
|
try testing.expect_equal(u32(4), state.tokens.items[0].span.end)
|
||||||
|
try testing.expect_equal(u32(5), state.tokens.items[1].span.start)
|
||||||
|
try testing.expect_equal(u32(10), state.tokens.items[1].span.end)
|
||||||
|
try testing.expect_equal("bool", strpool.get_str(&strpool.strings, state.tokens.items[2].data.str_id)?)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ TokenKind :: enum {
|
|||||||
if, else
|
if, else
|
||||||
for, while
|
for, while
|
||||||
proc, return
|
proc, return
|
||||||
|
true, false
|
||||||
|
|
||||||
# literals
|
# literals
|
||||||
ident, string
|
ident, string
|
||||||
|
|||||||
+112
-13
@@ -25,6 +25,7 @@ State :: struct {
|
|||||||
module @mut ir.Module
|
module @mut ir.Module
|
||||||
storage []mut Storage
|
storage []mut Storage
|
||||||
function usize = 0
|
function usize = 0
|
||||||
|
block ir.BlockId = ir.BlockId(0)
|
||||||
terminated bool = false
|
terminated bool = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +90,7 @@ lower_type proc(type typecheck.Type) ir.Type {
|
|||||||
return match type.kind {
|
return match type.kind {
|
||||||
.int: .int
|
.int: .int
|
||||||
.float: .float
|
.float: .float
|
||||||
|
.bool: .bool
|
||||||
.string: .string
|
.string: .string
|
||||||
.void: .void
|
.void: .void
|
||||||
.function: .function
|
.function: .function
|
||||||
@@ -136,7 +138,7 @@ find_entry proc(state @mut State) void ! (mem.AllocError | diagnostics.CapacityE
|
|||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
trap proc(state @mut State, diagnostic diagnostics.DiagnosticId, span diagnostics.Span) void {
|
trap proc(state @mut State, diagnostic diagnostics.DiagnosticId, span diagnostics.Span) void {
|
||||||
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
|
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
|
||||||
kind = .trap,
|
kind = .trap,
|
||||||
diagnostic = diagnostic,
|
diagnostic = diagnostic,
|
||||||
span = span,
|
span = span,
|
||||||
@@ -162,7 +164,7 @@ trap_binding proc(state @mut State, id ast.NodeId) bool {
|
|||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void {
|
finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void {
|
||||||
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
|
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
|
||||||
value = value,
|
value = value,
|
||||||
span = span,
|
span = span,
|
||||||
}
|
}
|
||||||
@@ -177,7 +179,7 @@ emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! mem.AllocEr
|
|||||||
operation.destination = ir.ValueId(function.value_count)
|
operation.destination = ir.ValueId(function.value_count)
|
||||||
function.value_count += 1
|
function.value_count += 1
|
||||||
}
|
}
|
||||||
try arraylist.append(&function.blocks.items[0].instructions, operation)
|
try arraylist.append(&function.blocks.items[usize(state.block)].instructions, operation)
|
||||||
return operation.destination
|
return operation.destination
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +192,7 @@ constant proc(state @mut State, value ir.Value, span diagnostics.Span) ir.ValueI
|
|||||||
lower_global proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
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)]
|
global :: state.module.globals.items[usize(state.storage[usize(id)].index)]
|
||||||
state.function = usize(global.initializer)
|
state.function = usize(global.initializer)
|
||||||
|
state.block = ir.BlockId(0)
|
||||||
state.terminated = false
|
state.terminated = false
|
||||||
state.module.functions.items[state.function].result = global.type
|
state.module.functions.items[state.function].result = global.type
|
||||||
if (trap_binding(state, id)) return
|
if (trap_binding(state, id)) return
|
||||||
@@ -205,6 +208,7 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di
|
|||||||
tree :: state.tree
|
tree :: state.tree
|
||||||
declaration :: ast.function_decl(tree, id)
|
declaration :: ast.function_decl(tree, id)
|
||||||
state.function = usize(state.storage[usize(id)].index)
|
state.function = usize(state.storage[usize(id)].index)
|
||||||
|
state.block = ir.BlockId(0)
|
||||||
state.terminated = false
|
state.terminated = false
|
||||||
function := &state.module.functions.items[state.function]
|
function := &state.module.functions.items[state.function]
|
||||||
function.result = lower_type(typecheck.type_of(state.checked, declaration.return_type))
|
function.result = lower_type(typecheck.type_of(state.checked, declaration.return_type))
|
||||||
@@ -215,30 +219,112 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di
|
|||||||
try arraylist.append(&function.parameters, type)
|
try arraylist.append(&function.parameters, type)
|
||||||
try arraylist.append(&function.locals, type)
|
try arraylist.append(&function.locals, type)
|
||||||
}
|
}
|
||||||
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
|
try predeclare_locals(state, declaration.body)
|
||||||
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.
|
# 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
|
if (trap_binding(state, id) or trap_type(state, declaration.return_type)) return
|
||||||
for (0..ast.function_param_count(tree, id)) |i| {
|
for (0..ast.function_param_count(tree, id)) |i| {
|
||||||
if (trap_type(state, ast.function_param(tree, id, i))) return
|
if (trap_type(state, ast.function_param(tree, id, i))) return
|
||||||
}
|
}
|
||||||
if (trap_type(state, id)) return
|
if (trap_type(state, id)) return
|
||||||
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
|
try lower_block(state, declaration.body)
|
||||||
if (state.terminated) return
|
if (state.terminated) return
|
||||||
try lower_statement(state, ast.block_statement(tree, declaration.body, i))
|
# aggregate arm errors are not demanded by fallthrough; only missing_return is.
|
||||||
|
body_type :: typecheck.type_of(state.checked, declaration.body)
|
||||||
|
if (function.result != .void and body_type.kind == .invalid) {
|
||||||
|
if (diagnostics.get(state.diagnostics, body_type.diagnostic)) |diagnostic| {
|
||||||
|
if (diagnostic.code == .missing_return) {
|
||||||
|
trap(state, body_type.diagnostic, ast.node_span(tree, declaration.body))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (state.terminated or trap_type(state, declaration.body)) return
|
|
||||||
finish(state, ir.no_value, ast.node_span(tree, declaration.body))
|
finish(state, ir.no_value, ast.node_span(tree, declaration.body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
predeclare_locals proc(state @mut State, block ast.NodeId) void ! mem.AllocError {
|
||||||
|
for (0..ast.block_statement_count(state.tree, block)) |i| {
|
||||||
|
statement :: ast.block_statement(state.tree, block, i)
|
||||||
|
match ast.stmt_kind(state.tree, statement) {
|
||||||
|
.declaration: {
|
||||||
|
function := &state.module.functions.items[state.function]
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
.if_stmt: {
|
||||||
|
conditional :: ast.if_stmt(state.tree, statement)
|
||||||
|
try predeclare_locals(state, conditional.then_block)
|
||||||
|
if (conditional.else_block) |alternative| try predeclare_locals(state, alternative)
|
||||||
|
}
|
||||||
|
else: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
lower_block proc(state @mut State, block ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
for (0..ast.block_statement_count(state.tree, block)) |i| {
|
||||||
|
if (state.terminated) return
|
||||||
|
try lower_statement(state, ast.block_statement(state.tree, block, i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
add_block proc(state @mut State) ir.BlockId ! mem.AllocError {
|
||||||
|
function := &state.module.functions.items[state.function]
|
||||||
|
id :: ir.BlockId(function.blocks.items.len)
|
||||||
|
try arraylist.append(&function.blocks, ir.init_block(state.module.allocator))
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
lower_if proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
conditional :: ast.if_stmt(state.tree, id)
|
||||||
|
condition :: try lower_expression(state, conditional.condition)
|
||||||
|
if (state.terminated) return
|
||||||
|
origin :: state.block
|
||||||
|
consequent :: try add_block(state)
|
||||||
|
alternative :: try add_block(state)
|
||||||
|
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
|
||||||
|
kind = .branch,
|
||||||
|
value = condition,
|
||||||
|
target = consequent,
|
||||||
|
alternative = alternative,
|
||||||
|
span = ast.node_span(state.tree, conditional.condition),
|
||||||
|
}
|
||||||
|
state.block = consequent
|
||||||
|
try lower_block(state, conditional.then_block)
|
||||||
|
consequent_end :: state.block
|
||||||
|
consequent_falls :: !state.terminated
|
||||||
|
state.block = alternative
|
||||||
|
state.terminated = false
|
||||||
|
if (conditional.else_block) |block| try lower_block(state, block)
|
||||||
|
alternative_end :: state.block
|
||||||
|
alternative_falls :: !state.terminated
|
||||||
|
if (!consequent_falls and !alternative_falls) {
|
||||||
|
state.terminated = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
join :: try add_block(state)
|
||||||
|
if (consequent_falls) {
|
||||||
|
state.module.functions.items[state.function].blocks.items[usize(consequent_end)].terminator = ir.Terminator{
|
||||||
|
kind = .jump, target = join, span = conditional.span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alternative_falls) {
|
||||||
|
state.module.functions.items[state.function].blocks.items[usize(alternative_end)].terminator = ir.Terminator{
|
||||||
|
kind = .jump, target = join, span = conditional.span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.block = join
|
||||||
|
state.terminated = false
|
||||||
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
tree :: state.tree
|
tree :: state.tree
|
||||||
match ast.stmt_kind(tree, id) {
|
match ast.stmt_kind(tree, id) {
|
||||||
|
.if_stmt: try lower_if(state, id)
|
||||||
.invalid: {
|
.invalid: {
|
||||||
invalid :: ast.invalid(tree, id)
|
invalid :: ast.invalid(tree, id)
|
||||||
trap(state, invalid.diagnostic, invalid.span)
|
trap(state, invalid.diagnostic, invalid.span)
|
||||||
@@ -282,6 +368,13 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
|
|||||||
if (trap_type(state, id)) return ir.no_value
|
if (trap_type(state, id)) return ir.no_value
|
||||||
return try lower_number(state, id)
|
return try lower_number(state, id)
|
||||||
}
|
}
|
||||||
|
.literal_bool: {
|
||||||
|
if (trap_type(state, id)) return ir.no_value
|
||||||
|
return try constant(state, ir.Value{
|
||||||
|
type = .bool,
|
||||||
|
data = ir.ValueData{ boolean = mem.eql(ast.source_text(tree, span), "true") },
|
||||||
|
}, span)
|
||||||
|
}
|
||||||
.literal_string: {
|
.literal_string: {
|
||||||
if (trap_type(state, id)) return ir.no_value
|
if (trap_type(state, id)) return ir.no_value
|
||||||
return try lower_string(state, id)
|
return try lower_string(state, id)
|
||||||
@@ -325,6 +418,12 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
|
|||||||
.subtract: .subtract
|
.subtract: .subtract
|
||||||
.multiply: .multiply
|
.multiply: .multiply
|
||||||
.divide: .divide
|
.divide: .divide
|
||||||
|
.equal: .equal
|
||||||
|
.not_equal: .not_equal
|
||||||
|
.less: .less
|
||||||
|
.less_equal: .less_equal
|
||||||
|
.greater: .greater
|
||||||
|
.greater_equal: .greater_equal
|
||||||
}
|
}
|
||||||
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
|
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
|
||||||
}
|
}
|
||||||
@@ -332,7 +431,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
|
|||||||
callee :: try lower_expression(state, ast.call_expr(tree, id).callee)
|
callee :: try lower_expression(state, ast.call_expr(tree, id).callee)
|
||||||
if (state.terminated) return ir.no_value
|
if (state.terminated) return ir.no_value
|
||||||
# reserve this call's packed slice before nested calls append theirs.
|
# reserve this call's packed slice before nested calls append theirs.
|
||||||
block := &state.module.functions.items[state.function].blocks.items[0]
|
block := &state.module.functions.items[state.function].blocks.items[usize(state.block)]
|
||||||
start :: block.arguments.items.len
|
start :: block.arguments.items.len
|
||||||
count :: ast.call_arg_count(tree, id)
|
count :: ast.call_arg_count(tree, id)
|
||||||
for (0..count) |_| try arraylist.append(&block.arguments, ir.no_value)
|
for (0..count) |_| try arraylist.append(&block.arguments, ir.no_value)
|
||||||
|
|||||||
@@ -410,11 +410,23 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
|
|||||||
`text :: "owned\nstring"
|
`text :: "owned\nstring"
|
||||||
`factor float :: 3.5
|
`factor float :: 3.5
|
||||||
`identity proc(value string) string {
|
`identity proc(value string) string {
|
||||||
|
` if false {
|
||||||
|
` return value
|
||||||
|
` }
|
||||||
` local :: value
|
` local :: value
|
||||||
` return local
|
` return local
|
||||||
`}
|
`}
|
||||||
`main proc() string {
|
`main proc() string {
|
||||||
` computed float :: factor * 2
|
` computed float :: factor * 2
|
||||||
|
` if computed > 0 {
|
||||||
|
` saved :: identity(text)
|
||||||
|
` if computed < 10 {
|
||||||
|
` return saved
|
||||||
|
` }
|
||||||
|
` } else {
|
||||||
|
` fallback :: "fallback"
|
||||||
|
` identity(fallback)
|
||||||
|
` }
|
||||||
` return identity(text)
|
` return identity(text)
|
||||||
`}
|
`}
|
||||||
scan_state := lexer.init(mem.c_allocator)
|
scan_state := lexer.init(mem.c_allocator)
|
||||||
@@ -450,3 +462,182 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
|
|||||||
try testing.expect(succeeded)
|
try testing.expect(succeeded)
|
||||||
try testing.expect(failures > 0)
|
try testing.expect(failures > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recursive_factorial_and_conditional_returns_execute_after_frontend_cleanup test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`factorial proc(n int) int {
|
||||||
|
` if n <= 1 {
|
||||||
|
` return 1
|
||||||
|
` } else {
|
||||||
|
` return n * factorial(n - 1)
|
||||||
|
` }
|
||||||
|
`}
|
||||||
|
`main proc() int {
|
||||||
|
` return factorial(5)
|
||||||
|
`}
|
||||||
|
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(120), outcome.value.data.integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
nested_branches_joins_calls_and_lazy_globals_preserve_execution_position test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`seed :: classify(2)
|
||||||
|
`enabled bool :: true
|
||||||
|
`identity proc(value int) int { return value }
|
||||||
|
`classify proc(n int) int {
|
||||||
|
` if n < 0 {
|
||||||
|
` return 3
|
||||||
|
` } else if n == 0 {
|
||||||
|
` return 5
|
||||||
|
` } else {
|
||||||
|
` if n > 1 {
|
||||||
|
` local :: identity(7)
|
||||||
|
` identity(local)
|
||||||
|
` } else {
|
||||||
|
` local :: identity(9)
|
||||||
|
` identity(local)
|
||||||
|
` }
|
||||||
|
` return identity(11)
|
||||||
|
` }
|
||||||
|
`}
|
||||||
|
`choose proc(flag bool) int {
|
||||||
|
` if flag {
|
||||||
|
` local :: identity(seed)
|
||||||
|
` identity(local)
|
||||||
|
` } else {
|
||||||
|
` local :: identity(13)
|
||||||
|
` identity(local)
|
||||||
|
` }
|
||||||
|
` return identity(seed) + classify(0) + classify(-1)
|
||||||
|
`}
|
||||||
|
`main proc() int {
|
||||||
|
` return choose(enabled) + choose(false) + classify(1) - 7
|
||||||
|
`}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_storage_results_and_numeric_comparisons_execute test {
|
||||||
|
sources [16][]u8 :: [
|
||||||
|
"main proc() bool { return 2 == 2 }\n",
|
||||||
|
"main proc() bool { return 2 != 3 }\n",
|
||||||
|
"main proc() bool { return -2 < 1 }\n",
|
||||||
|
"main proc() bool { return 2 <= 2 }\n",
|
||||||
|
"main proc() bool { return 3 > 2 }\n",
|
||||||
|
"main proc() bool { return 2 >= 2 }\n",
|
||||||
|
"main proc() bool { return 2 == 2.0 }\n",
|
||||||
|
"main proc() bool { return 2 != 3.0 }\n",
|
||||||
|
"main proc() bool { return 2 < 2.5 }\n",
|
||||||
|
"main proc() bool { return 2 <= 2.0 }\n",
|
||||||
|
"main proc() bool { return 3 > 2.5 }\n",
|
||||||
|
"main proc() bool { return 2 >= 2.0 }\n",
|
||||||
|
"main proc() bool { return true == true }\n",
|
||||||
|
"main proc() bool { return false != true }\n",
|
||||||
|
"flag bool :: false\nidentity proc(value bool) bool { return value }\nmain proc() bool {\n local bool :: identity(flag)\n return identity(local == false)\n}\n",
|
||||||
|
"main proc() bool {\n local :: 2\n return local < 2.5\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.bool, outcome.value.type)
|
||||||
|
try testing.expect(outcome.value.data.boolean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conditional_errors_trap_only_when_demanded_and_preserve_original_identity test {
|
||||||
|
sources [7][]u8 :: [
|
||||||
|
"main proc() int {\n if true {\n return 42\n } else {\n return unknown\n }\n}\n",
|
||||||
|
"main proc() int {\n if false {\n return 42\n } else {\n return unknown\n }\n}\n",
|
||||||
|
"main proc() int {\n if true {\n return 42\n }\n}\n",
|
||||||
|
"main proc() int {\n if false {\n return 42\n }\n}\n",
|
||||||
|
"main proc() void {\n if false {\n unknown\n }\n}\n",
|
||||||
|
"main proc() int {\n if 1 {\n return 42\n } else {\n return 0\n }\n}\n",
|
||||||
|
"main proc() int {\n if unknown {\n return 42\n }\n}\n",
|
||||||
|
]
|
||||||
|
for (sources) |source, index| {
|
||||||
|
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(1, diagnostics.count(&diagnostic_store))
|
||||||
|
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
if (index == 0 or index == 2 or index == 4) {
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
if (index == 4) try testing.expect_equal(ir.Type.void, outcome.value.type)
|
||||||
|
else try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
} else {
|
||||||
|
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
|
||||||
|
code := diagnostics.Code.unknown_name
|
||||||
|
if (index == 3) code = .missing_return
|
||||||
|
if (index == 5) code = .condition_not_bool
|
||||||
|
try testing.expect_equal(code, original.code)
|
||||||
|
if (index == 5) try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], "1"))
|
||||||
|
}
|
||||||
|
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
missing_return_does_not_replace_reached_arm_error test {
|
||||||
|
sources [3][]u8 :: [
|
||||||
|
"main proc() int {\n if true {\n return unknown\n }\n}\n",
|
||||||
|
"main proc() int {\n if false {\n return unknown\n }\n}\n",
|
||||||
|
"main proc() int {\n if false {\n return unknown\n }\n return 42\n}\n",
|
||||||
|
]
|
||||||
|
for (sources) |source, index| {
|
||||||
|
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 (index == 2) {
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
} else {
|
||||||
|
code diagnostics.Code :: if (index == 0) .unknown_name else .missing_return
|
||||||
|
try expect_trap(outcome, &diagnostic_store, code)
|
||||||
|
for (0..diagnostics.count(&diagnostic_store)) |i| {
|
||||||
|
diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(i))?
|
||||||
|
if (diagnostic.code == code) try testing.expect_equal(diagnostics.DiagnosticId(i), outcome.trap?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nested_locals_are_predeclared_before_any_arm_executes test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`main proc() int {
|
||||||
|
` if true {
|
||||||
|
` if true {
|
||||||
|
` before :: later
|
||||||
|
` later :: 42
|
||||||
|
` return before
|
||||||
|
` }
|
||||||
|
` }
|
||||||
|
` return 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 expect_trap(outcome, &diagnostic_store, .read_before_initialization)
|
||||||
|
}
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ print_result proc(output io.Writer, value ir.Value) void ! io.WriteError {
|
|||||||
match value.type {
|
match value.type {
|
||||||
.int: try io.print("{}\n", output, {value.data.integer})
|
.int: try io.print("{}\n", output, {value.data.integer})
|
||||||
.float: try io.print("{d}\n", output, {value.data.real})
|
.float: try io.print("{d}\n", output, {value.data.real})
|
||||||
|
.bool: try io.print("{}\n", output, {value.data.boolean})
|
||||||
.string: try io.print("{s}\n", output, {value.data.string})
|
.string: try io.print("{s}\n", output, {value.data.string})
|
||||||
.void: {}
|
.void: {}
|
||||||
else: unreachable
|
else: unreachable
|
||||||
|
|||||||
+147
-10
@@ -143,7 +143,7 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
skip_newlines(state)
|
skip_newlines(state)
|
||||||
while !next_is(state, .close_curly) and !next_is(state, .eof) {
|
while !next_is(state, .close_curly) and !next_is(state, .eof) {
|
||||||
start :: state.next_token
|
start :: state.next_token
|
||||||
statement :: parse_statement(state) catch |err| {
|
statement :: parse_statement(state, false) catch |err| {
|
||||||
match err {
|
match err {
|
||||||
.unexpected_token: {
|
.unexpected_token: {
|
||||||
recovered :: try recover_statement(state, start)
|
recovered :: try recover_statement(state, start)
|
||||||
@@ -167,7 +167,7 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
parse_statement proc(state @mut State) ast.NodeId ! ParseError {
|
parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
||||||
if (
|
if (
|
||||||
next_is(state, .ident)
|
next_is(state, .ident)
|
||||||
and (
|
and (
|
||||||
@@ -179,8 +179,9 @@ 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)
|
.if: return try parse_if(state)
|
||||||
.ident, .int, .float, .string, .minus, .open_paren: {
|
.return: return try parse_return(state, allow_else)
|
||||||
|
.ident, .int, .float, .string, .true, .false, .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)
|
||||||
}
|
}
|
||||||
@@ -189,13 +190,19 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
parse_return proc(state @mut State) ast.NodeId ! ParseError {
|
parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
||||||
keyword :: try consume(state, .return)
|
keyword :: try consume(state, .return)
|
||||||
if (next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)) {
|
if (
|
||||||
|
next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)
|
||||||
|
or (allow_else and next_is(state, .else))
|
||||||
|
) {
|
||||||
return try ast.add_return_stmt(&state.builder, keyword.span, null)
|
return try ast.add_return_stmt(&state.builder, keyword.span, null)
|
||||||
}
|
}
|
||||||
value :: try parse_expr(state)
|
value :: try parse_expr(state)
|
||||||
if (!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)) {
|
if (
|
||||||
|
!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)
|
||||||
|
and !(allow_else and next_is(state, .else))
|
||||||
|
) {
|
||||||
return .unexpected_token
|
return .unexpected_token
|
||||||
}
|
}
|
||||||
# include consumed grouping punctuation, which has no separate expression node.
|
# include consumed grouping punctuation, which has no separate expression node.
|
||||||
@@ -207,6 +214,116 @@ parse_return proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
parse_if proc(state @mut State) ast.NodeId ! ParseError {
|
||||||
|
keyword :: try consume(state, .if)
|
||||||
|
condition_start :: state.next_token
|
||||||
|
condition := parse_expr(state) catch |err| {
|
||||||
|
match err {
|
||||||
|
.unexpected_token: {}
|
||||||
|
else: return err
|
||||||
|
}
|
||||||
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
||||||
|
yield try ast.add_invalid_expr(&state.builder, current(state).span, diagnostic)
|
||||||
|
}
|
||||||
|
if (ast.builder_expr_kind(&state.builder, condition) == .invalid) {
|
||||||
|
# discard a malformed guard's tail, but leave its arm and enclosing block intact.
|
||||||
|
while (
|
||||||
|
!next_is(state, .open_curly) and !next_is(state, .close_curly)
|
||||||
|
and !next_is(state, .newline) and !next_is(state, .eof)
|
||||||
|
and !next_is(state, .else) and !next_is(state, .return) and !next_is(state, .if)
|
||||||
|
) {
|
||||||
|
state.next_token += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!next_is(state, .open_curly)
|
||||||
|
and !parenthesized_guard(state, condition_start)
|
||||||
|
and ast.builder_expr_kind(&state.builder, condition) != .call
|
||||||
|
and ast.builder_expr_kind(&state.builder, condition) != .invalid
|
||||||
|
) {
|
||||||
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
||||||
|
condition = try ast.add_invalid_expr(
|
||||||
|
&state.builder,
|
||||||
|
ast.builder_span(&state.builder, condition),
|
||||||
|
diagnostic,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
braced :: next_is(state, .open_curly)
|
||||||
|
then_block :: try parse_arm(state)
|
||||||
|
# only a braced arm permits its else on a later line.
|
||||||
|
after_then :: state.next_token
|
||||||
|
if (braced) skip_newlines(state)
|
||||||
|
alternative ?ast.NodeId := null
|
||||||
|
if next_is(state, .else) {
|
||||||
|
state.next_token += 1
|
||||||
|
alternative = try parse_arm(state)
|
||||||
|
} else {
|
||||||
|
state.next_token = after_then
|
||||||
|
}
|
||||||
|
end :: if (alternative) |block|
|
||||||
|
ast.builder_span(&state.builder, block).end
|
||||||
|
else
|
||||||
|
ast.builder_span(&state.builder, then_block).end
|
||||||
|
return try ast.add_if_stmt(
|
||||||
|
&state.builder,
|
||||||
|
ast.Span{ start = keyword.span.start, end = end },
|
||||||
|
condition,
|
||||||
|
then_block,
|
||||||
|
alternative,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
parenthesized_guard proc(state @State, start usize) bool {
|
||||||
|
if (state.tokens[start].kind != .open_paren) return false
|
||||||
|
depth usize := 0
|
||||||
|
for start..state.next_token |index| {
|
||||||
|
kind :: state.tokens[index].kind
|
||||||
|
if (kind == .open_paren) depth += 1
|
||||||
|
if (kind == .close_paren) {
|
||||||
|
depth -= 1
|
||||||
|
if (depth == 0) return index + 1 == state.next_token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
parse_arm proc(state @mut State) ast.NodeId ! ParseError {
|
||||||
|
if next_is(state, .open_curly) return try parse_block(state)
|
||||||
|
start :: state.next_token
|
||||||
|
statement :: parse_statement(state, true) catch |err| {
|
||||||
|
match err {
|
||||||
|
.unexpected_token: {}
|
||||||
|
else: return err
|
||||||
|
}
|
||||||
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
||||||
|
span :: synchronize_arm(state, start)
|
||||||
|
yield try ast.add_invalid_stmt(&state.builder, span, diagnostic)
|
||||||
|
}
|
||||||
|
statements [1]ast.NodeId :: [statement]
|
||||||
|
return try ast.add_block(
|
||||||
|
&state.builder,
|
||||||
|
ast.builder_span(&state.builder, statement),
|
||||||
|
statements[..],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
synchronize_arm proc(state @mut State, start usize) ast.Span {
|
||||||
|
first :: state.tokens[start].span
|
||||||
|
end := first.end
|
||||||
|
while (
|
||||||
|
!next_is(state, .eof) and !next_is(state, .newline)
|
||||||
|
and !next_is(state, .close_curly) and !next_is(state, .else)
|
||||||
|
) {
|
||||||
|
end = current(state).span.end
|
||||||
|
state.next_token += 1
|
||||||
|
}
|
||||||
|
return ast.Span{ start = first.start, end = end }
|
||||||
|
}
|
||||||
|
|
||||||
@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)
|
||||||
@@ -295,6 +412,10 @@ parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError {
|
|||||||
!next_is(state, .close_paren)
|
!next_is(state, .close_paren)
|
||||||
and !next_is(state, .newline)
|
and !next_is(state, .newline)
|
||||||
and !next_is(state, .close_curly)
|
and !next_is(state, .close_curly)
|
||||||
|
and !next_is(state, .open_curly)
|
||||||
|
and !next_is(state, .else)
|
||||||
|
and !next_is(state, .return)
|
||||||
|
and !next_is(state, .if)
|
||||||
and !next_is(state, .eof)
|
and !next_is(state, .eof)
|
||||||
) {
|
) {
|
||||||
argument :: try parse_expr(state)
|
argument :: try parse_expr(state)
|
||||||
@@ -347,6 +468,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
state.next_token += 1
|
state.next_token += 1
|
||||||
return try ast.add_literal(&state.builder, .string, token.span)
|
return try ast.add_literal(&state.builder, .string, token.span)
|
||||||
}
|
}
|
||||||
|
.true, .false: {
|
||||||
|
state.next_token += 1
|
||||||
|
return try ast.add_literal(&state.builder, .bool, token.span)
|
||||||
|
}
|
||||||
.ident: {
|
.ident: {
|
||||||
state.next_token += 1
|
state.next_token += 1
|
||||||
return try ast.add_identifier(&state.builder, token.span)
|
return try ast.add_identifier(&state.builder, token.span)
|
||||||
@@ -362,6 +487,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
and token.kind != .comma
|
and token.kind != .comma
|
||||||
and token.kind != .close_paren
|
and token.kind != .close_paren
|
||||||
and token.kind != .close_curly
|
and token.kind != .close_curly
|
||||||
|
and token.kind != .open_curly
|
||||||
|
and token.kind != .else
|
||||||
|
and token.kind != .return
|
||||||
|
and token.kind != .if
|
||||||
and token.kind != .eof
|
and token.kind != .eof
|
||||||
) {
|
) {
|
||||||
state.next_token += 1
|
state.next_token += 1
|
||||||
@@ -451,7 +580,7 @@ consume_either proc(state @mut State, kinds []lexer.TokenKind) lexer.Token ! Syn
|
|||||||
@hide:file
|
@hide:file
|
||||||
prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
|
prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
|
||||||
return match kind {
|
return match kind {
|
||||||
.minus: BindingPowerScalar(30)
|
.minus: BindingPowerScalar(50)
|
||||||
else: null
|
else: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,8 +588,10 @@ prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
|
|||||||
@hide:file
|
@hide:file
|
||||||
infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
|
infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
|
||||||
return match kind {
|
return match kind {
|
||||||
.plus, .minus: BindingPower{ left = BindingPowerScalar(10), right = BindingPowerScalar(11) }
|
.equal_equal, .not_equal: BindingPower{ left = BindingPowerScalar(10), right = BindingPowerScalar(11) }
|
||||||
.star, .slash: BindingPower{ left = BindingPowerScalar(20), right = BindingPowerScalar(21) }
|
.less, .less_equal, .greater, .greater_equal: BindingPower{ left = BindingPowerScalar(20), right = BindingPowerScalar(21) }
|
||||||
|
.plus, .minus: BindingPower{ left = BindingPowerScalar(30), right = BindingPowerScalar(31) }
|
||||||
|
.star, .slash: BindingPower{ left = BindingPowerScalar(40), right = BindingPowerScalar(41) }
|
||||||
else: null
|
else: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,6 +603,12 @@ binary_operator proc(kind lexer.TokenKind) ast.BinaryOperator {
|
|||||||
.minus: .subtract
|
.minus: .subtract
|
||||||
.star: .multiply
|
.star: .multiply
|
||||||
.slash: .divide
|
.slash: .divide
|
||||||
|
.equal_equal: .equal
|
||||||
|
.not_equal: .not_equal
|
||||||
|
.less: .less
|
||||||
|
.less_equal: .less_equal
|
||||||
|
.greater: .greater
|
||||||
|
.greater_equal: .greater_equal
|
||||||
else: unreachable
|
else: unreachable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,3 +330,183 @@ parses_return_boundaries_and_recovers_trailing_tokens test {
|
|||||||
try testing.expect_equal("return", ast.source_text(&tree, last.span))
|
try testing.expect_equal("return", ast.source_text(&tree, last.span))
|
||||||
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
|
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parses_boolean_comparison_precedence_and_associativity 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 ::
|
||||||
|
`value :: -f() * 2 + 3 < 4 <= 5 > 6 >= 7 == true != false
|
||||||
|
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)
|
||||||
|
different :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer)
|
||||||
|
try testing.expect_equal(ast.BinaryOperator.not_equal, different.operator)
|
||||||
|
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, different.right))
|
||||||
|
try testing.expect_equal("false", ast.source_text(&tree, ast.node_span(&tree, different.right)))
|
||||||
|
equal :: ast.binary_expr(&tree, different.left)
|
||||||
|
try testing.expect_equal(ast.BinaryOperator.equal, equal.operator)
|
||||||
|
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, equal.right))
|
||||||
|
relation := equal.left
|
||||||
|
operators [4]ast.BinaryOperator :: [.greater_equal, .greater, .less_equal, .less]
|
||||||
|
for (operators) |operator| {
|
||||||
|
binary :: ast.binary_expr(&tree, relation)
|
||||||
|
try testing.expect_equal(operator, binary.operator)
|
||||||
|
relation = binary.left
|
||||||
|
}
|
||||||
|
addition :: ast.binary_expr(&tree, relation)
|
||||||
|
try testing.expect_equal(ast.BinaryOperator.add, addition.operator)
|
||||||
|
product :: ast.binary_expr(&tree, addition.left)
|
||||||
|
try testing.expect_equal(ast.BinaryOperator.multiply, product.operator)
|
||||||
|
negate :: ast.unary_expr(&tree, product.left)
|
||||||
|
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, negate.operand))
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|
||||||
|
parses_factorial_and_canonical_conditional_arms 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 ::
|
||||||
|
`factorial proc(n int) int {
|
||||||
|
` if (n <= 1) return 1 else return n * factorial(n - 1)
|
||||||
|
`}
|
||||||
|
`main proc() int {
|
||||||
|
` if true {
|
||||||
|
` local :: factorial(5)
|
||||||
|
` if (false) return 0 else return local
|
||||||
|
` }
|
||||||
|
` else if ready() return 2 else { return 3 }
|
||||||
|
`}
|
||||||
|
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)
|
||||||
|
factorial :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
|
||||||
|
try testing.expect_equal(1, ast.block_statement_count(&tree, factorial.body))
|
||||||
|
guard :: ast.if_stmt(&tree, ast.block_statement(&tree, factorial.body, 0))
|
||||||
|
try testing.expect_equal("n <= 1", ast.source_text(&tree, ast.node_span(&tree, guard.condition)))
|
||||||
|
try testing.expect_equal(1, ast.block_statement_count(&tree, guard.then_block))
|
||||||
|
try testing.expect_equal("return 1", ast.source_text(&tree, ast.return_stmt(&tree, ast.block_statement(&tree, guard.then_block, 0)).span))
|
||||||
|
try testing.expect_equal(1, ast.block_statement_count(&tree, guard.else_block?))
|
||||||
|
try testing.expect_equal(ast.StmtKind.return_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, guard.else_block?, 0)))
|
||||||
|
main :: ast.function_decl(&tree, ast.package_decl(&tree, 1))
|
||||||
|
outer :: ast.if_stmt(&tree, ast.block_statement(&tree, main.body, 0))
|
||||||
|
try testing.expect_equal(2, ast.block_statement_count(&tree, outer.then_block))
|
||||||
|
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, outer.then_block, 0)))
|
||||||
|
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.then_block, 1))
|
||||||
|
try testing.expect(nested.else_block != null)
|
||||||
|
try testing.expect_equal(1, ast.block_statement_count(&tree, outer.else_block?))
|
||||||
|
chain :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.else_block?, 0))
|
||||||
|
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, chain.condition))
|
||||||
|
try testing.expect(chain.else_block != null)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|
||||||
|
attaches_dangling_else_to_nearest_if 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 :: "main proc() int { if (true) if (false) return 1 else return 2 else return 3 }"
|
||||||
|
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)
|
||||||
|
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||||
|
outer :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||||
|
inner :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.then_block, 0))
|
||||||
|
inner_return :: ast.return_stmt(&tree, ast.block_statement(&tree, inner.else_block?, 0))
|
||||||
|
outer_return :: ast.return_stmt(&tree, ast.block_statement(&tree, outer.else_block?, 0))
|
||||||
|
try testing.expect_equal("2", ast.source_text(&tree, ast.node_span(&tree, inner_return.value?)))
|
||||||
|
try testing.expect_equal("3", ast.source_text(&tree, ast.node_span(&tree, outer_return.value?)))
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|
||||||
|
recovers_conditional_conditions_and_arms_without_losing_siblings 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 ::
|
||||||
|
`main proc() void {
|
||||||
|
` if { good() } else { other() }
|
||||||
|
` if (true) else return
|
||||||
|
` if broken( { kept() }
|
||||||
|
` after :: 7
|
||||||
|
` return 1 else return 2
|
||||||
|
` final :: 8
|
||||||
|
`}
|
||||||
|
`tail :: 9
|
||||||
|
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))
|
||||||
|
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||||
|
try testing.expect_equal(6, ast.block_statement_count(&tree, body))
|
||||||
|
first :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||||
|
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, first.condition))
|
||||||
|
condition_diagnostic :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, first.condition).diagnostic)?
|
||||||
|
try testing.expect_equal("{", ast.source_text(&tree, condition_diagnostic.span))
|
||||||
|
try testing.expect_equal(1, ast.block_statement_count(&tree, first.then_block))
|
||||||
|
second :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 1))
|
||||||
|
bad_arm :: ast.block_statement(&tree, second.then_block, 0)
|
||||||
|
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, bad_arm))
|
||||||
|
arm_diagnostic :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, bad_arm).diagnostic)?
|
||||||
|
try testing.expect_equal("else", ast.source_text(&tree, arm_diagnostic.span))
|
||||||
|
try testing.expect(second.else_block != null)
|
||||||
|
third :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 2))
|
||||||
|
try testing.expect_equal(1, ast.block_statement_count(&tree, third.then_block))
|
||||||
|
after :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 3))
|
||||||
|
try testing.expect_equal("after", ast.source_text(&tree, after.name))
|
||||||
|
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 4)))
|
||||||
|
final :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 5))
|
||||||
|
try testing.expect_equal("final", ast.source_text(&tree, final.name))
|
||||||
|
try testing.expect_equal(4, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|
||||||
|
recovers_invalid_guard_syntax_and_missing_arm_at_enclosing_brace 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 ::
|
||||||
|
`main proc() int {
|
||||||
|
` if true return 1 else return 2
|
||||||
|
` if (true junk) { kept() } else { other() }
|
||||||
|
` sibling :: 3
|
||||||
|
` if (false) }
|
||||||
|
`tail :: 4
|
||||||
|
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))
|
||||||
|
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||||
|
try testing.expect_equal(4, ast.block_statement_count(&tree, body))
|
||||||
|
first :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||||
|
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, first.condition))
|
||||||
|
guard_error :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, first.condition).diagnostic)?
|
||||||
|
try testing.expect_equal("return", ast.source_text(&tree, guard_error.span))
|
||||||
|
try testing.expect(first.else_block != null)
|
||||||
|
second :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 1))
|
||||||
|
try testing.expect_equal(1, ast.block_statement_count(&tree, second.then_block))
|
||||||
|
try testing.expect(second.else_block != null)
|
||||||
|
sibling :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 2))
|
||||||
|
try testing.expect_equal("sibling", ast.source_text(&tree, sibling.name))
|
||||||
|
last :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 3))
|
||||||
|
missing :: ast.block_statement(&tree, last.then_block, 0)
|
||||||
|
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, missing))
|
||||||
|
arm_error :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, missing).diagnostic)?
|
||||||
|
try testing.expect_equal("}", ast.source_text(&tree, arm_error.span))
|
||||||
|
try testing.expect_equal(3, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|||||||
+119
-58
@@ -5,7 +5,7 @@ import "@source/ast"
|
|||||||
import "@source/diagnostics"
|
import "@source/diagnostics"
|
||||||
sema :: import "@source/sema"
|
sema :: import "@source/sema"
|
||||||
|
|
||||||
TypeKind :: enum { none, invalid, int, float, string, void, function }
|
TypeKind :: enum { none, invalid, int, float, bool, string, void, function }
|
||||||
|
|
||||||
Type :: struct {
|
Type :: struct {
|
||||||
kind TypeKind = .none
|
kind TypeKind = .none
|
||||||
@@ -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, return_stmt }
|
Role :: enum { none, expression, declaration, parameter, function, annotation, invalid, body, return_stmt, if_stmt }
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
Cell :: struct {
|
Cell :: struct {
|
||||||
@@ -161,6 +161,7 @@ check proc(
|
|||||||
.unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand)
|
.unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand)
|
||||||
.binary: {
|
.binary: {
|
||||||
expression :: ast.binary_expr(tree, id)
|
expression :: ast.binary_expr(tree, id)
|
||||||
|
if (bool_equality(&state, id)) continue
|
||||||
try require_numeric(&state, id, expression.left)
|
try require_numeric(&state, id, expression.left)
|
||||||
try require_numeric(&state, id, expression.right)
|
try require_numeric(&state, id, expression.right)
|
||||||
}
|
}
|
||||||
@@ -178,12 +179,16 @@ check proc(
|
|||||||
.unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand)
|
.unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand)
|
||||||
.binary: {
|
.binary: {
|
||||||
expression :: ast.binary_expr(tree, id)
|
expression :: ast.binary_expr(tree, id)
|
||||||
|
if (comparison(expression.operator)) {
|
||||||
|
if (!bool_equality(&state, id)) try comparison_operands(&state, id)
|
||||||
|
} else {
|
||||||
try numeric_operand(&state, id, expression.left)
|
try numeric_operand(&state, id, expression.left)
|
||||||
try numeric_operand(&state, id, expression.right)
|
try numeric_operand(&state, id, expression.right)
|
||||||
if (expression.operator == .divide) {
|
if (expression.operator == .divide) {
|
||||||
try impose(&state, id, .float, .integer_division, expression.span)
|
try impose(&state, id, .float, .integer_division, expression.span)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else: {}
|
else: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,6 +251,28 @@ check proc(
|
|||||||
# errors travel only from dependencies to users. A rejected use never poisons
|
# errors travel only from dependencies to users. A rejected use never poisons
|
||||||
# an explicitly typed declaration, and body errors do not erase signatures.
|
# an explicitly typed declaration, and body errors do not erase signatures.
|
||||||
propagate(&state, queue)
|
propagate(&state, queue)
|
||||||
|
# validate conditions after operand errors have reached their users.
|
||||||
|
for (0..count) |i| {
|
||||||
|
if (cells[i].role != .if_stmt) continue
|
||||||
|
condition :: ast.if_stmt(tree, ast.NodeId(i)).condition
|
||||||
|
actual :: types[usize(condition)]
|
||||||
|
if (actual.kind != .invalid and actual.kind != .bool) {
|
||||||
|
try fail(&state, condition, .condition_not_bool, ast.node_span(tree, condition))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
propagate(&state, queue)
|
||||||
|
for (0..ast.package_decl_count(tree)) |i| {
|
||||||
|
id :: ast.package_decl(tree, i)
|
||||||
|
if (ast.decl_kind(tree, id) != .function) continue
|
||||||
|
function :: ast.function_decl(tree, id)
|
||||||
|
result :: types[usize(function.return_type)]
|
||||||
|
if (result.kind == .void or result.kind == .invalid or types[usize(id)].kind == .invalid) continue
|
||||||
|
if (block_falls_through(&state, function.body)) {
|
||||||
|
# lowering must find this diagnostic even when another arm failed.
|
||||||
|
diagnostic :: try diagnostics.add(diagnostic_store, .missing_return, ast.node_span(tree, function.body))
|
||||||
|
types[usize(function.body)] = invalid_type(diagnostic)
|
||||||
|
}
|
||||||
|
}
|
||||||
return Result{ storage = Storage{ types = types, allocator = allocator } }
|
return Result{ storage = Storage{ types = types, allocator = allocator } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,6 +398,7 @@ check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (
|
|||||||
type.kind = match binding.data.builtin {
|
type.kind = match binding.data.builtin {
|
||||||
.int: .int
|
.int: .int
|
||||||
.float: .float
|
.float: .float
|
||||||
|
.bool: .bool
|
||||||
.string: .string
|
.string: .string
|
||||||
.void: .void
|
.void: .void
|
||||||
}
|
}
|
||||||
@@ -427,25 +455,44 @@ collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError |
|
|||||||
state.cells[usize(id)].type = invalid_type(binding.data.diagnostic)
|
state.cells[usize(id)].type = invalid_type(binding.data.diagnostic)
|
||||||
state.cells[usize(id)].type.function = id
|
state.cells[usize(id)].type.function = id
|
||||||
}
|
}
|
||||||
state.cells[usize(function.body)].role = .body
|
try collect_block(state, function.body, function.return_type)
|
||||||
for (0..ast.block_statement_count(tree, function.body)) |i| {
|
}
|
||||||
statement :: ast.block_statement(tree, function.body, i)
|
|
||||||
|
@hide:file
|
||||||
|
collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
tree :: state.tree
|
||||||
|
state.cells[usize(id)].role = .body
|
||||||
|
for (0..ast.block_statement_count(tree, id)) |i| {
|
||||||
|
statement :: ast.block_statement(tree, id, i)
|
||||||
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: {
|
.return_stmt: {
|
||||||
state.cells[usize(statement)].role = .return_stmt
|
state.cells[usize(statement)].role = .return_stmt
|
||||||
state.cells[usize(statement)].type = Type{ kind = .void }
|
state.cells[usize(statement)].type = Type{ kind = .void }
|
||||||
state.cells[usize(statement)].return_annotation = function.return_type
|
state.cells[usize(statement)].return_annotation = return_annotation
|
||||||
if (ast.return_stmt(tree, statement).value) |value| {
|
if (ast.return_stmt(tree, statement).value) |value| {
|
||||||
try collect_expression(state, value)
|
try collect_expression(state, value)
|
||||||
try depend(state, value, statement)
|
try depend(state, value, statement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.if_stmt: {
|
||||||
|
conditional :: ast.if_stmt(tree, statement)
|
||||||
|
state.cells[usize(statement)].role = .if_stmt
|
||||||
|
state.cells[usize(statement)].type = Type{ kind = .void }
|
||||||
|
try collect_expression(state, conditional.condition)
|
||||||
|
try depend(state, conditional.condition, statement)
|
||||||
|
try collect_block(state, conditional.then_block, return_annotation)
|
||||||
|
try depend(state, conditional.then_block, id)
|
||||||
|
if (conditional.else_block) |other| {
|
||||||
|
try collect_block(state, other, return_annotation)
|
||||||
|
try depend(state, other, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
.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
|
||||||
try depend(state, source, function.body)
|
try depend(state, source, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,6 +507,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
|
|||||||
state.cells[usize(id)].anchored = true
|
state.cells[usize(id)].anchored = true
|
||||||
}
|
}
|
||||||
.literal_float: state.cells[usize(id)].type = Type{ kind = .float }
|
.literal_float: state.cells[usize(id)].type = Type{ kind = .float }
|
||||||
|
.literal_bool: state.cells[usize(id)].type = Type{ kind = .bool }
|
||||||
.literal_string: state.cells[usize(id)].type = Type{ kind = .string }
|
.literal_string: state.cells[usize(id)].type = Type{ kind = .string }
|
||||||
.identifier: {
|
.identifier: {
|
||||||
binding :: sema.binding(state.resolution, id)
|
binding :: sema.binding(state.resolution, id)
|
||||||
@@ -476,8 +524,12 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
|
|||||||
try depend(state, operand, id)
|
try depend(state, operand, id)
|
||||||
}
|
}
|
||||||
.binary: {
|
.binary: {
|
||||||
state.cells[usize(id)].numeric = true
|
|
||||||
expression :: ast.binary_expr(tree, id)
|
expression :: ast.binary_expr(tree, id)
|
||||||
|
if (comparison(expression.operator)) {
|
||||||
|
state.cells[usize(id)].type = Type{ kind = .bool }
|
||||||
|
} else {
|
||||||
|
state.cells[usize(id)].numeric = true
|
||||||
|
}
|
||||||
try collect_expression(state, expression.left)
|
try collect_expression(state, expression.left)
|
||||||
try collect_expression(state, expression.right)
|
try collect_expression(state, expression.right)
|
||||||
try depend(state, expression.left, id)
|
try depend(state, expression.left, id)
|
||||||
@@ -502,6 +554,40 @@ numeric_kind proc(kind TypeKind) bool {
|
|||||||
return kind == .int or kind == .float
|
return kind == .int or kind == .float
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
comparison proc(operator ast.BinaryOperator) bool {
|
||||||
|
return (
|
||||||
|
operator == .equal or operator == .not_equal or operator == .less
|
||||||
|
or operator == .less_equal or operator == .greater or operator == .greater_equal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
bool_equality proc(state @mut State, id ast.NodeId) bool {
|
||||||
|
expression :: ast.binary_expr(state.tree, id)
|
||||||
|
return (
|
||||||
|
(expression.operator == .equal or expression.operator == .not_equal)
|
||||||
|
and group_type(state, expression.left).kind == .bool
|
||||||
|
and group_type(state, expression.right).kind == .bool
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
comparison_operands proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
if (state.types[usize(id)].kind == .invalid) return
|
||||||
|
expression :: ast.binary_expr(state.tree, id)
|
||||||
|
left :: root(state, usize(expression.left))
|
||||||
|
right :: root(state, usize(expression.right))
|
||||||
|
# only operand groups receive numeric demands; the result stays bool.
|
||||||
|
if (state.cells[left].numeric or state.cells[left].type.kind == .none) {
|
||||||
|
state.cells[left].numeric = true
|
||||||
|
try numeric_constraint(state, expression.left, expression.right, id)
|
||||||
|
} else if (state.cells[right].numeric or state.cells[right].type.kind == .none) {
|
||||||
|
state.cells[right].numeric = true
|
||||||
|
try numeric_constraint(state, expression.right, expression.left, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
impose proc(state @mut State, id ast.NodeId, kind TypeKind, code diagnostics.Code, span ast.Span) void ! (mem.AllocError | diagnostics.CapacityError) {
|
impose proc(state @mut State, id ast.NodeId, kind TypeKind, code diagnostics.Code, span ast.Span) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
index :: root(state, usize(id))
|
index :: root(state, usize(id))
|
||||||
@@ -516,6 +602,11 @@ impose proc(state @mut State, id ast.NodeId, kind TypeKind, code diagnostics.Cod
|
|||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
numeric_operand proc(state @mut State, expression, operand ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
numeric_operand proc(state @mut State, expression, operand ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
try numeric_constraint(state, expression, operand, expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
numeric_constraint proc(state @mut State, expression, operand, operation ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
index :: root(state, usize(operand))
|
index :: root(state, usize(operand))
|
||||||
type :: state.cells[index].type
|
type :: state.cells[index].type
|
||||||
if (state.types[usize(expression)].kind == .invalid or state.types[usize(operand)].kind == .invalid) return
|
if (state.types[usize(expression)].kind == .invalid or state.types[usize(operand)].kind == .invalid) return
|
||||||
@@ -525,13 +616,13 @@ numeric_operand proc(state @mut State, expression, operand ast.NodeId) void ! (m
|
|||||||
operand_demand :: state.cells[index].demand
|
operand_demand :: state.cells[index].demand
|
||||||
join(state, expression, operand)
|
join(state, expression, operand)
|
||||||
if (expression_demand != .none) {
|
if (expression_demand != .none) {
|
||||||
try impose(state, expression, expression_demand, .invalid_operand, ast.node_span(state.tree, expression))
|
try impose(state, expression, expression_demand, .invalid_operand, ast.node_span(state.tree, operation))
|
||||||
}
|
}
|
||||||
if (operand_demand != .none) {
|
if (operand_demand != .none) {
|
||||||
try impose(state, expression, operand_demand, .invalid_operand, ast.node_span(state.tree, expression))
|
try impose(state, expression, operand_demand, .invalid_operand, ast.node_span(state.tree, operation))
|
||||||
}
|
}
|
||||||
} else if (numeric_kind(type.kind)) {
|
} else if (numeric_kind(type.kind)) {
|
||||||
try impose(state, expression, type.kind, .invalid_operand, ast.node_span(state.tree, expression))
|
try impose(state, expression, type.kind, .invalid_operand, ast.node_span(state.tree, operation))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,20 +659,6 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
|
|||||||
}
|
}
|
||||||
if (declaration.type) |type| try compatible(state, declaration.initializer, state.types[usize(type)])
|
if (declaration.type) |type| try compatible(state, declaration.initializer, state.types[usize(type)])
|
||||||
}
|
}
|
||||||
.function: {
|
|
||||||
function :: ast.function_decl(tree, id)
|
|
||||||
result :: state.types[usize(function.return_type)]
|
|
||||||
if (result.kind == .void or result.kind == .invalid or state.types[usize(id)].kind == .invalid) return
|
|
||||||
for (0..ast.block_statement_count(tree, function.body)) |i| {
|
|
||||||
if (ast.stmt_kind(tree, ast.block_statement(tree, function.body, i)) == .return_stmt) return
|
|
||||||
}
|
|
||||||
if (syntactically_valid_body(tree, function.body)) {
|
|
||||||
diagnostic :: try diagnostics.add(state.diagnostics, .missing_return, ast.node_span(tree, function.body))
|
|
||||||
if (state.types[usize(function.body)].kind != .invalid) {
|
|
||||||
state.types[usize(function.body)] = invalid_type(diagnostic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.return_stmt: {
|
.return_stmt: {
|
||||||
statement :: ast.return_stmt(tree, id)
|
statement :: ast.return_stmt(tree, id)
|
||||||
expected :: state.types[usize(state.cells[usize(id)].return_annotation)]
|
expected :: state.types[usize(state.cells[usize(id)].return_annotation)]
|
||||||
@@ -611,6 +688,8 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
|
|||||||
left :: state.types[usize(expression.left)]
|
left :: state.types[usize(expression.left)]
|
||||||
right :: state.types[usize(expression.right)]
|
right :: state.types[usize(expression.right)]
|
||||||
if (left.kind == .invalid or right.kind == .invalid) return
|
if (left.kind == .invalid or right.kind == .invalid) return
|
||||||
|
if ((expression.operator == .equal or expression.operator == .not_equal)
|
||||||
|
and left.kind == .bool and right.kind == .bool) return
|
||||||
if (!numeric_kind(left.kind) or !numeric_kind(right.kind) or left.kind != right.kind) {
|
if (!numeric_kind(left.kind) or !numeric_kind(right.kind) or left.kind != right.kind) {
|
||||||
try fail(state, id, .invalid_operand, expression.span)
|
try fail(state, id, .invalid_operand, expression.span)
|
||||||
} else if (expression.operator == .divide and left.kind == .int) {
|
} else if (expression.operator == .divide and left.kind == .int) {
|
||||||
@@ -649,44 +728,26 @@ validate_call proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | dia
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
syntactically_valid_expression proc(tree @ast.Tree, id ast.NodeId) bool {
|
block_falls_through proc(state @State, id ast.NodeId) bool {
|
||||||
match ast.expr_kind(tree, id) {
|
tree :: state.tree
|
||||||
.invalid: return false
|
for (0..ast.block_statement_count(tree, id)) |i| {
|
||||||
.unary: return syntactically_valid_expression(tree, ast.unary_expr(tree, id).operand)
|
statement :: ast.block_statement(tree, id, i)
|
||||||
.binary: {
|
kind :: ast.stmt_kind(tree, statement)
|
||||||
expression :: ast.binary_expr(tree, id)
|
source :: if (kind == .expression) ast.statement_expr(tree, statement) else statement
|
||||||
return syntactically_valid_expression(tree, expression.left) and syntactically_valid_expression(tree, expression.right)
|
if (state.types[usize(source)].kind == .invalid) return false
|
||||||
}
|
match kind {
|
||||||
.call: {
|
.return_stmt, .invalid: return false
|
||||||
if (!syntactically_valid_expression(tree, ast.call_expr(tree, id).callee)) return false
|
.if_stmt: {
|
||||||
for (0..ast.call_arg_count(tree, id)) |i| {
|
conditional :: ast.if_stmt(tree, statement)
|
||||||
if (!syntactically_valid_expression(tree, ast.call_arg(tree, id, i))) return false
|
then_falls :: block_falls_through(state, conditional.then_block)
|
||||||
|
if (conditional.else_block) |other| {
|
||||||
|
if (!then_falls and !block_falls_through(state, other)) return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else: {}
|
else: {}
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@hide:file
|
|
||||||
syntactically_valid_body proc(tree @ast.Tree, id ast.NodeId) bool {
|
|
||||||
for (0..ast.block_statement_count(tree, id)) |i| {
|
|
||||||
statement :: ast.block_statement(tree, id, i)
|
|
||||||
match ast.stmt_kind(tree, statement) {
|
|
||||||
.invalid: return false
|
|
||||||
.expression: {
|
|
||||||
if (!syntactically_valid_expression(tree, ast.statement_expr(tree, statement))) return false
|
|
||||||
}
|
|
||||||
.return_stmt: {
|
|
||||||
if (ast.return_stmt(tree, statement).value) |value| {
|
|
||||||
if (!syntactically_valid_expression(tree, value)) return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.declaration: {
|
|
||||||
if (!syntactically_valid_expression(tree, ast.statement_decl(tree, statement).initializer)) return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -731,3 +731,240 @@ return_errors_preserve_identity_check_unreachable_code_and_suppress_missing_retu
|
|||||||
try testing.expect_equal(2, code_count(&diagnostic_store, .unexpected_token))
|
try testing.expect_equal(2, code_count(&diagnostic_store, .unexpected_token))
|
||||||
try testing.expect_equal(8, diagnostics.count(&diagnostic_store))
|
try testing.expect_equal(8, diagnostics.count(&diagnostic_store))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checks_bool_comparisons_recursive_returns_and_nested_numeric_context 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 ::
|
||||||
|
`flag bool :: true
|
||||||
|
`same :: flag == false
|
||||||
|
`different :: true != flag
|
||||||
|
`less :: alias < 4.0
|
||||||
|
`less_equal :: seed <= 5
|
||||||
|
`greater :: 6 > seed
|
||||||
|
`greater_equal :: seed >= 1
|
||||||
|
`equal :: seed == 3
|
||||||
|
`not_equal :: seed != 2
|
||||||
|
`alias :: seed
|
||||||
|
`seed := 3
|
||||||
|
`identity proc(value bool) bool { return value }
|
||||||
|
`factorial proc(n int) int {
|
||||||
|
` if n <= 1 { return 1 }
|
||||||
|
` return n * factorial(n - 1)
|
||||||
|
`}
|
||||||
|
`fraction proc(value bool) float {
|
||||||
|
` if value {
|
||||||
|
` before :: later
|
||||||
|
` later := 3
|
||||||
|
` return -before / 2
|
||||||
|
` } else if false {
|
||||||
|
` later :: 4
|
||||||
|
` return later
|
||||||
|
` } else {
|
||||||
|
` return 5
|
||||||
|
` }
|
||||||
|
`}
|
||||||
|
`main proc() bool { return identity(flag) }
|
||||||
|
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 (0..usize(9)) |i| {
|
||||||
|
declaration :: ast.package_decl(&tree, i)
|
||||||
|
try expect_kind(&result, declaration, .bool)
|
||||||
|
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .bool)
|
||||||
|
if (i < 3) continue
|
||||||
|
comparison_view :: ast.binary_expr(&tree, ast.statement_decl(&tree, declaration).initializer)
|
||||||
|
try expect_kind(&result, comparison_view.left, .float)
|
||||||
|
try expect_kind(&result, comparison_view.right, .float)
|
||||||
|
}
|
||||||
|
for (usize(9)..usize(11)) |i| try expect_kind(&result, ast.package_decl(&tree, i), .float)
|
||||||
|
identity :: ast.package_decl(&tree, 11)
|
||||||
|
try expect_kind(&result, ast.param(&tree, ast.function_param(&tree, identity, 0)).type, .bool)
|
||||||
|
try expect_kind(&result, ast.function_decl(&tree, identity).return_type, .bool)
|
||||||
|
factorial :: ast.package_decl(&tree, 12)
|
||||||
|
factorial_view :: ast.function_decl(&tree, factorial)
|
||||||
|
try expect_kind(&result, factorial, .function)
|
||||||
|
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, factorial_view.body, 0))
|
||||||
|
try expect_kind(&result, conditional.condition, .bool)
|
||||||
|
operands :: ast.binary_expr(&tree, conditional.condition)
|
||||||
|
try expect_kind(&result, operands.left, .int)
|
||||||
|
try expect_kind(&result, operands.right, .int)
|
||||||
|
fraction :: ast.function_decl(&tree, ast.package_decl(&tree, 13))
|
||||||
|
arms :: ast.if_stmt(&tree, ast.block_statement(&tree, fraction.body, 0))
|
||||||
|
for (0..usize(2)) |i| {
|
||||||
|
declaration :: ast.block_statement(&tree, arms.then_block, i)
|
||||||
|
try expect_kind(&result, declaration, .float)
|
||||||
|
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .float)
|
||||||
|
}
|
||||||
|
if (arms.else_block) |other| {
|
||||||
|
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, other, 0))
|
||||||
|
try expect_kind(&result, ast.block_statement(&tree, nested.then_block, 0), .float)
|
||||||
|
} else {
|
||||||
|
return .expectation_failed
|
||||||
|
}
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|
||||||
|
rejects_bool_numeric_mixing_and_preserves_invalid_condition_identity 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 ::
|
||||||
|
`sum :: true + false
|
||||||
|
`order :: true < false
|
||||||
|
`mixed :: true == 1
|
||||||
|
`strings :: "x" == "x"
|
||||||
|
`wrong_bool bool :: 1
|
||||||
|
`wrong_int int :: false
|
||||||
|
`main proc() void {
|
||||||
|
` if (1) { okay :: true }
|
||||||
|
` if "text" { okay :: false }
|
||||||
|
` if absent { okay :: true }
|
||||||
|
` if true + false { okay :: false }
|
||||||
|
` if true { return } else { -"bad" }
|
||||||
|
`}
|
||||||
|
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 (0..usize(4)) |i| {
|
||||||
|
expression :: ast.statement_decl(&tree, ast.package_decl(&tree, i)).initializer
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, expression, .invalid_operand, ast.node_span(&tree, expression))
|
||||||
|
}
|
||||||
|
for (usize(4)..usize(6)) |i| {
|
||||||
|
expression :: ast.statement_decl(&tree, ast.package_decl(&tree, i)).initializer
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, expression, .type_mismatch, ast.node_span(&tree, expression))
|
||||||
|
}
|
||||||
|
function :: ast.package_decl(&tree, 6)
|
||||||
|
body :: ast.function_decl(&tree, function).body
|
||||||
|
for (0..usize(2)) |i| {
|
||||||
|
statement :: ast.block_statement(&tree, body, i)
|
||||||
|
condition :: ast.if_stmt(&tree, statement).condition
|
||||||
|
diagnostic :: try expect_error(&result, &diagnostic_store, condition, .condition_not_bool, ast.node_span(&tree, condition))
|
||||||
|
try testing.expect_equal(diagnostic, type_of(&result, statement).diagnostic)
|
||||||
|
}
|
||||||
|
unknown :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 2)).condition
|
||||||
|
original :: sema.binding(&resolution, unknown).data.diagnostic
|
||||||
|
try testing.expect_equal(original, type_of(&result, unknown).diagnostic)
|
||||||
|
failed :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 3)).condition
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, failed, .invalid_operand, ast.node_span(&tree, failed))
|
||||||
|
final_if :: ast.block_statement(&tree, body, 4)
|
||||||
|
try expect_kind(&result, final_if, .void)
|
||||||
|
final_view :: ast.if_stmt(&tree, final_if)
|
||||||
|
if (final_view.else_block) |other| {
|
||||||
|
error_expr :: ast.statement_expr(&tree, ast.block_statement(&tree, other, 0))
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, error_expr, .invalid_operand, ast.node_span(&tree, error_expr))
|
||||||
|
} else {
|
||||||
|
return .expectation_failed
|
||||||
|
}
|
||||||
|
try expect_kind(&result, function, .function)
|
||||||
|
try testing.expect_equal(2, code_count(&diagnostic_store, .condition_not_bool))
|
||||||
|
try testing.expect_equal(6, code_count(&diagnostic_store, .invalid_operand))
|
||||||
|
try testing.expect_equal(2, code_count(&diagnostic_store, .type_mismatch))
|
||||||
|
try testing.expect_equal(1, code_count(&diagnostic_store, .unknown_name))
|
||||||
|
try testing.expect_equal(11, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|
||||||
|
analyzes_return_paths_after_trap_propagation_without_poisoning_signatures 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 ::
|
||||||
|
`both proc(flag bool) int {
|
||||||
|
` if flag { return 1 } else { return 2 }
|
||||||
|
`}
|
||||||
|
`missing proc() int { if true { return 1 } }
|
||||||
|
`bad_arm proc(flag bool) int { if flag { -"bad" } }
|
||||||
|
`trap_and_return proc(flag bool) int {
|
||||||
|
` if flag { return 1 } else { bad int :: false }
|
||||||
|
`}
|
||||||
|
`invalid_condition proc() int { if 1 {} }
|
||||||
|
`invalid_expression proc() int { -"bad" }
|
||||||
|
`invalid_declaration proc() int { bad bool :: 1 }
|
||||||
|
`syntax_trap proc() int {
|
||||||
|
` return 1 stray
|
||||||
|
`}
|
||||||
|
`unreachable proc() int {
|
||||||
|
` return 1
|
||||||
|
` -"bad"
|
||||||
|
`}
|
||||||
|
`nested proc(a bool, b bool) int {
|
||||||
|
` if a { if b { return 1 } else { return 2 } } else { return 3 }
|
||||||
|
`}
|
||||||
|
`join proc(flag bool) int {
|
||||||
|
` if flag {} else { return 1 }
|
||||||
|
` return 2
|
||||||
|
`}
|
||||||
|
`answer :: bad_arm(false)
|
||||||
|
`wrong_return proc(flag bool) int {
|
||||||
|
` if flag { return false } else { return 1 }
|
||||||
|
`}
|
||||||
|
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 (0..usize(11)) |i| try expect_kind(&result, ast.package_decl(&tree, i), .function)
|
||||||
|
for (usize(1)..usize(3)) |i| {
|
||||||
|
body :: ast.function_decl(&tree, ast.package_decl(&tree, i)).body
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body))
|
||||||
|
}
|
||||||
|
bad_body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body
|
||||||
|
conditional :: ast.block_statement(&tree, bad_body, 0)
|
||||||
|
try expect_kind(&result, conditional, .void)
|
||||||
|
arm :: ast.if_stmt(&tree, conditional).then_block
|
||||||
|
operand :: ast.statement_expr(&tree, ast.block_statement(&tree, arm, 0))
|
||||||
|
original :: try expect_error(&result, &diagnostic_store, operand, .invalid_operand, ast.node_span(&tree, operand))
|
||||||
|
try testing.expect_equal(original, type_of(&result, arm).diagnostic)
|
||||||
|
try testing.expect(type_of(&result, bad_body).diagnostic != original)
|
||||||
|
condition_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
|
||||||
|
condition :: ast.if_stmt(&tree, ast.block_statement(&tree, condition_body, 0)).condition
|
||||||
|
condition_diag :: try expect_error(&result, &diagnostic_store, condition, .condition_not_bool, ast.node_span(&tree, condition))
|
||||||
|
try testing.expect_equal(condition_diag, type_of(&result, condition_body).diagnostic)
|
||||||
|
recovered :: ast.function_decl(&tree, ast.package_decl(&tree, 7)).body
|
||||||
|
syntax :: ast.block_statement(&tree, recovered, 0)
|
||||||
|
try testing.expect_equal(ast.invalid(&tree, syntax).diagnostic, type_of(&result, recovered).diagnostic)
|
||||||
|
unreachable_body :: ast.function_decl(&tree, ast.package_decl(&tree, 8)).body
|
||||||
|
checked_after_return :: ast.statement_expr(&tree, ast.block_statement(&tree, unreachable_body, 1))
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, checked_after_return, .invalid_operand, ast.node_span(&tree, checked_after_return))
|
||||||
|
try expect_kind(&result, ast.package_decl(&tree, 11), .int)
|
||||||
|
wrong_function :: ast.package_decl(&tree, 12)
|
||||||
|
try expect_kind(&result, wrong_function, .function)
|
||||||
|
wrong_body :: ast.function_decl(&tree, wrong_function).body
|
||||||
|
wrong_if :: ast.block_statement(&tree, wrong_body, 0)
|
||||||
|
try expect_kind(&result, wrong_if, .void)
|
||||||
|
wrong_arm :: ast.if_stmt(&tree, wrong_if).then_block
|
||||||
|
wrong_statement :: ast.block_statement(&tree, wrong_arm, 0)
|
||||||
|
if (ast.return_stmt(&tree, wrong_statement).value) |value| {
|
||||||
|
mismatch :: try expect_error(&result, &diagnostic_store, value, .return_type_mismatch, ast.node_span(&tree, value))
|
||||||
|
try testing.expect_equal(mismatch, type_of(&result, wrong_statement).diagnostic)
|
||||||
|
try testing.expect_equal(mismatch, type_of(&result, wrong_body).diagnostic)
|
||||||
|
} else {
|
||||||
|
return .expectation_failed
|
||||||
|
}
|
||||||
|
try testing.expect_equal(1, code_count(&diagnostic_store, .return_type_mismatch))
|
||||||
|
try testing.expect_equal(2, code_count(&diagnostic_store, .missing_return))
|
||||||
|
try testing.expect_equal(1, code_count(&diagnostic_store, .condition_not_bool))
|
||||||
|
}
|
||||||
|
|||||||
+31
-12
@@ -6,12 +6,13 @@ import "@std/strmap"
|
|||||||
import "@source/ast"
|
import "@source/ast"
|
||||||
import "@source/diagnostics"
|
import "@source/diagnostics"
|
||||||
|
|
||||||
BuiltinType :: enum { int, float, string, void }
|
BuiltinType :: enum { int, float, bool, string, void }
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
builtins strmap.StaticStringMap(BuiltinType) :: strmap.init([
|
builtins strmap.StaticStringMap(BuiltinType) :: strmap.init([
|
||||||
{ "int", .int },
|
{ "int", .int },
|
||||||
{ "float", .float },
|
{ "float", .float },
|
||||||
|
{ "bool", .bool },
|
||||||
{ "string", .string },
|
{ "string", .string },
|
||||||
{ "void", .void },
|
{ "void", .void },
|
||||||
])
|
])
|
||||||
@@ -175,20 +176,38 @@ resolve_function proc(
|
|||||||
parameter :: ast.function_param(tree, id, i)
|
parameter :: ast.function_param(tree, id, i)
|
||||||
try predeclare(state, &scope, parameter, ast.param(tree, parameter).name)
|
try predeclare(state, &scope, parameter, ast.param(tree, parameter).name)
|
||||||
}
|
}
|
||||||
for (0..ast.block_statement_count(tree, function.body)) |i| {
|
try resolve_block(state, &scope, function.body)
|
||||||
statement :: ast.block_statement(tree, function.body, i)
|
|
||||||
if ast.stmt_kind(tree, statement) == .declaration {
|
|
||||||
try predeclare(state, &scope, statement, ast.statement_decl(tree, statement).name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (0..ast.block_statement_count(tree, function.body)) |i| {
|
@hide:file
|
||||||
statement :: ast.block_statement(tree, function.body, i)
|
resolve_arm proc(state @mut State, parent @Scope, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
scope := Scope{ names = hashmap.init(state.allocator), parent = parent }
|
||||||
|
defer hashmap.deinit(&scope.names)
|
||||||
|
try resolve_block(state, &scope, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
tree :: state.tree
|
||||||
|
for (0..ast.block_statement_count(tree, id)) |i| {
|
||||||
|
statement :: ast.block_statement(tree, id, i)
|
||||||
|
if ast.stmt_kind(tree, statement) == .declaration {
|
||||||
|
try predeclare(state, scope, statement, ast.statement_decl(tree, statement).name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (0..ast.block_statement_count(tree, id)) |i| {
|
||||||
|
statement :: ast.block_statement(tree, id, i)
|
||||||
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: {
|
.return_stmt: {
|
||||||
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, &scope, value)
|
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value)
|
||||||
|
}
|
||||||
|
.if_stmt: {
|
||||||
|
conditional :: ast.if_stmt(tree, statement)
|
||||||
|
try resolve_expr(state, scope, conditional.condition)
|
||||||
|
try resolve_arm(state, scope, conditional.then_block)
|
||||||
|
if (conditional.else_block) |other| try resolve_arm(state, scope, other)
|
||||||
}
|
}
|
||||||
.invalid: resolve_invalid(state, statement)
|
.invalid: resolve_invalid(state, statement)
|
||||||
}
|
}
|
||||||
@@ -225,7 +244,7 @@ resolve_expr proc(
|
|||||||
if (state.bindings[usize(id)].kind != .none) return
|
if (state.bindings[usize(id)].kind != .none) return
|
||||||
tree :: state.tree
|
tree :: state.tree
|
||||||
match ast.expr_kind(tree, id) {
|
match ast.expr_kind(tree, id) {
|
||||||
.literal_int, .literal_float, .literal_string: {}
|
.literal_int, .literal_float, .literal_bool, .literal_string: {}
|
||||||
.identifier: {
|
.identifier: {
|
||||||
span :: ast.node_span(tree, id)
|
span :: ast.node_span(tree, id)
|
||||||
name :: ast.source_text(tree, span)
|
name :: ast.source_text(tree, span)
|
||||||
|
|||||||
@@ -298,3 +298,71 @@ preserves_syntax_diagnostics_and_resolves_shared_unknown_types_once test {
|
|||||||
try testing.expect_equal(later, binding(&resolution, valid_ref).data.declaration)
|
try testing.expect_equal(later, binding(&resolution, valid_ref).data.declaration)
|
||||||
try testing.expect_equal(syntax_count + 3, diagnostics.count(&diagnostic_store))
|
try testing.expect_equal(syntax_count + 3, diagnostics.count(&diagnostic_store))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolves_conditional_arm_scopes_without_leaks_or_shadowing 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 ::
|
||||||
|
`choose proc(flag bool, value int) void {
|
||||||
|
` if flag {
|
||||||
|
` before :: later
|
||||||
|
` later :: value
|
||||||
|
` if before == value { nested :: later }
|
||||||
|
` } else {
|
||||||
|
` later :: value
|
||||||
|
` }
|
||||||
|
` later
|
||||||
|
` if later { later :: 1 }
|
||||||
|
` if flag {
|
||||||
|
` value :: 2
|
||||||
|
` bool :: false
|
||||||
|
` if flag { value }
|
||||||
|
` }
|
||||||
|
`}
|
||||||
|
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)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||||
|
defer deinit(&resolution)
|
||||||
|
|
||||||
|
function :: ast.package_decl(&tree, 0)
|
||||||
|
view :: ast.function_decl(&tree, function)
|
||||||
|
flag :: ast.function_param(&tree, function, 0)
|
||||||
|
value :: ast.function_param(&tree, function, 1)
|
||||||
|
try testing.expect_equal(BuiltinType.bool, binding(&resolution, ast.param(&tree, flag).type).data.builtin)
|
||||||
|
first :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 0))
|
||||||
|
try testing.expect_equal(flag, binding(&resolution, first.condition).data.declaration)
|
||||||
|
before :: ast.block_statement(&tree, first.then_block, 0)
|
||||||
|
later :: ast.block_statement(&tree, first.then_block, 1)
|
||||||
|
try testing.expect_equal(later, binding(&resolution, ast.statement_decl(&tree, before).initializer).data.declaration)
|
||||||
|
try testing.expect_equal(value, binding(&resolution, ast.statement_decl(&tree, later).initializer).data.declaration)
|
||||||
|
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, first.then_block, 2))
|
||||||
|
nested_decl :: ast.block_statement(&tree, nested.then_block, 0)
|
||||||
|
try testing.expect_equal(later, binding(&resolution, ast.statement_decl(&tree, nested_decl).initializer).data.declaration)
|
||||||
|
if (first.else_block) |other| {
|
||||||
|
sibling :: ast.block_statement(&tree, other, 0)
|
||||||
|
try testing.expect_equal(BindingKind.declaration, binding(&resolution, sibling).kind)
|
||||||
|
try testing.expect_equal(sibling, binding(&resolution, sibling).data.declaration)
|
||||||
|
} else {
|
||||||
|
return .expectation_failed
|
||||||
|
}
|
||||||
|
outside :: ast.statement_expr(&tree, ast.block_statement(&tree, view.body, 1))
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, outside).kind)
|
||||||
|
independent :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 2))
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, independent.condition).kind)
|
||||||
|
try testing.expect_equal(BindingKind.declaration, binding(&resolution, ast.block_statement(&tree, independent.then_block, 0)).kind)
|
||||||
|
forbidden :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 3))
|
||||||
|
shadow :: ast.block_statement(&tree, forbidden.then_block, 0)
|
||||||
|
builtin :: ast.block_statement(&tree, forbidden.then_block, 1)
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, shadow).kind)
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, builtin).kind)
|
||||||
|
poisoned :: ast.if_stmt(&tree, ast.block_statement(&tree, forbidden.then_block, 2))
|
||||||
|
use :: ast.statement_expr(&tree, ast.block_statement(&tree, poisoned.then_block, 0))
|
||||||
|
try testing.expect_equal(binding(&resolution, shadow).data.diagnostic, binding(&resolution, use).data.diagnostic)
|
||||||
|
try testing.expect_equal(4, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user