From 441215e3a159b8bc416945f220380dea759d4476 Mon Sep 17 00:00:00 2001 From: proboscis Date: Fri, 12 Jun 2026 02:03:04 +0900 Subject: [PATCH 1/2] Fail loud on malformed VM legacy tags --- docs/crystallization/algebra-draft.md | 2 +- .../constraint-graph-conductor.md | 2 +- packages/doeff-traverse/pyproject.toml | 7 ++ .../doeff-vm/src/python_generator_stream.rs | 80 +++++++++---------- packages/doeff-vm/tests/test_pyvm.py | 42 ++++++++++ 5 files changed, 88 insertions(+), 45 deletions(-) diff --git a/docs/crystallization/algebra-draft.md b/docs/crystallization/algebra-draft.md index bcc077576..613c3eb21 100644 --- a/docs/crystallization/algebra-draft.md +++ b/docs/crystallization/algebra-draft.md @@ -87,7 +87,7 @@ OCaml 5から輸入: `perform` / `match_with`(WithHandler) / `resume`(non-tail) ### 導出物(ライブラリ)の導出経路 - `Try` = エラーチャネル(コア)上のハンドラ -- `Local(e, p)` = `WithHandler(reader(e ⊕ outer_env), p)` + 内側ハンドラ再設置 +- `Local(e, p)` = `reader(e ⊕ outer_env)(p)` + 内側ハンドラ再設置 - `Listen(p, T)` = ローカルWithHandlerで型Tを横取り+Pass - `Gather(ts)` = `traverse Wait ts`(scheduler内部で導出済み) - `Race/Cancel/Promise/Semaphore` = Spawn/Wait+キュー状態(scheduler.pyで実証) diff --git a/docs/crystallization/constraint-graph-conductor.md b/docs/crystallization/constraint-graph-conductor.md index 3ed6660fb..98fbd1a71 100644 --- a/docs/crystallization/constraint-graph-conductor.md +++ b/docs/crystallization/constraint-graph-conductor.md @@ -48,7 +48,7 @@ workspace識別(C8) <──同型── 「識別は式座標の純関数」 ─ ### 核K4: await×並行性核 — C6 ⇔ C1 ⇔ 本体B10(スケジューラ=ハンドラ) -- D1が「並行性は構造的」と宣言し(C6)、D1/D6がワーカー待ちをagentd RPCにし(C1)、スケジューラは協調的ハンドラである(本体B10)。**3つの選択は個々に正しいが、「RPC待ちは協調スケジューラとどう合成するか」だけが決められなかった**: `DaemonAgentHandler.handle_await_result`(doeff-agents handlers/daemon.py:298)→ `AgentdClient.await_result`(agentd_client.py:135)が同期ブロックし、`run(scheduled(WithHandler(conductor_handler, program)))`(api.py:152)のループごと止める +- D1が「並行性は構造的」と宣言し(C6)、D1/D6がワーカー待ちをagentd RPCにし(C1)、スケジューラは協調的ハンドラである(本体B10)。**3つの選択は個々に正しいが、「RPC待ちは協調スケジューラとどう合成するか」だけが決められなかった**: `DaemonAgentHandler.handle_await_result`(doeff-agents handlers/daemon.py:298)→ `AgentdClient.await_result`(agentd_client.py:135)が同期ブロックし、`run(scheduled(conductor_handler(program)))`(api.py:152)のループごと止める - 帰結: **parallelは形だけで実行は直列** — live実証 2026-06-12、run `doeff-review-20260612-1`(6-branch parallel、起動5分後もセッション1個。兄弟branchはlaunchすら起きない=スケジューラ飢餓)。conductor srcに`Await`/スレッド退避は0箇所(唯一の言及はutils.py:51の深層皮肉「use Await effects in programs」) - **law候補**: - **L-K4-1(非ブロックハンドラ)**: エフェクトハンドラは無制限ブロッキングI/Oを同期実行しない。無制限待ちはスケジューラのAwait/external completion経路(scheduler.pyのexternal_queueが既に受け皿)からのみ入る diff --git a/packages/doeff-traverse/pyproject.toml b/packages/doeff-traverse/pyproject.toml index 80cd814e1..79d6f25fa 100644 --- a/packages/doeff-traverse/pyproject.toml +++ b/packages/doeff-traverse/pyproject.toml @@ -7,6 +7,13 @@ dependencies = [ "doeff-core-effects", ] +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["doeff_traverse"] + [dependency-groups] test = [ "doeff-time", diff --git a/packages/doeff-vm/src/python_generator_stream.rs b/packages/doeff-vm/src/python_generator_stream.rs index 5054afc90..a88a4efb1 100644 --- a/packages/doeff-vm/src/python_generator_stream.rs +++ b/packages/doeff-vm/src/python_generator_stream.rs @@ -653,15 +653,12 @@ fn classify_tagged_to_doctrl( }; Ok(DoCtrl::Perform { effect }) } - 6 => extract_continuation_and_value(py, obj) - .map(|(k, v)| DoCtrl::Resume { k, value: v }) - .map_err(|_| "Resume: failed to extract continuation/value".to_string()), - 7 => extract_continuation_and_value(py, obj) - .map(|(k, v)| DoCtrl::Transfer { k, value: v }) - .map_err(|_| "Transfer: failed to extract continuation/value".to_string()), - 8 => extract_effect_and_continuation(py, obj) - .map(|(effect, k)| DoCtrl::Delegate { effect, k }) - .map_err(|_| "Delegate: failed to extract effect/continuation".to_string()), + 6 => extract_continuation_and_value(py, obj, "Resume") + .map(|(k, v)| DoCtrl::Resume { k, value: v }), + 7 => extract_continuation_and_value(py, obj, "Transfer") + .map(|(k, v)| DoCtrl::Transfer { k, value: v }), + 8 => extract_effect_and_continuation(py, obj, "Delegate") + .map(|(effect, k)| DoCtrl::Delegate { effect, k }), 16 => { let f_obj = obj .getattr("f") @@ -693,9 +690,8 @@ fn classify_tagged_to_doctrl( expr: Box::new(expr_doctrl), }) } - 19 => extract_effect_and_continuation(py, obj) - .map(|(effect, k)| DoCtrl::Pass { effect, k }) - .map_err(|_| "Pass: failed to extract effect/continuation".to_string()), + 19 => extract_effect_and_continuation(py, obj, "Pass") + .map(|(effect, k)| DoCtrl::Pass { effect, k }), 20 => { let handler_obj = obj .getattr("handler") @@ -714,12 +710,10 @@ fn classify_tagged_to_doctrl( body: Box::new(body_doctrl), }) } - 21 => extract_continuation_and_exception(py, obj) - .map(|(k, exc)| DoCtrl::ResumeThrow { k, exception: exc }) - .map_err(|_| "ResumeThrow: failed to extract continuation/exception".to_string()), - 22 => extract_continuation_and_exception(py, obj) - .map(|(k, exc)| DoCtrl::TransferThrow { k, exception: exc }) - .map_err(|_| "TransferThrow: failed to extract continuation/exception".to_string()), + 21 => extract_continuation_and_exception(py, obj, "ResumeThrow") + .map(|(k, exc)| DoCtrl::ResumeThrow { k, exception: exc }), + 22 => extract_continuation_and_exception(py, obj, "TransferThrow") + .map(|(k, exc)| DoCtrl::TransferThrow { k, exception: exc }), 23 => { let k_obj = obj .getattr("continuation") @@ -804,51 +798,51 @@ fn classify_tagged_to_doctrl( fn extract_continuation_and_value( py: Python<'_>, obj: &Bound<'_, PyAny>, -) -> Result<(doeff_vm_core::Continuation, Value), Value> { + label: &str, +) -> Result<(doeff_vm_core::Continuation, Value), String> { let k_obj = obj .getattr("continuation") - .map_err(|e| Value::Opaque(PyShared::new(e.value(py).clone().into_any().unbind())))?; + .map_err(|_| format!("{}: missing 'continuation' attribute", label))?; let k_ref = k_obj .downcast::() - .map_err(|_| Value::String("expected K".into()))?; + .map_err(|_| format!("{}: continuation must be K", label))?; let mut k_borrowed = k_ref.borrow_mut(); let k = k_borrowed .take() - .ok_or_else(|| Value::String("continuation consumed".into()))?; + .ok_or_else(|| format!("{}: continuation consumed", label))?; let continuation = match k { doeff_vm_core::OwnedControlContinuation::Started(k) => k, - _ => return Err(Value::String("expected started continuation".into())), + _ => return Err(format!("{}: expected started continuation", label)), }; let value = obj .getattr("value") - .ok() - .map(|v| python_to_value(py, &v)) - .unwrap_or(Value::Unit); + .map_err(|_| format!("{}: missing 'value' attribute", label))?; + let value = python_to_value(py, &value); Ok((continuation, value)) } fn extract_effect_and_continuation( - py: Python<'_>, + _py: Python<'_>, obj: &Bound<'_, PyAny>, -) -> Result<(Value, doeff_vm_core::Continuation), Value> { + label: &str, +) -> Result<(Value, doeff_vm_core::Continuation), String> { let effect = obj .getattr("effect") - .ok() - .map(|e| Value::Opaque(PyShared::new(e.unbind()))) - .unwrap_or(Value::Unit); + .map_err(|_| format!("{}: missing 'effect' attribute", label))?; + let effect = Value::Opaque(PyShared::new(effect.unbind())); let k_obj = obj .getattr("continuation") - .map_err(|e| Value::Opaque(PyShared::new(e.value(py).clone().into_any().unbind())))?; + .map_err(|_| format!("{}: missing 'continuation' attribute", label))?; let k_ref = k_obj .downcast::() - .map_err(|_| Value::String("expected K".into()))?; + .map_err(|_| format!("{}: continuation must be K", label))?; let mut k_borrowed = k_ref.borrow_mut(); let k = k_borrowed .take() - .ok_or_else(|| Value::String("continuation consumed".into()))?; + .ok_or_else(|| format!("{}: continuation consumed", label))?; let continuation = match k { doeff_vm_core::OwnedControlContinuation::Started(k) => k, - _ => return Err(Value::String("expected started continuation".into())), + _ => return Err(format!("{}: expected started continuation", label)), }; Ok((effect, continuation)) } @@ -856,26 +850,26 @@ fn extract_effect_and_continuation( fn extract_continuation_and_exception( py: Python<'_>, obj: &Bound<'_, PyAny>, -) -> Result<(doeff_vm_core::Continuation, Value), Value> { + label: &str, +) -> Result<(doeff_vm_core::Continuation, Value), String> { let k_obj = obj .getattr("continuation") - .map_err(|e| Value::Opaque(PyShared::new(e.value(py).clone().into_any().unbind())))?; + .map_err(|_| format!("{}: missing 'continuation' attribute", label))?; let k_ref = k_obj .downcast::() - .map_err(|_| Value::String("expected K".into()))?; + .map_err(|_| format!("{}: continuation must be K", label))?; let mut k_borrowed = k_ref.borrow_mut(); let k = k_borrowed .take() - .ok_or_else(|| Value::String("continuation consumed".into()))?; + .ok_or_else(|| format!("{}: continuation consumed", label))?; let continuation = match k { doeff_vm_core::OwnedControlContinuation::Started(k) => k, - _ => return Err(Value::String("expected started continuation".into())), + _ => return Err(format!("{}: expected started continuation", label)), }; let exception = obj .getattr("exception") - .ok() - .map(|e| Value::Opaque(PyShared::new(e.unbind()))) - .unwrap_or(Value::String("unknown exception".into())); + .map_err(|_| format!("{}: missing 'exception' attribute", label))?; + let exception = Value::Opaque(PyShared::new(exception.unbind())); Ok((continuation, exception)) } diff --git a/packages/doeff-vm/tests/test_pyvm.py b/packages/doeff-vm/tests/test_pyvm.py index caee4f662..24597683f 100644 --- a/packages/doeff-vm/tests/test_pyvm.py +++ b/packages/doeff-vm/tests/test_pyvm.py @@ -1756,6 +1756,48 @@ def pure_prog() -> Program[int]: assert result == 99 +class _LegacyParserProbeEffect(doeff_vm.EffectBase): + def __init__(self, tag): + self.tag = tag + + +class _LegacyControlMissingPayload: + def __init__(self, tag, continuation): + self.tag = tag + self.continuation = continuation + + +@do +def _legacy_parser_probe_program(tag): + return (yield _LegacyParserProbeEffect(tag)) + + +def _run_legacy_missing_payload(tag): + def handler(effect, k): + if not isinstance(effect, _LegacyParserProbeEffect): + return doeff_vm.Pass(effect, k) + return _LegacyControlMissingPayload(tag, k) + + vm = doeff_vm.PyVM() + return vm.run(_with_handlers(_legacy_parser_probe_program(tag), handler)) + + +@pytest.mark.parametrize( + ("tag", "tag_name", "attribute"), + [ + (6, "Resume", "value"), + (7, "Transfer", "value"), + (8, "Delegate", "effect"), + (19, "Pass", "effect"), + (21, "ResumeThrow", "exception"), + (22, "TransferThrow", "exception"), + ], +) +def test_legacy_tag_parser_missing_payload_attributes_fail_loud(tag, tag_name, attribute): + with pytest.raises(RuntimeError, match=rf"{tag_name}: missing '{attribute}' attribute"): + _run_legacy_missing_payload(tag) + + ## -- ISSUE-VM-001: Transfer abandonment + forwarding decomposition ------------ From d91f56e057bded48cebecd67c85632535cd603d7 Mon Sep 17 00:00:00 2001 From: proboscis Date: Fri, 12 Jun 2026 02:49:37 +0900 Subject: [PATCH 2/2] Move VM legacy parser regression into root tests --- packages/doeff-vm/tests/test_pyvm.py | 42 ----------------- .../test_vm_legacy_tag_parser_fail_loud.py | 46 +++++++++++++++++++ 2 files changed, 46 insertions(+), 42 deletions(-) create mode 100644 tests/core/test_vm_legacy_tag_parser_fail_loud.py diff --git a/packages/doeff-vm/tests/test_pyvm.py b/packages/doeff-vm/tests/test_pyvm.py index 24597683f..caee4f662 100644 --- a/packages/doeff-vm/tests/test_pyvm.py +++ b/packages/doeff-vm/tests/test_pyvm.py @@ -1756,48 +1756,6 @@ def pure_prog() -> Program[int]: assert result == 99 -class _LegacyParserProbeEffect(doeff_vm.EffectBase): - def __init__(self, tag): - self.tag = tag - - -class _LegacyControlMissingPayload: - def __init__(self, tag, continuation): - self.tag = tag - self.continuation = continuation - - -@do -def _legacy_parser_probe_program(tag): - return (yield _LegacyParserProbeEffect(tag)) - - -def _run_legacy_missing_payload(tag): - def handler(effect, k): - if not isinstance(effect, _LegacyParserProbeEffect): - return doeff_vm.Pass(effect, k) - return _LegacyControlMissingPayload(tag, k) - - vm = doeff_vm.PyVM() - return vm.run(_with_handlers(_legacy_parser_probe_program(tag), handler)) - - -@pytest.mark.parametrize( - ("tag", "tag_name", "attribute"), - [ - (6, "Resume", "value"), - (7, "Transfer", "value"), - (8, "Delegate", "effect"), - (19, "Pass", "effect"), - (21, "ResumeThrow", "exception"), - (22, "TransferThrow", "exception"), - ], -) -def test_legacy_tag_parser_missing_payload_attributes_fail_loud(tag, tag_name, attribute): - with pytest.raises(RuntimeError, match=rf"{tag_name}: missing '{attribute}' attribute"): - _run_legacy_missing_payload(tag) - - ## -- ISSUE-VM-001: Transfer abandonment + forwarding decomposition ------------ diff --git a/tests/core/test_vm_legacy_tag_parser_fail_loud.py b/tests/core/test_vm_legacy_tag_parser_fail_loud.py new file mode 100644 index 000000000..82021bd69 --- /dev/null +++ b/tests/core/test_vm_legacy_tag_parser_fail_loud.py @@ -0,0 +1,46 @@ +import pytest + +import doeff_vm +from doeff import do + + +class _LegacyParserProbeEffect(doeff_vm.EffectBase): + def __init__(self, tag): + self.tag = tag + + +class _LegacyControlMissingPayload: + def __init__(self, tag, continuation): + self.tag = tag + self.continuation = continuation + + +@do +def _legacy_parser_probe_program(tag): + return (yield _LegacyParserProbeEffect(tag)) + + +def _run_legacy_missing_payload(tag): + def handler(effect, k): + if not isinstance(effect, _LegacyParserProbeEffect): + return doeff_vm.Pass(effect, k) + return _LegacyControlMissingPayload(tag, k) + + vm = doeff_vm.PyVM() + return vm.run(doeff_vm.WithHandler(handler, _legacy_parser_probe_program(tag))) + + +@pytest.mark.parametrize( + ("tag", "tag_name", "attribute"), + [ + (6, "Resume", "value"), + (7, "Transfer", "value"), + (8, "Delegate", "effect"), + (19, "Pass", "effect"), + (21, "ResumeThrow", "exception"), + (22, "TransferThrow", "exception"), + ], +) +def test_legacy_tag_parser_missing_payload_attributes_fail_loud(tag, tag_name, attribute): + with pytest.raises(RuntimeError, match=rf"{tag_name}: missing '{attribute}' attribute"): + _run_legacy_missing_payload(tag)