Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Undefined Value Graphs (UVGs) #717

Merged
merged 22 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading