Skip to content
47 changes: 45 additions & 2 deletions ownlang/cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ def lower_let(self, st: A.Let, cur: Block) -> Block:
self.diags.append(Diagnostic(
"OWN030", f"undefined resource '{rhs.resource}'", rhs.line))
sym = self.declare(st.name, Kind.OWNED, st.line)
sym.type_name = rhs.resource
cur.instrs.append(Acquire(sym, rhs.resource, st.line))
return cur
if isinstance(rhs, A.BufferIntent):
Expand All @@ -350,6 +351,7 @@ def lower_let(self, st: A.Let, cur: Block) -> Block:
# original buffer in the report.
dst.buffer = src.buffer
dst.origin = src.origin
dst.type_name = src.type_name # the moved value keeps its type
cur.instrs.append(MoveInto(dst, src, st.line))
return cur
if isinstance(rhs, A.VarRef):
Expand Down Expand Up @@ -492,15 +494,56 @@ def lower_if(self, st: A.If, cur: Block) -> Block | None:
return merge

def lower_return(self, st: A.Return, cur: Block) -> Block | None:
ret = self.fn.ret
sym: Symbol | None = None
if st.var is not None:
if st.var is None:
# `return;` with no value — only valid in a function with no return
# type. Otherwise the analyzer would treat the function as a valid
# terminal and codegen would emit `return;` from a non-void method.
if ret is not None:
self.diags.append(Diagnostic(
"OWN035",
f"'{self.fn.name}' returns '{ret.name}' but this 'return' "
f"provides no value", st.line))
else:
sym = self.lookup(st.var, st.line)
if sym is not None and sym.kind == Kind.BORROW:
if ret is None:
# returning a value from a function with no declared return type
# lowers to `return x;` inside a `void` method — uncompilable.
if sym is not None:
self.diags.append(Diagnostic(
"OWN035",
f"'{self.fn.name}' has no return type but returns "
f"'{st.var}'", st.line))
sym = None
elif sym is not None and sym.kind == Kind.BORROW:
self.diags.append(Diagnostic(
"OWN004",
f"'{st.var}' is a borrow and cannot be returned (it would "
f"outlive the resource it borrows)", st.line))
sym = None
elif (not ret.borrowed and ret.name in self.resource_names
and sym is not None and sym.buffer is None
and (sym.kind != Kind.OWNED or sym.type_name != ret.name)):
# the return type is an owned resource; the returned value must be
# an owned resource OF THE SAME type. Both a plain (`return n;`)
# and a different resource (`return c;` where c is a Conn but the
# function returns Buffer) lower to an uncompilable method, and
# the analyzer would otherwise pass them. (Buffers have their own
# escape rules -- OWN015/016/017 -- so they are left to the
# analyzer rather than reported here.)
if sym.kind != Kind.OWNED:
what = "not an owned resource"
sym = None # a plain value: nothing escapes
else:
# a real (but wrong-typed) owned resource still leaves the
# function; keep it so it is marked escaped, not leaked --
# the type mismatch is the error, an extra OWN001 is noise.
what = f"an owned '{sym.type_name}'"
self.diags.append(Diagnostic(
"OWN035",
f"'{self.fn.name}' returns '{ret.name}' but '{st.var}' is "
f"{what}", st.line))
elif sym is not None and sym.kind == Kind.PLAIN:
sym = None
cur.instrs.append(Return(sym, st.line))
Expand Down
1 change: 1 addition & 0 deletions ownlang/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Severity(Enum):
"OWN032": "owned resource copied without 'move'",
"OWN033": "function must return a value on all paths",
"OWN034": "operation requires an owned resource",
"OWN035": "return type mismatch",
# ---- extern / call boundary ----
"OWN040": "call to an undeclared function (unknown calls are forbidden)",
"OWN041": "call argument mismatch",
Expand Down
64 changes: 64 additions & 0 deletions tests/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ def codes(src: str) -> list[str]:
"fn f(){ let a = acquire Buffer(1); let b = a; release a; }", ["OWN032"]),
("missing_return",
"fn f() -> Buffer { let b = acquire Buffer(1); release b; }", ["OWN033"]),
("return_plain_as_resource",
"fn f(n: int) -> Buffer { return n; }", ["OWN035"]),
("return_empty_as_resource",
"fn f() -> Buffer { return; }", ["OWN035"]),
("return_value_from_void",
"fn g(n: int){ return n; }", ["OWN035"]),
("return_wrong_resource_type",
"fn f() -> Buffer { let c = acquire Conn(1); return c; }", ["OWN035"]),
("return_wrong_resource_param",
"fn f(c: Conn) -> Buffer { return c; }", ["OWN035"]),
("ok_return_owned_conn",
"fn f() -> Conn { let c = acquire Conn(1); return c; }", []),
("ok_bare_return_void",
"fn f(){ let b = acquire Buffer(1); release b; return; }", []),
("loop_rejected", "fn f(){ while (x) { use x; } }", ["OWN020"]),
("async_rejected", "fn f(){ async { use x; } }", ["OWN020"]),

Expand All @@ -160,6 +174,56 @@ def codes(src: str) -> list[str]:
"fn f(){ let a = acquire Buffer(1); let b = acquire Buffer(2); "
"release b; use b; }", ["OWN001", "OWN002"]),

# ---- ownership/borrow soundness (adversarial) ----
# Clean programs that must NOT be rejected (false-positive guard).
("ok_sequential_mut_borrows",
"fn f(){ let b = acquire Buffer(1); borrow_mut b as m1 { use m1; } "
"borrow_mut b as m2 { use m2; } release b; }", []),
("ok_consume_both_arms",
"fn f(){ let b = acquire Buffer(1); if (c) { Store(b); } "
"else { Store(b); } }", []),
("ok_move_chain",
"fn f(){ let a = acquire Buffer(1); let b = move a; let c = move b; "
"release c; }", []),
("ok_temp_borrows_then_consume",
"fn f(){ let b = acquire Buffer(1); Hash(b); Fill(b); Store(b); }", []),
("ok_nested_shared_borrows",
"fn f(){ let b = acquire Buffer(1); borrow b as s { borrow b as t "
"{ use t; } } release b; }", []),
# Bad programs that must be rejected (false-negative guard).
("borrow_moved_name",
"fn f(){ let a = acquire Buffer(1); let c = move a; borrow a as s "
"{ use s; } release c; }", ["OWN005"]),
("release_moved_name",
"fn f(){ let a = acquire Buffer(1); let c = move a; release a; "
"release c; }", ["OWN005"]),
("void_return_in_borrow_leaks",
"fn f(){ let b = acquire Buffer(1); borrow b as s { use s; return; } }",
["OWN001"]),
("double_consume",
"fn f(){ let b = acquire Buffer(1); Store(b); Store(b); }", ["OWN002"]),
("consume_borrow_binding",
"fn f(){ let b = acquire Buffer(1); borrow b as s { Store(s); } "
"release b; }", ["OWN034"]),
("mut_call_under_shared",
"fn f(){ let b = acquire Buffer(1); borrow b as s { Fill(b); } "
"release b; }", ["OWN006"]),
("leak_inner_acquire_in_borrow",
"fn f(){ let a = acquire Buffer(1); borrow a as s { let b = acquire "
"Buffer(2); use s; } release a; }", ["OWN001"]),
("move_borrow_binding",
"fn f(){ let b = acquire Buffer(1); borrow b as s { let c = move s; "
"release c; } release b; }", ["OWN034"]),
("shadow_borrow_binding",
"fn f(){ let b = acquire Buffer(1); borrow b as s { borrow b as s "
"{ use s; } } release b; }", ["OWN031"]),
("return_moved_and_leak",
"fn f() -> Buffer { let a = acquire Buffer(1); let b = move a; "
"return a; }", ["OWN001", "OWN005"]),
("plain_copy_to_resource_param",
"fn f(){ let a = acquire Buffer(1); let b = a; Hash(b); release a; }",
["OWN032", "OWN041"]),

# ---- buffer storage policies: clean ----
("buf_scratch_ok",
"fn f(n: int){ let b = Buffer.scratch(n, inline = 1024); "
Expand Down
Loading