///|
pub(all) struct Plan {
unit : @data.IRUnit
suite : @protocol.Suite
library : @moonbit.Output
driver : Array[@moonbit.Artifact]
target : String
}
///|
/// Providers own executable storage; the engine never assumes a filesystem.
pub(all) struct InvocationResults {
values : Array[@protocol.Value]
evidence : Json
}
///|
pub(all) struct Session {
id : String
build : (Plan, @observe.TraceContext?) -> Json raise
invoke : (@protocol.Suite, @observe.TraceContext?) -> InvocationResults raise
dispose : () -> Unit raise
}
///|
pub(all) struct Provider {
targets : Array[String]
acquire : () -> Session raise
}
///|
pub(all) struct Evaluator {
profile : String
evaluate : (
@data.IRUnit,
@protocol.Call,
@protocol.ValueType,
Array[@protocol.CustomType],
) -> @protocol.Value raise
}
///|
pub(all) struct CallResult {
id : String
actual : @protocol.Value
expected : @protocol.Value?
matches : Bool
mismatch_path : String?
}
///|
pub(all) struct Report {
calls : Array[CallResult]
evidence : Json
invocation_evidence : Json
evaluator : String?
target : String
comparison : @protocol.ComparisonPolicy
extensions : Bytes?
terminals : Array[Terminal]
failure : Failure?
}
///|
pub fn Report::successful(self : Report) -> Bool {
self.failure is None &&
!self.terminals.is_empty() &&
self.terminals.all(t => t.status == "succeeded") &&
self.calls.length() == self.terminals.length() &&
self.calls.all(c => c.matches)
}
///|
pub fn Report::to_json(self : Report) -> Json {
{
"profile": "morphir-execution-v1",
"successful": self.successful(),
"target": self.target,
"targetStatus": self.status(),
"terminals": self.terminals.map(t => {
({ "id": t.id, "target": self.target, "status": t.status } : Json)
}),
"failure": self.failure.map(f => f.to_json()).unwrap_or(Json::null()),
"comparison": self.comparison.to_json(),
"extensions": match self.extensions {
Some(bytes) =>
{
"format": "ion-binary",
"bytes": bytes.to_array().map(b => b.to_int()),
}
None => Json::null()
},
"evaluator": match self.evaluator {
Some(s) => Json::string(s)
None => Json::null()
},
"evidence": self.evidence,
"invocationEvidence": self.invocation_evidence,
"calls": self.calls.map(c => {
(
{
"id": c.id,
"actual": c.actual.to_json(),
"expected": match c.expected {
Some(v) => v.to_json()
None => Json::null()
},
"matches": c.matches,
"mismatchPath": c.mismatch_path
.map(Json::string)
.unwrap_or(Json::null()),
} : Json)
}),
}
}
///|
/// Validation and pure generation occur before acquiring a provider session.
pub fn plan(
unit : @data.IRUnit,
suite : @protocol.Suite,
target~ : String = "js",
available_targets~ : Array[String] = ["js", "wasm-gc", "native"],
) -> Plan raise {
suite.validate()
if !["js", "wasm-gc", "native", "llvm"].contains(target) ||
!available_targets.contains(target) {
raise @protocol.Invalid("execution.unsupported_target: " + target)
}
let library = @moonbit.generate({
unit_id: unit.id,
ir: unit.distribution,
metadata: unit.metadata.map(m => { node_id: m.node_id, data: m.data, }),
annotations: unit.annotations,
provenance: unit.provenance,
})
validate_calls(library, suite)
{
unit,
suite,
library,
driver: @moonbit.invocation_driver(library, suite.calls.map(c => c.entry)),
target,
}
}
///|
fn validate_calls(
library : @moonbit.Output,
suite : @protocol.Suite,
) -> Unit raise {
@protocol.validate_invocations(
suite,
library.invocations.map(e => @protocol.PublicEntry::{
name: e.fqname,
inputs: e.inputs,
output: e.output,
}),
library.invocation_types,
rejections=library.invocation_rejections,
)
for call in suite.calls {
for argument in call.arguments {
validate_sdk_boundary(argument)
}
}
}
///|
pub fn Evaluator::scheme() -> Evaluator {
{
profile: "scheme-portable-v1/shared-sdk/bounded",
evaluate: fn(unit, call, expected, definitions) raise {
let runtime = @scheme.Runtime::new(limits={
steps: 1000000,
depth: 128,
frames: 100000,
})
@backend.install(runtime, accelerators=false, model_errors=true)
try {
let result = @backend.run_file_in(
runtime,
unit.distribution,
@ir.FQName::parse(call.entry),
call.arguments.map(scheme_input),
)
let value = scheme_output(result, expected, definitions, 0)
@protocol.validate_as(value, expected, definitions, output=true)
value
} catch {
@scheme.Domain(code, _) => {
let value = @protocol.Value::ModelError(code)
@protocol.validate_as(value, expected, definitions, output=true)
value
}
error => raise error
}
},
}
}
///|
pub fn run_report(
plan : Plan,
provider : Provider,
evaluator~ : Evaluator? = None,
observer~ : @observe.Observer = @observe.Observer::noop(),
cancelled~ : () -> Bool = fn() { false },
comparison~ : @protocol.ComparisonPolicy = @protocol.Exact,
) -> Report raise {
run_report_inner(
plan,
Ready(provider),
evaluator~,
observer~,
cancelled~,
comparison~,
)
}
///|
/// Produce the existing terminal report when cancellation precedes provider
/// capability discovery. No executable storage or provider is acquired.
pub fn cancelled_report(
plan : Plan,
evaluator~ : Evaluator? = None,
observer~ : @observe.Observer = @observe.Observer::noop(),
comparison~ : @protocol.ComparisonPolicy = @protocol.Exact,
) -> Report raise {
run_report_inner(plan, Cancelled, evaluator~, observer~, comparison~)
}
///|
priv enum ExecutionAccess {
Ready(Provider)
Cancelled
}
///|
fn run_report_inner(
plan : Plan,
access : ExecutionAccess,
evaluator~ : Evaluator? = None,
observer~ : @observe.Observer = @observe.Observer::noop(),
cancelled~ : () -> Bool = fn() { false },
comparison~ : @protocol.ComparisonPolicy = @protocol.Exact,
) -> Report raise {
let observer = observer.isolated(@observe.Health::new())
comparison.validate()
let targets = match access {
Ready(provider) => provider.targets
Cancelled => [plan.target]
}
if !targets.contains(plan.target) {
raise @protocol.Invalid("execution.provider_target")
}
let frozen = @protocol.Suite::decode(
plan.suite.encode("ion-binary"),
"ion-binary",
)
validate_calls(plan.library, frozen)
let source = @data.Registry::new().encode("ion-binary", plan.unit)
let frozen_unit = @data.Registry::new().decode("ion-binary", {
unit_id: plan.unit.id,
content: source,
})
let frozen_plan = freeze_plan(
frozen_unit,
frozen,
target=plan.target,
available_targets=targets,
)
if frozen_plan.library.artifacts != plan.library.artifacts ||
frozen_plan.driver != plan.driver {
raise @protocol.Invalid("execution.plan_changed")
}
let plan = frozen_plan
let artifacts = plan.library.artifacts.copy()
let driver = plan.driver.copy()
let suite_identity = frozen.encode("ion-binary")
let required_ids = frozen.calls.map(c => c.id)
let manifest = manifest_snapshot(plan)
let results : Array[CallResult] = []
let mut evidence = Json::null()
let mut invocation_evidence = Json::null()
let mut failure : Failure? = None
let mut phase = "acquire"
fn check() -> Unit raise {
if access is Cancelled || cancelled() {
raise @protocol.Invalid("Cancelled")
}
if @data.Registry::new().encode("ion-binary", plan.unit) != source ||
plan.library.artifacts != artifacts ||
plan.driver != driver ||
frozen.encode("ion-binary") != suite_identity ||
manifest_snapshot(plan) != manifest {
raise @protocol.Invalid("execution.input_mutation")
}
}
observer
.scope()
.observe(@observe.Execute, fn(scope) raise {
let mut session : Session? = None
let mut cause : Error? = None
try {
check()
let lease = match access {
Ready(provider) => (provider.acquire)()
Cancelled => raise @protocol.Invalid("Cancelled")
}
session = Some(lease)
if lease.id == "" {
raise @protocol.Invalid("execution.invalid_lease")
}
phase = "build"
evidence = scope.observe(@observe.Build, fn(child) raise {
check()
(lease.build)(plan, child.trace_context())
})
phase = "invoke"
let invocation = scope.observe(@observe.Invoke, fn(child) raise {
check()
(lease.invoke)(frozen, child.trace_context())
})
check()
invocation_evidence = invocation.evidence
let actual = invocation.values.copy()
if actual.length() != frozen.calls.length() {
raise @protocol.Invalid("execution.result_count")
}
for i = 0; i < frozen.calls.length(); i = i + 1 {
let call = frozen.calls[i]
let entry = plan.library.invocations.filter(e => e.fqname == call.entry)[0]
@protocol.validate_as(
actual[i],
entry.output,
plan.library.invocation_types,
output=true,
)
validate_sdk_boundary(actual[i])
actual[i] = @protocol.Value::from_ion(actual[i].to_ion())
results.push({
id: call.id,
actual: actual[i],
expected: None,
matches: false,
mismatch_path: None,
})
}
for i = 0; i < frozen.calls.length(); i = i + 1 {
check()
let call = frozen.calls[i]
let entry = plan.library.invocations.filter(e => e.fqname == call.entry)[0]
phase = "evaluate"
let expected = match evaluator {
Some(e) =>
Some(
scope.observe(
@observe.Evaluate,
fn(_) raise {
let value = (e.evaluate)(
plan.unit,
call,
entry.output,
plan.library.invocation_types,
)
@protocol.validate_as(
value,
entry.output,
plan.library.invocation_types,
output=true,
)
@protocol.Value::from_ion(value.to_ion())
},
details=Some({ entry: call.entry, code: "execution.evaluate", }),
),
)
None => None
}
phase = "compare"
let mismatch_path = scope.observe(@observe.Compare, fn(_) raise {
check()
match expected {
Some(value) =>
@protocol.mismatch_path(value, actual[i], policy=comparison)
None => None
}
})
results[i] = {
id: call.id,
actual: actual[i],
expected,
matches: mismatch_path is None,
mismatch_path,
}
}
check()
} catch {
error => cause = Some(error)
}
let mut cleanup : Error? = None
if session is Some(lease) {
scope.observe(@observe.Cleanup, fn(_) raise { (lease.dispose)() }) catch {
error => cleanup = Some(error)
}
}
if cause is None {
check() catch {
error => cause = Some(error)
}
}
if cause is Some(_) || cleanup is Some(_) {
failure = Some({
phase: if cause is None {
"cleanup"
} else {
phase
},
cause,
cleanup,
})
// The enclosing scope still observes a logical failure/cancellation.
if cause is Some(error) {
raise error
}
raise CleanupFailed(None, cleanup.unwrap())
}
}) catch {
_ => ()
}
let terminals = required_ids.map(id => {
let status = match results.filter(r => r.id == id).get(0) {
Some(r) =>
if r.matches {
"succeeded"
} else if r.mismatch_path is Some(_) {
"mismatch"
} else if failure is Some(f) && f.cancelled() {
"cancelled"
} else {
"failed"
}
None =>
if failure is Some(f) && f.cancelled() {
"cancelled"
} else {
"failed"
}
}
Terminal::{ id, status, }
})
{
calls: results,
evidence,
invocation_evidence,
evaluator: evaluator.map(e => e.profile),
target: plan.target,
comparison,
extensions: frozen.extensions,
terminals,
failure,
}
}
///|
/// Compatibility API preserves typed causes. Reporting callers use run_report.
pub fn run(
plan : Plan,
provider : Provider,
evaluator~ : Evaluator? = None,
observer~ : @observe.Observer = @observe.Observer::noop(),
cancelled~ : () -> Bool = fn() { false },
comparison~ : @protocol.ComparisonPolicy = @protocol.Exact,
) -> Report raise {
let report = run_report(
plan,
provider,
evaluator~,
observer~,
cancelled~,
comparison~,
)
if report.failure is Some(f) {
if f.cleanup is Some(error) {
raise CleanupFailed(f.cause, error)
}
raise f.cause.unwrap()
}
report
}
///|
fn freeze_plan(
unit : @data.IRUnit,
suite : @protocol.Suite,
target~ : String,
available_targets~ : Array[String],
) -> Plan raise {
plan(unit, suite, target~, available_targets~)
}
///|
fn manifest_snapshot(plan : Plan) -> String {
let entries = plan.library.invocations.map(e => {
(
{
"fqname": e.fqname,
"generated_name": e.generated_name,
"inputs": e.inputs.map(t => t.to_json()),
"output": e.output.to_json(),
"input_codecs": e.input_codecs,
"output_codec": e.output_codec,
} : Json)
})
let types = plan.library.invocation_types.map(t => {
(
{
"name": t.name,
"constructors": t.constructors.map(c => {
({ "name": c.name, "inputs": c.inputs.map(t => t.to_json()) } : Json)
}),
} : Json)
})
({ "entries": entries, "types": types } : Json).stringify()
}