Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
69 changes: 69 additions & 0 deletions conformance/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import copy
import math
import os
from dataclasses import dataclass
from pathlib import Path
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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"),
Expand All @@ -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(
Expand Down Expand Up @@ -199,6 +231,7 @@ def _assert_result(
"rejection",
"fault",
"caller_still_owns_input",
"caller_still_owns_state",
"state",
"config",
"variables",
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion conformance/pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from pathlib import Path

CONFORMANCE_COMMIT = "fc4842010ab8d83bf4c5c6280a5627ca86829f7f"
CONFORMANCE_COMMIT = "ffbc65cbce49733803119a7dabf02a9727819ba8"
SPEC_COMMIT = "4bd4d9588d11b75d376380b6120676a056a4bc45"

ROOT = Path(__file__).resolve().parent.parent
Expand Down
2 changes: 1 addition & 1 deletion conformance/test_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
52 changes: 39 additions & 13 deletions src/determa/state/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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()
)


Expand Down
16 changes: 16 additions & 0 deletions src/determa/state/yaml12.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading