Skip to content

Commit

Permalink
Introduce Undefined Value Graphs (UVGs) (#717)
Browse files Browse the repository at this point in the history
  • Loading branch information
Akuli authored Jan 30, 2025
1 parent a0efb7f commit 057f1bf
Show file tree
Hide file tree
Showing 12 changed files with 1,191 additions and 179 deletions.
29 changes: 29 additions & 0 deletions broken_tests/other_errors/undefined_variable.jou
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import "stdlib/io.jou"

@public
def maybe_undefined(n: int) -> None:
for i = 0; i < n; i++:
message = "Hi"
puts(message) # Warning: the value of 'message' may be undefined

@public
def surely_undefined_loop() -> None:
while False:
message = "Hi" # Warning: this code will never run
puts(message) # Warning: the value of 'message' is undefined

@public
def surely_undefined_annotation() -> None:
x: byte*
puts(x) # Warning: the value of 'x' is undefined

@public
def surely_undefined_assignments() -> None:
a: int
b = &a
c = b
d = c
e = *d # TODO: should emit warning, but this is too "advanced" for UVGs

def main() -> int:
return 0
135 changes: 81 additions & 54 deletions compiler/builders/ast_to_builder.jou
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,44 @@ import "../errors_and_warnings.jou"
import "../evaluate.jou"
import "../types.jou"
import "../types_in_ast.jou"
import "./llvm_builder.jou"
import "./either_builder.jou"


class LocalVar:
name: byte[100]
# All local variables are represented as pointers to stack space, even
# if they are never reassigned. LLVM will optimize the mess.
ptr: BuilderValue
ptr: EitherBuilderValue


class Loop:
on_break: BuilderBlock
on_continue: BuilderBlock
on_break: EitherBuilderBlock
on_continue: EitherBuilderBlock


class AstToBuilder:
builder: Builder*
builder: EitherBuilder*
locals: LocalVar*
nlocals: int
loops: Loop*
nloops: int
returns_a_value: bool

def begin_function(self, sig: Signature*, locals: LocalVariable*, nlocals: int, public: bool) -> None:
location: Location

# Returns old location. Use it to restore the location when you're done.
def set_location(self, location: Location) -> Location:
old = self->location
self->location = location
# If no reasonable location is available (e.g. implicit return at end of function),
# continue using the previous location for now.
if location.path != NULL and location.lineno != 0:
self->builder->set_location(location)
return old

def begin_function(self, sig: Signature*, location: Location, locals: LocalVariable*, nlocals: int, public: bool) -> None:
old = self->set_location(location)

# First n local variables are the arguments
assert sig->nargs >= 0
assert sig->nargs <= nlocals
Expand All @@ -45,7 +59,7 @@ class AstToBuilder:
for i = 0; i < nlocals; i++:
var_name = locals[i].name
var_type = locals[i].type
var_ptr = self->builder->stack_alloc(var_type)
var_ptr = self->builder->stack_alloc(var_type, var_name)
self->locals[i] = LocalVar{name = var_name, ptr = var_ptr}
if i < sig->nargs:
# First n local variables are the function arguments
Expand All @@ -56,11 +70,10 @@ class AstToBuilder:
jou_startup_sig = Signature{name = "_jou_startup"}
self->builder->call(&jou_startup_sig, NULL, 0)

self->set_location(old)

def end_function(self) -> None:
if self->returns_a_value:
self->builder->unreachable()
else:
self->builder->ret(NULL) # implicit "return" when falling off end of function
self->builder->ret(NULL) # implicit "return" when falling off end of function
self->builder->end_function()

def local_var_exists(self, name: byte*) -> bool:
Expand All @@ -69,27 +82,27 @@ class AstToBuilder:
return True
return False

def local_var_ptr(self, name: byte*) -> BuilderValue:
def local_var_ptr(self, name: byte*) -> EitherBuilderValue:
for i = 0; i < self->nlocals; i++:
if strcmp(self->locals[i].name, name) == 0:
return self->locals[i].ptr
assert False

def build_function_call(self, call: AstCall*) -> BuilderValue:
def build_function_call(self, call: AstCall*) -> EitherBuilderValue:
assert call->method_call_self == NULL

assert call->nargs <= 100
args: BuilderValue[100]
args: EitherBuilderValue[100]
for i = 0; i < call->nargs; i++:
args[i] = self->build_expression(&call->args[i])
return self->builder->call(call->called_signature, args, call->nargs)

def build_method_call(self, call: AstCall*) -> BuilderValue:
def build_method_call(self, call: AstCall*) -> EitherBuilderValue:
assert call->method_call_self != NULL

# leave room for self
assert call->nargs <= 99
args: BuilderValue[100]
args: EitherBuilderValue[100]

k = 0

Expand All @@ -108,7 +121,7 @@ class AstToBuilder:

return self->builder->call(call->called_signature, args, k)

def build_binop(self, op: AstExpressionKind, lhs: BuilderValue, rhs: BuilderValue) -> BuilderValue:
def build_binop(self, op: AstExpressionKind, lhs: EitherBuilderValue, rhs: EitherBuilderValue) -> EitherBuilderValue:
match op:
case AstExpressionKind.Eq:
return self->builder->eq(lhs, rhs)
Expand Down Expand Up @@ -142,11 +155,11 @@ class AstToBuilder:
new_value = self->build_binop(op, old_value, rhs_value)
self->builder->set_ptr(lhs_ptr, new_value)

def build_instantiation(self, class_type: Type*, inst: AstInstantiation*) -> BuilderValue:
def build_instantiation(self, class_type: Type*, inst: AstInstantiation*) -> EitherBuilderValue:
assert class_type != NULL
assert class_type->kind == TypeKind.Class

inst_ptr = self->builder->stack_alloc(class_type)
inst_ptr = self->builder->stack_alloc(class_type, NULL)
self->builder->memset_to_zero(inst_ptr)
for i = 0; i < inst->nfields; i++:
field_ptr = self->builder->class_field_pointer(inst_ptr, inst->field_names[i])
Expand All @@ -155,7 +168,7 @@ class AstToBuilder:

return self->builder->dereference(inst_ptr)

def build_and(self, lhsexpr: AstExpression*, rhsexpr: AstExpression*) -> BuilderValue:
def build_and(self, lhsexpr: AstExpression*, rhsexpr: AstExpression*) -> EitherBuilderValue:
# Must be careful with side effects.
#
# # lhs returning False means we don't evaluate rhs
Expand All @@ -166,7 +179,7 @@ class AstToBuilder:
lhstrue = self->builder->add_block()
lhsfalse = self->builder->add_block()
done = self->builder->add_block()
resultptr = self->builder->stack_alloc(boolType)
resultptr = self->builder->stack_alloc(boolType, NULL)

# if lhs:
self->builder->branch(self->build_expression(lhsexpr), lhstrue, lhsfalse)
Expand All @@ -185,7 +198,7 @@ class AstToBuilder:

return self->builder->dereference(resultptr)

def build_or(self, lhsexpr: AstExpression*, rhsexpr: AstExpression*) -> BuilderValue:
def build_or(self, lhsexpr: AstExpression*, rhsexpr: AstExpression*) -> EitherBuilderValue:
# Must be careful with side effects.
#
# # lhs returning True means we don't evaluate rhs
Expand All @@ -196,7 +209,7 @@ class AstToBuilder:
lhstrue = self->builder->add_block()
lhsfalse = self->builder->add_block()
done = self->builder->add_block()
resultptr = self->builder->stack_alloc(boolType)
resultptr = self->builder->stack_alloc(boolType, NULL)

# if lhs:
self->builder->branch(self->build_expression(lhsexpr), lhstrue, lhsfalse)
Expand All @@ -215,7 +228,7 @@ class AstToBuilder:

return self->builder->dereference(resultptr)

def build_increment_or_decrement(self, inner: AstExpression*, pre: bool, diff: int) -> BuilderValue:
def build_increment_or_decrement(self, inner: AstExpression*, pre: bool, diff: int) -> EitherBuilderValue:
assert diff == 1 or diff == -1 # 1=increment, -1=decrement

ptr = self->build_address_of_expression(inner)
Expand All @@ -234,11 +247,11 @@ class AstToBuilder:
else:
return old_value

def build_array(self, t: Type*, items: AstExpression*, nitems: int) -> BuilderValue:
def build_array(self, t: Type*, items: AstExpression*, nitems: int) -> EitherBuilderValue:
assert t->kind == TypeKind.Array
assert t->array.len == nitems

arr_ptr = self->builder->stack_alloc(t)
arr_ptr = self->builder->stack_alloc(t, NULL)
first_item_ptr = self->builder->cast(arr_ptr, t->array.item_type->pointer_type())

for i = 0; i < nitems; i++:
Expand All @@ -249,7 +262,7 @@ class AstToBuilder:

return self->builder->dereference(arr_ptr)

def build_expression_without_implicit_cast(self, expr: AstExpression*) -> BuilderValue:
def build_expression_without_implicit_cast(self, expr: AstExpression*) -> EitherBuilderValue:
match expr->kind:
case AstExpressionKind.String:
if expr->types.orig_type == byteType->pointer_type():
Expand Down Expand Up @@ -309,7 +322,7 @@ class AstToBuilder:
# We need to copy, because it's not always possible to evaluate &foo.
# For example, consider evaluating some_function().some_field.
instance = self->build_expression(expr->class_field.instance)
ptr = self->builder->stack_alloc(instance.type)
ptr = self->builder->stack_alloc(expr->class_field.instance->types.implicit_cast_type, NULL)
self->builder->set_ptr(ptr, instance)
fieldptr = self->builder->class_field_pointer(ptr, expr->class_field.field_name)
return self->builder->dereference(fieldptr)
Expand Down Expand Up @@ -360,21 +373,28 @@ class AstToBuilder:
return self->builder->not_(self->build_expression(&expr->operands[0]))
assert False

def build_expression(self, expr: AstExpression*) -> BuilderValue:
if expr->types.implicit_array_to_pointer_cast:
return self->builder->cast(self->build_address_of_expression(expr), expr->types.implicit_cast_type)
def build_expression(self, expr: AstExpression*) -> EitherBuilderValue:
old_location = self->set_location(expr->location)

raw = self->build_expression_without_implicit_cast(expr)
if expr->types.orig_type == NULL and expr->types.implicit_cast_type == NULL:
# Function/method call that returns no value
assert expr->kind == AstExpressionKind.Call
return BuilderValue{}
if expr->types.implicit_array_to_pointer_cast:
result = self->builder->cast(self->build_address_of_expression(expr), expr->types.implicit_cast_type)
else:
assert expr->types.orig_type != NULL
assert expr->types.implicit_cast_type != NULL
return self->builder->cast(raw, expr->types.implicit_cast_type)
raw = self->build_expression_without_implicit_cast(expr)
if expr->types.orig_type == NULL and expr->types.implicit_cast_type == NULL:
# Function/method call that returns no value
assert expr->kind == AstExpressionKind.Call
result = EitherBuilderValue{}
else:
assert expr->types.orig_type != NULL
assert expr->types.implicit_cast_type != NULL
result = self->builder->cast(raw, expr->types.implicit_cast_type)

self->set_location(old_location)
return result

def build_address_of_expression(self, expr: AstExpression*) -> EitherBuilderValue:
old_location = self->set_location(expr->location)

def build_address_of_expression(self, expr: AstExpression*) -> BuilderValue:
match expr->kind:
case AstExpressionKind.GetClassField:
if expr->class_field.uses_arrow_operator:
Expand All @@ -383,25 +403,28 @@ class AstToBuilder:
else:
# &obj.field = &obj + memory offset
ptr = self->build_address_of_expression(expr->class_field.instance)
return self->builder->class_field_pointer(ptr, expr->class_field.field_name)
result = self->builder->class_field_pointer(ptr, expr->class_field.field_name)
case AstExpressionKind.Self:
return self->local_var_ptr("self")
result = self->local_var_ptr("self")
case AstExpressionKind.GetVariable:
if self->local_var_exists(expr->varname):
return self->local_var_ptr(expr->varname)
result = self->local_var_ptr(expr->varname)
else:
return self->builder->global_var_ptr(expr->varname, expr->types.orig_type)
result = self->builder->global_var_ptr(expr->varname, expr->types.orig_type)
case AstExpressionKind.Indexing:
# &ptr[index] = ptr + memory offset
ptr = self->build_expression(&expr->operands[0])
index = self->build_expression(&expr->operands[1])
return self->builder->indexed_pointer(ptr, index)
result = self->builder->indexed_pointer(ptr, index)
case AstExpressionKind.Dereference:
# &*ptr = ptr
return self->build_expression(&expr->operands[0])
result = self->build_expression(&expr->operands[0])
case _:
assert False

self->set_location(old_location)
return result

def build_if_statement(self, ifst: AstIfStatement*) -> None:
done = self->builder->add_block()
for i = 0; i < ifst->n_if_and_elifs; i++:
Expand Down Expand Up @@ -464,15 +487,15 @@ class AstToBuilder:
done = self->builder->add_block()
for i = 0; i < match_stmt->ncases; i++:
then = self->builder->add_block()
otherwise = BuilderBlock{} # will be replaced by loop below
otherwise = EitherBuilderBlock{} # will be replaced by loop below
for k = 0; k < match_stmt->cases[i].n_case_objs; k++:
case_obj = self->build_expression(&match_stmt->cases[i].case_objs[k])
if match_stmt->func_name[0] == '\0':
cond = self->builder->eq(match_obj, case_obj)
else:
args = [match_obj, case_obj]
func_ret = self->builder->call(&match_stmt->func_signature, args, 2)
zero = self->builder->integer(func_ret.type, 0)
zero = self->builder->integer(match_stmt->func_signature.returntype, 0)
cond = self->builder->eq(func_ret, zero)
otherwise = self->builder->add_block()
self->builder->branch(cond, then, otherwise)
Expand Down Expand Up @@ -519,6 +542,8 @@ class AstToBuilder:
self->builder->set_current_block(ok_block)

def build_statement(self, stmt: AstStatement*) -> None:
old_location = self->set_location(stmt->location)

match stmt->kind:
case AstStatementKind.If:
self->build_if_statement(&stmt->if_statement)
Expand Down Expand Up @@ -575,21 +600,23 @@ class AstToBuilder:
# other statements shouldn't occur inside functions/methods
assert False

self->set_location(old_location)

def build_body(self, body: AstBody*) -> None:
for i = 0; i < body->nstatements; i++:
self->build_statement(&body->statements[i])


@public
def feed_ast_to_builder(ast: AstFunctionOrMethod*, builder: Builder*) -> None:
def feed_ast_to_builder(func_ast: AstFunctionOrMethod*, func_location: Location, builder: EitherBuilder*) -> None:
public = (
ast->public
or ast->types.signature.is_main_function()
or ast->types.signature.get_self_class() != NULL
func_ast->public
or func_ast->types.signature.is_main_function()
or func_ast->types.signature.get_self_class() != NULL
)
ast2ir = AstToBuilder{builder = builder}
ast2ir.begin_function(&ast->types.signature, ast->types.locals, ast->types.nlocals, public)
ast2ir.build_body(&ast->body)
ast2ir.begin_function(&func_ast->types.signature, func_location, func_ast->types.locals, func_ast->types.nlocals, public)
ast2ir.build_body(&func_ast->body)
ast2ir.end_function()
free(ast2ir.locals)
free(ast2ir.loops)
Loading

0 comments on commit 057f1bf

Please sign in to comment.