undefined assignment
This commit is contained in:
@@ -166,6 +166,23 @@
|
||||
no hand-writable spelling and are emitted as `# unsupported in bindings:` comments
|
||||
(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
|
||||
|
||||
Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated.
|
||||
|
||||
@@ -71,6 +71,7 @@ Expr_Kind :: enum u8 {
|
||||
Bool,
|
||||
Array,
|
||||
None,
|
||||
Undefined,
|
||||
Name,
|
||||
Enum_Literal,
|
||||
Address,
|
||||
|
||||
+210
-28
@@ -35,6 +35,9 @@ Spec :: struct {
|
||||
Infer_Local :: struct {
|
||||
name: symbol.Id,
|
||||
type: types.Type,
|
||||
declared: types.Type,
|
||||
statement: ast.Stmt_Id,
|
||||
mutable: bool,
|
||||
}
|
||||
|
||||
Build_Local :: struct {
|
||||
@@ -53,6 +56,7 @@ Build_Ctx :: struct {
|
||||
pkg: ast.Package_Id,
|
||||
file: ast.File_Id,
|
||||
result: types.Type,
|
||||
local_types: []types.Type,
|
||||
locals: ^[dynamic]Build_Local,
|
||||
hir_locals: ^[dynamic]hir.Local,
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
length: u64
|
||||
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)
|
||||
case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
|
||||
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 {
|
||||
for index := len(locals) - 1; index >= 0; index -= 1 {
|
||||
if locals[index].name == name {
|
||||
if index, ok := find_infer_local_index(locals, name); ok {
|
||||
return locals[index].type
|
||||
}
|
||||
}
|
||||
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 {
|
||||
function := checker.ast_module.functions[template]
|
||||
for spec, index in checker.specs {
|
||||
@@ -1123,6 +1141,8 @@ infer_compound_expr :: proc(
|
||||
return types.array(store, element, u64(len(expr.args)), false)
|
||||
case .None:
|
||||
return types.INVALID
|
||||
case .Undefined:
|
||||
return types.INVALID
|
||||
case .Enum_Literal:
|
||||
return types.INVALID
|
||||
case .Address:
|
||||
@@ -1247,7 +1267,7 @@ infer_expr :: proc(
|
||||
case .Float:
|
||||
last = types.F64
|
||||
_ = 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,
|
||||
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
|
||||
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded)
|
||||
@@ -1505,34 +1525,127 @@ flatten_conditional_unwrap_operands :: proc(
|
||||
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(
|
||||
checker: ^Checker,
|
||||
statements: []ast.Stmt_Id,
|
||||
locals: ^[dynamic]Infer_Local,
|
||||
local_types: []types.Type,
|
||||
pkg: ast.Package_Id,
|
||||
file: ast.File_Id,
|
||||
demanded: ^[dynamic]Spec_Id,
|
||||
result: ^types.Type,
|
||||
result_hint := types.INVALID,
|
||||
) {
|
||||
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)
|
||||
declared_local := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
|
||||
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) {
|
||||
value_type = declared_local
|
||||
}
|
||||
append(locals, Infer_Local{name = statement.name, type = value_type})
|
||||
case .Assignment, .Expression:
|
||||
local := Infer_Local{
|
||||
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 {
|
||||
_ = 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)
|
||||
case .Return:
|
||||
if statement.expr != ast.INVALID_EXPR {
|
||||
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^) {
|
||||
result^ = returned
|
||||
} else if !types.equal(result^, returned) {
|
||||
@@ -1558,27 +1671,27 @@ infer_statements :: proc(
|
||||
types.is_optional(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 {
|
||||
_ = 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)
|
||||
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(operands)
|
||||
} else {
|
||||
_ = 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)
|
||||
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
|
||||
infer_statements(checker, statement.else_body, locals, local_types, pkg, file, demanded, result, result_hint)
|
||||
}
|
||||
case .While:
|
||||
_ = 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 {
|
||||
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:
|
||||
iterable_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
|
||||
@@ -1596,43 +1709,55 @@ infer_statements :: proc(
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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, 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]
|
||||
function := checker.ast_module.functions[spec.template]
|
||||
declared := type_from_syntax(function.result)
|
||||
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
|
||||
declared = types.I32
|
||||
}
|
||||
result_hint := declared if is_runtime_type(checker, declared) else types.INVALID
|
||||
|
||||
locals: [dynamic]Infer_Local
|
||||
locals.allocator = checker.allocator
|
||||
defer delete(locals)
|
||||
local_types := make([]types.Type, len(checker.ast_module.statements), checker.allocator)
|
||||
for param, index in function.params {
|
||||
param_type := types.INVALID
|
||||
if index < len(spec.args) {
|
||||
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
|
||||
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) {
|
||||
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 {
|
||||
@@ -2296,7 +2421,7 @@ build_compound_expr :: proc(
|
||||
infer_locals := make([]Infer_Local, len(locals), checker.allocator)
|
||||
defer delete(infer_locals, checker.allocator)
|
||||
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 {
|
||||
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,
|
||||
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:
|
||||
if !types.is_enum(expected, store) {
|
||||
id := source.addf(
|
||||
@@ -2762,7 +2894,7 @@ build_expr :: proc(
|
||||
continue
|
||||
}
|
||||
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,
|
||||
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range,
|
||||
.Enum_Literal:
|
||||
@@ -3290,15 +3422,54 @@ build_block :: proc(
|
||||
switch statement.kind {
|
||||
case .Declaration:
|
||||
declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
|
||||
if statement_id != ast.INVALID_STMT && int(statement_id) < len(ctx.local_types) &&
|
||||
is_runtime_type(checker, ctx.local_types[statement_id]) &&
|
||||
(types.is_constraint(declared) || is_undefined_expr(checker, statement.expr)) {
|
||||
declared = ctx.local_types[statement_id]
|
||||
}
|
||||
expected := types.INVALID
|
||||
value := hir.INVALID_EXPR
|
||||
value_type := types.INVALID
|
||||
if is_undefined_expr(checker, statement.expr) {
|
||||
if statement.immutable {
|
||||
id := source.add(
|
||||
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(
|
||||
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
|
||||
if is_runtime_type(checker, declared) {
|
||||
value = coerce_expr(checker, value, declared, statement.span)
|
||||
value_type = checker.module.exprs[value].type
|
||||
@@ -3307,6 +3478,7 @@ build_block :: proc(
|
||||
value = invalid_hir_expr(checker, statement.span, id)
|
||||
value_type = types.INVALID
|
||||
}
|
||||
}
|
||||
if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found {
|
||||
id := source.addf(
|
||||
checker.diagnostics, statement.span,
|
||||
@@ -3332,7 +3504,9 @@ build_block :: proc(
|
||||
kind = .Declaration, span = statement.span, local = local_id, expr = value,
|
||||
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
if value != hir.INVALID_EXPR {
|
||||
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
|
||||
}
|
||||
case .Assignment:
|
||||
if statement.target != ast.INVALID_EXPR {
|
||||
target_expr := build_expr(
|
||||
@@ -3888,6 +4062,13 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
||||
global_reads.allocator = checker.allocator
|
||||
calls: [dynamic]hir.Function_Id
|
||||
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 {
|
||||
local_id := hir.local_id(len(hir_locals))
|
||||
@@ -3946,6 +4127,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
||||
pkg = function.pkg,
|
||||
file = function.file,
|
||||
result = spec.result,
|
||||
local_types = local_types,
|
||||
locals = &locals,
|
||||
hir_locals = &hir_locals,
|
||||
global_reads = &global_reads,
|
||||
|
||||
@@ -81,6 +81,7 @@ Opcode :: enum u8 {
|
||||
Field_Address,
|
||||
Load,
|
||||
Store,
|
||||
Fill,
|
||||
Slice,
|
||||
Length,
|
||||
Slice_Ptr,
|
||||
|
||||
@@ -25,6 +25,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
|
||||
case "return": return .Keyword_Return
|
||||
case "mut": return .Keyword_Mut
|
||||
case "none": return .Keyword_None
|
||||
case "undefined": return .Keyword_Undefined
|
||||
case "orelse": return .Keyword_Orelse
|
||||
case "and": return .Keyword_And
|
||||
case "or": return .Keyword_Or
|
||||
|
||||
+13
-2
@@ -260,7 +260,7 @@ valid_value :: proc(
|
||||
.Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Pointer_Add, .Not, .Compare, .Call:
|
||||
return true
|
||||
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
|
||||
@@ -964,6 +964,17 @@ emit_instruction_stream :: proc(
|
||||
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)
|
||||
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:
|
||||
if !valid_instruction(instructions, instruction.a) {
|
||||
emit_recovery_value(emitter, instruction_index, instruction, "invalid slice container")
|
||||
@@ -1990,7 +2001,7 @@ emit_messages :: 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}
|
||||
overflow_intrinsics := [?]string{"sadd", "uadd", "ssub", "usub", "smul", "umul"}
|
||||
for bits in widths {
|
||||
|
||||
@@ -613,7 +613,6 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
|
||||
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,
|
||||
@@ -632,6 +631,19 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
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{
|
||||
op=.Store,
|
||||
span=statement.span,
|
||||
|
||||
@@ -576,6 +576,15 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
|
||||
right=ast.INVALID_EXPR,
|
||||
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:
|
||||
advance(parser)
|
||||
return add_expr(parser, ast.Expr{
|
||||
|
||||
@@ -59,6 +59,7 @@ Kind :: enum u8 {
|
||||
Keyword_Return,
|
||||
Keyword_Mut,
|
||||
Keyword_None,
|
||||
Keyword_Undefined,
|
||||
Keyword_Orelse,
|
||||
Keyword_And,
|
||||
Keyword_Or,
|
||||
|
||||
@@ -375,6 +375,32 @@ main :: func() void {}
|
||||
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)
|
||||
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"}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@(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)
|
||||
compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) {
|
||||
// A compound assignment to an indexed lvalue must compute the element address
|
||||
|
||||
Reference in New Issue
Block a user