diff --git a/.gitattributes b/.gitattributes index 12af7521..d55dac89 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,3 +12,7 @@ tools/extension-qualification/Cargo.toml text eol=lf tools/extension-qualification/*.rs text eol=lf tools/extension-qualification/src/*.rs text eol=lf tools/extension-qualification/src/bin/*.rs text eol=lf + +# Golden model outputs are digested bytes; never convert their line endings. +rust/crates/morphir-python-binding/tests/golden/* -text +rust/crates/morphir-python-binding/tests/fixtures/** text eol=lf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa000028..078ead60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,11 @@ repos: rev: v5.0.0 hooks: - id: trailing-whitespace + # Golden payloads are exact digested bytes with no final newline. + exclude: ^rust/crates/morphir-python-binding/tests/golden/ - id: end-of-file-fixer + # Golden payloads are exact digested bytes with no final newline. + exclude: ^rust/crates/morphir-python-binding/tests/golden/ - id: check-yaml - id: check-toml - id: check-added-large-files diff --git a/openspec/changes/qualify-mep-model-line/.openspec.yaml b/openspec/changes/qualify-mep-model-line/.openspec.yaml new file mode 100644 index 00000000..e6331266 --- /dev/null +++ b/openspec/changes/qualify-mep-model-line/.openspec.yaml @@ -0,0 +1,2 @@ +schema: html-design +created: 2026-10-31 diff --git a/openspec/changes/qualify-mep-model-line/design.html b/openspec/changes/qualify-mep-model-line/design.html new file mode 100644 index 00000000..f2014772 --- /dev/null +++ b/openspec/changes/qualify-mep-model-line/design.html @@ -0,0 +1,83 @@ + + + + + +qualify-mep-model-line: design + + + + + + + + +
+

qualify-mep-model-line: design

+

OpenSpec change · Bead morphir-ukvn.1.6.9 · 2026-10-31 · in progress

+ + + +

Context

+

See proposal.md, "Why". The model line is in model_transport.rs on morphir-extension-sdk 0.4.0. The SDK selects the intersection of the host offer and the guest support in the host's order, and the guest admits each call against the session tuples. These facts were true in extension/python/v0.5.0:

+ + + + + + + + +
Item0.5.0
Protocol versions0.2.0-draft.1, 0.1
Claims version0.1.0-draft.2
Model tuplescompileModel and generateModel × morphir-ir / morphir-json / 0.1.0-draft.1 / IR 3.0.0 and 4.0.0
IonNot offered. Ion evidence is refused on WASM and native alike.
+ +

Goals and non-goals

+ + +

Decisions

+ +

TST-1: golden files hold the exact payload bytes

+

The envelope golden file replaces payload.content with a placeholder, and a second file holds the decoded payload bytes as the guest wrote them. The SDK decodes the payload only after it checks length and SHA-256, so the payload file's digest equals the envelope digest. Pretty-printed IR 3 was 215 KB against 18 KB of exact bytes, and it would not catch a change in serialization. The files are -text in .gitattributes and excluded from the whitespace hooks.

+

Set MORPHIR_UPDATE_GOLDEN=1 to write them again after an intended change.

+ +

TST-2: run golden outputs through a negotiated session

+

Calling compile_model directly skips the SDK's session admission. The golden test sends JSON-RPC messages to a session that a host initialized with all four tuples, so it also covers selection, admission and serialization of the result.

+ +

QUAL-1: qualify without changing finos/morphir-moonbit

+

The host's Python suite pins 0.4.1 and 0.4.2 and checks MEP 0.1 only. A separate script outside both repositories drives the CLIs built from a clean main checkout with the same contract 0.2.0 WASM pin shape, the same environments (no Node on the native CLI's PATH) and the same checks as the host's Elm model-line suite. The receipt records the CLI program digests and the commit, which prove that the host is unchanged.

+ +

REL-1: release 0.6.0 as a minor version

+

The protocol surface is the same as in 0.5.0, but the compiler now builds with Ruff 0.0.20 and current dependencies. That can change parsing or generated text, so it is a new minor version, not a patch. The extension tag builds its native wheel from source in the same run, so it needs no native or CLI release.

+ +

Risks and trade-offs

+ + +

Migration plan

+

No migration. Hosts on MEP 0.1 keep working: the finos/morphir Rust host pins extension/python/v0.4.2, and the 0.1 suite repeats for 0.4.1 and 0.4.2. To roll back, hosts keep or restore their 0.5.0 pin; released bytes do not change.

+
+ + diff --git a/openspec/changes/qualify-mep-model-line/proposal.md b/openspec/changes/qualify-mep-model-line/proposal.md new file mode 100644 index 00000000..4bf676e9 --- /dev/null +++ b/openspec/changes/qualify-mep-model-line/proposal.md @@ -0,0 +1,32 @@ +# Proposal + +## Why + +Every Morphir extension must be on the latest MEP, `0.2.0-draft.1`. The Python extension `0.5.0` already declares it, with `0.1` kept, and serves `morphir.frontend.compileModel` and `morphir.backend.generateModel` for the tuples `morphir-ir` / `morphir-json` / envelope `0.1.0-draft.1` / IR `3.0.0` and `4.0.0`. Three gaps remain. No test shows the guest agreeing on JSON when the host's default is Ion but the host also offers JSON (the negotiation rule that finos/morphir-gleam#31 tests). The model operations have no golden outputs. The MoonBit worker host (finos/morphir-moonbit `main`) qualified Python `0.4.1` and `0.4.2` on MEP `0.1` only, not the model line. + +## What Changes + +- Add session tests: host that prefers Ion but also offers JSON; host that offers only Ion; compile admission that does not grant generation; wrong envelope version in each direction; the process executable serving both model operations and stopping by itself. +- Add golden outputs for `compileModel` (envelope and exact payload bytes) and `generateModel` (artifacts) for IR `3.0.0` and `4.0.0`, through a real negotiated session. +- Add the host-default scenario to `python/model-transport`. +- Qualify the candidate on the MoonBit worker host from the native and npm CLIs: `extension describe`, `compile` and `generate` on the model line with a contract `0.2.0` pin, recompile of generated sources, and worker shutdown. Repeat the existing `0.1`-only suite for `0.4.1` and `0.4.2`. +- Release `extension/python/v0.6.0` with the current dependencies (Ruff `0.0.20`). No protocol, claim or tuple changes. No Ion. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `python/model-transport`: the admission requirement gains a scenario for a host whose default format the guest does not serve. + +## Beads + +- **Tracked by**: `morphir-ukvn.1.6.9` +- **Related**: `morphir-5hxy` (delivered the model line in `0.5.0`); `morphir-1q2c` (MEP spec: model-format default and required rule); `morphir-hv6m` (MCK check of model-format selection order); `morphir-ukvn.5.64.36.7` (MoonBit worker host qualification of published extensions). + +## Impact + +Tests and golden files in `rust/crates/morphir-python-binding` and `rust/crates/morphir-python-extension`, the compiler crate version (`0.6.0`), `.gitattributes` and pre-commit excludes for byte-exact golden files, and the extension documentation after release. finos/morphir keeps its `extension/python/v0.4.2` pin; its bundle pins move in a separate PR. Merge and tag need maintainer approval. diff --git a/openspec/changes/qualify-mep-model-line/specs/python/model-transport/spec.md b/openspec/changes/qualify-mep-model-line/specs/python/model-transport/spec.md new file mode 100644 index 00000000..64ed8726 --- /dev/null +++ b/openspec/changes/qualify-mep-model-line/specs/python/model-transport/spec.md @@ -0,0 +1,20 @@ +## MODIFIED Requirements + +### Requirement: Model operation admission is bilateral +Each model operation SHALL require an initialized, advertised tuple matching operation, profile, codec, envelope version and semantic IR version. Session tuples and limits MUST remain within both peers' offers and permitted static claims. Unknown required guarantees and unadmitted tuples MUST be refused. (bead: morphir-5hxy) + +#### Scenario: Compile support does not grant generation +- **WHEN** only a compile tuple was admitted and the host requests model generation +- **THEN** the operation is refused without invoking the generator + +#### Scenario: Independent lists would produce an unsupported combination +- **WHEN** an operation combines otherwise known tuple members in a combination not admitted as a complete tuple +- **THEN** the extension refuses that combination rather than forming a Cartesian product + +#### Scenario: Host default not supported +- **WHEN** the host offers an Ion tuple first, as its default, and also offers a JSON tuple this extension serves, without requiring Ion +- **THEN** the session agrees on the JSON tuple in the host's order, and the model operations for that tuple succeed + +#### Scenario: Host offers only formats the extension does not serve +- **WHEN** the host offers only Ion tuples +- **THEN** the session agrees on no model tuple, model operations are refused, no payload is converted to JSON, and the MEP `0.1` operations stay available diff --git a/openspec/changes/qualify-mep-model-line/tasks.md b/openspec/changes/qualify-mep-model-line/tasks.md new file mode 100644 index 00000000..a34e0899 --- /dev/null +++ b/openspec/changes/qualify-mep-model-line/tasks.md @@ -0,0 +1,31 @@ +# Tasks + +Qualification receipts for 3.1 and 3.2 (finos/morphir-moonbit `main` 40623b8, macOS arm64) are on bead `morphir-ukvn.1.6.9.3`: the `0.1` suite passed 32 of 32 rows, and the candidate model line passed 32 of 32 rows for the WASM and process guests from both CLIs. The host refuses IR 3 on the model line; see design.html, Risks. + +## 1. Test negotiation, golden outputs and refusals (morphir-ukvn.1.6.9.1) + +- [x] 1.1 Add session tests for an Ion-preferring host that also offers JSON, an Ion-only host, compile-only admission and a wrong envelope version; verify `cargo test -p morphir-python-binding --test model_session` passes. +- [x] 1.2 Add golden outputs for `compileModel` and `generateModel` for IR `3.0.0` and `4.0.0` through a negotiated session, with byte-exact payload files kept out of line-ending conversion and end-of-file hooks; verify `cargo test -p morphir-python-binding --test model_golden` passes and each payload file's SHA-256 equals its envelope digest. +- [x] 1.3 Add a process test that runs both model operations in a `0.2.0-draft.1` session and checks that the process stops by itself; verify `cargo test -p morphir-python-extension` passes. + +## 2. Specify and document (morphir-ukvn.1.6.9.2) + +- [x] 2.1 Add the host-default and Ion-only scenarios to `python/model-transport`; verify `openspec validate qualify-mep-model-line --strict` passes. +- [x] 2.2 Set the compiler crate version to `0.6.0`; verify `mise run rust:check` and `mise run check` pass. + +## 3. Qualify on the MoonBit worker host (morphir-ukvn.1.6.9.3) + +- [x] 3.1 Repeat the `0.1`-only Python suite of finos/morphir-moonbit `main` (`mise run test:cli-extism-python`) for `0.4.1` and `0.4.2`; verify every row passes from the native and npm CLIs. +- [x] 3.2 Run `extension describe`, model-line `compile` (IR 4), `generate`, recompile and a consumer check of the generated Python for the candidate WASM with a contract `0.2.0` pin from the native and npm CLIs; verify protocol `0.2.0-draft.1`, the JSON tuple, equal outputs across CLIs and `exited-after-exit` worker shutdown, and record the receipt on the epic bead. + +## 4. Release (morphir-ukvn.1.6.9.4) + +- [ ] 4.1 Open the PR and get CI green; check and handle each review finding. +- [ ] 4.2 Run the Release workflow manually on the candidate branch; verify every required job passes and no release is published. +- [ ] 4.3 After maintainer approval, merge, push `extension/python/v0.6.0` at the reviewed commit and verify the published assets, attestations, `release.json` descriptors and `.sha256` files; requalify the published WASM on the MoonBit worker host. +- [ ] 4.4 Point the extension documentation and `site/integrations.json` at `0.6.0` with its published digests; verify `mise run docs:test`. + +## Workflow follow-up + +- Archive the change after the release and the documentation update merge. +- Damian's main session moves the finos/morphir bundle pins in a separate PR. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 46561a06..70eb8eb3 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1451,7 +1451,7 @@ dependencies = [ [[package]] name = "morphir-python-binding" -version = "0.5.0" +version = "0.6.0" dependencies = [ "cucumber", "morphir-core", diff --git a/rust/crates/morphir-python-binding/Cargo.toml b/rust/crates/morphir-python-binding/Cargo.toml index 1be1816c..94132e29 100644 --- a/rust/crates/morphir-python-binding/Cargo.toml +++ b/rust/crates/morphir-python-binding/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "morphir-python-binding" -version = "0.5.0" +version = "0.6.0" edition.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/rust/crates/morphir-python-binding/tests/golden/compile-model-ir3.json b/rust/crates/morphir-python-binding/tests/golden/compile-model-ir3.json new file mode 100644 index 00000000..faad7b21 --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/golden/compile-model-ir3.json @@ -0,0 +1,22 @@ +{ + "model": { + "envelopeVersion": "0.1.0-draft.1", + "modelId": "acme", + "revision": "sha256:01d40fe60c313fd0de6114693531c412366b0d4eb704697bdce4850158e24b72", + "irVersion": "3.0.0", + "profile": "morphir-ir", + "payload": { + "codec": "morphir-json", + "layout": "single-document", + "decodedByteLength": 18206, + "sha256": "01d40fe60c313fd0de6114693531c412366b0d4eb704697bdce4850158e24b72", + "content": "" + }, + "evidence": [] + }, + "diagnostics": [], + "modules": [ + "acme/functions", + "acme/models" + ] +} diff --git a/rust/crates/morphir-python-binding/tests/golden/compile-model-ir3.payload.json b/rust/crates/morphir-python-binding/tests/golden/compile-model-ir3.payload.json new file mode 100644 index 00000000..26870996 --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/golden/compile-model-ir3.payload.json @@ -0,0 +1 @@ +{"formatVersion":3,"distribution":["Library",[["acme"]],[],{"modules":[[[["acme"],["functions"]],{"access":"Public","value":{"types":[],"values":[[["apply"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["transform"],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],"body":["Apply",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Variable",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["transform"]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]]}}}],[["capture"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],"body":["Lambda",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["AsPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WildcardPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["ignored"]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]]}}}],[["choose"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["flag"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]]],[["first"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],[["second"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],"body":["IfThenElse",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["flag"]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["first"]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["second"]]]}}}],[["conditional"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["flag"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]]]],"outputType":["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],"body":["IfThenElse",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["flag"]],["Reference",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],[[["acme"]],[["acme"],["functions"]],["identity"]]],["Lambda",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["AsPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WildcardPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["item"]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["item"]]]]}}}],[["constant"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[],"outputType":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],"body":["Literal",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WholeNumberLiteral",42]]}}}],[["curry"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["flag"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]]]],"outputType":["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"body":["Lambda",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],["AsPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WildcardPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["first"]],["Lambda",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["AsPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WildcardPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["second"]],["IfThenElse",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["flag"]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["first"]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["second"]]]]]}}}],[["forward"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],"body":["Apply",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],[[["acme"]],[["acme"],["functions"]],["later"]]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]]}}}],[["identity"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],"body":["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]}}}],[["immediate"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],"body":["Apply",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Lambda",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["AsPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WildcardPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["item"]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["item"]]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]]}}}],[["later"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],"body":["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]}}}],[["read","constant"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],"body":["Reference",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],[[["acme"]],[["acme"],["functions"]],["constant"]]]}}}],[["recurse"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["flag"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]]],[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],"body":["IfThenElse",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["flag"]],["Apply",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Apply",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["Reference",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],[[["acme"]],[["acme"],["functions"]],["recurse"]]],["Literal",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["BoolLiteral",false]]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]]}}}],[["run"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],"body":["Apply",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Apply",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["Apply",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],["Reference",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]]],[[["acme"]],[["acme"],["functions"]],["choose"]]],["Literal",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["BoolLiteral",true]]],["Apply",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Apply",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["Reference",["Function",{},["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],[[["acme"]],[["acme"],["functions"]],["apply"]]],["Lambda",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["AsPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WildcardPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["item"]],["Apply",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],[[["acme"]],[["acme"],["functions"]],["identity"]]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["item"]]]]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]]],["Apply",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Apply",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["Apply",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],["Reference",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]]],[[["acme"]],[["acme"],["functions"]],["curry"]]],["Literal",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]],["BoolLiteral",false]]],["Literal",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WholeNumberLiteral",1]]],["Literal",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WholeNumberLiteral",2]]]]}}}],[["shadow"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],"body":["Lambda",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["AsPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WildcardPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["value"]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]]}}}],[["tupled"],{"access":"Public","value":{"doc":"","value":{"inputTypes":[[["value"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"outputType":["Tuple",{},[["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],"body":["Tuple",["Tuple",{},[["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]]],[["Lambda",["Function",{},["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["AsPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["WildcardPattern",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]],["ignored"]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]],["Variable",["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]],["value"]]]]}}}]],"doc":null}}],[[["acme"],["models"]],{"access":"Public","value":{"types":[[["address"],{"access":"Public","value":{"doc":"","value":["TypeAliasDefinition",[],["Record",{},[{"name":["street"],"tpe":["Reference",{},[[["morphir"],["s","d","k"]],[["string"]],["string"]],[]]},{"name":["zip","code"],"tpe":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["int"]],[]]}]]]}}],[["application"],{"access":"Public","value":{"doc":"","value":["TypeAliasDefinition",[],["Record",{},[{"name":["decision"],"tpe":["Reference",{},[[["acme"]],[["acme"],["models"]],["decision"]],[]]},{"name":["verified"],"tpe":["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["bool"]],[]]},{"name":["coordinates"],"tpe":["Tuple",{},[["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["float"]],[]],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["float"]],[]]]]}]]]}}],[["decision"],{"access":"Public","value":{"doc":"","value":["CustomTypeDefinition",[],{"access":"Public","value":[[["pending"],[]],[["approved"],[[["amount"],["Reference",{},[[["morphir"],["s","d","k"]],[["basics"]],["float"]],[]]],[["address"],["Reference",{},[[["acme"]],[["acme"],["models"]],["address"]],[]]]]]]}]}}]],"values":[],"doc":null}}]]}]} \ No newline at end of file diff --git a/rust/crates/morphir-python-binding/tests/golden/compile-model-ir4.json b/rust/crates/morphir-python-binding/tests/golden/compile-model-ir4.json new file mode 100644 index 00000000..b52c0f74 --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/golden/compile-model-ir4.json @@ -0,0 +1,22 @@ +{ + "model": { + "envelopeVersion": "0.1.0-draft.1", + "modelId": "acme", + "revision": "sha256:96647cf9745ad08392a039c2b7445380568b0ba17f5173dc2e18b586906365b0", + "irVersion": "4.0.0", + "profile": "morphir-ir", + "payload": { + "codec": "morphir-json", + "layout": "single-document", + "decodedByteLength": 5955, + "sha256": "96647cf9745ad08392a039c2b7445380568b0ba17f5173dc2e18b586906365b0", + "content": "" + }, + "evidence": [] + }, + "diagnostics": [], + "modules": [ + "acme/functions", + "acme/models" + ] +} diff --git a/rust/crates/morphir-python-binding/tests/golden/compile-model-ir4.payload.json b/rust/crates/morphir-python-binding/tests/golden/compile-model-ir4.payload.json new file mode 100644 index 00000000..2065524b --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/golden/compile-model-ir4.payload.json @@ -0,0 +1 @@ +{"formatVersion":4,"distribution":{"Library":{"packageName":"acme","dependencies":{},"def":{"modules":{"acme/functions":{"Public":{"types":{},"values":{"apply":{"Public":{"ExpressionBody":{"inputTypes":{"transform":{"Function":{"parameterType":"morphir/SDK:basics#int","returnType":"morphir/SDK:basics#int"}},"value":"morphir/SDK:basics#int"},"outputType":"morphir/SDK:basics#int","body":{"Apply":{"function":{"Variable":"transform"},"argument":{"Variable":"value"}}}}}},"capture":{"Public":{"ExpressionBody":{"inputTypes":{"value":"morphir/SDK:basics#int"},"outputType":{"Function":{"parameterType":"morphir/SDK:basics#int","returnType":"morphir/SDK:basics#int"}},"body":{"Lambda":{"pattern":{"AsPattern":{"pattern":{"WildcardPattern":{}},"name":"ignored"}},"body":{"Variable":"value"}}}}}},"choose":{"Public":{"ExpressionBody":{"inputTypes":{"flag":"morphir/SDK:basics#bool","first":"morphir/SDK:basics#int","second":"morphir/SDK:basics#int"},"outputType":"morphir/SDK:basics#int","body":{"IfThenElse":{"condition":{"Variable":"flag"},"then":{"Variable":"first"},"else":{"Variable":"second"}}}}}},"conditional":{"Public":{"ExpressionBody":{"inputTypes":{"flag":"morphir/SDK:basics#bool"},"outputType":{"Function":{"parameterType":"morphir/SDK:basics#int","returnType":"morphir/SDK:basics#int"}},"body":{"IfThenElse":{"condition":{"Variable":"flag"},"then":{"Reference":"acme:acme/functions#identity"},"else":{"Lambda":{"pattern":{"AsPattern":{"pattern":{"WildcardPattern":{}},"name":"item"}},"body":{"Variable":"item"}}}}}}}},"constant":{"Public":{"ExpressionBody":{"inputTypes":{},"outputType":"morphir/SDK:basics#int","body":{"Literal":{"IntegerLiteral":42}}}}},"curry":{"Public":{"ExpressionBody":{"inputTypes":{"flag":"morphir/SDK:basics#bool"},"outputType":{"Function":{"parameterType":"morphir/SDK:basics#int","returnType":{"Function":{"parameterType":"morphir/SDK:basics#int","returnType":"morphir/SDK:basics#int"}}}},"body":{"Lambda":{"pattern":{"AsPattern":{"pattern":{"WildcardPattern":{}},"name":"first"}},"body":{"Lambda":{"pattern":{"AsPattern":{"pattern":{"WildcardPattern":{}},"name":"second"}},"body":{"IfThenElse":{"condition":{"Variable":"flag"},"then":{"Variable":"first"},"else":{"Variable":"second"}}}}}}}}}},"forward":{"Public":{"ExpressionBody":{"inputTypes":{"value":"morphir/SDK:basics#int"},"outputType":"morphir/SDK:basics#int","body":{"Apply":{"function":{"Reference":"acme:acme/functions#later"},"argument":{"Variable":"value"}}}}}},"identity":{"Public":{"ExpressionBody":{"inputTypes":{"value":"morphir/SDK:basics#int"},"outputType":"morphir/SDK:basics#int","body":{"Variable":"value"}}}},"immediate":{"Public":{"ExpressionBody":{"inputTypes":{"value":"morphir/SDK:basics#int"},"outputType":"morphir/SDK:basics#int","body":{"Apply":{"function":{"Lambda":{"pattern":{"AsPattern":{"pattern":{"WildcardPattern":{}},"name":"item"}},"body":{"Variable":"item"}}},"argument":{"Variable":"value"}}}}}},"later":{"Public":{"ExpressionBody":{"inputTypes":{"value":"morphir/SDK:basics#int"},"outputType":"morphir/SDK:basics#int","body":{"Variable":"value"}}}},"read-constant":{"Public":{"ExpressionBody":{"inputTypes":{"value":"morphir/SDK:basics#int"},"outputType":"morphir/SDK:basics#int","body":{"Reference":"acme:acme/functions#constant"}}}},"recurse":{"Public":{"ExpressionBody":{"inputTypes":{"flag":"morphir/SDK:basics#bool","value":"morphir/SDK:basics#int"},"outputType":"morphir/SDK:basics#int","body":{"IfThenElse":{"condition":{"Variable":"flag"},"then":{"Apply":{"function":{"Apply":{"function":{"Reference":"acme:acme/functions#recurse"},"argument":{"Literal":{"BoolLiteral":false}}}},"argument":{"Variable":"value"}}},"else":{"Variable":"value"}}}}}},"run":{"Public":{"ExpressionBody":{"inputTypes":{"value":"morphir/SDK:basics#int"},"outputType":"morphir/SDK:basics#int","body":{"Apply":{"function":{"Apply":{"function":{"Apply":{"function":{"Reference":"acme:acme/functions#choose"},"argument":{"Literal":{"BoolLiteral":true}}}},"argument":{"Apply":{"function":{"Apply":{"function":{"Reference":"acme:acme/functions#apply"},"argument":{"Lambda":{"pattern":{"AsPattern":{"pattern":{"WildcardPattern":{}},"name":"item"}},"body":{"Apply":{"function":{"Reference":"acme:acme/functions#identity"},"argument":{"Variable":"item"}}}}}}},"argument":{"Variable":"value"}}}}},"argument":{"Apply":{"function":{"Apply":{"function":{"Apply":{"function":{"Reference":"acme:acme/functions#curry"},"argument":{"Literal":{"BoolLiteral":false}}}},"argument":{"Literal":{"IntegerLiteral":1}}}},"argument":{"Literal":{"IntegerLiteral":2}}}}}}}}},"shadow":{"Public":{"ExpressionBody":{"inputTypes":{"value":"morphir/SDK:basics#int"},"outputType":{"Function":{"parameterType":"morphir/SDK:basics#int","returnType":"morphir/SDK:basics#int"}},"body":{"Lambda":{"pattern":{"AsPattern":{"pattern":{"WildcardPattern":{}},"name":"value"}},"body":{"Variable":"value"}}}}}},"tupled":{"Public":{"ExpressionBody":{"inputTypes":{"value":"morphir/SDK:basics#int"},"outputType":{"Tuple":[{"Function":{"parameterType":"morphir/SDK:basics#int","returnType":"morphir/SDK:basics#int"}},"morphir/SDK:basics#int"]},"body":{"Tuple":[{"Lambda":{"pattern":{"AsPattern":{"pattern":{"WildcardPattern":{}},"name":"ignored"}},"body":{"Variable":"value"}}},{"Variable":"value"}]}}}}}}},"acme/models":{"Public":{"types":{"address":{"Public":{"TypeAliasDefinition":{"typeParams":[],"typeExp":{"Record":{"fields":{"street":"morphir/SDK:string#string","zip-code":"morphir/SDK:basics#int"}}}}}},"application":{"Public":{"TypeAliasDefinition":{"typeParams":[],"typeExp":{"Record":{"fields":{"decision":"acme:acme/models#decision","verified":"morphir/SDK:basics#bool","coordinates":{"Tuple":["morphir/SDK:basics#float","morphir/SDK:basics#float"]}}}}}}},"decision":{"Public":{"CustomTypeDefinition":{"typeParams":[],"access":"Public","constructors":{"pending":[],"approved":[["amount","morphir/SDK:basics#float"],["address","acme:acme/models#address"]]}}}}},"values":{}}}}}}}} \ No newline at end of file diff --git a/rust/crates/morphir-python-binding/tests/golden/generate-model-ir3.json b/rust/crates/morphir-python-binding/tests/golden/generate-model-ir3.json new file mode 100644 index 00000000..18104600 --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/golden/generate-model-ir3.json @@ -0,0 +1,16 @@ +{ + "success": true, + "artifacts": [ + { + "path": "acme/functions.py", + "content": "from __future__ import annotations\nfrom dataclasses import dataclass\nfrom collections.abc import Callable\nimport acme.functions as morphir_module_1\n\n\ndef apply(transform: Callable[[int], int], value: int) -> int:\n return transform(value)\n\n\ndef capture(value: int) -> Callable[[int], int]:\n return lambda ignored: value\n\n\ndef choose(flag: bool, first: int, second: int) -> int:\n if flag:\n return first\n else:\n return second\n\n\ndef conditional(flag: bool) -> Callable[[int], int]:\n if flag:\n return morphir_module_1.identity\n else:\n return lambda item: item\n\n\ndef constant() -> int:\n return 42\n\n\ndef curry(flag: bool) -> Callable[[int], Callable[[int], int]]:\n return lambda first: lambda second: first if flag else second\n\n\ndef forward(value: int) -> int:\n return morphir_module_1.later(value)\n\n\ndef identity(value: int) -> int:\n return value\n\n\ndef immediate(value: int) -> int:\n return (lambda item: item)(value)\n\n\ndef later(value: int) -> int:\n return value\n\n\ndef read_constant(value: int) -> int:\n return morphir_module_1.constant()\n\n\ndef recurse(flag: bool, value: int) -> int:\n if flag:\n return morphir_module_1.recurse(False, value)\n else:\n return value\n\n\ndef run(value: int) -> int:\n return morphir_module_1.choose(True, morphir_module_1.apply(lambda item: morphir_module_1.identity(item), value), morphir_module_1.curry(False)(1)(2))\n\n\ndef shadow(value: int) -> Callable[[int], int]:\n return lambda value: value\n\n\ndef tupled(value: int) -> tuple[Callable[[int], int], int]:\n return lambda ignored: value, value\n", + "binary": false + }, + { + "path": "acme/models.py", + "content": "from __future__ import annotations\nfrom dataclasses import dataclass\nfrom collections.abc import Callable\n\n\n@dataclass(frozen=True)\nclass Address:\n street: str\n zip_code: int\n\n\n@dataclass(frozen=True)\nclass Application:\n decision: Decision\n verified: bool\n coordinates: tuple[float, float]\n\n\n@dataclass(frozen=True)\nclass Pending:\n pass\n\n\n@dataclass(frozen=True)\nclass Approved:\n amount: float\n address: Address\n\n\ntype Decision = Pending | Approved\n", + "binary": false + } + ], + "diagnostics": [] +} diff --git a/rust/crates/morphir-python-binding/tests/golden/generate-model-ir4.json b/rust/crates/morphir-python-binding/tests/golden/generate-model-ir4.json new file mode 100644 index 00000000..18104600 --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/golden/generate-model-ir4.json @@ -0,0 +1,16 @@ +{ + "success": true, + "artifacts": [ + { + "path": "acme/functions.py", + "content": "from __future__ import annotations\nfrom dataclasses import dataclass\nfrom collections.abc import Callable\nimport acme.functions as morphir_module_1\n\n\ndef apply(transform: Callable[[int], int], value: int) -> int:\n return transform(value)\n\n\ndef capture(value: int) -> Callable[[int], int]:\n return lambda ignored: value\n\n\ndef choose(flag: bool, first: int, second: int) -> int:\n if flag:\n return first\n else:\n return second\n\n\ndef conditional(flag: bool) -> Callable[[int], int]:\n if flag:\n return morphir_module_1.identity\n else:\n return lambda item: item\n\n\ndef constant() -> int:\n return 42\n\n\ndef curry(flag: bool) -> Callable[[int], Callable[[int], int]]:\n return lambda first: lambda second: first if flag else second\n\n\ndef forward(value: int) -> int:\n return morphir_module_1.later(value)\n\n\ndef identity(value: int) -> int:\n return value\n\n\ndef immediate(value: int) -> int:\n return (lambda item: item)(value)\n\n\ndef later(value: int) -> int:\n return value\n\n\ndef read_constant(value: int) -> int:\n return morphir_module_1.constant()\n\n\ndef recurse(flag: bool, value: int) -> int:\n if flag:\n return morphir_module_1.recurse(False, value)\n else:\n return value\n\n\ndef run(value: int) -> int:\n return morphir_module_1.choose(True, morphir_module_1.apply(lambda item: morphir_module_1.identity(item), value), morphir_module_1.curry(False)(1)(2))\n\n\ndef shadow(value: int) -> Callable[[int], int]:\n return lambda value: value\n\n\ndef tupled(value: int) -> tuple[Callable[[int], int], int]:\n return lambda ignored: value, value\n", + "binary": false + }, + { + "path": "acme/models.py", + "content": "from __future__ import annotations\nfrom dataclasses import dataclass\nfrom collections.abc import Callable\n\n\n@dataclass(frozen=True)\nclass Address:\n street: str\n zip_code: int\n\n\n@dataclass(frozen=True)\nclass Application:\n decision: Decision\n verified: bool\n coordinates: tuple[float, float]\n\n\n@dataclass(frozen=True)\nclass Pending:\n pass\n\n\n@dataclass(frozen=True)\nclass Approved:\n amount: float\n address: Address\n\n\ntype Decision = Pending | Approved\n", + "binary": false + } + ], + "diagnostics": [] +} diff --git a/rust/crates/morphir-python-binding/tests/model_golden.rs b/rust/crates/morphir-python-binding/tests/model_golden.rs new file mode 100644 index 00000000..fdaaa5ae --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/model_golden.rs @@ -0,0 +1,165 @@ +//! Golden outputs of the MEP `0.2.0-draft.1` model operations. +//! +//! Each model operation runs through a real SDK session negotiated with a host +//! that offers every tuple this extension serves. The envelope, the decoded IR +//! payload and the generated files must equal the files in `tests/golden/`. +//! Set `MORPHIR_UPDATE_GOLDEN=1` to write the files again after an intended +//! change, then review the difference. +use morphir_extension_sdk::{NativeExtension, NativeProtocol}; +use morphir_python_binding::PythonExtension; +use serde_json::{Value, json}; +use std::path::PathBuf; + +const ENVELOPE: &str = "0.1.0-draft.1"; + +fn send(protocol: &dyn NativeProtocol, request: Value) -> Value { + serde_json::to_value(protocol.handle(serde_json::from_value(request).unwrap())).unwrap() +} + +fn tuple(operation: &str, ir_version: &str) -> Value { + json!({"operation":operation,"profile":"morphir-ir","codec":"morphir-json", + "envelopeVersion":ENVELOPE,"irVersion":ir_version}) +} + +fn every_tuple() -> Vec { + [ + "morphir.frontend.compileModel", + "morphir.backend.generateModel", + ] + .into_iter() + .flat_map(|operation| ["3.0.0", "4.0.0"].map(|version| tuple(operation, version))) + .collect() +} + +fn session() -> std::sync::Arc { + let extension = NativeExtension::frontend_backend(PythonExtension).unwrap(); + let session = extension.open_protocol(); + let selected = send( + session.as_ref(), + json!({"jsonrpc":"2.0","id":1,"method":"morphir.initialize","params":{ + "protocolVersions":["0.2.0-draft.1","0.1"], + "host":{"name":"golden-test","version":"1"}, + "modelTransport":{"tuples":every_tuple(), + "limits":{"maxEncodedBytes":1048576,"maxDecodedBytes":786432,"maxDepth":128,"maxNodes":100000}, + "guarantees":[]}, + }}), + ); + assert_eq!( + selected["result"]["protocolVersion"], "0.2.0-draft.1", + "{selected}" + ); + assert_eq!( + selected["result"]["capabilities"]["modelTransport"]["tuples"], + json!(every_tuple()) + ); + session +} + +fn fixture(name: &str) -> String { + std::fs::read_to_string( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("tests/fixtures/{name}")), + ) + .unwrap() +} + +fn compile_model(session: &dyn NativeProtocol, ir_version: &str) -> Value { + let documents: Vec = ["models.py", "functions.py"] + .iter() + .map(|name| json!({"uri":format!("acme/{name}"),"languageId":"python","version":1,"text":fixture(name)})) + .collect(); + send( + session, + json!({"jsonrpc":"2.0","id":2,"method":"morphir.frontend.compileModel","params":{ + "languageId":"python","sources":{"documents":documents}, + "package":{"name":"acme"},"dependencies":[], + "options":{"irVersion":ir_version,"typesOnly":false},"contextDigest":"a".repeat(64), + "output":tuple("morphir.frontend.compileModel", ir_version), + }}), + ) +} + +fn generate_model(session: &dyn NativeProtocol, model: &Value) -> Value { + send( + session, + json!({"jsonrpc":"2.0","id":3,"method":"morphir.backend.generateModel","params":{ + "model":model,"target":"python","contextDigest":"a".repeat(64), + "requiredGuarantees":[],"options":{}, + "input":tuple("morphir.backend.generateModel", model["irVersion"].as_str().unwrap()), + }}), + ) +} + +/// Compares `actual` with the golden file, or writes it when asked to. +fn golden(name: &str, actual: &Value) { + golden_text( + name, + &format!("{}\n", serde_json::to_string_pretty(actual).unwrap()), + ); +} + +fn golden_text(name: &str, text: &str) { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/golden") + .join(name); + if std::env::var_os("MORPHIR_UPDATE_GOLDEN").is_some() { + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, text).unwrap(); + return; + } + let expected = std::fs::read_to_string(&path).unwrap_or_else(|_| { + panic!( + "missing {}; run with MORPHIR_UPDATE_GOLDEN=1", + path.display() + ) + }); + assert!(text == expected, "{name} differs from its golden file"); +} + +/// The exact decoded payload, after the SDK checks its length and digest. +fn payload(model: &Value) -> String { + use morphir_extension_sdk::model::{ModelTransportLimits, MorphirModel, retain_model}; + let envelope: MorphirModel = serde_json::from_value(model.clone()).unwrap(); + let input = + serde_json::from_value(tuple("morphir.backend.generateModel", &envelope.ir_version)) + .unwrap(); + let limits = ModelTransportLimits { + max_encoded_bytes: 1024 * 1024, + max_decoded_bytes: 1024 * 1024, + max_depth: 128, + max_nodes: 100_000, + }; + let retained = retain_model(&envelope, &input, &limits).unwrap(); + String::from_utf8(retained.bytes().to_vec()).unwrap() +} + +#[test] +fn compile_model_and_generate_model_match_their_golden_outputs() { + for (ir, version) in [("ir3", "3.0.0"), ("ir4", "4.0.0")] { + let session = session(); + let compiled = compile_model(session.as_ref(), version); + assert!(compiled.get("error").is_none(), "{}", compiled["error"]); + let result = &compiled["result"]; + assert_eq!(result["success"], true, "{}", result["diagnostics"]); + let model = &result["model"]; + assert_eq!(model["envelopeVersion"], ENVELOPE); + assert_eq!(model["irVersion"], version); + + let mut envelope = model.clone(); + envelope["payload"]["content"] = json!(""); + golden( + &format!("compile-model-{ir}.json"), + &json!({"model":envelope,"diagnostics":result["diagnostics"],"modules":result["modules"]}), + ); + // The payload golden file holds the exact bytes the envelope digests. + golden_text(&format!("compile-model-{ir}.payload.json"), &payload(model)); + + let generated = generate_model(session.as_ref(), model); + assert!(generated.get("error").is_none(), "{}", generated["error"]); + assert_eq!( + generated["result"]["success"], true, + "{}", + generated["result"]["diagnostics"] + ); + golden(&format!("generate-model-{ir}.json"), &generated["result"]); + } +} diff --git a/rust/crates/morphir-python-binding/tests/model_session.rs b/rust/crates/morphir-python-binding/tests/model_session.rs index f9a37c79..adfb53f8 100644 --- a/rust/crates/morphir-python-binding/tests/model_session.rs +++ b/rust/crates/morphir-python-binding/tests/model_session.rs @@ -87,3 +87,187 @@ fn legacy_or_other_drafts_cannot_admit_model_operations() { assert_eq!(refused["error"]["code"], -32011); } } + +fn tuple(operation: &str, profile: &str, codec: &str, ir_version: &str) -> Value { + json!({"operation":operation,"profile":profile,"codec":codec, + "envelopeVersion":"0.1.0-draft.1","irVersion":ir_version}) +} + +fn offer(tuples: Vec) -> Value { + json!({"tuples":tuples, + "limits":{"maxEncodedBytes":1048576,"maxDecodedBytes":786432,"maxDepth":128,"maxNodes":100000}, + "guarantees":[]}) +} + +const COMPILE: &str = "morphir.frontend.compileModel"; +const GENERATE: &str = "morphir.backend.generateModel"; + +fn ion(operation: &str) -> Value { + tuple(operation, "morphir-model", "ion-binary", "4.0.0") +} + +fn json_ir4(operation: &str) -> Value { + tuple(operation, "morphir-ir", "morphir-json", "4.0.0") +} + +fn generate_model(model: &Value) -> Value { + json!({"jsonrpc":"2.0","id":3,"method":GENERATE,"params":{ + "model":model,"target":"python","contextDigest":"a".repeat(64), + "requiredGuarantees":[],"options":{}, + "input":tuple(GENERATE, "morphir-ir", "morphir-json", model["irVersion"].as_str().unwrap()), + }}) +} + +/// The response is a JSON-RPC error with this code and message text. +fn assert_refused(response: &Value, code: i64, text: &str) { + assert!(response.get("result").is_none(), "admitted: {response}"); + assert_eq!(response["error"]["code"], code, "{}", response["error"]); + let message = response["error"]["message"].as_str().unwrap(); + assert!(message.contains(text), "{message}"); +} + +/// The host default is Ion, which this guest does not serve. The host also +/// offers JSON and does not require Ion, so the guest agrees on JSON, in the +/// host's order, and both model operations work (finos/morphir-gleam#31). +#[test] +fn guest_negotiates_json_when_the_host_prefers_ion_but_offers_both() { + let extension = NativeExtension::frontend_backend(PythonExtension).unwrap(); + let session = extension.open_protocol(); + let host = offer(vec![ + ion(COMPILE), + ion(GENERATE), + json_ir4(COMPILE), + json_ir4(GENERATE), + ]); + let selected = send( + session.as_ref(), + initialize(json!(["0.2.0-draft.1", "0.1"]), Some(host)), + ); + assert!(selected.get("error").is_none(), "{selected}"); + assert_eq!(selected["result"]["protocolVersion"], "0.2.0-draft.1"); + assert_eq!( + selected["result"]["capabilities"]["modelTransport"]["tuples"], + json!([json_ir4(COMPILE), json_ir4(GENERATE)]) + ); + let compiled = send(session.as_ref(), compile_model("4.0.0")); + assert!(compiled.get("error").is_none(), "{}", compiled["error"]); + assert_eq!( + compiled["result"]["model"]["payload"]["codec"], + "morphir-json" + ); + let generated = send( + session.as_ref(), + generate_model(&compiled["result"]["model"]), + ); + assert!(generated.get("error").is_none(), "{}", generated["error"]); + assert_eq!(generated["result"]["success"], true); +} + +/// A host that offers only Ion leaves no tuple this guest can serve. The +/// session starts with no model tuple, the guest never converts to JSON, and +/// the MEP `0.1` compile method still works in that session. +#[test] +fn a_host_that_offers_only_ion_gets_no_model_operation() { + let extension = NativeExtension::frontend_backend(PythonExtension).unwrap(); + let session = extension.open_protocol(); + let selected = send( + session.as_ref(), + initialize( + json!(["0.2.0-draft.1"]), + Some(offer(vec![ion(COMPILE), ion(GENERATE)])), + ), + ); + assert!(selected.get("error").is_none(), "{}", selected["error"]); + assert_eq!(selected["result"]["protocolVersion"], "0.2.0-draft.1"); + assert_eq!( + selected["result"]["capabilities"]["modelTransport"]["tuples"], + json!([]) + ); + let refused = send(session.as_ref(), compile_model("4.0.0")); + assert_refused(&refused, -32013, "not negotiated"); + let mut legacy = compile_model("4.0.0"); + legacy["method"] = json!("morphir.frontend.compile"); + legacy["params"]["options"]["irVersion"] = json!("4"); + for member in ["output", "contextDigest"] { + legacy["params"].as_object_mut().unwrap().remove(member); + } + let compiled = send(session.as_ref(), legacy); + assert!(compiled.get("error").is_none(), "{}", compiled["error"]); + assert_eq!(compiled["result"]["success"], true); +} + +/// Admission of a compile tuple does not admit generation of that model. +#[test] +fn compile_support_does_not_grant_generation() { + let extension = NativeExtension::frontend_backend(PythonExtension).unwrap(); + let session = extension.open_protocol(); + let selected = send( + session.as_ref(), + initialize( + json!(["0.2.0-draft.1"]), + Some(offer(vec![json_ir4(COMPILE)])), + ), + ); + assert!(selected.get("error").is_none(), "{selected}"); + let compiled = send(session.as_ref(), compile_model("4.0.0")); + assert!(compiled.get("error").is_none(), "{}", compiled["error"]); + let refused = send( + session.as_ref(), + generate_model(&compiled["result"]["model"]), + ); + assert_refused(&refused, -32013, "not negotiated"); + + // Positive control: the same request succeeds once generation is agreed. + let both = extension.open_protocol(); + send( + both.as_ref(), + initialize( + json!(["0.2.0-draft.1"]), + Some(offer(vec![json_ir4(COMPILE), json_ir4(GENERATE)])), + ), + ); + let generated = send(both.as_ref(), generate_model(&compiled["result"]["model"])); + assert!(generated.get("error").is_none(), "{}", generated["error"]); + assert_eq!(generated["result"]["success"], true); +} + +/// A model operation that names another envelope version is refused, in +/// both directions, even when every other tuple member was agreed. +#[test] +fn a_wrong_envelope_version_is_refused() { + let extension = NativeExtension::frontend_backend(PythonExtension).unwrap(); + let session = extension.open_protocol(); + send( + session.as_ref(), + initialize( + json!(["0.2.0-draft.1"]), + Some(offer(vec![json_ir4(COMPILE), json_ir4(GENERATE)])), + ), + ); + let mut wrong_output = compile_model("4.0.0"); + wrong_output["params"]["output"]["envelopeVersion"] = json!("0.2.0-draft.1"); + let refused = send(session.as_ref(), wrong_output); + assert_refused(&refused, -32602, "unsupported_tuple"); + + let compiled = send(session.as_ref(), compile_model("4.0.0")); + assert!(compiled.get("error").is_none(), "{}", compiled["error"]); + let model = &compiled["result"]["model"]; + // Positive control: the unchanged request is admitted in this session. + let generated = send(session.as_ref(), generate_model(model)); + assert!(generated.get("error").is_none(), "{}", generated["error"]); + assert_eq!(generated["result"]["success"], true); + let mut wrong_input = generate_model(model); + wrong_input["params"]["input"]["envelopeVersion"] = json!("0.2.0-draft.1"); + assert_refused( + &send(session.as_ref(), wrong_input), + -32602, + "unsupported_tuple", + ); + let mut wrong_model = generate_model(model); + wrong_model["params"]["model"]["envelopeVersion"] = json!("0.2.0-draft.1"); + assert_refused( + &send(session.as_ref(), wrong_model), + -32602, + "unsupported_tuple", + ); +} diff --git a/rust/crates/morphir-python-binding/tests/model_transport.rs b/rust/crates/morphir-python-binding/tests/model_transport.rs index 9ee74cb4..d0557d4f 100644 --- a/rust/crates/morphir-python-binding/tests/model_transport.rs +++ b/rust/crates/morphir-python-binding/tests/model_transport.rs @@ -141,6 +141,8 @@ fn malformed_envelopes_guarantees_and_unadvertised_forms_are_refused() { ("/model/payload/content", json!("not canonical base64")), ("/model/critical", json!(["future.required"])), ("/model/irVersion", json!("3.0.0")), + ("/model/envelopeVersion", json!("0.2.0-draft.1")), + ("/input/envelopeVersion", json!("0.2.0-draft.1")), ("/input/codec", json!("ion-binary")), ("/input/profile", json!("rich-model")), ("/input/operation", json!("morphir.frontend.compileModel")), @@ -160,6 +162,9 @@ fn malformed_envelopes_guarantees_and_unadvertised_forms_are_refused() { let mut request = a_request("4.0.0"); request.output.ir_version = "3.1.0".into(); assert!(PythonExtension.compile_model(request).is_err()); + let mut request = a_request("4.0.0"); + request.output.envelope_version = "0.2.0-draft.1".into(); + assert!(PythonExtension.compile_model(request).is_err()); } #[test] diff --git a/rust/crates/morphir-python-extension/Cargo.toml b/rust/crates/morphir-python-extension/Cargo.toml index 9c806c4c..25b3aa03 100644 --- a/rust/crates/morphir-python-extension/Cargo.toml +++ b/rust/crates/morphir-python-extension/Cargo.toml @@ -10,7 +10,7 @@ description = "Native stdio MEP guest for the Morphir Python compiler" publish = false [dependencies] -morphir-python-binding = { path = "../morphir-python-binding", version = "0.5.0" } +morphir-python-binding = { path = "../morphir-python-binding", version = "0.6.0" } morphir-extension-sdk.workspace = true serde.workspace = true serde_json = { workspace = true, features = ["raw_value"] } diff --git a/rust/crates/morphir-python-extension/tests/session.rs b/rust/crates/morphir-python-extension/tests/session.rs index 75085420..15a3d3ea 100644 --- a/rust/crates/morphir-python-extension/tests/session.rs +++ b/rust/crates/morphir-python-extension/tests/session.rs @@ -251,3 +251,71 @@ fn description_is_fixed_across_initialization_and_reinitialization_refusal() { assert_eq!(replies[2]["result"], replies[4]["result"]); assert_eq!(replies[3]["error"]["code"], -32600); } + +/// The released process runs both model operations in a `0.2.0-draft.1` +/// session, then stops by itself after `morphir.exit`. +#[test] +fn standalone_process_serves_the_model_line_and_exits_by_itself() { + let tuple = |operation: &str| { + json!({"operation":operation,"profile":"morphir-ir","codec":"morphir-json", + "envelopeVersion":"0.1.0-draft.1","irVersion":"4.0.0"}) + }; + let mut start = initialize(); + start["params"]["protocolVersions"] = json!(["0.2.0-draft.1", "0.1"]); + start["params"]["modelTransport"] = json!({ + "tuples":[tuple("morphir.frontend.compileModel"), tuple("morphir.backend.generateModel")], + "limits":{"maxEncodedBytes":1048576,"maxDecodedBytes":786432,"maxDepth":128,"maxNodes":100000}, + "guarantees":[], + }); + let compile = json!({"jsonrpc":"2.0","id":2,"method":"morphir.frontend.compileModel","params":{ + "languageId":"python", + "sources":{"documents":[{"uri":"acme/rules.py","languageId":"python","version":1, + "text":"def identity(flag: bool) -> bool:\n return flag\n"}]}, + "package":{"name":"acme"},"dependencies":[], + "options":{"irVersion":"4.0.0","typesOnly":false},"contextDigest":"a".repeat(64), + "output":tuple("morphir.frontend.compileModel"), + }}); + let session = |messages: &[Value]| { + let mut child = Command::new(env!("CARGO_BIN_EXE_morphir-python-extension")) + .env_clear() + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child + .stdin + .take() + .unwrap() + .write_all(&wire(messages)) + .unwrap(); + let result = child.wait_with_output().unwrap(); + assert!(result.status.success(), "{:?}", result.stderr); + assert!(result.stderr.is_empty()); + responses(result.stdout) + }; + let shutdown = json!({"jsonrpc":"2.0","id":9,"method":"morphir.shutdown","params":{}}); + let exit = json!({"jsonrpc":"2.0","method":"morphir.exit"}); + let first = session(&[start.clone(), compile, shutdown.clone(), exit.clone()]); + assert_eq!(first.len(), 3); + assert_eq!(first[0]["result"]["protocolVersion"], "0.2.0-draft.1"); + assert!(first[1].get("error").is_none(), "{}", first[1]["error"]); + let model = &first[1]["result"]["model"]; + assert_eq!(model["envelopeVersion"], "0.1.0-draft.1"); + + let generate = json!({"jsonrpc":"2.0","id":3,"method":"morphir.backend.generateModel","params":{ + "model":model,"target":"python","contextDigest":"a".repeat(64),"requiredGuarantees":[], + "options":{},"input":tuple("morphir.backend.generateModel"), + }}); + let second = session(&[start, generate, shutdown, exit]); + assert_eq!(second.len(), 3); + assert!(second[1].get("error").is_none(), "{}", second[1]["error"]); + let artifacts = &second[1]["result"]["artifacts"]; + assert_eq!(artifacts[0]["path"], "acme/rules.py"); + assert!( + artifacts[0]["content"] + .as_str() + .unwrap() + .contains("def identity(flag: bool) -> bool:") + ); +}