From 1c79cbb30dba53e8f05c96ad4865c732706a8e74 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 16:58:45 +0000 Subject: [PATCH 1/5] Remove unnecessary .txt extensions from example and source files Rename the four example/source files to their real extensions so they match the names already used in the README and file headers: Program.cs.txt -> Program.cs buffer.own.txt -> buffer.own ok_pool.own.txt -> ok_pool.own bad_leak_branch.own.txt -> bad_leak_branch.own --- Program.cs.txt => Program.cs | 0 bad_leak_branch.own.txt => bad_leak_branch.own | 0 buffer.own.txt => buffer.own | 0 ok_pool.own.txt => ok_pool.own | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Program.cs.txt => Program.cs (100%) rename bad_leak_branch.own.txt => bad_leak_branch.own (100%) rename buffer.own.txt => buffer.own (100%) rename ok_pool.own.txt => ok_pool.own (100%) diff --git a/Program.cs.txt b/Program.cs similarity index 100% rename from Program.cs.txt rename to Program.cs diff --git a/bad_leak_branch.own.txt b/bad_leak_branch.own similarity index 100% rename from bad_leak_branch.own.txt rename to bad_leak_branch.own diff --git a/buffer.own.txt b/buffer.own similarity index 100% rename from buffer.own.txt rename to buffer.own diff --git a/ok_pool.own.txt b/ok_pool.own similarity index 100% rename from ok_pool.own.txt rename to ok_pool.own From b40b1059ef6d1b62fe767f8ae16f073560a326a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 17:01:17 +0000 Subject: [PATCH 2/5] Restructure into documented package layout so tests run The modules used relative imports (.parser, .cfg, ...) and run_tests.py imported the 'ownlang' package, but everything sat flat in the repo root, so 'python tests/run_tests.py' failed with ModuleNotFoundError. Move files into the layout already documented in the README: ownlang/ <- the 8 .py modules + new __init__.py tests/ <- run_tests.py examples/ <- ok_pool.own, bad_leak_branch.own examples/golden_arraypool <- buffer.own, Program.cs Now passing: 42/42 analysis, 11/11 codegen, golden PASS; and the documented CLI (python -m ownlang check/emit/cfg) works. --- bad_leak_branch.own => examples/bad_leak_branch.own | 0 Program.cs => examples/golden_arraypool/Program.cs | 0 buffer.own => examples/golden_arraypool/buffer.own | 0 ok_pool.own => examples/ok_pool.own | 0 ownlang/__init__.py | 1 + __main__.py => ownlang/__main__.py | 0 analysis.py => ownlang/analysis.py | 0 ast_nodes.py => ownlang/ast_nodes.py | 0 cfg.py => ownlang/cfg.py | 0 codegen.py => ownlang/codegen.py | 0 diagnostics.py => ownlang/diagnostics.py | 0 lexer.py => ownlang/lexer.py | 0 parser.py => ownlang/parser.py | 0 run_tests.py => tests/run_tests.py | 0 14 files changed, 1 insertion(+) rename bad_leak_branch.own => examples/bad_leak_branch.own (100%) rename Program.cs => examples/golden_arraypool/Program.cs (100%) rename buffer.own => examples/golden_arraypool/buffer.own (100%) rename ok_pool.own => examples/ok_pool.own (100%) create mode 100644 ownlang/__init__.py rename __main__.py => ownlang/__main__.py (100%) rename analysis.py => ownlang/analysis.py (100%) rename ast_nodes.py => ownlang/ast_nodes.py (100%) rename cfg.py => ownlang/cfg.py (100%) rename codegen.py => ownlang/codegen.py (100%) rename diagnostics.py => ownlang/diagnostics.py (100%) rename lexer.py => ownlang/lexer.py (100%) rename parser.py => ownlang/parser.py (100%) rename run_tests.py => tests/run_tests.py (100%) diff --git a/bad_leak_branch.own b/examples/bad_leak_branch.own similarity index 100% rename from bad_leak_branch.own rename to examples/bad_leak_branch.own diff --git a/Program.cs b/examples/golden_arraypool/Program.cs similarity index 100% rename from Program.cs rename to examples/golden_arraypool/Program.cs diff --git a/buffer.own b/examples/golden_arraypool/buffer.own similarity index 100% rename from buffer.own rename to examples/golden_arraypool/buffer.own diff --git a/ok_pool.own b/examples/ok_pool.own similarity index 100% rename from ok_pool.own rename to examples/ok_pool.own diff --git a/ownlang/__init__.py b/ownlang/__init__.py new file mode 100644 index 00000000..42bdc001 --- /dev/null +++ b/ownlang/__init__.py @@ -0,0 +1 @@ +"""OwnLang — a micro ownership/borrow checker PoC for .NET codegen.""" diff --git a/__main__.py b/ownlang/__main__.py similarity index 100% rename from __main__.py rename to ownlang/__main__.py diff --git a/analysis.py b/ownlang/analysis.py similarity index 100% rename from analysis.py rename to ownlang/analysis.py diff --git a/ast_nodes.py b/ownlang/ast_nodes.py similarity index 100% rename from ast_nodes.py rename to ownlang/ast_nodes.py diff --git a/cfg.py b/ownlang/cfg.py similarity index 100% rename from cfg.py rename to ownlang/cfg.py diff --git a/codegen.py b/ownlang/codegen.py similarity index 100% rename from codegen.py rename to ownlang/codegen.py diff --git a/diagnostics.py b/ownlang/diagnostics.py similarity index 100% rename from diagnostics.py rename to ownlang/diagnostics.py diff --git a/lexer.py b/ownlang/lexer.py similarity index 100% rename from lexer.py rename to ownlang/lexer.py diff --git a/parser.py b/ownlang/parser.py similarity index 100% rename from parser.py rename to ownlang/parser.py diff --git a/run_tests.py b/tests/run_tests.py similarity index 100% rename from run_tests.py rename to tests/run_tests.py From f2e3821eee2adbfdccf46e77e3777cc2bd4388b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 17:01:45 +0000 Subject: [PATCH 3/5] Add .gitignore for Python and .NET build artifacts Ignore __pycache__/ and *.pyc (generated when running the test suite) plus the .NET bin/ and obj/ output from the golden_arraypool demo, so build artifacts stay out of the repo. --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..32eadd0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ + +# .NET build output (golden_arraypool demo) +bin/ +obj/ From 49da9766c88867a9d7c563f6a84fdf1fa53d74db Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 17:10:05 +0000 Subject: [PATCH 4/5] Add the two example files README references; fix demo.csproj claims README's lead commands pointed at examples/ok_extern_calls.own and examples/bad_maybe_release.own, which never existed as files (they were only inline test cases). Add them as standalone .own files, verified with the checker here: ok_extern_calls.own -> checks clean (extern borrow/borrow_mut/consume) bad_maybe_release.own -> fires OWN009 + OWN001, cfg/emit behave as documented For demo.csproj (referenced but never shipped, and unverifiable without a .NET SDK in this sandbox) fix the README instead of adding an untested project file: show how to wrap Program.cs in a console project, and drop the demo.csproj entry from the structure tree. --- README.md | 7 +++++-- examples/bad_maybe_release.own | 18 ++++++++++++++++++ examples/ok_extern_calls.own | 30 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 examples/bad_maybe_release.own create mode 100644 examples/ok_extern_calls.own diff --git a/README.md b/README.md index 49d5e53b..a7298300 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,10 @@ python tests/run_tests.py # 42 кейса + c ```bash cd examples/golden_arraypool -dotnet run # требует .NET SDK; в песочнице PoC его нет +# Здесь лежат buffer.own (источник) и Program.cs (сгенерённый process + host). +# Своего .csproj PoC не возит; чтобы запустить — заверни Program.cs в console-проект: +dotnet new console -o demo && cp Program.cs demo/ && cd demo && dotnet run +# (требует .NET SDK; в песочнице PoC его нет — проверено по построению, не запуском) ``` `buffer.own` объявляет ресурс `Buffer` с шаблонами `emit_*`, отображающими его на @@ -389,6 +392,6 @@ ownlang/ examples/ ok_*.own # проходят bad_*.own # падают с конкретным кодом - golden_arraypool/ # buffer.own + Program.cs + demo.csproj (dotnet run) + golden_arraypool/ # buffer.own + Program.cs (host-код; .csproj не входит) tests/run_tests.py # 42 кейса анализа + codegen smoke + golden smoke ``` diff --git a/examples/bad_maybe_release.own b/examples/bad_maybe_release.own new file mode 100644 index 00000000..f14ce2ed --- /dev/null +++ b/examples/bad_maybe_release.own @@ -0,0 +1,18 @@ +module MaybeReleaseDemo + +resource Buffer { + acquire rent + release give +} + +// 'b' is released on the 'then' path only. At the join its state is +// inconsistent, so the following `use b` touches a maybe-released value +// (OWN009). The 'else' path never releases, so it also leaks (OWN001). +// Good for `python -m ownlang cfg examples/bad_maybe_release.own`. +fn use_after_maybe(flag: int) { + let b = acquire Buffer(flag); + if (flag) { + release b; + } + use b; +} diff --git a/examples/ok_extern_calls.own b/examples/ok_extern_calls.own new file mode 100644 index 00000000..671fba46 --- /dev/null +++ b/examples/ok_extern_calls.own @@ -0,0 +1,30 @@ +module ExternCalls + +resource Buffer { + acquire rent + release give +} + +// Host (C#) calls must declare what they do with ownership. The checker +// trusts these effect annotations and nothing else: an undeclared call that +// is handed an owned value is a hard OWN040. Borrow params are noescape, so +// the value cannot leak out through them. +extern fn Fill(borrow_mut Buffer); // writes through the buffer, hands it back +extern fn Hash(borrow Buffer); // reads only, hands it back +extern fn Store(consume Buffer); // takes ownership; caller must not reuse + +// Happy path: lend the buffer to the host mutably, then immutably, then hand +// it back ourselves. Both extern calls borrow, so 'b' is still owned after. +fn read_write(size: int) { + let b = acquire Buffer(size); + Fill(b); + Hash(b); + release b; +} + +// Happy path: give the buffer to the host for good. Store consumes it, so we +// neither release nor use 'b' afterwards — either of those would be OWN002. +fn hand_off(size: int) { + let b = acquire Buffer(size); + Store(b); +} From 9c08466644e9a788740624183f18870286b86ffa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 19:01:57 +0000 Subject: [PATCH 5/5] Fix return-under-borrow soundness hole and broken README quickstart Two blocking issues from code review, both reproduced before fixing: 1. Soundness hole: returning an owner while it is borrowed checked clean. 'return b' is an escape/consume of an owned resource, so under a live loan it must fail like move/consume do. The Return handler validated moved/released state but never the active loans on the returned owner. Now it fires OWN007 (mirrors move_while_borrowed/consume_while_borrowed). Added regression test return_while_borrowed; updated the OWN007 row in the README code table to list return alongside move/consume. 2. README quickstart was broken: 'cd ownlang' steps into the package dir, after which 'python -m ownlang' fails (No module named ownlang) and the examples/ paths no longer resolve. Replaced with a note to run from the repo root. Suite: 43/43 analysis (was 42 + new case), 11/11 codegen, golden PASS. --- README.md | 4 ++-- ownlang/analysis.py | 8 ++++++++ tests/run_tests.py | 3 +++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a7298300..78ae391d 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ C# (шаблоны emit_* → реальный .NET; try/finally на straight- ### Запуск ```bash -cd ownlang +# запускать из корня репозитория (там, где лежит пакет ownlang/ и examples/) python -m ownlang check examples/ok_extern_calls.own # проверка python -m ownlang emit examples/golden_arraypool/buffer.own # проверка + печать C# python -m ownlang cfg examples/bad_maybe_release.own # дамп CFG @@ -211,7 +211,7 @@ fn process(size: int) { | **OWN004** | borrow убегает из своей области (например, `return` borrow'а) | | **OWN005** | use/… после move (**definite**) | | **OWN006** | `borrow_mut` при живом shared borrow | -| **OWN007** | move/consume владельца под живым borrow'ом | +| **OWN007** | move/consume/return владельца под живым borrow'ом | | **OWN008** | release владельца под живым borrow'ом | | **OWN009** | операция над ресурсом, который **мог** быть освобождён на каком-то пути (**maybe**) | | **OWN010** | операция над ресурсом, который **мог** быть перемещён на каком-то пути (**maybe**) | diff --git a/ownlang/analysis.py b/ownlang/analysis.py index 2ab46e47..89c2645a 100644 --- a/ownlang/analysis.py +++ b/ownlang/analysis.py @@ -337,6 +337,14 @@ def step(self, ins, st: State) -> None: self.err("OWN002", f"'{ins.sym.name}' returned after it was released", ins.line) + else: + # returning an owner is an escape (consume): it needs Own + # permission, so a live loan on it is OWN007 just like move. + shared, mut = self.loans_on(st, ins.sym) + if shared or mut: + self.err("OWN007", + f"cannot return '{ins.sym.name}' while it is " + f"borrowed", ins.line) st.var[id(ins.sym)] = {VarState.ESCAPED} return diff --git a/tests/run_tests.py b/tests/run_tests.py index 58ae2523..54af40bd 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -98,6 +98,9 @@ def codes(src: str) -> list[str]: ("move_while_borrowed", "fn f(){ let b = acquire Buffer(1); borrow b as s { let c = move b; " "release c; } }", ["OWN007"]), + ("return_while_borrowed", + "fn f() -> Buffer { let b = acquire Buffer(1); borrow b as s " + "{ return b; } }", ["OWN007"]), ("release_while_borrowed", "fn f(){ let b = acquire Buffer(1); borrow b as s { release b; } }", ["OWN008"]),