booleans, comparisons, and if/else

This commit is contained in:
2026-06-21 20:20:55 +02:00
parent 19e9fbdd4b
commit c90ada608e
13 changed files with 1064 additions and 518 deletions
+6 -2
View File
@@ -81,10 +81,14 @@
- static inline functions (implemented)
- 5. control flow
- boolean expressions
- boolean expressions (implemented)
- `bool` type with `true` / `false` literals
- comparison operators: `==`, `!=`, `<`, `<=`, `>`, `>=` (numeric operands widen; `bool` supports only `==` / `!=`)
- operators: `and`, `or`, `!`
- lazy evaluation / short-circuit evaluation
- if statements. example: `if condition { ... } else if { ... } else { ... }`
- if statements (implemented). example: `if condition { ... } else if { ... } else { ... }`
- conditions must be `bool`; block-scoped locals with shadowing across blocks
- lowered through new `Label` / `Br` / `Cond_Br` IR opcodes (alloca-backed locals, no phi nodes)
- conditional unwrapping for optionals (`?T`): `if val |v| { ... } else { ... }` - unwrap `val` into `v` if it is not `none`
- conditional unwrapping with guard clause: `if val |v : v >= 10| { ... } else { ... }` - unwrap `val` into `v` if it is not `none`
- multi-unwrap (see section below)
+20
View File
@@ -68,6 +68,7 @@ Expr_Kind :: enum u8 {
Integer,
Float,
String,
Bool,
Array,
None,
Name,
@@ -81,7 +82,16 @@ Expr_Kind :: enum u8 {
Struct_Literal,
Keyed,
Negate,
Not,
Add,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
And,
Or,
Call,
}
@@ -109,6 +119,7 @@ Stmt_Kind :: enum u8 {
Assignment,
Return,
Expression,
If,
}
Stmt :: struct {
@@ -119,6 +130,11 @@ Stmt :: struct {
immutable: bool,
target: Expr_Id,
expr: Expr_Id,
// `If` statements use `expr` as the condition, `body` as the then-block, and
// `else_body` as the else-block. An `else if` chain is represented as an
// `else_body` holding a single nested `If` statement.
body: []Stmt_Id,
else_body: []Stmt_Id,
diagnostic: source.Diagnostic_Id,
}
@@ -234,6 +250,10 @@ destroy_module :: proc(module: ^Module) {
for expr in module.exprs {
delete(expr.args, module.allocator)
}
for statement in module.statements {
delete(statement.body, module.allocator)
delete(statement.else_body, module.allocator)
}
for function in module.functions {
delete(function.params, module.allocator)
delete(function.body, module.allocator)
+420 -382
View File
@@ -44,6 +44,23 @@ Build_Local :: struct {
id: hir.Local_Id,
}
// Build_Ctx threads the per-function accumulators through build_block so that
// nested control-flow blocks (if/else) can be built recursively. `locals` is a
// scope stack: each block records its entry length and truncates back to it on
// exit, while `hir_locals` keeps every allocated slot for the function.
Build_Ctx :: struct {
checker: ^Checker,
pkg: ast.Package_Id,
file: ast.File_Id,
result: types.Type,
locals: ^[dynamic]Build_Local,
hir_locals: ^[dynamic]hir.Local,
global_reads: ^[dynamic]hir.Global_Id,
calls: ^[dynamic]hir.Function_Id,
problematic: ^bool,
has_return: ^bool,
}
Constant_Kind :: enum {
Unknown,
Not_Constant,
@@ -593,11 +610,29 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as
if expr.left != ast.INVALID_EXPR {
append(&stack, expr.left)
}
case .Negate, .Address, .Deref, .Field, .Unwrap, .Keyed:
case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Keyed:
append(&stack, expr.left)
case .Add, .Index, .Orelse:
case .Add, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
append(&stack, expr.left, expr.right)
case .Invalid, .Integer, .Float, .String, .None, .Name:
case .Invalid, .Integer, .Float, .String, .Bool, .None, .Name:
}
}
}
mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, file: ast.File_Id) {
for statement_id in statements {
statement := checker.ast_module.statements[statement_id]
switch statement.kind {
case .Declaration, .Assignment, .Return, .Expression:
mark_expr_imports_used(checker, statement.expr, file)
if statement.target != ast.INVALID_EXPR {
mark_expr_imports_used(checker, statement.target, file)
}
case .If:
mark_expr_imports_used(checker, statement.expr, file)
mark_block_imports_used(checker, statement.body, file)
mark_block_imports_used(checker, statement.else_body, file)
case .Invalid:
}
}
}
@@ -772,17 +807,7 @@ validate_declarations :: proc(checker: ^Checker) {
)
}
}
for statement_id in function.body {
statement := checker.ast_module.statements[statement_id]
switch statement.kind {
case .Declaration, .Assignment, .Return, .Expression:
mark_expr_imports_used(checker, statement.expr, function.file)
if statement.target != ast.INVALID_EXPR {
mark_expr_imports_used(checker, statement.target, function.file)
}
case .Invalid:
}
}
mark_block_imports_used(checker, function.body, function.file)
delete(locals)
}
for function, function_id in checker.ast_module.functions {
@@ -988,6 +1013,15 @@ infer_compound_expr :: proc(
) -> types.Type {
store := &checker.module.types
#partial switch expr.kind {
case .Bool:
return types.BOOL
case .Not:
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
return types.BOOL
case .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded)
return types.BOOL
case .String:
return string_literal_type(checker, expr.integer)
case .Array:
@@ -1124,7 +1158,8 @@ infer_expr :: proc(
last = types.F64
_ = pop(&stack)
case .String, .Array, .None, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed:
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed,
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded)
_ = pop(&stack)
case .Name:
@@ -1342,6 +1377,49 @@ infer_expr :: proc(
return last
}
infer_statements :: proc(
checker: ^Checker,
statements: []ast.Stmt_Id,
locals: ^[dynamic]Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
demanded: ^[dynamic]Spec_Id,
result: ^types.Type,
) {
scope_start := len(locals^)
for statement_id in statements {
statement := checker.ast_module.statements[statement_id]
#partial switch statement.kind {
case .Declaration:
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
declared_local := type_from_syntax(statement.type)
if is_runtime_type(checker, declared_local) {
value_type = declared_local
}
append(locals, Infer_Local{name = statement.name, type = value_type})
case .Assignment, .Expression:
if statement.target != ast.INVALID_EXPR {
_ = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded)
}
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
case .Return:
if statement.expr != ast.INVALID_EXPR {
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
if !types.is_valid(result^) {
result^ = returned
} else {
result^ = types.widest(result^, returned)
}
}
case .If:
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result)
}
}
resize(locals, scope_start)
}
infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type {
spec := checker.specs[id]
function := checker.ast_module.functions[spec.template]
@@ -1362,32 +1440,7 @@ infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Sp
}
result := types.INVALID
for statement_id in function.body {
statement := checker.ast_module.statements[statement_id]
#partial switch statement.kind {
case .Declaration:
value_type := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file, demanded)
declared_local := type_from_syntax(statement.type)
if is_runtime_type(checker, declared_local) {
value_type = declared_local
}
append(&locals, Infer_Local{name = statement.name, type = value_type})
case .Assignment, .Expression:
if statement.target != ast.INVALID_EXPR {
_ = infer_expr(checker, statement.target, locals[:], function.pkg, function.file, demanded)
}
_ = infer_expr(checker, statement.expr, locals[:], function.pkg, function.file, demanded)
case .Return:
if statement.expr != ast.INVALID_EXPR {
returned := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file, demanded)
if !types.is_valid(result) {
result = returned
} else {
result = types.widest(result, returned)
}
}
}
}
infer_statements(checker, function.body, &locals, function.pkg, function.file, demanded, &result)
if types.is_constraint(declared) {
return result
}
@@ -2116,6 +2169,90 @@ build_compound_expr :: proc(
kind=.Orelse, span=expr.span, type=child, left=optional, right=fallback,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Bool:
return add_hir_expr(checker, hir.Expr{
kind=.Bool, span=expr.span, type=types.BOOL, integer=i64(expr.integer),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Not:
operand := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file)
operand_type := checker.module.exprs[operand].type
if checker.module.exprs[operand].kind != .Invalid && !types.is_bool(operand_type) {
id := source.add(checker.diagnostics, expr.span, "'!' requires a bool operand")
return invalid_hir_expr(checker, expr.span, id, types.BOOL)
}
return add_hir_expr(checker, hir.Expr{
kind=.Not, span=expr.span, type=types.BOOL, left=operand,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .And, .Or:
left := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file)
right := build_nested_expr(checker, expr.right, locals, global_reads, calls, types.BOOL, pkg, file)
left_type := checker.module.exprs[left].type
right_type := checker.module.exprs[right].type
left_ok := checker.module.exprs[left].kind == .Invalid || types.is_bool(left_type)
right_ok := checker.module.exprs[right].kind == .Invalid || types.is_bool(right_type)
if !left_ok || !right_ok {
id := source.add(checker.diagnostics, expr.span, "'and'/'or' require bool operands")
return invalid_hir_expr(checker, expr.span, id, types.BOOL)
}
return add_hir_expr(checker, hir.Expr{
kind=.And if expr.kind == .And else .Or, span=expr.span, type=types.BOOL,
left=left, right=right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Eq, .Ne, .Lt, .Le, .Gt, .Ge:
// Contextualize a bare integer-literal operand to the other operand's type
// so comparisons like `count > 0` or `0 < count` type-check.
left_const := eval_constant(checker, expr.left)
right_const := eval_constant(checker, expr.right)
left, right: hir.Expr_Id
if right_const.kind == .Value && left_const.kind != .Value {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
hint := checker.module.exprs[left].type
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, hint, pkg, file)
} else if left_const.kind == .Value && right_const.kind != .Value {
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
hint := checker.module.exprs[right].type
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, hint, pkg, file)
} else {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
}
left_type := checker.module.exprs[left].type
right_type := checker.module.exprs[right].type
if checker.module.exprs[left].kind == .Invalid || checker.module.exprs[right].kind == .Invalid {
return invalid_hir_expr(checker, expr.span, expr.diagnostic, types.BOOL)
}
operand_type := types.INVALID
if types.is_bool(left_type) && types.is_bool(right_type) {
if expr.kind != .Eq && expr.kind != .Ne {
id := source.add(checker.diagnostics, expr.span, "bool values only support '==' and '!='")
return invalid_hir_expr(checker, expr.span, id, types.BOOL)
}
operand_type = types.BOOL
} else {
operand_type = types.widest(left_type, right_type)
if !types.is_concrete_scalar(operand_type) || types.is_bool(operand_type) {
id := source.add(checker.diagnostics, expr.span, "comparison requires compatible numeric operands")
return invalid_hir_expr(checker, expr.span, id, types.BOOL)
}
}
left = coerce_expr(checker, left, operand_type, checker.module.exprs[left].span)
right = coerce_expr(checker, right, operand_type, checker.module.exprs[right].span)
compare_kind := hir.Expr_Kind.Eq
#partial switch expr.kind {
case .Eq: compare_kind = .Eq
case .Ne: compare_kind = .Ne
case .Lt: compare_kind = .Lt
case .Le: compare_kind = .Le
case .Gt: compare_kind = .Gt
case .Ge: compare_kind = .Ge
}
return add_hir_expr(checker, hir.Expr{
kind=compare_kind, span=expr.span, type=types.BOOL, left=left, right=right,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Struct_Literal:
target_pkg, available := expr_package(checker, expr, pkg, file, true)
struct_type := types.find_named(store, u32(target_pkg), u32(expr.name)) if available else types.INVALID
@@ -2230,7 +2367,8 @@ build_expr :: proc(
}
switch expr.kind {
case .String, .Array, .None, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed:
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed,
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
last = build_compound_expr(
checker, expr, locals, global_reads, calls, frame.expected, pkg, file,
)
@@ -2676,6 +2814,231 @@ make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string {
return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator)
}
build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id {
checker := ctx.checker
body: [dynamic]hir.Stmt_Id
body.allocator = checker.allocator
scope_start := len(ctx.locals^)
for statement_id in statements {
statement := checker.ast_module.statements[statement_id]
switch statement.kind {
case .Declaration:
declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
expected := types.INVALID
if is_runtime_type(checker, declared) {
expected = declared
}
value := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
expected, ctx.pkg, ctx.file,
)
value_type := checker.module.exprs[value].type
if is_runtime_type(checker, declared) {
value = coerce_expr(checker, value, declared, statement.span)
value_type = checker.module.exprs[value].type
} else if types.is_void(declared) {
id := source.add(checker.diagnostics, statement.span, "locals cannot have type void")
value = invalid_hir_expr(checker, statement.span, id)
value_type = types.INVALID
}
if _, found := find_build_local(ctx.locals^[scope_start:], statement.name); found {
id := source.addf(
checker.diagnostics, statement.span,
"duplicate local '%s'", symbol_text(checker, statement.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
local_id := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{
name = statement.name, type = value_type, mutable = !statement.immutable,
})
append(ctx.locals, Build_Local{
name = statement.name, type = value_type, mutable = !statement.immutable, id = local_id,
})
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = statement.span, local = local_id, expr = value,
diagnostic = source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
case .Assignment:
if statement.target != ast.INVALID_EXPR {
target_expr := build_expr(
checker, statement.target, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.INVALID, ctx.pkg, ctx.file,
)
target_type := checker.module.exprs[target_expr].type
if !hir_location_writable(checker, target_expr, ctx.locals^[:]) {
id := source.add(checker.diagnostics, statement.span, "assignment target is not writable")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL,
target=hir.INVALID_EXPR, expr=hir.INVALID_EXPR, diagnostic=id,
})
ctx.problematic^ = true
continue
}
value := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
target_type, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, target_type, statement.span)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL,
target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
continue
}
if statement.name == checker.sink_symbol {
value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
if types.is_void(checker.module.exprs[value].type) {
id := source.add(checker.diagnostics, statement.span, "cannot assign a void expression to '_'")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
} else {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Sink, span = statement.span, expr = value,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
continue
}
local, found := find_build_local(ctx.locals^[:], statement.name)
if !found {
id := source.addf(checker.diagnostics, statement.span, "cannot assign unresolved local '%s'", symbol_text(checker, statement.name))
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
if !local.mutable {
id := source.addf(checker.diagnostics, statement.span, "cannot assign immutable local '%s'", symbol_text(checker, statement.name))
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
value := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
local.type, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, local.type, statement.span)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Assignment, span = statement.span, expr = value, local = local.id,
target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
case .Return:
ctx.has_return^ = true
if statement.expr == ast.INVALID_EXPR {
if !types.is_void(ctx.result) {
id := source.add(checker.diagnostics, statement.span, "'return _' is only valid in a void function")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
} else {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Return, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
continue
}
if types.is_void(ctx.result) {
id := source.add(checker.diagnostics, statement.span, "void function cannot return a value")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
value := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
ctx.result, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, ctx.result, statement.span)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Return, span = statement.span, expr = value,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
case .Expression:
value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
if !types.is_void(checker.module.exprs[value].type) {
id := source.add(checker.diagnostics, statement.span, "non-void expression result must be consumed or assigned to '_'")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
} else {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Expression, span = statement.span, expr = value,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
case .If:
condition := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file)
if checker.module.exprs[condition].kind != .Invalid && !types.is_bool(checker.module.exprs[condition].type) {
id := source.add(checker.diagnostics, statement.span, "'if' condition must be a bool")
condition = invalid_hir_expr(checker, statement.span, id, types.BOOL)
ctx.problematic^ = true
}
then_body := build_block(ctx, statement.body)
else_body: []hir.Stmt_Id = nil
if statement.else_body != nil {
else_body = build_block(ctx, statement.else_body)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .If, span = statement.span, expr = condition,
then_body = then_body, else_body = else_body,
local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid
case .Invalid:
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = statement.diagnostic,
})
ctx.problematic^ = true
}
}
resize(ctx.locals, scope_start)
return body[:]
}
build_function :: proc(checker: ^Checker, id: Spec_Id) {
spec := checker.specs[id]
function := checker.ast_module.functions[spec.template]
@@ -2769,348 +3132,23 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
},
)
}
for statement_id in function.body {
statement := checker.ast_module.statements[statement_id]
switch statement.kind {
case .Declaration:
declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
expected := types.INVALID
if is_runtime_type(checker, declared) {
expected = declared
}
value := build_expr(
checker,
statement.expr,
locals[:],
&global_reads,
&calls,
expected,
function.pkg,
function.file,
)
value_type := checker.module.exprs[value].type
if is_runtime_type(checker, declared) {
value = coerce_expr(checker, value, declared, statement.span)
value_type = checker.module.exprs[value].type
} else if types.is_void(declared) {
id := source.add(
checker.diagnostics,
statement.span,
"locals cannot have type void",
)
value = invalid_hir_expr(checker, statement.span, id)
value_type = types.INVALID
}
if _, found := find_build_local(locals[:], statement.name); found {
id := source.addf(
checker.diagnostics,
statement.span,
"duplicate local '%s'",
symbol_text(checker, statement.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Trap,
span = statement.span,
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = id,
},
)
problematic = true
continue
}
local_id := hir.local_id(len(hir_locals))
append(
&hir_locals,
hir.Local {
name = statement.name,
type = value_type,
mutable = !statement.immutable,
},
)
append(
&locals,
Build_Local {
name = statement.name,
type = value_type,
mutable = !statement.immutable,
id = local_id,
},
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Declaration,
span = statement.span,
local = local_id,
expr = value,
diagnostic = source.INVALID_DIAGNOSTIC,
},
)
problematic = problematic || checker.module.exprs[value].kind == .Invalid
case .Assignment:
if statement.target != ast.INVALID_EXPR {
target_expr := build_expr(
checker, statement.target, locals[:], &global_reads, &calls,
types.INVALID, function.pkg, function.file,
)
target_type := checker.module.exprs[target_expr].type
if !hir_location_writable(checker, target_expr, locals[:]) {
id := source.add(checker.diagnostics, statement.span, "assignment target is not writable")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL,
target=hir.INVALID_EXPR, expr=hir.INVALID_EXPR, diagnostic=id,
})
problematic = true
continue
}
value := build_expr(
checker, statement.expr, locals[:], &global_reads, &calls,
target_type, function.pkg, function.file,
)
value = coerce_expr(checker, value, target_type, statement.span)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL,
target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC,
})
problematic = problematic || checker.module.exprs[value].kind == .Invalid
continue
}
if statement.name == checker.sink_symbol {
value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls, types.INVALID, function.pkg, function.file)
if types.is_void(checker.module.exprs[value].type) {
id := source.add(
checker.diagnostics,
statement.span,
"cannot assign a void expression to '_'",
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Trap,
span = statement.span,
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = id,
},
)
problematic = true
} else {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Sink,
span = statement.span,
expr = value,
local = hir.INVALID_LOCAL,
diagnostic = source.INVALID_DIAGNOSTIC,
},
)
}
continue
}
local, found := find_build_local(locals[:], statement.name)
if !found {
id := source.addf(
checker.diagnostics,
statement.span,
"cannot assign unresolved local '%s'",
symbol_text(checker, statement.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Trap,
span = statement.span,
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = id,
},
)
problematic = true
continue
}
if !local.mutable {
id := source.addf(
checker.diagnostics,
statement.span,
"cannot assign immutable local '%s'",
symbol_text(checker, statement.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Trap,
span = statement.span,
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = id,
},
)
problematic = true
continue
}
value := build_expr(
checker,
statement.expr,
locals[:],
&global_reads,
&calls,
local.type,
function.pkg,
function.file,
)
value = coerce_expr(checker, value, local.type, statement.span)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Assignment,
span = statement.span,
expr = value,
local = local.id,
target = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
},
)
problematic = problematic || checker.module.exprs[value].kind == .Invalid
case .Return:
has_return = true
if statement.expr == ast.INVALID_EXPR {
if !types.is_void(spec.result) {
id := source.add(
checker.diagnostics,
statement.span,
"'return _' is only valid in a void function",
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Trap,
span = statement.span,
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = id,
},
)
problematic = true
} else {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Return,
span = statement.span,
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = source.INVALID_DIAGNOSTIC,
},
)
}
continue
}
if types.is_void(spec.result) {
id := source.add(
checker.diagnostics,
statement.span,
"void function cannot return a value",
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Trap,
span = statement.span,
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = id,
},
)
problematic = true
continue
}
value := build_expr(
checker,
statement.expr,
locals[:],
&global_reads,
&calls,
spec.result,
function.pkg,
function.file,
)
value = coerce_expr(checker, value, spec.result, statement.span)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Return,
span = statement.span,
expr = value,
local = hir.INVALID_LOCAL,
diagnostic = source.INVALID_DIAGNOSTIC,
},
)
problematic = problematic || checker.module.exprs[value].kind == .Invalid
case .Expression:
value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls, types.INVALID, function.pkg, function.file)
if !types.is_void(checker.module.exprs[value].type) {
id := source.add(
checker.diagnostics,
statement.span,
"non-void expression result must be consumed or assigned to '_'",
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Trap,
span = statement.span,
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = id,
},
)
problematic = true
} else {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Expression,
span = statement.span,
expr = value,
local = hir.INVALID_LOCAL,
diagnostic = source.INVALID_DIAGNOSTIC,
},
)
}
case .Invalid:
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Trap,
span = statement.span,
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = statement.diagnostic,
},
)
problematic = true
ctx := Build_Ctx{
checker = checker,
pkg = function.pkg,
file = function.file,
result = spec.result,
locals = &locals,
hir_locals = &hir_locals,
global_reads = &global_reads,
calls = &calls,
problematic = &problematic,
has_return = &has_return,
}
block := build_block(&ctx, function.body)
for block_stmt in block {
append(&body, block_stmt)
}
delete(block, checker.allocator)
if !types.is_void(spec.result) && !has_return {
id := source.addf(
+19
View File
@@ -77,6 +77,7 @@ Expr_Kind :: enum u8 {
Integer,
Float,
String,
Bool,
Array,
Struct,
None,
@@ -99,8 +100,17 @@ Expr_Kind :: enum u8 {
Weaken_Slice,
Decay_Array_Pointer,
Negate,
Not,
Add,
Pointer_Add,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
And,
Or,
Call,
}
@@ -130,6 +140,7 @@ Stmt_Kind :: enum u8 {
Expression,
Sink,
Trap,
If,
}
Stmt :: struct {
@@ -138,6 +149,10 @@ Stmt :: struct {
local: Local_Id,
target: Expr_Id,
expr: Expr_Id,
// `If` statements use `expr` as the condition and `then_body`/`else_body` as
// the branch statement lists.
then_body: []Stmt_Id,
else_body: []Stmt_Id,
diagnostic: source.Diagnostic_Id,
}
@@ -204,6 +219,10 @@ destroy_module :: proc(module: ^Module) {
for expr in module.exprs {
delete(expr.args, module.allocator)
}
for statement in module.statements {
delete(statement.then_body, module.allocator)
delete(statement.else_body, module.allocator)
}
for function in module.functions {
delete(function.link_name, module.allocator)
delete(function.params, module.allocator)
+15
View File
@@ -95,12 +95,27 @@ Opcode :: enum u8 {
Neg_Checked,
Add_Checked,
Pointer_Add,
Not,
Compare,
Label,
Br,
Cond_Br,
Call,
Trap,
Return,
Return_Void,
}
// Compare_Predicate is stored in Instruction.integer for the Compare opcode.
Compare_Predicate :: enum u8 {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
Instruction :: struct {
span: source.Span,
type: types.Type,
+41 -1
View File
@@ -23,7 +23,14 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "mut": return .Keyword_Mut
case "none": return .Keyword_None
case "orelse": return .Keyword_Orelse
case "and": return .Keyword_And
case "or": return .Keyword_Or
case "if": return .Keyword_If
case "else": return .Keyword_Else
case "true": return .Keyword_True
case "false": return .Keyword_False
case "void": return .Keyword_Void
case "bool": return .Keyword_Bool
case "int": return .Keyword_Int
case "i8": return .Keyword_I8
case "i16": return .Keyword_I16
@@ -106,8 +113,41 @@ lex :: proc(
append_token(&stream, source_file, .Invalid, start, cursor, diagnostic=id)
}
case '=':
append_token(&stream, source_file, .Equal, cursor, cursor+1)
start := cursor
cursor += 1
if cursor < len(bytes) && bytes[cursor] == '=' {
cursor += 1
append_token(&stream, source_file, .Equal_Equal, start, cursor)
} else {
append_token(&stream, source_file, .Equal, start, cursor)
}
case '!':
start := cursor
cursor += 1
if cursor < len(bytes) && bytes[cursor] == '=' {
cursor += 1
append_token(&stream, source_file, .Bang_Equal, start, cursor)
} else {
append_token(&stream, source_file, .Bang, start, cursor)
}
case '<':
start := cursor
cursor += 1
if cursor < len(bytes) && bytes[cursor] == '=' {
cursor += 1
append_token(&stream, source_file, .Less_Equal, start, cursor)
} else {
append_token(&stream, source_file, .Less, start, cursor)
}
case '>':
start := cursor
cursor += 1
if cursor < len(bytes) && bytes[cursor] == '=' {
cursor += 1
append_token(&stream, source_file, .Greater_Equal, start, cursor)
} else {
append_token(&stream, source_file, .Greater, start, cursor)
}
case '+':
append_token(&stream, source_file, .Plus, cursor, cursor+1)
cursor += 1
+74 -2
View File
@@ -138,6 +138,9 @@ llvm_type :: proc(value: types.Type, store: ^types.Store = nil) -> string {
if types.is_void(value) {
return "void"
}
if types.is_bool(value) {
return "i1"
}
#partial switch types.kind(value, store) {
case .Pointer:
return "ptr"
@@ -201,6 +204,9 @@ emit_function_result :: proc(builder: ^strings.Builder, function: ir.Function, s
}
sentinel :: proc(value_type: types.Type, selected := target.DEFAULT) -> i64 {
if types.is_bool(value_type) {
return 0
}
switch types.bits(value_type, selected) {
case 8: return -86
case 16: return -21846
@@ -228,10 +234,10 @@ valid_value :: proc(
case .Param, .Const, .String, .Aggregate, .None, .Optional_Some,
.Load_Global, .Function_Address, .Address_Of, .Load, .Slice, .Length, .Slice_Ptr, .Unwrap, .Orelse,
.Widen, .C_Vararg_Promote, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer,
.Neg_Checked, .Add_Checked, .Pointer_Add, .Call:
.Neg_Checked, .Add_Checked, .Pointer_Add, .Not, .Compare, .Call:
return true
case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin,
.Store, .Trap, .Return, .Return_Void:
.Store, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void:
return false
}
return false
@@ -262,6 +268,10 @@ write_constant :: proc(builder: ^strings.Builder, value: i64, value_type: types.
strings.write_string(builder, "zeroinitializer")
return
}
if types.is_bool(value_type) {
strings.write_string(builder, "true" if value != 0 else "false")
return
}
selected := store.selected if store != nil else target.DEFAULT
if types.is_float(value_type, selected) {
text := ""
@@ -462,6 +472,30 @@ emit_unpack_c_record :: proc(
fmt.sbprintf(&emitter.builder, " %s = load %s, ptr %%abi_unpack_slot%d\n", result_name, llvm_type(value_type, &emitter.module.types), tag)
}
integer_predicate :: proc(predicate: ir.Compare_Predicate, signed: bool) -> string {
switch predicate {
case .Eq: return "eq"
case .Ne: return "ne"
case .Lt: return "slt" if signed else "ult"
case .Le: return "sle" if signed else "ule"
case .Gt: return "sgt" if signed else "ugt"
case .Ge: return "sge" if signed else "uge"
}
return "eq"
}
float_predicate :: proc(predicate: ir.Compare_Predicate) -> string {
switch predicate {
case .Eq: return "oeq"
case .Ne: return "une"
case .Lt: return "olt"
case .Le: return "ole"
case .Gt: return "ogt"
case .Ge: return "oge"
}
return "oeq"
}
emit_instruction_stream :: proc(
emitter: ^Emitter,
instructions: []ir.Instruction,
@@ -1335,6 +1369,44 @@ emit_instruction_stream :: proc(
100000+instruction_index,
)
}
case .Not:
if !valid_value(instructions, instruction.a, types.BOOL, &emitter.module.types) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid '!' operand")
continue
}
fmt.sbprintf(&emitter.builder, " %%v%d = xor i1 ", instruction_index)
write_operand(&emitter.builder, instructions, instruction.a, types.BOOL, &emitter.module.types)
strings.write_string(&emitter.builder, ", true\n")
case .Compare:
operand_type := instructions[instruction.a].type if valid_instruction(instructions, instruction.a) else types.INVALID
if !valid_value(instructions, instruction.a, operand_type, &emitter.module.types) ||
!valid_value(instructions, instruction.b, operand_type, &emitter.module.types) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid comparison operand")
continue
}
predicate := ir.Compare_Predicate(instruction.integer)
type_name := llvm_type(operand_type, &emitter.module.types)
if types.is_float(operand_type, emitter.module.target) {
fmt.sbprintf(&emitter.builder, " %%v%d = fcmp %s %s ", instruction_index, float_predicate(predicate), type_name)
} else {
fmt.sbprintf(&emitter.builder, " %%v%d = icmp %s %s ", instruction_index, integer_predicate(predicate, types.is_signed(operand_type, emitter.module.target)), type_name)
}
write_operand(&emitter.builder, instructions, instruction.a, operand_type, &emitter.module.types)
strings.write_string(&emitter.builder, ", ")
write_operand(&emitter.builder, instructions, instruction.b, operand_type, &emitter.module.types)
strings.write_string(&emitter.builder, "\n")
case .Label:
fmt.sbprintf(&emitter.builder, "bro_block_%d:\n", instruction.integer)
case .Br:
fmt.sbprintf(&emitter.builder, " br label %%bro_block_%d\n", instruction.integer)
case .Cond_Br:
if !valid_value(instructions, instruction.a, types.BOOL, &emitter.module.types) {
fmt.sbprintf(&emitter.builder, " br label %%bro_block_%d\n", u32(instruction.target))
continue
}
strings.write_string(&emitter.builder, " br i1 ")
write_operand(&emitter.builder, instructions, instruction.a, types.BOOL, &emitter.module.types)
fmt.sbprintf(&emitter.builder, ", label %%bro_block_%d, label %%bro_block_%d\n", instruction.integer, u32(instruction.target))
case .Trap:
message := diagnostic_message(emitter, instruction.diagnostic, instruction.span, "invalid recovered source")
emit_trap_call(emitter, message)
+229 -100
View File
@@ -13,10 +13,19 @@ State :: struct {
instructions: [dynamic]ir.Instruction,
local_values: []ir.Instruction_Id,
local_slots: []ir.Instruction_Id,
func_locals: []hir.Local,
func_result: types.Type,
expr_stack: [dynamic]Lower_Expr_Frame,
next_label: i64,
allocator: mem.Allocator,
}
fresh_label :: proc(state: ^State) -> i64 {
id := state.next_label
state.next_label += 1
return id
}
append_instruction :: proc(state: ^State, instruction: ir.Instruction) -> ir.Instruction_Id {
id := ir.instruction_id(len(state.instructions))
append(&state.instructions, instruction)
@@ -265,6 +274,77 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
target=ir.INVALID_REF, a=begin, b=fallback,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Not:
value := lower_nested_expr(state, expr.left)
return append_instruction(state, ir.Instruction{
op=.Not, span=expr.span, type=types.BOOL,
target=ir.INVALID_REF, a=value, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Eq, .Ne, .Lt, .Le, .Gt, .Ge:
left := lower_nested_expr(state, expr.left)
right := lower_nested_expr(state, expr.right)
predicate := ir.Compare_Predicate.Eq
#partial switch expr.kind {
case .Eq: predicate = .Eq
case .Ne: predicate = .Ne
case .Lt: predicate = .Lt
case .Le: predicate = .Le
case .Gt: predicate = .Gt
case .Ge: predicate = .Ge
}
return append_instruction(state, ir.Instruction{
op=.Compare, span=expr.span, type=types.BOOL, integer=i64(predicate),
target=ir.INVALID_REF, a=left, b=right,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .And, .Or:
// Short-circuit via a bool slot: store the left operand, branch on it, and
// only evaluate/store the right operand when needed. Avoids phi nodes.
slot := append_instruction(state, ir.Instruction{
op=.Alloca, span=expr.span, type=types.BOOL,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
left := lower_nested_expr(state, expr.left)
append_instruction(state, ir.Instruction{
op=.Store, span=expr.span, type=types.BOOL,
target=ir.INVALID_REF, a=slot, b=left, diagnostic=source.INVALID_DIAGNOSTIC,
})
rhs_lbl := fresh_label(state)
done_lbl := fresh_label(state)
true_target := rhs_lbl if expr.kind == .And else done_lbl
false_target := done_lbl if expr.kind == .And else rhs_lbl
append_instruction(state, ir.Instruction{
op=.Cond_Br, span=expr.span, type=types.VOID,
a=left, integer=true_target, target=ir.Ref(u32(false_target)),
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Label, span=expr.span, type=types.VOID, integer=rhs_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
right := lower_nested_expr(state, expr.right)
append_instruction(state, ir.Instruction{
op=.Store, span=expr.span, type=types.BOOL,
target=ir.INVALID_REF, a=slot, b=right, diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Br, span=expr.span, type=types.VOID, integer=done_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Label, span=expr.span, type=types.VOID, integer=done_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
return append_instruction(state, ir.Instruction{
op=.Load, span=expr.span, type=types.BOOL,
target=ir.INVALID_REF, a=slot, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
return append_recovery_value(state, expr.span, expr.type, expr.diagnostic)
}
@@ -295,14 +375,15 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
case .Invalid:
last = append_recovery_value(state, expr.span, expr.type, expr.diagnostic)
_ = pop(&stack)
case .Integer, .Float:
case .Integer, .Float, .Bool:
last = append_instruction(state, ir.Instruction{
op=.Const, span=expr.span, type=expr.type, integer=expr.integer,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
})
_ = pop(&stack)
case .String, .Array, .Struct, .None, .Optional_Some, .Address, .Deref,
.Index, .Slice, .Field, .Length, .Slice_Ptr, .Unwrap, .Orelse:
.Index, .Slice, .Field, .Length, .Slice_Ptr, .Unwrap, .Orelse,
.Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
last = lower_compound_expr(state, frame.expr)
_ = pop(&stack)
case .Local:
@@ -453,10 +534,155 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
return last
}
lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
hir_module := state.hir_module
for statement_id in statements {
statement := hir_module.statements[statement_id]
switch statement.kind {
case .Declaration:
value := lower_expr(state, statement.expr)
if statement.local == hir.INVALID_LOCAL || int(statement.local) >= len(state.func_locals) {
append_instruction(state, ir.Instruction{
op=.Trap, span=statement.span, type=types.VOID,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=statement.diagnostic,
})
continue
}
local := state.func_locals[statement.local]
slot := append_instruction(state, ir.Instruction{
op=.Alloca,
span=statement.span,
type=local.type,
target=ir.local_ref(ir.Local_Id(statement.local)),
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
state.local_slots[statement.local] = slot
append_instruction(state, ir.Instruction{
op=.Store,
span=statement.span,
type=local.type,
target=ir.INVALID_REF,
a=slot,
b=value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Assignment:
value := lower_expr(state, statement.expr)
slot := ir.INVALID_INSTRUCTION
value_type := types.INVALID
if statement.target != hir.INVALID_EXPR {
slot = lower_location(state, statement.target, true)
if int(statement.target) < len(hir_module.exprs) {
value_type = hir_module.exprs[statement.target].type
}
} else if statement.local != hir.INVALID_LOCAL && int(statement.local) < len(state.local_slots) {
slot = state.local_slots[statement.local]
value_type = state.func_locals[statement.local].type
}
if slot == ir.INVALID_INSTRUCTION || !types.is_valid(value_type) {
append_instruction(state, ir.Instruction{
op=.Trap, span=statement.span, type=types.VOID,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=statement.diagnostic,
})
continue
}
append_instruction(state, ir.Instruction{
op=.Store,
span=statement.span,
type=value_type,
target=ir.INVALID_REF,
a=slot,
b=value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Return:
if statement.expr == hir.INVALID_EXPR {
append_instruction(state, ir.Instruction{
op=.Return_Void,
span=statement.span,
type=types.VOID,
target=ir.INVALID_REF,
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
value := lower_expr(state, statement.expr)
append_instruction(state, ir.Instruction{
op=.Return,
span=statement.span,
type=state.func_result,
target=ir.INVALID_REF,
a=value,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
case .Expression, .Sink:
_ = lower_expr(state, statement.expr)
case .Trap:
append_instruction(state, ir.Instruction{
op=.Trap,
span=statement.span,
type=types.VOID,
target=ir.INVALID_REF,
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=statement.diagnostic,
})
case .If:
cond := lower_expr(state, statement.expr)
has_else := statement.else_body != nil
then_lbl := fresh_label(state)
else_lbl := fresh_label(state) if has_else else then_lbl
merge_lbl := fresh_label(state)
false_target := else_lbl if has_else else merge_lbl
append_instruction(state, ir.Instruction{
op=.Cond_Br, span=statement.span, type=types.VOID,
a=cond, integer=then_lbl, target=ir.Ref(u32(false_target)),
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Label, span=statement.span, type=types.VOID, integer=then_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
lower_statements(state, statement.then_body)
append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=merge_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
if has_else {
append_instruction(state, ir.Instruction{
op=.Label, span=statement.span, type=types.VOID, integer=else_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
lower_statements(state, statement.else_body)
append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=merge_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
append_instruction(state, ir.Instruction{
op=.Label, span=statement.span, type=types.VOID, integer=merge_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
}
lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: mem.Allocator) -> []ir.Instruction {
state := State{
hir_module=hir_module,
allocator=allocator,
func_locals=function.locals,
func_result=function.result,
local_values=make([]ir.Instruction_Id, len(function.locals), allocator),
local_slots=make([]ir.Instruction_Id, len(function.locals), allocator),
}
@@ -497,104 +723,7 @@ lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: m
state.local_slots[local_id] = slot
}
for statement_id in function.body {
statement := hir_module.statements[statement_id]
switch statement.kind {
case .Declaration:
value := lower_expr(&state, statement.expr)
if statement.local == hir.INVALID_LOCAL || int(statement.local) >= len(function.locals) {
append_instruction(&state, ir.Instruction{
op=.Trap, span=statement.span, type=types.VOID,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=statement.diagnostic,
})
continue
}
local := function.locals[statement.local]
slot := append_instruction(&state, ir.Instruction{
op=.Alloca,
span=statement.span,
type=local.type,
target=ir.local_ref(ir.Local_Id(statement.local)),
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
state.local_slots[statement.local] = slot
append_instruction(&state, ir.Instruction{
op=.Store,
span=statement.span,
type=local.type,
target=ir.INVALID_REF,
a=slot,
b=value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Assignment:
value := lower_expr(&state, statement.expr)
slot := ir.INVALID_INSTRUCTION
value_type := types.INVALID
if statement.target != hir.INVALID_EXPR {
slot = lower_location(&state, statement.target, true)
if int(statement.target) < len(hir_module.exprs) {
value_type = hir_module.exprs[statement.target].type
}
} else if statement.local != hir.INVALID_LOCAL && int(statement.local) < len(state.local_slots) {
slot = state.local_slots[statement.local]
value_type = function.locals[statement.local].type
}
if slot == ir.INVALID_INSTRUCTION || !types.is_valid(value_type) {
append_instruction(&state, ir.Instruction{
op=.Trap, span=statement.span, type=types.VOID,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=statement.diagnostic,
})
continue
}
append_instruction(&state, ir.Instruction{
op=.Store,
span=statement.span,
type=value_type,
target=ir.INVALID_REF,
a=slot,
b=value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Return:
if statement.expr == hir.INVALID_EXPR {
append_instruction(&state, ir.Instruction{
op=.Return_Void,
span=statement.span,
type=types.VOID,
target=ir.INVALID_REF,
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
value := lower_expr(&state, statement.expr)
append_instruction(&state, ir.Instruction{
op=.Return,
span=statement.span,
type=function.result,
target=ir.INVALID_REF,
a=value,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
case .Expression, .Sink:
_ = lower_expr(&state, statement.expr)
case .Trap:
append_instruction(&state, ir.Instruction{
op=.Trap,
span=statement.span,
type=types.VOID,
target=ir.INVALID_REF,
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=statement.diagnostic,
})
}
}
lower_statements(&state, function.body)
if len(state.instructions) == 0 ||
(state.instructions[len(state.instructions)-1].op != .Return &&
state.instructions[len(state.instructions)-1].op != .Return_Void) {
+124 -30
View File
@@ -20,6 +20,11 @@ Parser :: struct {
file: ast.File_Id,
cursor: int,
delimiter_depth: int,
// Suppresses `Name { ... }` struct-literal parsing at delimiter depth 0 so a
// control-flow condition like `if foo { ... }` does not swallow the block as a
// struct literal. Nested `(`/`[`/call-arg contexts (delimiter_depth > 0) still
// allow struct literals.
no_struct_literal: bool,
}
MAX_EXPRESSION_NESTING :: 256
@@ -90,7 +95,7 @@ is_type_token :: proc(kind: token.Kind) -> bool {
.Keyword_C_Short, .Keyword_C_Ushort, .Keyword_C_Int, .Keyword_C_Uint,
.Keyword_C_Long, .Keyword_C_Ulong, .Keyword_C_Longlong, .Keyword_C_Ulonglong,
.Keyword_C_Float, .Keyword_C_Double, .Keyword_C_Longdouble,
.Keyword_Void, .Keyword_C_Func, .Identifier, .Question, .At, .Star, .Left_Bracket:
.Keyword_Void, .Keyword_Bool, .Keyword_C_Func, .Identifier, .Question, .At, .Star, .Left_Bracket:
return true
}
return false
@@ -291,6 +296,9 @@ parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax {
case .Keyword_Void:
advance(parser)
return types.VOID
case .Keyword_Bool:
advance(parser)
return types.BOOL
case .Keyword_C_Func:
advance(parser)
if _, ok := allow(parser, .Left_Paren); !ok {
@@ -567,6 +575,16 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Keyword_True, .Keyword_False:
advance(parser)
return add_expr(parser, ast.Expr{
kind=.Bool,
span=tok.span,
integer=1 if tok.kind == .Keyword_True else 0,
left=ast.INVALID_EXPR,
right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Left_Bracket:
return parse_array_literal(parser, nesting)
case .Identifier:
@@ -583,7 +601,7 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
if current(parser).kind == .Left_Paren {
return parse_call(parser, qualifier, first, name, nesting)
}
if current(parser).kind == .Left_Brace {
if current(parser).kind == .Left_Brace && !(parser.no_struct_literal && parser.delimiter_depth == 0) {
return parse_struct_literal(parser, qualifier, first, name, nesting)
}
return add_expr(parser, ast.Expr{
@@ -633,15 +651,36 @@ infix_binding_power :: proc(kind: token.Kind) -> (left, right: int, ok: bool) {
#partial switch kind {
case .Keyword_Orelse:
return 2, 3, true
case .Keyword_Or:
return 4, 5, true
case .Keyword_And:
return 6, 7, true
case .Equal_Equal, .Bang_Equal, .Less, .Less_Equal, .Greater, .Greater_Equal:
return 8, 9, true
case .Plus:
return 10, 11, true
}
return 0, 0, false
}
infix_expr_kind :: proc(kind: token.Kind) -> ast.Expr_Kind {
#partial switch kind {
case .Keyword_Orelse: return .Orelse
case .Keyword_Or: return .Or
case .Keyword_And: return .And
case .Equal_Equal: return .Eq
case .Bang_Equal: return .Ne
case .Less: return .Lt
case .Less_Equal: return .Le
case .Greater: return .Gt
case .Greater_Equal: return .Ge
case: return .Add
}
}
prefix_binding_power :: proc(kind: token.Kind) -> (right: int, ok: bool) {
#partial switch kind {
case .Minus, .Ampersand:
case .Minus, .Ampersand, .Bang:
return 20, true
}
return 0, false
@@ -663,8 +702,13 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
}
operand := parse_expression_bp(parser, right_power, nesting+1)
operand_expr := parser.module.exprs[operand]
prefix_kind := ast.Expr_Kind.Negate
#partial switch operator.kind {
case .Ampersand: prefix_kind = .Address
case .Bang: prefix_kind = .Not
}
left = add_expr(parser, ast.Expr{
kind=.Address if operator.kind == .Ampersand else .Negate,
kind=prefix_kind,
span=span_from(operator.span, operand_expr.span),
left=operand,
right=ast.INVALID_EXPR,
@@ -787,7 +831,7 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
left_expr := parser.module.exprs[left]
right_expr := parser.module.exprs[right]
left = add_expr(parser, ast.Expr{
kind=.Orelse if operator.kind == .Keyword_Orelse else .Add,
kind=infix_expr_kind(operator.kind),
span=span_from(left_expr.span, right_expr.span),
left=left,
right=right,
@@ -881,6 +925,9 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
if current(parser).kind == .Keyword_Return {
return parse_return(parser)
}
if current(parser).kind == .Keyword_If {
return parse_if(parser)
}
if current(parser).kind == .Identifier || current(parser).kind == .Underscore {
start_cursor := parser.cursor
@@ -995,8 +1042,76 @@ parse_params :: proc(parser: ^Parser) -> ([]ast.Param, bool) {
return params[:], variadic
}
// parse_block parses a brace-delimited statement sequence `{ ... }`, consuming
// both braces, and returns the contained statement ids. Shared by function
// bodies and control-flow blocks.
parse_block :: proc(parser: ^Parser) -> []ast.Stmt_Id {
body: [dynamic]ast.Stmt_Id
body.allocator = parser.module.allocator
if _, ok := allow(parser, .Left_Brace); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '{' to open a block")
return body[:]
}
skip_newlines(parser)
for current(parser).kind != .Right_Brace && current(parser).kind != .Eof {
append(&body, parse_statement(parser))
if diagnostic := finish_statement(parser, true); diagnostic != source.INVALID_DIAGNOSTIC {
statement_id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.Invalid,
span=current(parser).span,
expr=ast.INVALID_EXPR,
diagnostic=diagnostic,
})
append(&body, statement_id)
}
}
if _, ok := allow(parser, .Right_Brace); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '}' to close a block")
}
return body[:]
}
parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
start := advance(parser) // consume 'if'
skip_newlines(parser)
saved := parser.no_struct_literal
parser.no_struct_literal = true
condition := parse_expression(parser)
parser.no_struct_literal = saved
skip_newlines(parser)
then_body := parse_block(parser)
else_body: []ast.Stmt_Id = nil
saved_cursor := parser.cursor
skip_newlines(parser)
if current(parser).kind == .Keyword_Else {
advance(parser)
skip_newlines(parser)
if current(parser).kind == .Keyword_If {
nested := parse_if(parser)
single := make([]ast.Stmt_Id, 1, parser.module.allocator)
single[0] = nested
else_body = single
} else {
else_body = parse_block(parser)
}
} else {
parser.cursor = saved_cursor
}
id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.If,
span=span_from(start.span, previous(parser).span),
expr=condition,
body=then_body,
else_body=else_body,
diagnostic=source.INVALID_DIAGNOSTIC,
})
return id
}
parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
func_token := advance(parser)
advance(parser)
if _, ok := allow(parser, .Left_Paren); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '(' after 'func'")
}
@@ -1030,29 +1145,8 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
})
return
}
advance(parser)
body: [dynamic]ast.Stmt_Id
body.allocator = parser.module.allocator
skip_newlines(parser)
for current(parser).kind != .Right_Brace && current(parser).kind != .Eof {
append(&body, parse_statement(parser))
if diagnostic := finish_statement(parser, true); diagnostic != source.INVALID_DIAGNOSTIC {
statement_id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.Invalid,
span=current(parser).span,
expr=ast.INVALID_EXPR,
diagnostic=diagnostic,
})
append(&body, statement_id)
}
}
end = current(parser)
if _, ok := allow(parser, .Right_Brace); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '}' after function body")
end = func_token
}
body := parse_block(parser)
end = previous(parser)
_ = ast.function_id(len(parser.module.functions))
append(&parser.module.functions, ast.Function{
span=span_from(name.span, end.span),
@@ -1064,7 +1158,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
variadic=variadic,
params=params,
result=result,
body=body[:],
body=body,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
+14
View File
@@ -15,6 +15,13 @@ Kind :: enum u8 {
Underscore,
Colon_Colon,
Equal,
Equal_Equal,
Bang,
Bang_Equal,
Less,
Less_Equal,
Greater,
Greater_Equal,
Plus,
Minus,
Dot,
@@ -42,7 +49,14 @@ Kind :: enum u8 {
Keyword_Mut,
Keyword_None,
Keyword_Orelse,
Keyword_And,
Keyword_Or,
Keyword_If,
Keyword_Else,
Keyword_True,
Keyword_False,
Keyword_Void,
Keyword_Bool,
Keyword_Int,
Keyword_I8,
Keyword_I16,
+11
View File
@@ -40,6 +40,8 @@ C_FLOAT :: Type(26)
C_DOUBLE :: Type(27)
C_LONGDOUBLE :: Type(28)
BOOL :: Type(29)
DYNAMIC_START :: Type(64)
Numeric_Category :: enum u8 {
@@ -236,6 +238,8 @@ kind :: proc(value: Type, store: ^Store = nil) -> Kind {
return .Void
case INT:
return .Int_Constraint
case BOOL:
return .Scalar
}
if value >= I8 && value <= C_LONGDOUBLE {
return .Scalar
@@ -268,6 +272,10 @@ is_void :: proc(value: Type) -> bool {
return value == VOID
}
is_bool :: proc(value: Type) -> bool {
return value == BOOL
}
is_constraint :: proc(value: Type) -> bool {
return value == INT
}
@@ -320,6 +328,8 @@ category :: proc(value: Type, selected := target.DEFAULT) -> Numeric_Category {
bits :: proc(value: Type, selected := target.DEFAULT) -> int {
switch value {
case BOOL:
return 1
case I8, U8:
return 8
case I16, U16:
@@ -945,6 +955,7 @@ name :: proc(value: Type) -> string {
switch value {
case INVALID: return "<invalid>"
case VOID: return "void"
case BOOL: return "bool"
case INT: return "int"
case I8: return "i8"
case I16: return "i16"
+21
View File
@@ -1793,6 +1793,27 @@ sentinel_pointer_views_compile_and_run :: proc(t: ^testing.T) {
testing.expect_value(t, state.exit_code, 303)
}
@(test)
control_flow_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-control-flow"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/control_flow", output)
testing.expect_value(t, status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
// if / else if / else, comparisons, logical and/or/not, bool locals, and
// block scoping together produce 42.
testing.expect_value(t, state.exit_code, 42)
// Short-circuit: `noisy()` is never reached, so its output must be absent,
// while the taken or-branch must print.
testing.expect(t, !strings.contains(string(stdout), "rhs-evaluated"))
testing.expect(t, strings.contains(string(stdout), "or-taken"))
}
@(test)
foreign_function_links_from_c_source :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-foreign-source"
+69
View File
@@ -0,0 +1,69 @@
# Milestone 5 foundation: booleans, comparisons, logical ops, if/else if/else.
printf :: c_func(format *c_char, ...) c_int
# Returns a distinct code per range using if / else if / else and comparisons.
classify :: func(n i32) i32 {
if n < 0 {
return 1
} else if n == 0 {
return 2
} else if n >= 100 {
return 3
} else {
return 4
}
}
# A bool-returning function with a visible side effect, used to prove
# short-circuit evaluation: it must only print when actually evaluated.
noisy :: func() bool {
_ = printf("rhs-evaluated\n")
return true
}
main :: func() i32 {
total i32 = 0
# comparisons drive if / else if / else
total = total + classify(-5) # 1
total = total + classify(0) # 2
total = total + classify(250) # 3
total = total + classify(42) # 4 -> 10
# bool variables and logical and / or / not
a :: true
b :: false
if a and !b {
total = total + 10 # 20
}
if b or a {
total = total + 10 # 30
}
if !(a and b) {
total = total + 5 # 35
}
# block scoping: inner x shadows outer x, outer is unchanged after the block
x i32 = 1
if x == 1 {
x i32 = 100
if x == 100 {
total = total + 5 # 40
}
}
if x == 1 {
total = total + 2 # 42
}
# short-circuit: `false and noisy()` must NOT call noisy()
if false and noisy() {
_ = printf("unreachable-and\n")
}
# short-circuit: `true or noisy()` must NOT call noisy()
if true or noisy() {
_ = printf("or-taken\n")
}
return total # expect exit code 42
}