undefined assignment

This commit is contained in:
2026-06-25 20:37:20 +02:00
parent e0b6f6049c
commit ca6cc89da0
10 changed files with 489 additions and 47 deletions
+17
View File
@@ -166,6 +166,23 @@
no hand-writable spelling and are emitted as `# unsupported in bindings:` comments no hand-writable spelling and are emitted as `# unsupported in bindings:` comments
(functions that reference an un-spellable union therefore keep a dangling reference) (functions that reference an un-spellable union therefore keep a dangling reference)
12. `undefined` as inspired by zig (implemented):
- allow mutable local declarations with `undefined`
- undefined values are assigned a poison value (0xaa...)
- allows for something like:
```
a int = undefined
if (condition) {
a = 42
} else {
a = -2
}
```
- disallow: `b :: undefined` since assigning undefined to something that can't change defeats the purpose
- disallow assigning `undefined` after declaration; use optionals and `none` for values that intentionally move back to an empty state
13. broaden type inference from surrounding context
## A word on multi-unwrap ## A word on multi-unwrap
Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated. Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated.
+1
View File
@@ -71,6 +71,7 @@ Expr_Kind :: enum u8 {
Bool, Bool,
Array, Array,
None, None,
Undefined,
Name, Name,
Enum_Literal, Enum_Literal,
Address, Address,
+226 -44
View File
@@ -33,8 +33,11 @@ Spec :: struct {
} }
Infer_Local :: struct { Infer_Local :: struct {
name: symbol.Id, name: symbol.Id,
type: types.Type, type: types.Type,
declared: types.Type,
statement: ast.Stmt_Id,
mutable: bool,
} }
Build_Local :: struct { Build_Local :: struct {
@@ -53,6 +56,7 @@ Build_Ctx :: struct {
pkg: ast.Package_Id, pkg: ast.Package_Id,
file: ast.File_Id, file: ast.File_Id,
result: types.Type, result: types.Type,
local_types: []types.Type,
locals: ^[dynamic]Build_Local, locals: ^[dynamic]Build_Local,
hir_locals: ^[dynamic]hir.Local, hir_locals: ^[dynamic]hir.Local,
global_reads: ^[dynamic]hir.Global_Id, global_reads: ^[dynamic]hir.Global_Id,
@@ -262,6 +266,13 @@ is_runtime_type :: proc(checker: ^Checker, value: types.Type) -> bool {
return types.is_runtime_value(value, &checker.module.types) return types.is_runtime_value(value, &checker.module.types)
} }
is_undefined_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
return checker.ast_module.exprs[expr_id].kind == .Undefined
}
string_literal_type :: proc(checker: ^Checker, string_id: u64) -> types.Type { string_literal_type :: proc(checker: ^Checker, string_id: u64) -> types.Type {
length: u64 length: u64
if string_id < u64(len(checker.ast_module.strings)) { if string_id < u64(len(checker.ast_module.strings)) {
@@ -636,7 +647,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as
append(&stack, expr.left) append(&stack, expr.left)
case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
append(&stack, expr.left, expr.right) append(&stack, expr.left, expr.right)
case .Invalid, .Integer, .Float, .String, .Bool, .None, .Name, .Enum_Literal: case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Name, .Enum_Literal:
} }
} }
} }
@@ -956,14 +967,21 @@ validate_type_nodes :: proc(checker: ^Checker) {
} }
find_infer_local :: proc(locals: []Infer_Local, name: symbol.Id) -> types.Type { find_infer_local :: proc(locals: []Infer_Local, name: symbol.Id) -> types.Type {
for index := len(locals) - 1; index >= 0; index -= 1 { if index, ok := find_infer_local_index(locals, name); ok {
if locals[index].name == name { return locals[index].type
return locals[index].type
}
} }
return types.INVALID return types.INVALID
} }
find_infer_local_index :: proc(locals: []Infer_Local, name: symbol.Id) -> (int, bool) {
for index := len(locals) - 1; index >= 0; index -= 1 {
if locals[index].name == name {
return index, true
}
}
return -1, false
}
find_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type) -> Spec_Id { find_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type) -> Spec_Id {
function := checker.ast_module.functions[template] function := checker.ast_module.functions[template]
for spec, index in checker.specs { for spec, index in checker.specs {
@@ -1123,6 +1141,8 @@ infer_compound_expr :: proc(
return types.array(store, element, u64(len(expr.args)), false) return types.array(store, element, u64(len(expr.args)), false)
case .None: case .None:
return types.INVALID return types.INVALID
case .Undefined:
return types.INVALID
case .Enum_Literal: case .Enum_Literal:
return types.INVALID return types.INVALID
case .Address: case .Address:
@@ -1247,7 +1267,7 @@ infer_expr :: proc(
case .Float: case .Float:
last = types.F64 last = types.F64
_ = pop(&stack) _ = pop(&stack)
case .String, .Array, .None, .Address, .Deref, .Index, .Slice, case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, .Enum_Literal, .Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, .Enum_Literal,
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded) last = infer_compound_expr(checker, expr, locals, pkg, file, demanded)
@@ -1505,34 +1525,127 @@ flatten_conditional_unwrap_operands :: proc(
append(operands, expr_id) append(operands, expr_id)
} }
record_infer_local_type :: proc(local: Infer_Local, local_types: []types.Type) {
if local.statement != ast.INVALID_STMT && int(local.statement) < len(local_types) {
local_types[local.statement] = local.type
}
}
merge_infer_local_type :: proc(
checker: ^Checker,
local: ^Infer_Local,
inferred: types.Type,
local_types: []types.Type,
) -> bool {
if !is_runtime_type(checker, inferred) {
return false
}
if types.is_constraint(local.declared) {
if !types.is_concrete_integer(inferred) {
return false
}
if !is_runtime_type(checker, local.type) {
local.type = inferred
record_infer_local_type(local^, local_types)
return true
}
if types.equal(local.type, inferred) {
return false
}
merged := types.widest(local.type, inferred)
if types.is_concrete_integer(merged) {
local.type = merged
record_infer_local_type(local^, local_types)
return true
}
return false
}
if is_runtime_type(checker, local.declared) {
local.type = local.declared
record_infer_local_type(local^, local_types)
return false
}
if !is_runtime_type(checker, local.type) {
local.type = inferred
record_infer_local_type(local^, local_types)
return true
}
if types.equal(local.type, inferred) {
return false
}
merged := types.widest(local.type, inferred)
if types.is_concrete_scalar(merged) {
local.type = merged
record_infer_local_type(local^, local_types)
return true
}
return false
}
infer_statements :: proc( infer_statements :: proc(
checker: ^Checker, checker: ^Checker,
statements: []ast.Stmt_Id, statements: []ast.Stmt_Id,
locals: ^[dynamic]Infer_Local, locals: ^[dynamic]Infer_Local,
local_types: []types.Type,
pkg: ast.Package_Id, pkg: ast.Package_Id,
file: ast.File_Id, file: ast.File_Id,
demanded: ^[dynamic]Spec_Id, demanded: ^[dynamic]Spec_Id,
result: ^types.Type, result: ^types.Type,
result_hint := types.INVALID,
) { ) {
scope_start := len(locals^) scope_start := len(locals^)
for statement_id in statements { for statement_id in statements {
statement := checker.ast_module.statements[statement_id] statement := checker.ast_module.statements[statement_id]
#partial switch statement.kind { #partial switch statement.kind {
case .Declaration: case .Declaration:
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) declared_local := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
declared_local := type_from_syntax(statement.type) value_type := types.INVALID
if !is_undefined_expr(checker, statement.expr) {
value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
}
if is_runtime_type(checker, declared_local) { if is_runtime_type(checker, declared_local) {
value_type = declared_local value_type = declared_local
} }
append(locals, Infer_Local{name = statement.name, type = value_type}) local := Infer_Local{
case .Assignment, .Expression: name=statement.name,
type=value_type,
declared=declared_local,
statement=statement_id,
mutable=!statement.immutable,
}
append(locals, local)
record_infer_local_type(local, local_types)
case .Assignment:
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
if statement.target != ast.INVALID_EXPR { if statement.target != ast.INVALID_EXPR {
_ = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded) _ = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded)
target_expr := checker.ast_module.exprs[statement.target]
if target_expr.kind == .Name && !symbol.is_valid(target_expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], target_expr.name); ok &&
locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
}
}
} else if statement.name != checker.sink_symbol {
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok &&
locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
}
} }
case .Expression:
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
case .Return: case .Return:
if statement.expr != ast.INVALID_EXPR { if statement.expr != ast.INVALID_EXPR {
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
if is_runtime_type(checker, result_hint) {
expr := checker.ast_module.exprs[statement.expr]
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok {
_ = merge_infer_local_type(checker, &locals^[local_index], result_hint, local_types)
returned = result_hint
}
}
}
if !types.is_valid(result^) { if !types.is_valid(result^) {
result^ = returned result^ = returned
} else if !types.equal(result^, returned) { } else if !types.equal(result^, returned) {
@@ -1558,27 +1671,27 @@ infer_statements :: proc(
types.is_optional(operand_types[index], &checker.module.types) { types.is_optional(operand_types[index], &checker.module.types) {
capture_type = types.child_type(operand_types[index], &checker.module.types) capture_type = types.child_type(operand_types[index], &checker.module.types)
} }
append(locals, Infer_Local{name=capture, type=capture_type}) append(locals, Infer_Local{name=capture, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT})
} }
if statement.guard != ast.INVALID_EXPR { if statement.guard != ast.INVALID_EXPR {
_ = infer_expr(checker, statement.guard, locals^[:], pkg, file, demanded) _ = infer_expr(checker, statement.guard, locals^[:], pkg, file, demanded)
} }
infer_statements(checker, statement.body, locals, pkg, file, demanded, result) infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start) resize(locals, capture_start)
infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result) infer_statements(checker, statement.else_body, locals, local_types, pkg, file, demanded, result, result_hint)
delete(operand_types, checker.allocator) delete(operand_types, checker.allocator)
delete(operands) delete(operands)
} else { } else {
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
infer_statements(checker, statement.body, locals, pkg, file, demanded, result) infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result) infer_statements(checker, statement.else_body, locals, local_types, pkg, file, demanded, result, result_hint)
} }
case .While: case .While:
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
infer_statements(checker, statement.body, locals, pkg, file, demanded, result) infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
if statement.update != ast.INVALID_STMT { if statement.update != ast.INVALID_STMT {
update := [1]ast.Stmt_Id{statement.update} update := [1]ast.Stmt_Id{statement.update}
infer_statements(checker, update[:], locals, pkg, file, demanded, result) infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint)
} }
case .For: case .For:
iterable_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) iterable_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
@@ -1596,43 +1709,55 @@ infer_statements :: proc(
} }
} }
if symbol.is_valid(statement.name) { if symbol.is_valid(statement.name) {
append(locals, Infer_Local{name=statement.name, type=capture_type}) append(locals, Infer_Local{name=statement.name, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT})
} }
if symbol.is_valid(statement.index_name) { if symbol.is_valid(statement.index_name) {
append(locals, Infer_Local{name=statement.index_name, type=types.USIZE}) append(locals, Infer_Local{name=statement.index_name, type=types.USIZE, declared=types.USIZE, statement=ast.INVALID_STMT})
} }
infer_statements(checker, statement.body, locals, pkg, file, demanded, result) infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start) resize(locals, capture_start)
} }
} }
resize(locals, scope_start) resize(locals, scope_start)
} }
infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type { infer_spec_locals_and_result :: proc(
checker: ^Checker,
id: Spec_Id,
demanded: ^[dynamic]Spec_Id = nil,
) -> ([]types.Type, types.Type) {
spec := checker.specs[id] spec := checker.specs[id]
function := checker.ast_module.functions[spec.template] function := checker.ast_module.functions[spec.template]
declared := type_from_syntax(function.result) declared := type_from_syntax(function.result)
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT { if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
declared = types.I32 declared = types.I32
} }
result_hint := declared if is_runtime_type(checker, declared) else types.INVALID
locals: [dynamic]Infer_Local locals: [dynamic]Infer_Local
locals.allocator = checker.allocator locals.allocator = checker.allocator
defer delete(locals) defer delete(locals)
local_types := make([]types.Type, len(checker.ast_module.statements), checker.allocator)
for param, index in function.params { for param, index in function.params {
param_type := types.INVALID param_type := types.INVALID
if index < len(spec.args) { if index < len(spec.args) {
param_type = spec.args[index] param_type = spec.args[index]
} }
append(&locals, Infer_Local{name = param.name, type = param_type}) append(&locals, Infer_Local{name=param.name, type=param_type, declared=param_type, statement=ast.INVALID_STMT})
} }
result := types.INVALID result := types.INVALID
infer_statements(checker, function.body, &locals, function.pkg, function.file, demanded, &result) infer_statements(checker, function.body, &locals, local_types, function.pkg, function.file, demanded, &result, result_hint)
if types.is_constraint(declared) { if types.is_constraint(declared) {
return result return local_types, result
} }
return declared return local_types, declared
}
infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type {
local_types, result := infer_spec_locals_and_result(checker, id, demanded)
delete(local_types, checker.allocator)
return result
} }
merge_inferred_type :: proc(store: ^types.Store, current: ^types.Type, inferred: types.Type) -> bool { merge_inferred_type :: proc(store: ^types.Store, current: ^types.Type, inferred: types.Type) -> bool {
@@ -2296,7 +2421,7 @@ build_compound_expr :: proc(
infer_locals := make([]Infer_Local, len(locals), checker.allocator) infer_locals := make([]Infer_Local, len(locals), checker.allocator)
defer delete(infer_locals, checker.allocator) defer delete(infer_locals, checker.allocator)
for local, index in locals { for local, index in locals {
infer_locals[index] = Infer_Local{name=local.name, type=local.type} infer_locals[index] = Infer_Local{name=local.name, type=local.type, declared=local.type, statement=ast.INVALID_STMT}
} }
for arg in expr.args { for arg in expr.args {
actual := infer_nested_expr(checker, arg, infer_locals, pkg, file, nil) actual := infer_nested_expr(checker, arg, infer_locals, pkg, file, nil)
@@ -2332,6 +2457,13 @@ build_compound_expr :: proc(
kind=.None, span=expr.span, type=expected, target=hir.INVALID_REF, kind=.None, span=expr.span, type=expected, target=hir.INVALID_REF,
left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
case .Undefined:
id := source.add(
checker.diagnostics,
expr.span,
"'undefined' is only valid as a mutable local declaration initializer",
)
return invalid_hir_expr(checker, expr.span, id, expected)
case .Enum_Literal: case .Enum_Literal:
if !types.is_enum(expected, store) { if !types.is_enum(expected, store) {
id := source.addf( id := source.addf(
@@ -2762,7 +2894,7 @@ build_expr :: proc(
continue continue
} }
switch expr.kind { switch expr.kind {
case .String, .Array, .None, .Address, .Deref, .Index, .Slice, case .String, .Array, .None, .Undefined, .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, .Range, .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range,
.Enum_Literal: .Enum_Literal:
@@ -3290,22 +3422,62 @@ build_block :: proc(
switch statement.kind { switch statement.kind {
case .Declaration: case .Declaration:
declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
expected := types.INVALID if statement_id != ast.INVALID_STMT && int(statement_id) < len(ctx.local_types) &&
if is_runtime_type(checker, declared) { is_runtime_type(checker, ctx.local_types[statement_id]) &&
expected = declared (types.is_constraint(declared) || is_undefined_expr(checker, statement.expr)) {
declared = ctx.local_types[statement_id]
} }
value := build_expr( expected := types.INVALID
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, value := hir.INVALID_EXPR
expected, ctx.pkg, ctx.file, value_type := types.INVALID
) if is_undefined_expr(checker, statement.expr) {
value_type := checker.module.exprs[value].type if statement.immutable {
if is_runtime_type(checker, declared) { id := source.add(
value = coerce_expr(checker, value, declared, statement.span) checker.diagnostics,
statement.span,
"'undefined' requires a mutable local declaration",
)
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 !is_runtime_type(checker, declared) {
id := source.addf(
checker.diagnostics,
statement.span,
"could not infer a concrete type for 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_type = declared
} else {
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 value_type = checker.module.exprs[value].type
} else if types.is_void(declared) { if is_runtime_type(checker, declared) {
id := source.add(checker.diagnostics, statement.span, "locals cannot have type void") value = coerce_expr(checker, value, declared, statement.span)
value = invalid_hir_expr(checker, statement.span, id) value_type = checker.module.exprs[value].type
value_type = types.INVALID } 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^[duplicate_start:], statement.name); found { if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found {
id := source.addf( id := source.addf(
@@ -3332,7 +3504,9 @@ build_block :: proc(
kind = .Declaration, span = statement.span, local = local_id, expr = value, kind = .Declaration, span = statement.span, local = local_id, expr = value,
diagnostic = source.INVALID_DIAGNOSTIC, diagnostic = source.INVALID_DIAGNOSTIC,
}) })
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid if value != hir.INVALID_EXPR {
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
}
case .Assignment: case .Assignment:
if statement.target != ast.INVALID_EXPR { if statement.target != ast.INVALID_EXPR {
target_expr := build_expr( target_expr := build_expr(
@@ -3888,6 +4062,13 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
global_reads.allocator = checker.allocator global_reads.allocator = checker.allocator
calls: [dynamic]hir.Function_Id calls: [dynamic]hir.Function_Id
calls.allocator = checker.allocator calls.allocator = checker.allocator
demanded: [dynamic]Spec_Id
demanded.allocator = checker.allocator
local_types, _ := infer_spec_locals_and_result(checker, id, &demanded)
defer {
delete(local_types, checker.allocator)
delete(demanded)
}
for param, index in function.params { for param, index in function.params {
local_id := hir.local_id(len(hir_locals)) local_id := hir.local_id(len(hir_locals))
@@ -3946,6 +4127,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
pkg = function.pkg, pkg = function.pkg,
file = function.file, file = function.file,
result = spec.result, result = spec.result,
local_types = local_types,
locals = &locals, locals = &locals,
hir_locals = &hir_locals, hir_locals = &hir_locals,
global_reads = &global_reads, global_reads = &global_reads,
+1
View File
@@ -81,6 +81,7 @@ Opcode :: enum u8 {
Field_Address, Field_Address,
Load, Load,
Store, Store,
Fill,
Slice, Slice,
Length, Length,
Slice_Ptr, Slice_Ptr,
+1
View File
@@ -25,6 +25,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "return": return .Keyword_Return case "return": return .Keyword_Return
case "mut": return .Keyword_Mut case "mut": return .Keyword_Mut
case "none": return .Keyword_None case "none": return .Keyword_None
case "undefined": return .Keyword_Undefined
case "orelse": return .Keyword_Orelse case "orelse": return .Keyword_Orelse
case "and": return .Keyword_And case "and": return .Keyword_And
case "or": return .Keyword_Or case "or": return .Keyword_Or
+13 -2
View File
@@ -260,7 +260,7 @@ valid_value :: proc(
.Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Pointer_Add, .Not, .Compare, .Call: .Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Pointer_Add, .Not, .Compare, .Call:
return true return true
case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin,
.Store, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void: .Store, .Fill, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void:
return false return false
} }
return false return false
@@ -964,6 +964,17 @@ emit_instruction_stream :: proc(
fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(instruction.type, &emitter.module.types)) fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(instruction.type, &emitter.module.types))
write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types)
fmt.sbprintf(&emitter.builder, ", ptr %%v%d\n", instruction.a) fmt.sbprintf(&emitter.builder, ", ptr %%v%d\n", instruction.a)
case .Fill:
if !valid_address(instructions, instruction.a, instruction.type, &emitter.module.types) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid fill slot")
continue
}
fmt.sbprintf(
&emitter.builder,
" call void @llvm.memset.p0.i64(ptr %%v%d, i8 -86, i64 %d, i1 false)\n",
instruction.a,
types.size(instruction.type, &emitter.module.types, emitter.module.target),
)
case .Slice: case .Slice:
if !valid_instruction(instructions, instruction.a) { if !valid_instruction(instructions, instruction.a) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid slice container") emit_recovery_value(emitter, instruction_index, instruction, "invalid slice container")
@@ -1990,7 +2001,7 @@ emit_messages :: proc(emitter: ^Emitter) {
} }
emit_declarations :: proc(emitter: ^Emitter) { emit_declarations :: proc(emitter: ^Emitter) {
strings.write_string(&emitter.builder, "declare i64 @write(i32, ptr, i64)\ndeclare void @llvm.trap()\ndeclare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)\n") strings.write_string(&emitter.builder, "declare i64 @write(i32, ptr, i64)\ndeclare void @llvm.trap()\ndeclare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)\ndeclare void @llvm.memset.p0.i64(ptr, i8, i64, i1 immarg)\n")
widths := [?]int{8, 16, 32, 64} widths := [?]int{8, 16, 32, 64}
overflow_intrinsics := [?]string{"sadd", "uadd", "ssub", "usub", "smul", "umul"} overflow_intrinsics := [?]string{"sadd", "uadd", "ssub", "usub", "smul", "umul"}
for bits in widths { for bits in widths {
+13 -1
View File
@@ -613,7 +613,6 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
statement := hir_module.statements[statement_id] statement := hir_module.statements[statement_id]
switch statement.kind { switch statement.kind {
case .Declaration: case .Declaration:
value := lower_expr(state, statement.expr)
if statement.local == hir.INVALID_LOCAL || int(statement.local) >= len(state.func_locals) { if statement.local == hir.INVALID_LOCAL || int(statement.local) >= len(state.func_locals) {
append_instruction(state, ir.Instruction{ append_instruction(state, ir.Instruction{
op=.Trap, span=statement.span, type=types.VOID, op=.Trap, span=statement.span, type=types.VOID,
@@ -632,6 +631,19 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
state.local_slots[statement.local] = slot state.local_slots[statement.local] = slot
if statement.expr == hir.INVALID_EXPR {
append_instruction(state, ir.Instruction{
op=.Fill,
span=statement.span,
type=local.type,
target=ir.INVALID_REF,
a=slot,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
continue
}
value := lower_expr(state, statement.expr)
append_instruction(state, ir.Instruction{ append_instruction(state, ir.Instruction{
op=.Store, op=.Store,
span=statement.span, span=statement.span,
+9
View File
@@ -576,6 +576,15 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
right=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
case .Keyword_Undefined:
advance(parser)
return add_expr(parser, ast.Expr{
kind=.Undefined,
span=tok.span,
left=ast.INVALID_EXPR,
right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Keyword_True, .Keyword_False: case .Keyword_True, .Keyword_False:
advance(parser) advance(parser)
return add_expr(parser, ast.Expr{ return add_expr(parser, ast.Expr{
+1
View File
@@ -59,6 +59,7 @@ Kind :: enum u8 {
Keyword_Return, Keyword_Return,
Keyword_Mut, Keyword_Mut,
Keyword_None, Keyword_None,
Keyword_Undefined,
Keyword_Orelse, Keyword_Orelse,
Keyword_And, Keyword_And,
Keyword_Or, Keyword_Or,
+207
View File
@@ -375,6 +375,32 @@ main :: func() void {}
testing.expect(t, strings.contains(diagnostics.items[0].message, "followed by a newline")) testing.expect(t, strings.contains(diagnostics.items[0].message, "followed by a newline"))
} }
@(test)
parser_accepts_undefined_expression :: proc(t: ^testing.T) {
text := `main :: func() void {
value i32 = undefined
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found_keyword := false
for tok in stream.items {
found_keyword = found_keyword || tok.kind == .Keyword_Undefined
}
statement := module.statements[module.functions[0].body[0]]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_keyword)
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.Undefined)
}
@(test) @(test)
pratt_parser_preserves_left_associative_addition_shape :: proc(t: ^testing.T) { pratt_parser_preserves_left_associative_addition_shape :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="value :: 1 + 2 + 3\nmain :: func() void {}\n"} source_file := source.Source{path="test.bro", text="value :: 1 + 2 + 3\nmain :: func() void {}\n"}
@@ -5013,6 +5039,187 @@ compound_assignment_preserves_operation_and_rhs :: proc(t: ^testing.T) {
testing.expect_value(t, module.exprs[statement.expr].integer, u64(5)) testing.expect_value(t, module.exprs[statement.expr].integer, u64(5))
} }
@(test)
undefined_inferred_local_lowers_to_fill :: proc(t: ^testing.T) {
text := `choose :: func(flag bool) i32 {
value int = undefined
if flag {
value = 42
} else {
value = -2
}
return value
}
main :: func() i32 {
return choose(true)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
choose_symbol := symbol.intern(&symbols, "choose")
value_symbol := symbol.intern(&symbols, "value")
found_value_i32 := false
fill_count := 0
for function, function_index in hir_module.functions {
if function.name != choose_symbol {
continue
}
for local in function.locals {
found_value_i32 = found_value_i32 || local.name == value_symbol && local.type == types.I32
}
for instruction in ir_module.functions[function_index].instructions {
fill_count += 1 if instruction.op == .Fill else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_value_i32)
testing.expect_value(t, fill_count, 1)
testing.expect(t, strings.contains(llvm_text, "declare void @llvm.memset.p0.i64"))
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memset.p0.i64"))
testing.expect(t, strings.contains(llvm_text, "i8 -86"))
}
@(test)
local_int_inference_widens_from_assignments :: proc(t: ^testing.T) {
text := `wide :: func() int {
value int = 1
value = 1000
return value
}
main :: func() void {
_ = wide()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
wide_symbol := symbol.intern(&symbols, "wide")
value_symbol := symbol.intern(&symbols, "value")
found_value_i16 := false
found_result_i16 := false
for function in hir_module.functions {
if function.name != wide_symbol {
continue
}
found_result_i16 = function.result == types.I16
for local in function.locals {
found_value_i16 = found_value_i16 || local.name == value_symbol && local.type == types.I16
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_result_i16)
testing.expect(t, found_value_i16)
}
@(test)
undefined_accepts_concrete_runtime_annotations :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
y i32
}
main :: func() void {
point Point = undefined
pointer @i32 = undefined
maybe ?i32 = undefined
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
main_symbol := symbol.intern(&symbols, "main")
fill_count := 0
for function, function_index in hir_module.functions {
if function.name != main_symbol {
continue
}
for instruction in ir_module.functions[function_index].instructions {
fill_count += 1 if instruction.op == .Fill else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, fill_count, 3)
}
@(test)
undefined_rejects_non_declaration_uses_and_unresolved_inference :: proc(t: ^testing.T) {
text := `global :: undefined
main :: func() void {
immutable :: undefined
typed_immutable int :: undefined
unresolved int = undefined
existing i32 = 1
existing = undefined
mismatch int = undefined
mismatch = 1
mismatch = 1.0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
immutable_count := 0
found_unresolved := false
found_assignment := false
found_incompatible := false
for diagnostic in diagnostics.items {
immutable_count += 1 if strings.contains(diagnostic.message, "'undefined' requires a mutable local declaration") else 0
found_unresolved = found_unresolved || strings.contains(diagnostic.message, "could not infer a concrete type for local 'unresolved'")
found_assignment = found_assignment || strings.contains(diagnostic.message, "'undefined' is only valid as a mutable local declaration initializer")
found_incompatible = found_incompatible || strings.contains(diagnostic.message, "cannot implicitly convert f64 to i8")
}
testing.expect(t, immutable_count >= 2)
testing.expect(t, found_unresolved)
testing.expect(t, found_assignment)
testing.expect(t, found_incompatible)
}
@(test) @(test)
compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) { compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) {
// A compound assignment to an indexed lvalue must compute the element address // A compound assignment to an indexed lvalue must compute the element address