From bf678258afeebb310d1c0b80524fe39fdf6bbbb2 Mon Sep 17 00:00:00 2001 From: Christian-Manuel Butzke Date: Tue, 28 Jul 2026 20:47:28 +0900 Subject: [PATCH] Fix remaining format 1 conformance gaps --- .github/workflows/test.yml | 2 +- AGENTS.md | 2 +- README.md | 4 +- conformance/harness.py | 69 +++++++++++ conformance/pins.py | 2 +- conformance/test_conformance.py | 2 +- src/determa/state/engine.py | 52 ++++++--- src/determa/state/yaml12.py | 16 +++ tests/test_engine.py | 195 ++++++++++++++++++++++++++++++++ 9 files changed, 325 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3371a04..6c5957c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: repository: fruwehq/determa-state-conformance - ref: fc4842010ab8d83bf4c5c6280a5627ca86829f7f + ref: ffbc65cbce49733803119a7dabf02a9727819ba8 path: .pinned/determa-state-conformance - name: Check out pinned specification uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 diff --git a/AGENTS.md b/AGENTS.md index 51df62d..ab36a74 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ The implementation is conformant only when it passes the language-neutral suite. Format-1 work currently uses these immutable pre-release inputs: - specification: `4bd4d9588d11b75d376380b6120676a056a4bc45`; -- conformance: `fc4842010ab8d83bf4c5c6280a5627ca86829f7f` (75 core cases). +- conformance: `ffbc65cbce49733803119a7dabf02a9727819ba8` (88 core cases). The package version is still `0.0.6`; the specification, conformance suite, Python engine, and Rust engine version together. diff --git a/README.md b/README.md index d6bbb78..628d199 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ a language-agnostic statechart engine with a shared normative conformance suite. This pre-release implements Determa State `format: 1` at the approved specification commit `4bd4d9588d11b75d376380b6120676a056a4bc45`. Correctness is determined by the -75-case core suite at conformance commit -`fc4842010ab8d83bf4c5c6280a5627ca86829f7f`. +88-case core suite at conformance commit +`ffbc65cbce49733803119a7dabf02a9727819ba8`. The package version remains `0.0.6` until the specification, conformance suite, Python engine, and Rust engine are released together. diff --git a/conformance/harness.py b/conformance/harness.py index 6914c7f..b66a3f4 100644 --- a/conformance/harness.py +++ b/conformance/harness.py @@ -3,6 +3,7 @@ from __future__ import annotations import copy +import math import os from dataclasses import dataclass from pathlib import Path @@ -55,6 +56,13 @@ def _load_test(path: Path) -> dict[str, Any]: def _materialize_driver_value(value: Any) -> Any: if isinstance(value, dict) and set(value) == {"invalid_unicode_scalar"}: return chr(int(value["invalid_unicode_scalar"], 16)) + if isinstance(value, dict) and set(value) == {"non_finite_double"}: + marker = value["non_finite_double"] + return { + "nan": math.nan, + "positive_infinity": math.inf, + "negative_infinity": -math.inf, + }[marker] if isinstance(value, dict): return {key: _materialize_driver_value(item) for key, item in value.items()} if isinstance(value, list): @@ -128,6 +136,9 @@ def run_case(case: CoreCase) -> None: ] target = {"spawned_instance": copy.deepcopy(reference)} target_runtime_id = reference["instance_id"] + elif "component" in send: + target = _component_target(state, send["component"]) + target_runtime_id = _target_runtime_id(target) envelope = { "event": send["event"], "event_id": send.get("event_id", f"conformance:{case.name}:step:{index}:input"), @@ -141,9 +152,30 @@ def run_case(case: CoreCase) -> None: elif "deliver" in step: delivery = step["deliver"] envelope = copy.deepcopy(captures[delivery["captured"]][delivery["index"]]) + replacement = _materialize_driver_value(delivery.get("replace") or {}) + if "payload" in replacement: + envelope["payload"] = copy.deepcopy(replacement["payload"]) + if "target" in replacement: + envelope["target"] = _driver_target(state, replacement["target"]) + if "spawned_instance_reference" in replacement: + envelope["target"]["spawned_instance"].update( + copy.deepcopy(replacement["spawned_instance_reference"]) + ) target_runtime_id = _target_runtime_id(envelope["target"]) envelope_snapshot = copy.deepcopy(envelope) result = dispatch(dispatch_bundle, state, {"internal": envelope}) + elif "inspect" in step: + mutation = step["inspect"]["corrupt_prior_state"] + prior_state = copy.deepcopy(state) + root = prior_state["runtimes"][prior_state["root_runtime_id"]] + selected = _visible_variable_storage(root, mutation["variable"]) + for member in mutation["path"][:-1]: + selected = selected[member] + selected[mutation["path"][-1]] = _materialize_driver_value(mutation["value"]) + prior_state_snapshot = copy.deepcopy(prior_state) + result = dispatch(dispatch_bundle, prior_state) + envelope = None + envelope_snapshot = None else: raise AssertionError(f"{case.name} step {index}: unsupported driver step") _assert_result( @@ -199,6 +231,7 @@ def _assert_result( "rejection", "fault", "caller_still_owns_input", + "caller_still_owns_state", "state", "config", "variables", @@ -232,6 +265,11 @@ def _assert_result( ) if result["disposition"] == "rejected": assert result["state"] is prior_state + if expected.get("caller_still_owns_state"): + assert prior_state is not None + assert prior_state_snapshot is not None + assert prior_state == prior_state_snapshot + assert result["state"] is prior_state if result["state"] is None: return state = result["state"] @@ -351,6 +389,13 @@ def _assert_emission( def _assert_partial(actual: Any, expected: Any, *, state: dict[str, Any] | None = None) -> None: if isinstance(expected, dict): + if expected == {"normalized_double": "positive_zero"}: + assert ( + type(actual) is float + and actual == 0.0 + and math.copysign(1.0, actual) == 1.0 + ), actual + return assert isinstance(actual, dict), (actual, expected) if set(expected) == {"instance_reference"}: assertion = expected["instance_reference"] @@ -415,6 +460,30 @@ def _root_target(state: dict[str, Any]) -> dict[str, Any]: } +def _component_target(state: dict[str, Any], component_id: str) -> dict[str, Any]: + root = state["runtimes"][state["root_runtime_id"]] + runtime_id = root["components"][component_id] + return copy.deepcopy(state["runtimes"][runtime_id]["target"]) + + +def _driver_target(state: dict[str, Any], selector: Any) -> dict[str, Any]: + if selector == "root": + return _root_target(state) + if "bound_instance" in selector: + root = state["runtimes"][state["root_runtime_id"]] + reference = _visible_variables(state, root)[selector["bound_instance"]] + return {"spawned_instance": copy.deepcopy(reference)} + return _component_target(state, selector["component"]) + + +def _visible_variable_storage(runtime: dict[str, Any], name: str) -> Any: + for path in reversed(runtime["active"]): + scope = runtime["scopes"].get(path, {}) + if name in scope: + return scope[name] + raise KeyError(name) + + def _target_runtime_id(target: dict[str, Any]) -> str: if "root" in target: return target["root"]["root_runtime_id"] diff --git a/conformance/pins.py b/conformance/pins.py index bc404af..0ee4e10 100644 --- a/conformance/pins.py +++ b/conformance/pins.py @@ -4,7 +4,7 @@ from pathlib import Path -CONFORMANCE_COMMIT = "fc4842010ab8d83bf4c5c6280a5627ca86829f7f" +CONFORMANCE_COMMIT = "ffbc65cbce49733803119a7dabf02a9727819ba8" SPEC_COMMIT = "4bd4d9588d11b75d376380b6120676a056a4bc45" ROOT = Path(__file__).resolve().parent.parent diff --git a/conformance/test_conformance.py b/conformance/test_conformance.py index c2d1ce8..86b4643 100644 --- a/conformance/test_conformance.py +++ b/conformance/test_conformance.py @@ -33,7 +33,7 @@ def _spec_root() -> Path | None: def test_suite_present() -> None: assert CORE_DIR.exists(), "pinned conformance suite is unavailable" - assert len(core_cases()) == 75 + assert len(core_cases()) == 88 def test_bundled_schema_matches_pinned_spec() -> None: diff --git a/src/determa/state/engine.py b/src/determa/state/engine.py index 9f54da8..a84557b 100644 --- a/src/determa/state/engine.py +++ b/src/determa/state/engine.py @@ -12,7 +12,7 @@ from .definition import Bundle, BundleSource, _escape_pointer, hash_identity, load_bundle from .errors import CelError, StepFault, ValidationError from .model import BundleModel, MachineModel, StateNode -from .yaml12 import validate_portable_values, validate_unicode +from .yaml12 import normalize_portable_values, validate_portable_values, validate_unicode Result = dict[str, Any] Delivery = dict[str, dict[str, Any]] | None @@ -197,15 +197,15 @@ def _value_matches(value: Any, type_name: str) -> bool: def _normalize_value(value: Any, type_name: str) -> Any: try: - validate_portable_values(value) + normalized = normalize_portable_values(value) except ValidationError as exc: raise ValueError(type_name) from exc - if not _value_matches(value, type_name): + if not _value_matches(normalized, type_name): raise ValueError(type_name) if type_name == "float": - number = float(value) + number = float(normalized) return 0.0 if number == 0.0 else number - return copy.deepcopy(value) + return normalized def _is_instance_reference(value: Any) -> bool: @@ -390,13 +390,26 @@ def dispatch( rejection = _validate_envelope(validated, models, prior_state, envelope, delivery_mode) if rejection is not None: return _rejected(prior_state, rejection) - state = copy.deepcopy(prior_state) + state = _copy_normalized_prior_state(prior_state) step_sequence = int(state["next_logical_step_sequence"]) execution = _Execution(validated, models, state, step_sequence=step_sequence) runtime = execution.runtime_for_target(envelope["target"]) normalized_envelope = copy.deepcopy(envelope) declaration = execution.event_declaration(runtime, envelope["event"]) - if envelope["event"] == "env" or envelope["event"] in _reserved_events(): + if envelope["event"] == "env": + runtime_root = _pointer_get(validated.raw, runtime["root_pointer"]) + external = { + name: variable + for name, variable in (runtime_root.get("variables") or {}).items() + if variable.get("external") is True + } + normalized_envelope["payload"] = { + "changed": { + name: _normalize_value(value, str(external[name]["type"])) + for name, value in envelope["payload"]["changed"].items() + } + } + elif envelope["event"] in _reserved_events(): normalized_envelope["payload"] = copy.deepcopy(envelope["payload"]) else: assert declaration is not None @@ -513,6 +526,21 @@ def visit(value: Any, path: tuple[str | int, ...], ancestors: set[int]) -> None: visit(state, (), set()) +def _copy_normalized_prior_state(state: dict[str, Any]) -> dict[str, Any]: + def visit(value: Any, path: tuple[str | int, ...]) -> Any: + if _is_prior_counter_path(path): + return value + if isinstance(value, float): + return 0.0 if value == 0.0 else value + if isinstance(value, list): + return [visit(item, (*path, index)) for index, item in enumerate(value)] + if isinstance(value, dict): + return {key: visit(item, (*path, key)) for key, item in value.items()} + return value + + return cast(dict[str, Any], visit(state, ())) + + def _validate_prior_state(state: dict[str, Any], bundle: Bundle) -> bool: required = { "validated_bundle_fingerprint", @@ -953,13 +981,11 @@ def _valid_spawned_relation( prefix = f"{state.pointer}/variables/" if not holder["pointer"].startswith(prefix): return False - encoded_name = holder["pointer"][len(prefix) :] - name = encoded_name.replace("~1", "/").replace("~0", "~") declarations = state.raw.get("variables") or {} - return bool( - name in declarations - and declarations[name].get("type") == "instance_reference" - and owner["scopes"][state_path].get(name) == runtime["instance_reference"] + return any( + holder["pointer"] == f"{prefix}{_escape_pointer(name)}" + and declaration.get("type") == "instance_reference" + for name, declaration in declarations.items() ) diff --git a/src/determa/state/yaml12.py b/src/determa/state/yaml12.py index 87121b4..018b4ad 100644 --- a/src/determa/state/yaml12.py +++ b/src/determa/state/yaml12.py @@ -76,6 +76,22 @@ def validate_portable_values(value: Any) -> None: _validate_portable_values(value, set()) +def normalize_portable_values(value: Any) -> Any: + """Copy a portable host value and normalize every binary64 negative zero.""" + validate_portable_values(value) + return _normalize_portable_values(value) + + +def _normalize_portable_values(value: Any) -> Any: + if isinstance(value, float): + return 0.0 if value == 0.0 else value + if isinstance(value, list): + return [_normalize_portable_values(item) for item in value] + if isinstance(value, dict): + return {key: _normalize_portable_values(item) for key, item in value.items()} + return value + + def _validate_portable_values(value: Any, ancestors: set[int]) -> None: if value is None or isinstance(value, (str, bool)): return diff --git a/tests/test_engine.py b/tests/test_engine.py index 3c068c5..ec3c642 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +import math import pytest @@ -54,6 +55,68 @@ settings: { type: map, input: true } """ +NUMERIC_BOUNDARY_BUNDLE = """ +format: 1 +namespace: example.numeric_boundary +events: + ordinary_values: + direction: input + payload: + mapped: { type: map, required: true } +machines: + - machine_id: numeric_boundary + root: + variables: + created_map: { type: map, input: true } + rate: { type: float, external: true } + stored_map: { type: map, init: {} } + guard_selected: { type: bool, init: false } + on_events: + ordinary_values: + action: + - assign: { stored_map: "event.payload.mapped" } + env: + - guard: "event.payload.changed.rate == 1.0" + action: + - assign: { guard_selected: "true" } + - refresh: {} + - action: + - assign: { guard_selected: "false" } + - refresh: {} +""" + +HOLDER_REUSE_BUNDLE = """ +format: 1 +namespace: example.holder_reuse +events: + prepare: { direction: input } + leave: { direction: input } +machines: + - machine_id: owner + root: + type: composite + initial: { transition_to: holding } + states: + holding: + variables: + child_reference: + type: instance_reference + machine_id: child + nullable: true + init: null + on_events: + prepare: + action: + - spawn: { machine_id: child, bind_to: child_reference } + - assign: { child_reference: "null" } + - spawn: { machine_id: child, bind_to: child_reference } + - assign: { child_reference: "null" } + leave: { transition_to: outside } + outside: {} + - machine_id: child + root: {} +""" + FROZEN_SUBTREE_BUNDLE = """ format: 1 namespace: example.frozen_subtree @@ -873,6 +936,138 @@ def test_nested_nonportable_creation_binding_is_rejected() -> None: assert result["state"] is None +def test_programmatic_values_are_recursively_validated_and_normalized() -> None: + bundle = load_bundle(NUMERIC_BOUNDARY_BUNDLE) + invalid = create( + bundle, + "numeric_boundary", + "numeric-invalid", + "numeric-invalid-create", + { + "input": {"created_map": {"nested": [math.nan]}}, + "external": {"rate": 0.0}, + }, + ) + assert invalid["rejection"] == {"code": "invalid_binding"} + assert invalid["state"] is None + + created = create( + bundle, + "numeric_boundary", + "numeric-boundary", + "numeric-create", + { + "input": { + "created_map": { + "integer": 1, + "double": 1.0, + "nested": [-0.0], + } + }, + "external": {"rate": -0.0}, + }, + ) + state = created["state"] + created_values = _root_variables(state) + assert type(created_values["created_map"]["integer"]) is int + assert type(created_values["created_map"]["double"]) is float + assert math.copysign(1.0, created_values["created_map"]["nested"][0]) == 1.0 + assert math.copysign(1.0, created_values["rate"]) == 1.0 + + envelope = _envelope(state, "ordinary_values", "ordinary-values") + envelope["payload"] = { + "mapped": { + "integer": 1, + "double": 1.0, + "nested": [-0.0], + } + } + envelope_snapshot = copy.deepcopy(envelope) + handled = dispatch(bundle, state, {"input": envelope}) + assert envelope == envelope_snapshot + stored = _root_variables(handled["state"])["stored_map"] + assert type(stored["integer"]) is int + assert type(stored["double"]) is float + assert math.copysign(1.0, stored["nested"][0]) == 1.0 + + rejected_envelope = _envelope(handled["state"], "ordinary_values", "non-finite") + rejected_envelope["payload"] = {"mapped": {"nested": [math.inf]}} + rejected = dispatch(bundle, handled["state"], {"input": rejected_envelope}) + assert rejected["rejection"] == {"code": "invalid_payload"} + assert rejected["state"] is handled["state"] + + malformed_state = copy.deepcopy(handled["state"]) + root = malformed_state["runtimes"][malformed_state["root_runtime_id"]] + root["scopes"]["root"]["stored_map"]["nested"][0] = math.nan + next_step = malformed_state["next_logical_step_sequence"] + rejected = dispatch(bundle, malformed_state) + assert rejected["rejection"] == {"code": "invalid_prior_state"} + assert rejected["state"] is malformed_state + assert malformed_state["next_logical_step_sequence"] == next_step + retained = malformed_state["runtimes"][root["runtime_id"]]["scopes"]["root"] + assert math.isnan(retained["stored_map"]["nested"][0]) + + +def test_env_changed_is_normalized_before_guard_and_refresh() -> None: + bundle = load_bundle(NUMERIC_BOUNDARY_BUNDLE) + state = create( + bundle, + "numeric_boundary", + "numeric-env", + "numeric-create", + { + "input": {"created_map": {}}, + "external": {"rate": 0}, + }, + )["state"] + root = state["runtimes"][state["root_runtime_id"]] + root["scopes"]["root"]["created_map"] = {"nested": [-0.0]} + envelope = _envelope(state, "env", "env-change") + envelope["payload"] = {"changed": {"rate": 1}} + envelope_snapshot = copy.deepcopy(envelope) + + result = dispatch(bundle, state, {"input": envelope}) + + assert envelope == envelope_snapshot + values = _root_variables(result["state"]) + assert values["guard_selected"] is True + assert type(values["rate"]) is float + assert values["rate"] == 1.0 + assert math.copysign(1.0, values["created_map"]["nested"][0]) == 1.0 + assert math.copysign(1.0, _root_variables(state)["created_map"]["nested"][0]) == -1.0 + + +def test_holder_association_survives_reference_clear_and_reuse() -> None: + bundle = load_bundle(HOLDER_REUSE_BUNDLE) + state = create(bundle, "owner", "holder-owner", "holder-create", {})["state"] + prepared = dispatch( + bundle, + state, + {"input": _envelope(state, "prepare", "prepare")}, + ) + prepared_state = prepared["state"] + children = [ + runtime + for runtime in prepared_state["runtimes"].values() + if runtime["role"] == "spawned" + ] + assert _root_variables(prepared_state)["child_reference"] is None + assert len(children) == 2 + assert {child["holder"]["pointer"] for child in children} == { + "/machines/0/root/states/holding/variables/child_reference" + } + + left = dispatch( + bundle, + prepared_state, + {"input": _envelope(prepared_state, "leave", "leave")}, + ) + + assert left["disposition"] == "handled" + assert len(left["state"]["runtimes"]) == 1 + assert _root_variables(left["state"]) == {} + + def test_cyclic_payload_is_rejected_without_recursion() -> None: bundle = load_bundle(COUNTER_BUNDLE) prior = create(bundle, "counter", "counter-1", "create-1", {})["state"]