diff --git a/ownlang/cfg.py b/ownlang/cfg.py index 6577938e..4bf92f21 100644 --- a/ownlang/cfg.py +++ b/ownlang/cfg.py @@ -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): @@ -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): @@ -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)) diff --git a/ownlang/diagnostics.py b/ownlang/diagnostics.py index 131ea41e..1a17406e 100644 --- a/ownlang/diagnostics.py +++ b/ownlang/diagnostics.py @@ -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", diff --git a/tests/run_tests.py b/tests/run_tests.py index 03cccf66..59e6ed01 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -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"]), @@ -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); "