diff --git a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/02-calculation.md b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/02-calculation.md index da5b6df3f1..60ef57d14b 100644 --- a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/02-calculation.md +++ b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/02-calculation.md @@ -3,13 +3,13 @@ | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| | Calc invocation with typed parameters | `invoke_calc.go` `InvokeCalc`/`invokeCalcShape` | `calc_simple_add.sysml` | ✅ Faithful | -| Result expression evaluation (the body's keyword-less trailing expression) | `invoke_calc.go` `calcBody`/`resultBindingExpr` (`calcShape.ResultExpr`) → `runCalcBody` + `eval.go` `Eval` | `calc_simple_add.sysml` | ✅ Faithful | -| Result as a bound return parameter (`return : T = ;`) | `invoke_calc.go` `resultBindingExpr` (`calcShape.ResultExpr`) | `calc_return_parameter.sysml` | ✅ Faithful | +| Result expression evaluation (the body's keyword-less trailing expression) | `lower/calc.go` `calcBodyWithOrder`/`resultBindingExpr` (`program/calc.go` `Calc.Result`) → `invoke_calc.go` `runCalcBody` + `eval.go` `Eval` | `calc_simple_add.sysml` | ✅ Faithful | +| Result as a bound return parameter (`return : T = ;`) | `lower/calc.go` `resultBindingExpr` (`program/calc.go` `Calc.Result`) | `calc_return_parameter.sysml` | ✅ Faithful | | Parameter binding (positional) | `invoke_calc.go` `bindCalcParameter` | `calc_simple_add.sysml` | ✅ Faithful | | Parameter binding (named arguments) | `eval.go` `evalInvocation` + `invoke_calc.go` `invokeCalcNamed` | `calc_named_arguments.sysml` | ✅ Faithful | | Parameter default when no argument is passed | `invoke_calc.go` `bindCalcParameter` | `calc_parameter_defaults.sysml` | ✅ Faithful | -| Parameters and result inherited through a typed calc usage | `invoke_calc.go` `calcShapeOf`/`calcChain`/`calcParameters` | `calc_inherited_parameters.sysml`, `calc_return_parameter.sysml` | ✅ Faithful | -| Redeclared parameter keeps its inherited position and default, which stays the expression the supertype wrote and is evaluated where that calc wrote it | `invoke_calc.go` `calcParameters` (the owner carried down with the inherited default) | `calc_return_parameter.sysml`, `calc_usage_nested_shadowed_input.sysml`, `calc_usage_nested_test.go:TestNestedCalcUsageInheritedDefaultOfRedeclaredInput` | ✅ Faithful | +| Parameters and result inherited through a typed calc usage | `invoke_calc.go` `calcShapeOf`; `lower/calc.go` `ToCalc`/`CalcChain`/`calcParameters` | `calc_inherited_parameters.sysml`, `calc_return_parameter.sysml` | ✅ Faithful | +| Redeclared parameter keeps its inherited position and default, which stays the expression the supertype wrote and is evaluated where that calc wrote it | `lower/calc.go` `calcParameters` (the owner carried down with the inherited default) | `calc_return_parameter.sysml`, `calc_usage_nested_shadowed_input.sysml`, `calc_usage_nested_test.go:TestNestedCalcUsageInheritedDefaultOfRedeclaredInput` | ✅ Faithful | | Nested calc invocation | `eval.go` `evalInvocation` → `invoke_calc.go` `invokeCalc` | `calc_nested_invocation.sysml` | ✅ Faithful | | Calc invoked from a constraint | `context.go` `EvaluateConstraint` → `eval.go` `evalInvocation` | `calc_from_constraint.sysml` | ✅ Faithful | | Deterministic evaluation trace (binding, sub-expression order, result) | `trace.go` `recordCalcEnter`/`recordCalcBind`/`endEval`, `eval.go` `Eval` | `*.trace.golden` via `TestExecutionTrace`, `trace_calc_test.go:TestCalcTraceIsStableAcrossRuns` | ✅ Faithful | @@ -28,20 +28,20 @@ | `ReturnParameterMember` (`:1961`) is `'return' UsageElement`, so a result may specialize without a name or a typing, its `Identification` may open with a short name, its `FeatureDeclaration` may open with a multiplicity, and a body may follow the identification directly — while a value or a body alone declares nothing: `return :> ISQ::power = e;`, `return r :> ISQ::speed = e;`, `return :> T[*] = e;`, `return result : T = e;`, `return :> T = e;`, `return ;`, `return [*] = xs;`, `return [*] :> xs;`, `return r { doc /* */ }`; `return = e;` and `return { … }` are reported once | `parser/behavior.go` `atReturnedUsage`, `parseResultMember` (`parser/namespace.go` `parseIdentification`, `parser/defusage.go` `parseRelationships`, `parseMultiplicity`) | `parse/calc_default_reference_usage.golden`, `behavior_test.go:TestParseResultMember_AnonymousAndBodiedForms`, `negative_test.go:calc_return_subsets_no_target`, `:calc_return_named_subsets_no_target`, `:calc_return_short_name_unclosed`, `:calc_return_short_name_empty`, `:calc_return_value_only`, `:calc_return_body_only`, `:calc_return_multiplicity_unclosed` | ✅ Faithful (the pinned validator accepts every form) | | Early `return` out of a branch or a loop unwinds the blocks entered | `lower/action_graph.go` `Return` + `runtime/statements.go` `flowReturn` | `calc_early_return_from_loop.sysml` | ✅ Faithful | | A body-local declaration of a branch or loop body is that block's own and does not leak | `runtime/statements.go` `stmtEnv` | `passes/typecheck_calc_body_test.go:TestCalcBodyLoopLocalIsNotVisibleOutside` | ✅ Faithful | -| An inherited body evaluates in the scope of the calculation declaring it | `invoke_calc.go` `calcBody` (specialization chain, as `calcParameters` and `resultBindingExpr`) + statement `Scope` carried by the lowered IR | `invoke_calc_body_test.go:TestInheritedCalcBodyRunsInDeclaringScope` | ✅ Faithful | +| An inherited body evaluates in the scope of the calculation declaring it | `lower/calc.go` `calcBodyWithOrder` (specialization chain, as `calcParameters` and `resultBindingExpr`) + statement `Scope` carried by the lowered IR | `invoke_calc_body_test.go:TestInheritedCalcBodyRunsInDeclaringScope` | ✅ Faithful | | A calculation is kept pure by the runtime: `send`, `accept`, `terminate` and an assignment to a feature it does not declare are rejected; a `perform` of an action with no effect is run | `runtime/calc_statements.go` (`runtime/errors.go` `errCalcSideEffect`, `errCalcExternalAssignment`) | `robustness_calc_invocation_test.go:testCalcSendIsRejected`, `:testCalcTerminateIsRejected`, `:testCalcAssignmentOutsideTheCalc` | ⚠️ Approximate (SysML §7.19.1 only recommends that a calculation "should be pure" and allows calculation and action usages as steps of the computation; KerML §7.4.8.1 allows a function side effects. The runtime refuses the four forms so that a calculation's value cannot depend on or alter the world outside it; the pinned pilot raises no diagnostic for an action step in a calc) | | A loop in a calculation terminates or fails: every iteration spends a step of the budget | `runtime/statements.go` `loop`/`forLoop` → `context.go` `incrementStep`; `invoke_calc.go` `InvokeCalc`/`invokeCalcNamed` `context.go` `beginRun` | `robustness_calc_invocation_test.go:testCalcNonTerminatingLoop`, `budget_test.go:TestStepBudgetIsPerRunForInstancesAndCalcs` | ✅ Faithful | -| A body running to its end without returning leaves the result parameter unbound, which is *no value* (`ErrNoValue`, the error an `out` the body never assigns reads as) rather than a null result or a distinct typed error: KerML 7.4.8.2 and SysML 7.19.2 say only how a result *is* bound (the result expression's result is implicitly bound to the result parameter, or the result parameter is bound explicitly), so a result nothing binds has no value asserted. The detail names the calc that ended without a return | `invoke_calc.go` `runCalcBody`, `calc_usage.go` `calcShape.designatedOutput`, `calc_usage.go` `runCalcUsage` (`errors.go` `ErrNoValue`) | `robustness_calc_invocation_test.go:testCalcBodyNeverReturns`, `robustness_unbound_result_test.go:testBodyFallsThroughIsNoValue`, `:testUnboundResultReadsAsUnassignedOut`, `:testEmptySequenceResultIsAValue` (a `return` of `()` binds the result to an empty sequence, which is a value), `calc_body_falls_through_is_no_value.sysml` conformance | ✅ Faithful | +| A body running to its end without returning leaves the result parameter unbound, which is *no value* (`ErrNoValue`, the error an `out` the body never assigns reads as) rather than a null result or a distinct typed error: KerML 7.4.8.2 and SysML 7.19.2 say only how a result *is* bound (the result expression's result is implicitly bound to the result parameter, or the result parameter is bound explicitly), so a result nothing binds has no value asserted. The detail names the calc that ended without a return | `invoke_calc.go` `runCalcBody`, `calc_usage.go` `loadedCalc.designatedOutput`, `calc_usage.go` `runCalcUsage` (`errors.go` `ErrNoValue`) | `robustness_calc_invocation_test.go:testCalcBodyNeverReturns`, `robustness_unbound_result_test.go:testBodyFallsThroughIsNoValue`, `:testUnboundResultReadsAsUnassignedOut`, `:testEmptySequenceResultIsAValue` (a `return` of `()` binds the result to an empty sequence, which is a value), `calc_body_falls_through_is_no_value.sysml` conformance | ✅ Faithful | | Control flow (if/else) in calc, including the conditional expression `if c ? a else b` evaluated lazily | `runtime/statements.go` `ifStatement`; `eval.go` `evalConditional` | `calc_conditional_branch.sysml`, `calc_conditional_operator_base_case.sysml` | ✅ Faithful | | The type tier reaches the expressions of a body whose result is the value of its last expression, as it reaches an explicit `return` — including the dimensional warning | `passes/typecheck.go` `checkBehaviorMember` (an expression body member inferred as the value it computes) | `typecheck_calc_implicit_result_test.go:TestCalcBodyImplicitResultIsChecked`, `:TestCalcBodyImplicitResultWarnsOnDimensions`, `:TestCalcUsageImplicitResultIsChecked`, `:TestCalcBodyImplicitResultSkippedAfterNameError`, `calc_recursion_implicit_result.sysml` | ✅ Faithful | | A non-Boolean `if`/loop condition in a calc is a type error, and a diagnostic at runtime | `passes/typecheck.go` `checkBehaviorMember`; `runtime/statements.go` `condition` | `typecheck_calc_body_test.go:TestCalcBodyNonBooleanWhileCondition`, `robustness_calc_usage_test.go:testCalcNonBooleanCondition` | ✅ Faithful | | Statements and loop iterations in the evaluation trace | `trace.go` `recordStatement`/`recordLoopIteration` | `calc_iterative_factorial.trace.golden`, `calc_early_return_from_loop.trace.golden` | ✅ Faithful | -| Missing return expression (no result expression, no returning body, no output features and no output its body assigns) leaves the result parameter unbound, which is *no value* (`ErrNoValue`, as the row above and as an `out` never assigned) rather than a distinct typed error — KerML 7.4.8.2 and SysML 7.19.2 make no function with an unbound result ill-formed; a `return` that declares a result parameter without binding it (`return h;`) is named in the error's detail with the two forms that state a computed result (`unboundResultHint`), so the reader still learns why there is no value | `invoke_calc.go` `calcShapeOf` (`calcShape.Uncomputed`, `errors.go` `ErrNoValue`) | `robustness_calc_invocation_test.go:testCalcWithoutResult`, `invoke_calc_body_test.go:TestUnboundResultParameterHint`, `robustness_unbound_result_test.go:testNoResultExpressionKeepsTheHint`, `f64_calc_return_usage_without_value.sysml` conformance | ✅ Faithful | -| An assignment in a body to an `out` the calculation declares binds that output for the activation, so a calc usage reads it (KerML 7.4.9: an invocation's outputs are features of that one evaluation). The assignment is written `assign a := expr;`; a kind-less `a = expr;` declares a feature `a` of the body instead (see the statement-body rows above) | `runtime/statements.go` `stmtEnv.assign`/`stmtEnv.assignLocal` → `runtime/calc_statements.go` `calcStmtHost.declaredOutput`/`assignOuter`; `runtime/calc_usage.go` `calcRun.output`, `assignedOutputs`; `invoke_calc.go` `calcShape.BodyOutputs` | `tests/parser/testdata/parse/calc_output_assignment.golden`, `calc_output_assigned_in_body.sysml` conformance (a loop and a branch computing outputs) | ✅ Faithful | -| A body that both assigns an output and returns a value keeps the two apart: the return is the invocation's value, the assignment the output's, so reading the output answers what the body assigned | `runtime/calc_usage.go` `runCalcUsage` (memoizing a returned value under an output's name only for `calcOutput.IsResult`, the rule the row on a valued output and a return states) | `calc_output_assigned_in_body.sysml` conformance (`AssignAndReturn`: `cr.a` is 6, `AssignAndReturn(5)` is 500) | ✅ Faithful | +| Missing return expression (no result expression, no returning body, no output features and no output its body assigns) leaves the result parameter unbound, which is *no value* (`ErrNoValue`, as the row above and as an `out` never assigned) rather than a distinct typed error — KerML 7.4.8.2 and SysML 7.19.2 make no function with an unbound result ill-formed; a `return` that declares a result parameter without binding it (`return h;`) is named in the error's detail with the two forms that state a computed result (`unboundResultHint`), so the reader still learns why there is no value | `invoke_calc.go` `calcShapeOf` (`loadedCalc.Uncomputed`, `errors.go` `ErrNoValue`) | `robustness_calc_invocation_test.go:testCalcWithoutResult`, `invoke_calc_body_test.go:TestUnboundResultParameterHint`, `robustness_unbound_result_test.go:testNoResultExpressionKeepsTheHint`, `f64_calc_return_usage_without_value.sysml` conformance | ✅ Faithful | +| An assignment in a body to an `out` the calculation declares binds that output for the activation, so a calc usage reads it (KerML 7.4.9: an invocation's outputs are features of that one evaluation). The assignment is written `assign a := expr;`; a kind-less `a = expr;` declares a feature `a` of the body instead (see the statement-body rows above) | `runtime/statements.go` `stmtEnv.assign`/`stmtEnv.assignLocal` → `runtime/calc_statements.go` `calcStmtHost.declaredOutput`/`assignOuter`; `runtime/calc_usage.go` `calcRun.output`; `lower/calc.go` `assignedOutputs`; `program/calc.go` `Calc.BodyOutputs` | `tests/parser/testdata/parse/calc_output_assignment.golden`, `calc_output_assigned_in_body.sysml` conformance (a loop and a branch computing outputs) | ✅ Faithful | +| A body that both assigns an output and returns a value keeps the two apart: the return is the invocation's value, the assignment the output's, so reading the output answers what the body assigned | `runtime/calc_usage.go` `runCalcUsage` (memoizing a returned value under an output's name only for `program/calc.go` `CalcOutput.IsResult`, the rule the row on a valued output and a return states) | `calc_output_assigned_in_body.sysml` conformance (`AssignAndReturn`: `cr.a` is 6, `AssignAndReturn(5)` is 500) | ✅ Faithful | | A calculation the model declares is evaluated from its own body — a result expression, or a body assigning an output it declares — and, without one, has no result; a library function of the same name never answers in its place | `runtime/library_functions.go` `libraryFunctionFor` (`Context.libraryDeclared`) | `library_functions_test.go:TestLibraryFunctionDoesNotHijackADeclaredBody`, `:TestLibraryFunctionDoesNotHijackAnOutputAssignedInABody`, `:TestLibraryFunctionNeverAnswersAModelDeclaration` | ✅ Faithful | | An assignment target naming more than one segment (`assign other::c := 1`) reaches outside the body no host binds, so it is lowered as unsupported and reported rather than writing the last segment | `lower/action_graph.go` `lowerStatement` (`Unsupported`) | `robustness_state_body_test.go:testQualifiedAssignmentTargetInAStateEffect` | ✅ Faithful (an assignment through a feature chain, `a.b := 1`, is reported the same way; neither form is bound) | -| An `inout` is bound by the invocation and rebound by an assignment in the body, the read answering what the body left | `runtime/calc_usage.go` `calcOutput.IsInOut`/`calcRun.output`; `runtime/calc_statements.go` `calcStmtHost.declaredOutput` | `calc_output_assigned_in_body.sysml` conformance (`Bump`) | ✅ Faithful | +| An `inout` is bound by the invocation and rebound by an assignment in the body, the read answering what the body left | `program/calc.go` `CalcOutput.IsInOut`; `runtime/calc_usage.go` `calcRun.output`; `runtime/calc_statements.go` `calcStmtHost.declaredOutput` | `calc_output_assigned_in_body.sysml` conformance (`Bump`) | ✅ Faithful | | An output given a value by its declaration *and* assigned in the body is a typed error rather than a silent pick (the precedent of a feature valued two ways). Assignments to an output within the body are imperative like a body local's: the last one to run is the activation's binding, so an output may be initialized and then accumulated into, including once per loop iteration | `runtime/calc_statements.go` `calcStmtHost.assignOuter` (`runtime/errors.go` `errConflictingOutput`) | `robustness_calc_usage_test.go:testCalcOutputValuedAndAssigned`, `:testCalcOutputAssignedTwice`, `calc_output_assigned_in_body.sysml` conformance (`Accum`) | ✅ Faithful | | Reading a declared output the activation never assigned reports that output, not a missing result expression; an output only a branch that did not run would assign is unassigned for that activation | `runtime/calc_usage.go` `calcRun.output` (`errOutputNotAssigned`, a kind of `ErrNoValue`) | `robustness_calc_usage_test.go:testCalcOutputNeverAssignedByTheBody`, `:testCalcOutputAssignedInABranchNotTaken` | ✅ Faithful | | A calc usage's members are the parameters and outputs of the calc it is typed by, reachable through a feature chain (SysML 7.6.6, 7.17) | `resolve/target.go` `ResolveTarget`/`memberChain` + `resolve/document.go` `resolveMemberChain` → `semantics` `Model.LookupMember` | `passes/typecheck_calc_usage_test.go:TestCalcUsageOutputTypesAsTheOutputItNames`, `:TestCalcUsageOutputInsideAPartDefinition` | ✅ Faithful | @@ -49,7 +49,7 @@ | Reading a name the calc declares no output for is unresolved, reported once by the name-resolution tier | `resolve/document.go` `resolveMemberChain`; `runtime/calc_usage.go` `calcRun.output` (`runtime/errors.go` `errUnknownOutput`) | `passes/typecheck_calc_usage_test.go:TestCalcUsageUnknownOutputIsUnresolved`, `robustness_calc_usage_test.go:testCalcUsageUnknownOutput` | ✅ Faithful | | A calc usage evaluates its body once and every `out` feature it declares is readable from that run (SysML 7.17) | `runtime/calc_usage.go` `calcUsageOutput`/`CalcUsageOutputs`/`calcUsageRun` → `invoke_calc.go` `calcShapeOf`/`bindCalcParameters` → `runtime/statements.go` `stmtEngine` | `calc_usage_multiple_outputs.sysml`, `calc_usage_statement_body.sysml`, `calc_usage_inherited_parameters.sysml`, `calc_usage_instance_slots.sysml` | ✅ Faithful | | Reading several outputs of one usage runs the body once, per usage and per object, reset with the run | `runtime/calc_usage.go` `calcUsageRun` (`calcUsageKey`) + `context.go` `beginRun`/`beginExecutorRun` | `calc_usage_multiple_outputs.trace.golden`, `calc_usage_statement_body.trace.golden`, `calc_usage_inherited_parameters.trace.golden` | ✅ Faithful | -| A usage's inputs bind from its own member values, falling back to the defaults declared along its specialization chain, and may name a sibling feature of the object carrying the usage | `runtime/calc_usage.go` `calcUsageRun` → `invoke_calc.go` `bindCalcParameters`/`calcParameters` | `calc_usage_inherited_parameters.sysml`, `calc_usage_instance_slots.sysml` | ✅ Faithful | +| A usage's inputs bind from its own member values, falling back to the defaults declared along its specialization chain, and may name a sibling feature of the object carrying the usage | `runtime/calc_usage.go` `calcUsageRun` → `invoke_calc.go` `bindCalcParameters`; `lower/calc.go` `calcParameters` | `calc_usage_inherited_parameters.sysml`, `calc_usage_instance_slots.sysml` | ✅ Faithful | | A usage declared in a behavior's body — a calc's or an action's — binds its inputs in the environment of the evaluation reading it: the enclosing parameters and locals as the running body holds them, then the enclosing lexical scope, so an input naming an attribute the body assigned reads the assigned value rather than the declared one (SysML 7.17, a usage is a feature of the body declaring it) | `runtime/calc_usage.go` `bindCalcUsage`/`enclosedByBehaviorBody`, `invoke_calc.go` `isActionSymbol`, `eval.go` `EvalContext.nestedEnv`, `invoke_calc.go` `bindCalcParameters` | `calc_usage_nested_in_calc.sysml`, `action_body_local_calc_usage.sysml`, `calc_usage_nested_test.go:TestNestedCalcUsageBindsFromEnclosingParameters`, `:TestNestedCalcUsageBindsFromEnclosingLocals`, `:TestNestedCalcUsageChain`, `:TestNestedCalcUsageReadsEnclosingObject`, `action_body_local_usage_test.go:TestActionBodyLocalUsageBindsCurrentValues`, `:TestActionBodyLocalUsageBindsPerIteration`, `classifier_behavior_test.go:TestCalcOperationNestedUsageSeesPerformingObject`, `:TestObjectScopedCalcUsageSeesPerformingObject` | ✅ Faithful (the steps of a usage's body see the object of the evaluation context that reached them — `runCalcUsage` passes the reading context's object to `runCalcSteps` — so a usage read from an object, or from a calc operation invoked on one, reads that object's current feature values rather than the declared defaults; a calc reached with no object context still has no performer, and a body's send, effect and assignment restrictions are unchanged) | | A nested input bound from a name of its own (`in vx = vx`) reads the enclosing binding: the inputs being bound are not in the environment their own values are evaluated in, so every one of them resolves names in the enclosing environment alike — `in n = m; in m = n;` swaps the two values rather than the second reading the first's fresh binding | `runtime/calc_usage.go` `bindCalcUsage`, `eval.go` `EvalContext.nestedEnv`/`Lookup` | `calc_usage_nested_shadowed_input.sysml` (shadowed and swapped names), `calc_usage_nested_test.go:TestNestedCalcUsageShadowsEnclosingName`, `:TestNestedCalcUsageInputsDoNotSeeSiblings`, `robustness_calc_usage_test.go:testNestedCalcUsageSelfCycle` (a default with nothing outside to name stays `ErrCyclicFeatureValue`) | ✅ Faithful | | One run per usage, object and activation: two reads from one enclosing invocation run the body once, two invocations do not share it, and an iteration of a loop is an activation of its own. The activation replaces the input-value hash the memo key used, so two invocations whose inputs coincide still get their own run and no read can be answered from a run bound to other values | `runtime/calc_usage.go` `calcUsageKey`/`calcUsageRun`, `context.go` `newActivation`/`endActivation` | `calc_usage_nested_in_calc.trace.golden`, `calc_usage_nested_shadowed_input.trace.golden`, `calc_usage_nested_test.go:TestNestedCalcUsageRunsPerInputs`, `:TestNestedCalcUsageOwnOutputPerInvocation`, `calc_usage_snapshot_test.go:TestCalcUsageMemoDistinguishesEnclosingArguments` | ✅ Faithful | @@ -66,15 +66,15 @@ | A usage leaving an input unbound, or reading an output with no value, is reported | `runtime/calc_usage.go` `calcUsageRun` (`runtime/errors.go` `errUnboundParameter`), `runtime/calc_usage.go` `calcRun.output` (`runtime/errors.go` `ErrNoValue`) | `robustness_calc_usage_test.go:testCalcUsageUnboundInput`, `:testCalcUsageOutputWithoutAValue` | ✅ Faithful | | A calc usage typed by something that is not a calc is reported | `invoke_calc.go` `calcShapeOf` (`errors.go` `ErrNotACalc`) | `robustness_calc_usage_test.go:testCalcUsageSpecializesANonCalc` | ✅ Faithful | | The step budget bounds a usage's body the way it bounds an invocation | `runtime/calc_usage.go` `calcUsageOutput` `runtime/context.go` `beginRun` → `context.go` `incrementStep` (`errors.go` `ErrStepLimitExceeded`) | `robustness_calc_usage_test.go:testCalcUsageStepBudget` | ✅ Faithful | -| An invocation yields exactly one result (KerML 7.4.9), so invoking a calc that computes several outputs and designates no result is rejected rather than answered with the first of them — and the diagnostic writes out the calc usage to declare instead, with the inputs to bind | `runtime/calc_usage.go` `calcShape.designatedOutput`/`usageSpelling` (`runtime/errors.go` `errAmbiguousResult`) | `robustness_calc_usage_test.go:testMultipleOutputsInvokedAsAnExpression`, `repl/runtime_commands_test.go:TestCalcWithSeveralOutputsIsNotInvocable` | ✅ Faithful | -| A calc with exactly one output and no `return` is invocable, that output being its result | `runtime/calc_usage.go` `calcShape.designatedOutput` | `calc_usage_single_output.sysml` | ✅ Faithful | -| A `return` in a calc that also states an output supplies the invocation's value only: an output keeps the value its own binding computes, so a body returning something else does not change what reading that output answers. Only the result parameter (`return : Real = …`) takes the returned value directly | `runtime/calc_usage.go` `runCalcUsage` (memoizing a returned value under an output's name only for `calcOutput.IsResult`) | `calc_valued_output_with_return.sysml` conformance (`Apart`: `ca.a` is 6 while `Apart(5)` is 500; `Together`, `Both`) | ✅ Faithful | +| An invocation yields exactly one result (KerML 7.4.9), so invoking a calc that computes several outputs and designates no result is rejected rather than answered with the first of them — and the diagnostic writes out the calc usage to declare instead, with the inputs to bind | `runtime/calc_usage.go` `loadedCalc.designatedOutput`; `program/calc.go` `Calc.UsageSpelling` (`runtime/errors.go` `errAmbiguousResult`) | `robustness_calc_usage_test.go:testMultipleOutputsInvokedAsAnExpression`, `repl/runtime_commands_test.go:TestCalcWithSeveralOutputsIsNotInvocable` | ✅ Faithful | +| A calc with exactly one output and no `return` is invocable, that output being its result | `runtime/calc_usage.go` `loadedCalc.designatedOutput` | `calc_usage_single_output.sysml` | ✅ Faithful | +| A `return` in a calc that also states an output supplies the invocation's value only: an output keeps the value its own binding computes, so a body returning something else does not change what reading that output answers. Only the result parameter (`return : Real = …`) takes the returned value directly | `runtime/calc_usage.go` `runCalcUsage` (memoizing a returned value under an output's name only for `program/calc.go` `CalcOutput.IsResult`) | `calc_valued_output_with_return.sysml` conformance (`Apart`: `ca.a` is 6 while `Apart(5)` is 500; `Together`, `Both`) | ✅ Faithful | | `%calc` on a calc usage lists the outputs of one evaluation; a chain into a usage evaluates at the prompt | `repl/meta.go` `doCalc`/`calcUsageOutputs` | `repl/runtime_commands_test.go:TestCalcUsageOutputsAtThePrompt` | ✅ Faithful | | A calc usage's outputs are evaluation results, not feature values of an object | `runtime/calc_usage.go` (no instance materialization) | `calc_usage_instance_slots.sysml` (the features fed by the outputs are feature values; the usage itself is not), pilot-exec-diff `w6d:calc-usage` | ⚠️ Approximate (unrefereeable: the pinned artifact answers a `CalculationUsage` node rather than an output value. `%instances` and export show the features valued from outputs, not the usage's outputs themselves) | | A calc definition, a calc usage with an unsupplied input, or an `in calc` parameter named where a value is expected is a **function value** (KerML 1.1 §7.4.8: a Function is a Behavior with a `result`, an Expression a Step typed by one, and a feature reference to either denotes it; §8.3.4.7.4 `Function::result`, §8.3.4.8.5 `FeatureReferenceExpression`; SysML v2 §7.19: a calc def is a Function, a calc usage an Expression). The value is the calc's lowered invocation interface (`calcShape`) together with the environment it was read in — its declaring scope and the object it was read off — and nothing else: no statement closure is built, and the value is invoked through the same path a calc usage invocation takes (`invokeCalcShapeIn`). Reading a calc usage whose inputs are all bound evaluates it as before; a library function the runtime implements natively (`RealFunctions::sqrt`, `floor`) reads as a value carrying that implementation, while a library operation that binds its arguments unevaluated (`SequenceFunctions::size` and the other `->` operations) is refused as `errNotAFunction` | `runtime/value.go` `ValFunction`, `runtime/function_value.go` `functionValue`/`EvalContext.functionValueOf`/`Context.readsAsFunction`/`EvalContext.calcAsValue`, `invoke_calc.go` `calcShapeOf` (a natively implemented library function computes), `eval.go` `evalFeatureReference`, `describe.go` (`the function Sq`), `trace.go` `FormatTraceValue` (`calc(Sq)`), `repl/meta.go` | `function_value_read.sysml`, `function_value_probe.sysml` (`Fn(Sq, 3.0)` is `9.0`), `function_value_calc_usage.sysml`, `function_value_library.sysml`, `robustness_function_values_test.go:function_value_of_a_built_in`, `value_kinds_test.go:TestFunctionValueIdentity`, `:TestEveryValueKindIsDispatched`, `eval_no_value_test.go`, `repl/evalin_test.go`; compiled: `tests/codegen/compiled_calcs_test.go` `Fn::ApplySq`, `Fn::ApplyUsage`, `Fn::ApplySqrt`, `Fn::ApplyFloor`, `Refused::FunctionAsValue`, `Refused::UnevaluatedFunction` | ✅ Faithful; compiled: approximate — natively the value is fixed at compile time (`codegen/compile_fn.go` `functionValueOf`: a calc def, a calc usage with an unsupplied input owned by packages alone, or a compiled library function), so it is passed to `in calc` parameters and invoked but never read where a value is expected (`the function value Sq where a value is expected`), and a library operation binding its arguments unevaluated is refused as it is interpreted | -| An `in calc` parameter of a calc or an action (SysML v2 §7.19, §8.3.19.3 `CalculationUsage` as a parameter) accepts a function value or null and nothing else, positionally or by name; the body invokes it as `f(a)`, through a chain (`p.f(a)`), nested (`f(f(a))`) and as an argument to another calc-typed parameter, binding the callee's inputs positionally and by name as a direct invocation does. A calc usage bound as an action input (`in f = sq;`) is the function value it reads as. A calc-typed parameter whose own value names a calc (`in calc f = twice;`) applies that calc wherever no run bound it — called by its bare name or its qualified name (`Apply::f(a)`) alike — and a run's binding takes precedence | `runtime/function_value.go` `calcParameter.checkFunction`, `function_value.go` `EvalContext.invokeFunction`, `EvalContext.boundFunction`/`qualifiedBoundFunction`/`declaredFunction`, `eval.go` `evalInvocation`/`evalFeatureChain`, `parser/behavior.go` `parameterKindKeywords` (`runtime/compile_constructs_test.go` `calc`) | `function_value_probe.sysml` + `function_value_probe.trace.golden` (`TestExecutionTrace`), `function_value_named_args.sysml`, `function_value_chain_call.sysml`, `function_value_action_parameter.sysml`, `function_value_qualified_call.sysml`, `function_value_qualified_default.sysml`, parser golden `action_calc_parameter.sysml`, `robustness_function_values_test.go:function_value_call_of_a_non_function` (`errNotAFunction`), `:function_value_bound_to_a_non_function` (`errNotAFunction`), `:function_value_arity_mismatch` (`errCalcArity`), `testCalcUnboundParameter` (`errUnboundParameter`); compiled: `tests/codegen/compiled_calcs_test.go` `Fn::PassSq`, `Fn::IterateSq`, `Fn::TwoSq`, `Fn::ByNameSq`, `Fn::IntBoth`, `Fn::Fold2Add`, `Fn::CondApply`, `Refused::CalcParam`, `Refused::ValueAsFunction`, `Refused::ChosenFunction`, `Refused::ReturnedFunction`, `Refused::WrongArity`, `Refused::WrongName` | ✅ Faithful; compiled: approximate — `codegen/compile_fn.go` specializes the callee per function value bound (`Compiler.compileCalcWith`), so `f(a)` and `f(v = a)` are direct calls binding the value's own parameters as the interpreter does, and the value may be passed on to another `in calc` parameter or through recursion; a parameter typed by a calc (`in calc f : Sq`) takes only a function value whose calc conforms to it (`paramDecl.typ`, checked in `bindArgs` as `checkFuncArgType`; the interpreter's `type mismatch` at the binding becomes `cannot bind the function value … to a parameter typed by …`, `Refused::UnrelatedTyped`, `LibraryTyped`, `GeneralForSub`, `ForwardedUnrelated`; `Fn::TypedSq`, `TypedUsage`, `TypedSub` compile); the argument must name a calc statically (an `if` choosing one, an invocation producing one, or a plain value in its place is refused at compile time, as is a calc's own `in calc` parameter on the command line), a chain call `p.f(a)` is refused with every feature chain, and an arity or name mismatch is a compile-time refusal where the interpreter fails at run time | +| An `in calc` parameter of a calc or an action (SysML v2 §7.19, §8.3.19.3 `CalculationUsage` as a parameter) accepts a function value or null and nothing else, positionally or by name; the body invokes it as `f(a)`, through a chain (`p.f(a)`), nested (`f(f(a))`) and as an argument to another calc-typed parameter, binding the callee's inputs positionally and by name as a direct invocation does. A calc usage bound as an action input (`in f = sq;`) is the function value it reads as. A calc-typed parameter whose own value names a calc (`in calc f = twice;`) applies that calc wherever no run bound it — called by its bare name or its qualified name (`Apply::f(a)`) alike — and a run's binding takes precedence | `runtime/function_value.go` `checkFunction`, `function_value.go` `EvalContext.invokeFunction`, `EvalContext.boundFunction`/`qualifiedBoundFunction`/`declaredFunction`, `eval.go` `evalInvocation`/`evalFeatureChain`, `parser/behavior.go` `parameterKindKeywords` (`runtime/compile_constructs_test.go` `calc`) | `function_value_probe.sysml` + `function_value_probe.trace.golden` (`TestExecutionTrace`), `function_value_named_args.sysml`, `function_value_chain_call.sysml`, `function_value_action_parameter.sysml`, `function_value_qualified_call.sysml`, `function_value_qualified_default.sysml`, parser golden `action_calc_parameter.sysml`, `robustness_function_values_test.go:function_value_call_of_a_non_function` (`errNotAFunction`), `:function_value_bound_to_a_non_function` (`errNotAFunction`), `:function_value_arity_mismatch` (`errCalcArity`), `testCalcUnboundParameter` (`errUnboundParameter`); compiled: `tests/codegen/compiled_calcs_test.go` `Fn::PassSq`, `Fn::IterateSq`, `Fn::TwoSq`, `Fn::ByNameSq`, `Fn::IntBoth`, `Fn::Fold2Add`, `Fn::CondApply`, `Refused::CalcParam`, `Refused::ValueAsFunction`, `Refused::ChosenFunction`, `Refused::ReturnedFunction`, `Refused::WrongArity`, `Refused::WrongName` | ✅ Faithful; compiled: approximate — `codegen/compile_fn.go` specializes the callee per function value bound (`Compiler.compileCalcWith`), so `f(a)` and `f(v = a)` are direct calls binding the value's own parameters as the interpreter does, and the value may be passed on to another `in calc` parameter or through recursion; a parameter typed by a calc (`in calc f : Sq`) takes only a function value whose calc conforms to it (`paramDecl.typ`, checked in `bindArgs` as `checkFuncArgType`; the interpreter's `type mismatch` at the binding becomes `cannot bind the function value … to a parameter typed by …`, `Refused::UnrelatedTyped`, `LibraryTyped`, `GeneralForSub`, `ForwardedUnrelated`; `Fn::TypedSq`, `TypedUsage`, `TypedSub` compile); the argument must name a calc statically (an `if` choosing one, an invocation producing one, or a plain value in its place is refused at compile time, as is a calc's own `in calc` parameter on the command line), a chain call `p.f(a)` is refused with every feature chain, and an arity or name mismatch is a compile-time refusal where the interpreter fails at run time | | A call through a feature chain, `holder.scale(3.0)` (KerMLExpressions `InstantiatedTypeMember` → `OwnedFeatureChain`), is checked statically as a direct call is: the chain names the calc feature applied, its arguments are held to that feature's effective inputs positionally and by name (a wrong type, an unknown name, too many arguments, an unbound default-less input), a chain to a non-behavior is refused, and the call is typed by the calc's result, so binding it to an incompatible declared type is reported. At run time the chain in call position denotes the calc feature applied over the receiver's object whatever its inputs — one every input of which a default supplies, or one with no input, is applied (`holder.scaled()` is `10.0`) where the bare read `holder.scaled` computes its result — and any other member is read as written and must hold a function | `passes/typecheck_expr.go` `inferChainInvocation`, `semantic/semantics/invocation.go` `ChainCallee`/`InvocationArgs`, `passes/typecheck_value.go` `invocationResultParameter`; `runtime/eval.go` `evalChainInvocation`/`invokeChainOn` | `passes/typecheck_expr_test.go:TestExprChainInvocationChecked`; `function_value_chain_call.sysml`, `function_value_chain_defaults.sysml` | ✅ Faithful | -| A calc read off an object (`twice.scale`, a calc usage owned by a part) is a function value over that object: invoked later it reads that object's features, so the same calc read off two parts is two values. A calc declared inside a behavior body (a calc's or an action's) closes over the bindings of the run it was read in — its enclosing parameters and locals — and a function returned from a calc (`return : Unary = scale;`) keeps them after the run ends. The closure reaches only the code written inside that body: a body the nested calc inherits from a calc declared elsewhere (`calc again : RecursiveStep;`) reads none of the enclosing run's bindings, so a case performing itself as a step nests its frames no deeper than its recursion. The run closed over is a run of the behavior the calc is declared in — the innermost active invocation of that calc (or of one specializing it) or performance of that action — not whatever calc happens to be evaluating: a nested calc applied from a calc between it and its owner reads the owner's `k`, not the caller's, and one applied (`Outer::inner(2.0)`) or read (`Fn(Outer::inner, 2.0)`) while no run of its owner is active closes over nothing, so its body's `k` is unresolved rather than a same-named parameter of the caller | `function_value.go` `functionValue.self`/`.enclosing`, `EvalContext.functionValueOf` (`enclosedByBehaviorBody`); `eval.go` `EvalContext.enclosingRun`; `frame.go` `frame.runs` (a calc frame by `calcShape.qualifiedBy`, a performance frame by its scope's owner, a snapshot by the action it was copied from: `frame.performs`/`frame.snapshot`); `calc_usage.go` `runOf`, `calcShape.bodyEnclosing`/`declaredWithin`; `invoke_calc.go` `Context.invokeCalcShapeIn` | `function_value_feature_closure.sysml` (`Fn(twice.scale, 5.0) + Fn(thrice.scale, 5.0)` is `25.0`), `function_value_body_closure.sysml` (`Outer`, `Maker`, `Shadowed`), `function_value_action_closure.sysml` (an action-local calc passed as a function applies another that reads the performance's input and local), `function_value_sampled_closure.sysml`, `action_nested_calc_reads_performance.sysml`; `robustness_function_values_test.go:function_value_inherited_body_outside_the_closure`, `:function_value_nested_calc_outside_its_run`; `analysis_robustness_test.go:recursion_through_a_step`; compiled: `tests/codegen/compiled_calcs_test.go` `Refused::ObjectClosure`, `Refused::ObjectCalc`, `Refused::BodyClosure`, `Refused::OuterClosure`, `Refused::EscapingParam` | ✅ Faithful; compiled: deliberate divergence — the native backends carry no closure: a calc owned by a part (`twice.scale`, `Scaler::scale`) or declared inside a behavior body is refused by name (`whose function value closes over that object` / `that run's bindings`), and a function value cannot escape as a result (`the function value f escaping as the result`); `native-compilation.md` (Function values) states the trade-off | +| A calc read off an object (`twice.scale`, a calc usage owned by a part) is a function value over that object: invoked later it reads that object's features, so the same calc read off two parts is two values. A calc declared inside a behavior body (a calc's or an action's) closes over the bindings of the run it was read in — its enclosing parameters and locals — and a function returned from a calc (`return : Unary = scale;`) keeps them after the run ends. The closure reaches only the code written inside that body: a body the nested calc inherits from a calc declared elsewhere (`calc again : RecursiveStep;`) reads none of the enclosing run's bindings, so a case performing itself as a step nests its frames no deeper than its recursion. The run closed over is a run of the behavior the calc is declared in — the innermost active invocation of that calc (or of one specializing it) or performance of that action — not whatever calc happens to be evaluating: a nested calc applied from a calc between it and its owner reads the owner's `k`, not the caller's, and one applied (`Outer::inner(2.0)`) or read (`Fn(Outer::inner, 2.0)`) while no run of its owner is active closes over nothing, so its body's `k` is unresolved rather than a same-named parameter of the caller | `function_value.go` `functionValue.self`/`.enclosing`, `EvalContext.functionValueOf` (`enclosedByBehaviorBody`); `eval.go` `EvalContext.enclosingRun`; `frame.go` `frame.runs` (a calc frame by `loadedCalc.qualifiedBy`, a performance frame by its scope's owner, a snapshot by the action it was copied from: `frame.performs`/`frame.snapshot`); `calc_usage.go` `runOf`, `loadedCalc.bodyEnclosing`/`declaredWithin`; `invoke_calc.go` `Context.invokeCalcShapeIn` | `function_value_feature_closure.sysml` (`Fn(twice.scale, 5.0) + Fn(thrice.scale, 5.0)` is `25.0`), `function_value_body_closure.sysml` (`Outer`, `Maker`, `Shadowed`), `function_value_action_closure.sysml` (an action-local calc passed as a function applies another that reads the performance's input and local), `function_value_sampled_closure.sysml`, `action_nested_calc_reads_performance.sysml`; `robustness_function_values_test.go:function_value_inherited_body_outside_the_closure`, `:function_value_nested_calc_outside_its_run`; `analysis_robustness_test.go:recursion_through_a_step`; compiled: `tests/codegen/compiled_calcs_test.go` `Refused::ObjectClosure`, `Refused::ObjectCalc`, `Refused::BodyClosure`, `Refused::OuterClosure`, `Refused::EscapingParam` | ✅ Faithful; compiled: deliberate divergence — the native backends carry no closure: a calc owned by a part (`twice.scale`, `Scaler::scale`) or declared inside a behavior body is refused by name (`whose function value closes over that object` / `that run's bindings`), and a function value cannot escape as a result (`the function value f escaping as the result`); `native-compilation.md` (Function values) states the trade-off | | Function values are equal, and key a set, by the calc together with the object it was read off (`Sq == Sq`, `twice.scale != thrice.scale`); a value closing over a behavior body's bindings is equal to every read of the same calc within the same run of that body and to none from another run (`inner == inner` within one calc body; `Maker(2.0) != Maker(2.0)` across two invocations). Adoption into another runtime rebinds an ordinary function value to the same calc of the adopting model, the object it was read off adopted with it; one closing over a body's bindings is refused as a typed error (the run those bindings belonged to has ended and cannot be reconstructed) | `eval.go` `valueEqual`, `value_equality.go` `valueKeyFunc` (`ValFunction` arm), `adopt.go` | `value_kinds_test.go:TestFunctionValueIdentity`, conformance `function_value_closure_equality`, `adopt_test.go:TestAdoptRebindsAFunctionValue` | ✅ Faithful | | `SampledFunctions::Sample(f, domain)` samples a user calc passed as its `in calc calculation` argument: the library's own body runs, `domainValues->collect { in x; new SamplePair(x, calculation(x)) }` invoking the function value inside the collection body, and `Range` of the result reads the samples back | the library body under `invoke_calc.go` `invokeCalcShapeIn`, `function_value.go` `EvalContext.invokeFunction` (from a collection body's frame), `collections.go` | `function_value_sampled.sysml` (`Range(Sample(Sq, (1.0, 2.0, 3.0)))` is `[1.0, 4.0, 9.0]`), `function_value_sampled_closure.sysml` (a calc read off a part, sampled); compiled: `tests/codegen/compiled_calcs_test.go` `Fn::SqRange`, `Fn::RecipRange`, `Fn::HalfDomain`, `Fn::IncBoth`, `Fn::QuarterRange`, `Fn::SqrtRange`, `Fn::UsageRange`, `Fn::SumRange`, `Fn::DownRange`/`DownDomain` (the recursion budget's last frames), `TestCompiledBudgetChargesInputsAndWidening` `Fn::SizeDomain`/`SizeRange` (each read charged), `Refused::SampleArity`, `Refused::SampledValue`, `Refused::SampledEscapes` | ✅ Faithful; compiled: approximate — `codegen/compile_fn.go` `compileSample` collects the calc over the domain in order when the sample is taken (the first failing element is the failure; `Sample`, `Domain` and `Range` are each one frame against the recursion budget and each `Domain`/`Range` read a fresh sequence charged to the element budget, as the library calcs are; a null domain samples to `[]`, a literal `null` domain typed by the sampled calc's parameter — `Fn::NullDomain`, `NullRange`, `NullSqrtRange`; Real for a kind-preserving library function, so `Refused::IntegerNullRange` is refused where the interpreter computes `[]`), and a `SampledFunction` is compiled only bound to an attribute or read at once by `Domain`/`Range`; one returned, passed on or used as a value, and `SamplePair` access, are refused | | A feature's values are a sequence its multiplicity constrains (KerML 1.1 §7.3.4.1 Features Overview, §7.4.12 Multiplicities, §8.3.3.3 Features), so a `[0..*]` member holding one value denotes that value wherever one value is taken, and one binding rule serves every such place: an operator's operand (`+`, `-`, `<`, `==`, `not`, unary `-`), a `[1]` parameter of a user calc or of a library function, and a cast or classification operand (`as`, `@`, `istype`, `hastype`) each take a one-element collection as its element; a collection of several stays what it is, refused as before (a `type mismatch` operand, a `multiplicity violation` argument), and an empty one still holds no value. `SampledFunctions::SamplePair::domainValue :>> key` states no multiplicity and so inherits `KeyValuePair::key`'s `[0..*]` (§7.3.4.5), which is why `s.samples#(1).domainValue - 1.0` and the library's own `interpolateLinear` read a one-element sequence; the same held for `q.zs + 1.0` with `zs : Real[0..*] = (2.5)`, no inheritance involved. Both now bind the element, and a member redefined `[1]` reads as it did. The same rule serves every place a performed action hands one value on, since a bare action parameter holds a one-element sequence (the Action row on pin shape): the performer or object a behavior is started on, a called function value, a send's target, the quantity `ConvertQuantity` converts, a vector or measurement-reference argument, the two ends a binding connector compares, a tool-protocol input and a solver pin each take the element of a one-element sequence; a repeated step's pin reads are flat, one sequence of all its performances' values | `runtime/collections.go` `soleElement`; `runtime/eval.go` `valueOperand`, `boolOperand`, `evalEquality`, `evalTypeClassification`; `runtime/cast.go` `evalCast`; `runtime/invoke_calc.go` `calcMemberDecl.check`; `runtime/instance.go` `Context.admitted`; the scalar readers of `runtime/library_functions.go`, `library_conversions.go`, `vector_functions.go`; `runtime/invoke_action.go` `performerOf`, `runtime/start_behavior.go` `startTarget`, `runtime/function_value.go` (a function-valued parameter and its invocation), `runtime/eval.go` `Context.equalValues` and the chained callee, `runtime/scale_conversion.go`, `runtime/tool.go` `ToolValueOf`, `solve/pin.go` `soleHeld`, `runtime/action_frame.go` `actionFrame.repeatedPin` | conformance `calc_sample_pair_arithmetic` (`domainValue`/`rangeValue` arithmetic, `interpolateLinear`), `calc_singleton_member_binds_scalar` (`q.zs` through every operand kind), `calc_two_valued_member_refused_scalar`, `calc_inherited_member_redefined_scalar` (redefined `[1]`, redefined without a multiplicity, genuinely two-valued still a sequence), `robustness_expression_reads_test.go:two_valued_member_in_scalar_context`; refereed against the pilot with `tools/referee/exec` on the `q.zs` singleton and two-valued cases; `robustness_pin_shape_test.go` (`bare_pin_holding_one_object_addresses_a_send`, `two_values_in_a_bare_pin_are_no_operand`), conformance `performed_pin_shape_bare` (`value + doubled` over two one-element pins), `performed_pin_shape_nested` (a bare pin fed from a bare pin holds one element, not a sequence of a sequence) | ✅ Faithful | diff --git a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/03-analysis-case.md b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/03-analysis-case.md index 7fe12eca1f..1c70226dcd 100644 --- a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/03-analysis-case.md +++ b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/03-analysis-case.md @@ -2,7 +2,7 @@ An analysis case is a `Case`, a `Case` is a `Calculation`, and a `Calculation` is an `Action`, so running one is running an action whose result is its `out` and `return` parameters. The runtime -runs a case through the calc-usage machinery (`calcShape`/`calcRun`) with the body's action nodes +runs a case through the calc-usage machinery (`program.Calc` lowered by `lower.ToCalc`, held as `loadedCalc`/`calcRun`) with the body's action nodes lowered as one `lower.Block` over the `ActionGraph` they state and performed by the action executor (`calcStmtHost` with `performances`), the subject as an `in` parameter, and the objective evaluated afterwards by the requirement engine. No analysis-specific executor exists, and none for trade @@ -13,22 +13,22 @@ question, the boundary stated under [Objective optimization](#objective-optimiza | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| -| An analysis definition or usage is a performable calculation: `RunAnalysis` runs it and answers its outputs; `-calc`/`%calc`/`EvaluateCalc` refuse it by kind, saying to run it as an analysis | `runtime/analysis_run.go` `Context.RunAnalysis`, `Context.analysisRun`; `runtime/analysis_run.go` `Context.RequireAnalysisCase`, `runtime/errors.go` `ErrNotAnAnalysis`; `runtime/calc_usage.go` `isCalcUsageSymbol`, `Context.checkCalcTyping` | `analysis_subject_bound_in_usage.sysml`, `analysis_robustness_test.go:not_an_analysis`, `repl/analysis_test.go:TestAnalysisErrors`, `grpc/analysis_test.go:TestRunAnalysisFailures` | ✅ Faithful | -| `subject s : T;` is an `in` parameter of the case: a usage binding it (`subject s = ship;`) supplies the object, the object a run is asked on supplies it otherwise, and a case run with neither is refused naming the subject — never run with an empty one | `runtime/analysis_run.go` `subjectDeclaration`, `Context.subjectParameter`, `calcShape.subjectParameter`, `calcShape.analysisArgs`, `calcShape.unboundSubject` | `analysis_subject_bound_in_usage.sysml`, `analysis_subject_at_run_time.sysml`, `analysis_robustness_test.go:unbound_subject`, `:subject_of_the_wrong_type`, `repl/analysis_test.go:TestAnalysisDefinitionOnAnObject` | ✅ Faithful | +| An analysis definition or usage is a performable calculation: `RunAnalysis` runs it and answers its outputs; `-calc`/`%calc`/`EvaluateCalc` refuse it by kind, saying to run it as an analysis | `runtime/analysis_run.go` `Context.RunAnalysis`, `Context.analysisRun`; `runtime/analysis_run.go` `Context.RequireAnalysisCase`, `runtime/errors.go` `ErrNotAnAnalysis`; `lower/calc.go` `IsCalcUsageSymbol`; `runtime/calc_usage.go` `Context.checkCalcTyping` | `analysis_subject_bound_in_usage.sysml`, `analysis_robustness_test.go:not_an_analysis`, `repl/analysis_test.go:TestAnalysisErrors`, `grpc/analysis_test.go:TestRunAnalysisFailures` | ✅ Faithful | +| `subject s : T;` is an `in` parameter of the case: a usage binding it (`subject s = ship;`) supplies the object, the object a run is asked on supplies it otherwise, and a case run with neither is refused naming the subject — never run with an empty one | `lower/calc.go` `SubjectDeclaration`, `subjectParameter`; `program/calc.go` `Calc.SubjectParameter`; `runtime/analysis_run.go` `loadedCalc.analysisArgs`, `loadedCalc.unboundSubject` | `analysis_subject_bound_in_usage.sysml`, `analysis_subject_at_run_time.sysml`, `analysis_robustness_test.go:unbound_subject`, `:subject_of_the_wrong_type`, `repl/analysis_test.go:TestAnalysisDefinitionOnAnObject` | ✅ Faithful | | A nested case binding no subject of its own runs on the enclosing case's subject (§7.22 `AnalysisCase`, the pilot's `10a-Analysis.sysml` `massAnalysisCase` comment) | `runtime/analysis_run.go` `Context.enclosingSubject`, `enclosingBehavior` | `analysis_nested_default_subject.sysml` | ✅ Faithful | -| `in` parameters are bound positionally or by name from the arguments, or from their declared defaults; the subject is never a positional parameter; a missing value, a surplus argument and an unknown name are typed refusals | `runtime/analysis_run.go` `calcShape.analysisArgs`; `runtime/invoke_calc.go` `bindCalcParameter` | `analysis_in_parameter_positional.sysml`, `analysis_in_parameter_named.sysml`, `analysis_robustness_test.go:missing_in_parameter`, `:too_many_arguments`, `:unknown_named_argument` | ✅ Faithful | -| Actors bind by name only, never by position (§7.22.2, §7.24.2: `ActorMembership` is a `ParameterMembership`, yet an actor is no input of the case): a positional argument to a case or requirement declaring an `actor` before an `in` parameter binds that input, positional arguments falling on the non-actor parameters in declaration order; the positional arity counts the non-actor parameters, so a call with exactly the inputs is accepted and a surplus argument is a typed refusal; a named actor argument binds as before | `runtime/invoke_calc.go` `bindCalcParameters`, `bindCalcParameter`, `calcShape.positionalArity`, `calcShape.checkArgs`; `runtime/calc_usage.go` `calcShape.unsuppliedParameters`; `runtime/function_value.go` `functionValue.arity`; `runtime/invocation_statement_order.go` `canonicalInvocationArguments`; `runtime/evaluation_log.go` `calcShape.argumentsByPosition`; `runtime/compile.go` `calcCompiler.compile` | `case_actor_before_input_positional.sysml`, `case_actor_before_input_named.sysml`, `case_actor_before_input_mixed.sysml`, `robustness_case_actor_positional_test.go:case_actor_surplus_positional_argument`, `:case_actor_exact_positional_arguments`, `:case_actor_positional_beside_named`, `:case_actor_function_value_arity`, `:case_actor_invocation_memo_key` | ✅ Faithful | -| A case usage's subject, actors and `in` parameters are features of it as its return parameter is (§7.22.2, §7.24.2; `ActorMembership` is a `ParameterMembership`), as a calc usage's inputs are (§7.19.2): read through the usage (`p1.v.s`, `vp.who.id`, `vp.limit`, `c1.x`) each answers the value the run bound, beside `verdict`/`result`; a local or part the body declares reads as the run left it (`check.depth`, `check.gear.id`); the objective and the other members the case declares read through the case's occurrence (`scaled.fits.result`); a subject, actor or input nothing binds is `` whatever its lower bound — a parameter is the caller's to bind (§7.17.2), so `in vals : Integer[0..*]` nothing supplies reads as the unbound `actor many : Operator[0..*]` does, not as `[]` (an unbound actor or input leaves the case running); an actor bound to several values keeps the range of the actor it redefines (§7.6.3); an actor renamed at its position redefines the general's actor (KerML §7.4.7.3) and binds it; the usage's occurrence is one per owner object, so two parts' `check.gear` are two objects; a name the case declares nothing under stays `unknown output` | `runtime/calc_usage.go` `EvalContext.evalCalcUsageMembers`, `calcRun.memberValue`, `calcShape.unsuppliedParameters`, `calcRun.lookupBody`, `Context.unboundSubjectRead`, `EvalContext.occurrenceMember`, `Context.usageOccurrenceOf`; `runtime/invoke_calc.go` `calcParameter.IsActor`, `calcShape.parameter`, `Context.calcParameters`, `Context.redeclaredIndex`, `Context.bindCalcParameters` | `case_usage_member_reads.sysml`, `robustness_case_member_reads_test.go:TestRuntimeRobustnessCaseMemberReads` | ✅ Faithful | +| `in` parameters are bound positionally or by name from the arguments, or from their declared defaults; the subject is never a positional parameter; a missing value, a surplus argument and an unknown name are typed refusals | `runtime/analysis_run.go` `loadedCalc.analysisArgs`; `runtime/invoke_calc.go` `bindCalcParameter` | `analysis_in_parameter_positional.sysml`, `analysis_in_parameter_named.sysml`, `analysis_robustness_test.go:missing_in_parameter`, `:too_many_arguments`, `:unknown_named_argument` | ✅ Faithful | +| Actors bind by name only, never by position (§7.22.2, §7.24.2: `ActorMembership` is a `ParameterMembership`, yet an actor is no input of the case): a positional argument to a case or requirement declaring an `actor` before an `in` parameter binds that input, positional arguments falling on the non-actor parameters in declaration order; the positional arity counts the non-actor parameters, so a call with exactly the inputs is accepted and a surplus argument is a typed refusal; a named actor argument binds as before | `runtime/invoke_calc.go` `bindCalcParameters`, `bindCalcParameter`, `loadedCalc.checkArgs`; `program/calc.go` `Calc.PositionalArity`; `runtime/calc_usage.go` `loadedCalc.unsuppliedParameters`; `runtime/function_value.go` `functionValue.arity`; `runtime/invocation_statement_order.go` `canonicalInvocationArguments`; `runtime/evaluation_log.go` `loadedCalc.argumentsByPosition`; `runtime/compile.go` `calcCompiler.compile` | `case_actor_before_input_positional.sysml`, `case_actor_before_input_named.sysml`, `case_actor_before_input_mixed.sysml`, `robustness_case_actor_positional_test.go:case_actor_surplus_positional_argument`, `:case_actor_exact_positional_arguments`, `:case_actor_positional_beside_named`, `:case_actor_function_value_arity`, `:case_actor_invocation_memo_key` | ✅ Faithful | +| A case usage's subject, actors and `in` parameters are features of it as its return parameter is (§7.22.2, §7.24.2; `ActorMembership` is a `ParameterMembership`), as a calc usage's inputs are (§7.19.2): read through the usage (`p1.v.s`, `vp.who.id`, `vp.limit`, `c1.x`) each answers the value the run bound, beside `verdict`/`result`; a local or part the body declares reads as the run left it (`check.depth`, `check.gear.id`); the objective and the other members the case declares read through the case's occurrence (`scaled.fits.result`); a subject, actor or input nothing binds is `` whatever its lower bound — a parameter is the caller's to bind (§7.17.2), so `in vals : Integer[0..*]` nothing supplies reads as the unbound `actor many : Operator[0..*]` does, not as `[]` (an unbound actor or input leaves the case running); an actor bound to several values keeps the range of the actor it redefines (§7.6.3); an actor renamed at its position redefines the general's actor (KerML §7.4.7.3) and binds it; the usage's occurrence is one per owner object, so two parts' `check.gear` are two objects; a name the case declares nothing under stays `unknown output` | `runtime/calc_usage.go` `EvalContext.evalCalcUsageMembers`, `calcRun.memberValue`, `loadedCalc.unsuppliedParameters`, `calcRun.lookupBody`, `Context.unboundSubjectRead`, `EvalContext.occurrenceMember`, `Context.usageOccurrenceOf`; `program/calc.go` `CalcParam.IsActor`, `Calc.Parameter`; `lower/calc.go` `calcParameters`, `redeclaredIndex`; `runtime/invoke_calc.go` `Context.bindCalcParameters` | `case_usage_member_reads.sysml`, `robustness_case_member_reads_test.go:TestRuntimeRobustnessCaseMemberReads` | ✅ Faithful | | The body's action nodes — owned `action` usages, `perform` steps and nested `analysis` usages — are the steps of the case, lowered as one `Block` over the `ActionGraph` they state: `then` successions, `first`, forks, joins, decisions and merges through `ToActionGraph` (`Block.Stated`), while a body stating no succession leaves its steps unordered, as an action definition's subactions are (`Cases.sysml` `Case::subcases`/`actions` `:> subactions`), so `explore`, `-engine check`, replay and seeded runs interleave them; its locals are bound before the steps and its results evaluated after them; a directed (`in`/`out`/`inout`) analysis or verification usage is a parameter of the body, referential and so no step of it | `lower/case_body.go` `PerformsSteps`, `caseSteps`, `CaseFlowStart`; `lower/calc_body.go` `CalcBody`; `lower/block_graph.go` `IsCaseNode`, `recordNodeScope`; `lower/action_starts.go` `unorderedSubactions`; `runtime/action_subflow.go` `scheduleSubflowStep` | `analysis_steps_then_sequenced.sysml` (+ `.trace.golden`), `analysis_steps_first_then_reorders.sysml` (+ `.trace.golden`), `analysis_step_perform.sysml`, `analysis_step_typed_action.sysml`, `analysis_nested_step_then.sysml`, `analysis_explore_step_order.sysml` (+ default and `seed-1` trace goldens), `verification_explore_step_order.sysml`, `analysis_case_step_order_commuting.sysml`, `analysis_case_step_order_stated.sysml`; `case_step_order_test.go`, `action_directed_case_parameters.sysml`; `case_step_order_test.go`, `directed_usage_parameters_test.go:TestDirectedCaseUsagesAreNotSteps`; `robustness_case_step_order_test.go` | ⚠️ Approximate (`declared` and `reverse` (the default) perform unordered steps in declaration order, a tool-defined linearization; `explore`, `-engine check`, replay and seeded runs reach every interleaving) | | Each step is a subperformance run by the action executor, and a later step reads an earlier one's output by `step.pin`; a nested `analysis` usage runs as a step through the same dispatch, its outputs read as features of it | `runtime/calc_statements.go` `calcStmtHost.attachPerformances`, `calcStmtHost.performNode`, `calcStmtHost.runFlow`, `calcStmtHost.assignAround`; `runtime/action_statements.go` `performances.performNode`; `runtime/action_executor.go` (token traversal, data flow, deadlock detection, step budget) | `analysis_steps_then_sequenced.sysml`, `analysis_nested_step_then.sysml`, `analysis_nested_default_subject.sysml`, `analysis_robustness_test.go:failing_step`, `:step_budget`, `:deadlocked_body`, `:cyclic_successions`, `:unordered_steps` | ✅ Faithful | | A step waiting on the clock (`action sail accept after 60.0 [s];`, `accept at`) advances the run's time as an action's does: a case run on its own (`RunAnalysis`, `RunVerification`, `-analysis`) drives the clock through its flow, which is on the clock for the run and off it afterwards, whether the run ends or fails; a case an action body performs as a step (`verification run : Scene;`) pauses that body, whose executor lists the case's waits among its own and resumes the step when the instant comes, so a wait for a message nothing posts deadlocks the performing action (`ErrAcceptDeadlock`) and a wait under a behavior holding the clock is refused (`errStateBehaviorWaits`). An expression reading a case's output (`assign x := station.voyage.total;`) takes the case whole, so a wait under it is the typed `errCaseReadWaits` naming the wait, the clock left where it was | `runtime/calc_usage.go` `Context.runCalcUsage` (a run with no body to pause puts the flow on the clock), `Context.finishCalcUsage`, `Context.caseReadWaits`; `runtime/case_step.go` `performances.performCase`, `caseStepFrame`; `runtime/action_body_run.go` `bodyWait.waiter`, `Token.pausedWaiter`, `Token.hostedFlow`; `runtime/action_executor.go` `ActionExecutor.pausedWaiters`, `hostedFlows`, `armedWaits`, `visibleArmedWaits`, `hasDuePausedWork`; `runtime/snapshot.go` `executorCaptures.captureBody` | conformance `analysis_steps_wait_on_clock.sysml` + trace golden, `action_case_step_waits_on_clock.sysml` + trace golden; `robustness_case_clock_wait_test.go:TestRuntimeRobustnessCaseClockWait` | ✅ Faithful | -| `return x = expr;`, the body's trailing result expression, and `out p : T = expr;` are evaluated in the case's frame after the steps complete, reading the subject, the `in` parameters, the steps' outputs and calling `calc def`s; units are preserved; an output never bound, one reading itself and outputs reading each other are typed refusals | `runtime/analysis_run.go` `calcRun.outputValues`; `runtime/calc_usage.go` `calcRun.value`, `calcRun.output`, `calcShape.resultOutput` | `analysis_return_vs_out.sysml`, `analysis_units_preserved.sysml`, `analysis_robustness_test.go:output_never_bound`, `:usage_reading_its_own_output`, `:outputs_reading_each_other` | ✅ Faithful | +| `return x = expr;`, the body's trailing result expression, and `out p : T = expr;` are evaluated in the case's frame after the steps complete, reading the subject, the `in` parameters, the steps' outputs and calling `calc def`s; units are preserved; an output never bound, one reading itself and outputs reading each other are typed refusals | `runtime/analysis_run.go` `calcRun.outputValues`; `runtime/calc_usage.go` `calcRun.value`, `calcRun.output`; `program/calc.go` `Calc.ResultOutput` | `analysis_return_vs_out.sysml`, `analysis_units_preserved.sysml`, `analysis_robustness_test.go:output_never_bound`, `:usage_reading_its_own_output`, `:outputs_reading_each_other` | ✅ Faithful | | The `objective` is a requirement the case frames, evaluated after the body: its `require`/`assume` constraints are checked against the case's results by the requirement engine and reported as `satisfied`, `not satisfied` (with the condition) or `undecided` (with the reason), never executed as a step; an `assert constraint` in the body is checked the same way, after the body | `runtime/analysis_run.go` `Context.analysisVerdicts`, `Context.analysisVerdict`, `assertionName`, `calcRun.bindings`, `VerdictStatus`, `AnalysisVerdict`; the condition checks of `edit/requirement.go` | `analysis_objective_satisfied.sysml`, `analysis_objective_not_satisfied.sysml`, `analysis_objective_undecided.sysml`, `analysis_assert_constraint.sysml` | ⚠️ Approximate — the objective's own, its body's and its inherited definition's conditions are checked (`ObjectivesOf`), and every asserted condition of the case is checked once the whole body has completed, against the values the run bound, rather than at the body position it follows; the two differ only where a later step reassigns a case local the assertion reads | | An objective typed by a requirement def is a requirement usage (§8.3.22, the `RequirementUsage` an `ObjectiveMembership` owns): its own members bind the def's subject before its conditions are checked, through the requirement engine's member binding — by keyword alone (`subject = ship;`, an `*ast.SubjectMember`), by name (`subject s = ship;`) or by redefinition (`subject :>> s = ship;`) — and the binding reads the case's subject, `in` parameters, locals and its steps' outputs, an action step's pins (`weigh.m`) included; a binding that cannot be evaluated leaves the objective `undecided` naming it | `runtime/analysis_run.go` `Context.objectiveBindings`; `runtime/context.go` `Context.memberBindings` (the enclosing bindings the case run supplies); `semantics/redefinition.go` `behaviorLike` (an objective's subject implicitly redefines the def's) | `analysis_objective_subject_keyword.sysml`, `analysis_objective_subject_keyword_violated.sysml`, `analysis_objective_subject_action_step_output.sysml`, `analysis_objective_binding_test.go:TestObjectiveSubjectBinding`, `:TestObjectiveSubjectBindsAnActionStepOutput`, `:TestObjectiveBindingFailureIsUndecided`, `:TestObjectiveSubjectBindingOnAnObject`, `cmd/sysml/run_test.go:TestRunAnalysis` | ✅ Faithful | -| An objective subject nothing binds holds the case feature the library's objective states for it, read through the redefinition chain the objective's subject folds (`obj :>> Case::obj`, its `subj` redeclared by the requirement's own subject): in an analysis case that is the case's result, since `Cases::Case::obj` declares `objective obj : RequirementCheck { subject subj default Case::result; }` (§7.22, `Cases.sysml`) and no redefinition restates it — the objective checks the value the case `return`s; a result of another type is `undecided` naming the mismatch (`subject s defaults to the case's result (Cases::Case::obj): type mismatch: 1000.0 (a Real) is not a Ship`), one that breaks the subject's multiplicity (`Ship[2]` given one `Ship`, or two given `Ship`) is `undecided` as a `multiplicity violation` — a redeclaration stating none (`subject :>> pair;`) keeps the multiplicity and type it redefines (KerML §7.3.4.5) — and a case returning nothing leaves the subject unbound, the verdict saying to bind it or return a result. Once the result passes, its objects are classified by the subject as a declared feature value's are (KerML §7.3.4.1, `classifyHeld`), so a `Ship` returned to a `subject t : Tanker` answers `t.cargo`; a subject or actor bound by an expression (`subject = ship;`) is admitted and classified the same way, in an objective as in a requirement usage — a value the member cannot hold refused as a `type mismatch`, more or fewer values than it declares (folded along what it redefines, `subject :>> pair;`) as a `multiplicity violation` | `runtime/analysis_run.go` `Context.objectiveBindings`, `Context.unboundObjectiveSubject`, `Context.statedSubjectValue`, `Context.statedCaseMember`, `statedAsDefault`; `runtime/calc_usage.go` `calcShape.memberName`, `Context.calcMemberNames`; `runtime/context.go` `Context.memberBindings`, `Context.holdBound`, `Context.holdAs`; `runtime/invoke_calc.go` `calcMemberDecl.admits`, `Context.boundMemberDecl`; `runtime/errors.go` `subjectUnboundError` (kind `objective`) | `analysis_objective_subject_default.sysml`, `analysis_objective_subject_default_mismatch.sysml`, `analysis_objective_subject_default_multiplicity.sysml`, `analysis_objective_subject_redeclared_multiplicity.sysml`, `analysis_objective_subject_classified.sysml`, `analysis_objective_subject_bound_multiplicity.sysml`, `analysis_objective_subject_bound_pair.sysml`, `analysis_objective_subject_default_over_case_subject.sysml` (an analysis case with a subject of the requirement's type still checks its result), `analysis_objective_binding_test.go:TestObjectiveSubjectDefaultsToTheResult`, `:TestObjectiveSubjectDefaultHonoursMultiplicity`, `:TestObjectiveBindingKeepsErrorIdentity`, `:TestBoundSubjectIsClassifiedByItsType`, `:TestBoundSubjectHonoursMultiplicity` | ✅ Faithful | -| A case's result parameter is a feature of the case named by qualified name: `MassAnalysisCase::result` (§7.22, the OMG examples' `objective : MassAnalysisObjective { subject = MassAnalysisCase::result; }`) denotes the run's result whether the case names it (`return m : Real = ...`) or not (a trailing expression, `return : Real = ...`), and equally `Cases::Case::result` inherited from the library; in an objective's subject binding, an `assert constraint` in the body, and as `inner.result` read from the case performing `inner` as a step; the qualifier decides whose run it is — the running case or one it specializes reads that run, while a sibling definition's `Other::result` never reads the running case's value and a sibling usage's `light::result` is the sibling's own run | `runtime/eval.go` `EvalContext.frameFeatureValue` (a qualified feature reads the run whose calc the qualifier names or is specialized by); `runtime/calc_usage.go` `calcShape.qualifiedBy`, `calcShape.memberName`, `Context.calcMemberNames` (declared member → the name the run binds; the library's `result` → the run's designated or synthetic result), `calcShape.anonymousResult`; `runtime/frame.go` `frame.owner` | `analysis_objective_subject_qualified_result.sysml`, `analysis_objective_subject_qualified_result_violated.sysml`, `analysis_nested_step_implicit_result.sysml`, `analysis_objective_subject_sibling_result.sysml`, `analysis_objective_binding_test.go:TestObjectiveSubjectBindsTheQualifiedResult`, `:TestQualifiedResultNamesItsOwnCase`, `cmd/sysml/run_test.go:TestRunAnalysis` | ✅ Faithful | +| An objective subject nothing binds holds the case feature the library's objective states for it, read through the redefinition chain the objective's subject folds (`obj :>> Case::obj`, its `subj` redeclared by the requirement's own subject): in an analysis case that is the case's result, since `Cases::Case::obj` declares `objective obj : RequirementCheck { subject subj default Case::result; }` (§7.22, `Cases.sysml`) and no redefinition restates it — the objective checks the value the case `return`s; a result of another type is `undecided` naming the mismatch (`subject s defaults to the case's result (Cases::Case::obj): type mismatch: 1000.0 (a Real) is not a Ship`), one that breaks the subject's multiplicity (`Ship[2]` given one `Ship`, or two given `Ship`) is `undecided` as a `multiplicity violation` — a redeclaration stating none (`subject :>> pair;`) keeps the multiplicity and type it redefines (KerML §7.3.4.5) — and a case returning nothing leaves the subject unbound, the verdict saying to bind it or return a result. Once the result passes, its objects are classified by the subject as a declared feature value's are (KerML §7.3.4.1, `classifyHeld`), so a `Ship` returned to a `subject t : Tanker` answers `t.cargo`; a subject or actor bound by an expression (`subject = ship;`) is admitted and classified the same way, in an objective as in a requirement usage — a value the member cannot hold refused as a `type mismatch`, more or fewer values than it declares (folded along what it redefines, `subject :>> pair;`) as a `multiplicity violation` | `runtime/analysis_run.go` `Context.objectiveBindings`, `Context.unboundObjectiveSubject`, `Context.statedSubjectValue`, `Context.statedCaseMember`, `statedAsDefault`; `runtime/calc_usage.go` `loadedCalc.memberName`, `Context.calcMemberNames`; `runtime/context.go` `Context.memberBindings`, `Context.holdBound`, `Context.holdAs`; `runtime/invoke_calc.go` `calcMemberDecl.admits`, `Context.boundMemberDecl`; `runtime/errors.go` `subjectUnboundError` (kind `objective`) | `analysis_objective_subject_default.sysml`, `analysis_objective_subject_default_mismatch.sysml`, `analysis_objective_subject_default_multiplicity.sysml`, `analysis_objective_subject_redeclared_multiplicity.sysml`, `analysis_objective_subject_classified.sysml`, `analysis_objective_subject_bound_multiplicity.sysml`, `analysis_objective_subject_bound_pair.sysml`, `analysis_objective_subject_default_over_case_subject.sysml` (an analysis case with a subject of the requirement's type still checks its result), `analysis_objective_binding_test.go:TestObjectiveSubjectDefaultsToTheResult`, `:TestObjectiveSubjectDefaultHonoursMultiplicity`, `:TestObjectiveBindingKeepsErrorIdentity`, `:TestBoundSubjectIsClassifiedByItsType`, `:TestBoundSubjectHonoursMultiplicity` | ✅ Faithful | +| A case's result parameter is a feature of the case named by qualified name: `MassAnalysisCase::result` (§7.22, the OMG examples' `objective : MassAnalysisObjective { subject = MassAnalysisCase::result; }`) denotes the run's result whether the case names it (`return m : Real = ...`) or not (a trailing expression, `return : Real = ...`), and equally `Cases::Case::result` inherited from the library; in an objective's subject binding, an `assert constraint` in the body, and as `inner.result` read from the case performing `inner` as a step; the qualifier decides whose run it is — the running case or one it specializes reads that run, while a sibling definition's `Other::result` never reads the running case's value and a sibling usage's `light::result` is the sibling's own run | `runtime/eval.go` `EvalContext.frameFeatureValue` (a qualified feature reads the run whose calc the qualifier names or is specialized by); `runtime/calc_usage.go` `loadedCalc.qualifiedBy`, `loadedCalc.memberName`, `Context.calcMemberNames` (declared member → the name the run binds; the library's `result` → the run's designated or synthetic result), `loadedCalc.anonymousResult`; `runtime/frame.go` `frame.owner` | `analysis_objective_subject_qualified_result.sysml`, `analysis_objective_subject_qualified_result_violated.sysml`, `analysis_nested_step_implicit_result.sysml`, `analysis_objective_subject_sibling_result.sysml`, `analysis_objective_binding_test.go:TestObjectiveSubjectBindsTheQualifiedResult`, `:TestQualifiedResultNamesItsOwnCase`, `cmd/sysml/run_test.go:TestRunAnalysis` | ✅ Faithful | | A case usage owned by a part definition is a feature of every object of the type, as a `calc` usage owned by a part is: `holder.inner.total` runs the case on first read, memoized per activation and invalidated when a value it read changes; an attribute redefined from an analysis output (`attribute :>> x = a.result;`) evaluates through the same path; a package-level usage's outputs are read the same way (`An::shipCost.total`) ; run by name (`-analysis`, `%analysis`, `RunAnalysis`), a case a type owns takes the object given as the object it is a feature of — binding a subject it declares and leaves unbound too — else the single object of the type the run holds, refusing several (`ErrAmbiguousSubject`) and an object of another type (`ErrNotCarried`), and running on no object when the run holds none, as a constraint the type owns is checked | `runtime/calc_usage.go` `EvalContext.evalCalcUsageMembers`, `Context.calcUsageRun`, `Context.forgetCalcUsage`, `Context.enterCalcUsage`; `runtime/analysis_run.go` `Context.RunAnalysis` (usage-bound path), `Context.casePerformer` | `analysis_read_from_attribute_redefinition.sysml`, `analysis_subject_bound_in_usage.sysml`, `cmd/sysml/run_test.go:TestRunAnalysis`, `repl/analysis_test.go:TestAnalysisUsageRuns`; conformance `verification_definition_owned`, `verification_definition_owned_fail`, `runtime/definition_owned_case_test.go`, `repl/definition_owned_case_test.go`, `grpc/definition_owned_case_test.go`, `cmd/sysml/verification_test.go:TestRunVerificationOnADefinitionOwnedCase` | ✅ Faithful | -| The nested features of a calc, constraint or state usage a part definition owns are features of it, read through a feature chain as any usage's members are (KerML 1.1 §7.3.4.6 feature chaining, §7.4.9.3 feature chain expressions; SysML v2 §7.19.2 calculation usages, §7.20.2 constraint usages, §7.18.2 state usages): `rig.area.w` and `rig.area.scale` are the input the one evaluation of the usage bound and the body local it left, read from the same run that answers `rig.area.result`; `rig.fits.limit` and `rig.fits.margin` the input and local of the check, evaluated over the object as the check evaluates them, an input nothing binds reading `` — the body's name is the parameter (KerML §8.2.3.5), which SysML v2 §7.20.1's direct reference to context features does not bind (⚠️ the check itself still binds such an input to the object's same-named feature, as it did before; a default reading an unsupplied input — `in y : Integer = x + 1` — is undetermined with it, since the open input binds as its undetermined value rather than a null; an unsupplied input whose range admits none, `in m : Real[0..1]`, binds empty as an omitted argument does, and the run still computes); a usage read as a function value (an input nothing supplies, previous rows) answers an input's default and leaves the unsupplied input ``; a member the usage declares nowhere is `unknown output` (calc) or `no such feature` (constraint). A typed state usage whose body declares an entry, do or exit action or a substate materializes as its own declaration, so its object carries them: `rig.sm.e` is the entry action it holds, `rig.sm.idle` an unstarted substate of it, read as a nested action usage of a part is. One chain is one read: `new Rig().area.w` is read from the constructed object through `area` then `w`, not from the value of `new Rig().area`. A parameter of a usage nothing has invoked holds no value on an instance any more than at model level (SysML v2 §7.17.2 directed features of a behavior usage's body are its parameters, bound by an invocation; KerML §7.4.8): `new P().act.i` for an uninvoked `action act { in i : Integer; }` reads `` as `p.act.i` does, not the empty value its multiplicity admits; a body attribute of the action reads its value. A constraint usage whose default-less `in` nothing binds reads `` naming that input — itself, through `result`, in its owner's body and on a constructed object — rather than failing with `unbound parameter`: an unbound input leaves a read open (KerML §7.4.8), as its members and a verification case's unbound subject already read, and a calc usage with such an input reads its `result` as `` too, as it reads the input — whatever lower bound the input declares (`in w : Real[1]` as `in w : Real`: a usage read as a feature passes no arguments, so a parameter nothing supplies is left open rather than refused — KerML §7.4.9.4, an invocation binds parameters, and `area()`, a case run with arguments and a usage a behavior body invokes keep the `unbound parameter` error; `calcShape.readsOpen`), a calc's body does not run and its outputs and body locals read `` naming the input (`calcUsageStart.openInput`, `calcRun.openValue`), and so does a case usage's (analysis, verification) on a directed `in` whose range admits none — its result, verdict and body locals read `` naming the input — while the case still runs without a subject, an actor or an input whose range admits none, which read ``, and `RunAnalysis` (`-analysis`) refuses an open run with `unbound parameter`; and the count of an unsupplied input is the parameter's effective range on a constraint as on a calc (`[0..*]` for an unstated `in`, SysML v2 §7.6.3), so `size()` of either is undetermined alike; the bare name of a constraint usage in its owner's body keeps evaluating to the usage's values, the constraint checks (KerML §8.4.4.9.3), not their `result` | `runtime/undetermined.go` `EvalContext.openFeatureRead`, `Context.unboundParameter`; `runtime/calc_usage.go` `calcRun.memberValue`, `calcShape.readsOpen`, `calcUsageStart.openInput`, `calcRun.openValue`, `calcShape.unsuppliedParameters`, `Context.bindCalcUsage`; `runtime/return_parameters.go` `EvalContext.enactedConstraint`, `constraintMemberValue`, `constraintMemberSegment`, `unboundInput`; `runtime/eval.go` `chainFeatureValue`, `evalFeatureChain` (the flattened base); `runtime/instance.go` `declaresAFeature` | conformance `usage_members_through_chain`, `usage_members_through_chain_unbound`, `usage_members_through_chain_exhibited` (the entry action of an exhibited state is the held action object on the model's part and on a constructed one); `robustness_usage_members_test.go:TestRuntimeRobustnessUsageMembers`; `return_parameters_test.go:TestReturnParameterReadsAgreeWithChecks` | ⚠️ Approximate (calc and constraint members and a state's entry, do and exit actions faithful; a substate of definite multiplicity still reads `` at model level and an uninitialized feature on a constructed object until substates are held as the unstarted performance objects their owner holds) | +| The nested features of a calc, constraint or state usage a part definition owns are features of it, read through a feature chain as any usage's members are (KerML 1.1 §7.3.4.6 feature chaining, §7.4.9.3 feature chain expressions; SysML v2 §7.19.2 calculation usages, §7.20.2 constraint usages, §7.18.2 state usages): `rig.area.w` and `rig.area.scale` are the input the one evaluation of the usage bound and the body local it left, read from the same run that answers `rig.area.result`; `rig.fits.limit` and `rig.fits.margin` the input and local of the check, evaluated over the object as the check evaluates them, an input nothing binds reading `` — the body's name is the parameter (KerML §8.2.3.5), which SysML v2 §7.20.1's direct reference to context features does not bind (⚠️ the check itself still binds such an input to the object's same-named feature, as it did before; a default reading an unsupplied input — `in y : Integer = x + 1` — is undetermined with it, since the open input binds as its undetermined value rather than a null; an unsupplied input whose range admits none, `in m : Real[0..1]`, binds empty as an omitted argument does, and the run still computes); a usage read as a function value (an input nothing supplies, previous rows) answers an input's default and leaves the unsupplied input ``; a member the usage declares nowhere is `unknown output` (calc) or `no such feature` (constraint). A typed state usage whose body declares an entry, do or exit action or a substate materializes as its own declaration, so its object carries them: `rig.sm.e` is the entry action it holds, `rig.sm.idle` an unstarted substate of it, read as a nested action usage of a part is. One chain is one read: `new Rig().area.w` is read from the constructed object through `area` then `w`, not from the value of `new Rig().area`. A parameter of a usage nothing has invoked holds no value on an instance any more than at model level (SysML v2 §7.17.2 directed features of a behavior usage's body are its parameters, bound by an invocation; KerML §7.4.8): `new P().act.i` for an uninvoked `action act { in i : Integer; }` reads `` as `p.act.i` does, not the empty value its multiplicity admits; a body attribute of the action reads its value. A constraint usage whose default-less `in` nothing binds reads `` naming that input — itself, through `result`, in its owner's body and on a constructed object — rather than failing with `unbound parameter`: an unbound input leaves a read open (KerML §7.4.8), as its members and a verification case's unbound subject already read, and a calc usage with such an input reads its `result` as `` too, as it reads the input — whatever lower bound the input declares (`in w : Real[1]` as `in w : Real`: a usage read as a feature passes no arguments, so a parameter nothing supplies is left open rather than refused — KerML §7.4.9.4, an invocation binds parameters, and `area()`, a case run with arguments and a usage a behavior body invokes keep the `unbound parameter` error; `loadedCalc.readsOpen`), a calc's body does not run and its outputs and body locals read `` naming the input (`calcUsageStart.openInput`, `calcRun.openValue`), and so does a case usage's (analysis, verification) on a directed `in` whose range admits none — its result, verdict and body locals read `` naming the input — while the case still runs without a subject, an actor or an input whose range admits none, which read ``, and `RunAnalysis` (`-analysis`) refuses an open run with `unbound parameter`; and the count of an unsupplied input is the parameter's effective range on a constraint as on a calc (`[0..*]` for an unstated `in`, SysML v2 §7.6.3), so `size()` of either is undetermined alike; the bare name of a constraint usage in its owner's body keeps evaluating to the usage's values, the constraint checks (KerML §8.4.4.9.3), not their `result` | `runtime/undetermined.go` `EvalContext.openFeatureRead`, `Context.unboundParameter`; `runtime/calc_usage.go` `calcRun.memberValue`, `loadedCalc.readsOpen`, `calcUsageStart.openInput`, `calcRun.openValue`, `loadedCalc.unsuppliedParameters`, `Context.bindCalcUsage`; `runtime/return_parameters.go` `EvalContext.enactedConstraint`, `constraintMemberValue`, `constraintMemberSegment`, `unboundInput`; `runtime/eval.go` `chainFeatureValue`, `evalFeatureChain` (the flattened base); `runtime/instance.go` `declaresAFeature` | conformance `usage_members_through_chain`, `usage_members_through_chain_unbound`, `usage_members_through_chain_exhibited` (the entry action of an exhibited state is the held action object on the model's part and on a constructed one); `robustness_usage_members_test.go:TestRuntimeRobustnessUsageMembers`; `return_parameters_test.go:TestReturnParameterReadsAgreeWithChecks` | ⚠️ Approximate (calc and constraint members and a state's entry, do and exit actions faithful; a substate of definite multiplicity still reads `` at model level and an uninitialized feature on a constructed object until substates are held as the unstarted performance objects their owner holds) | | A case that runs itself, directly or through a step, is refused naming the case rather than recursing; a case recursing without bound through a nested `analysis` step, or a `calc def` through a `calc` usage member typed by itself, hits the depth limit with one line collapsing the repeated frames to a count (`analysis An::Rec::again: … 9999 frames: …`), as a `calc def` calling itself does, and still `errors.Is` the limit | `runtime/calc_usage.go` `Context.enterCalcUsage`, `runtime/errors.go` `errCalcUsageRecursion`, `runtime/calc_usage.go` `Context.runCalcUsage` and `calcRun.value` (framed by `calcFrame`); `runtime/errors.go` `calcFrameError`, `calcFrame`; `runtime/errors.go` `ErrCalcRecursionLimit` | `analysis_robustness_test.go:self_recursion`, `:recursion_through_a_step`, `:recursion_through_a_calc_usage`, `:usage_reading_its_own_output` | ✅ Faithful | | `%analysis [()] []` and the repeatable `-analysis` run a case, printing its outputs with units and each verdict; a usage owned by a part runs as a feature of the session's object for that part; the exit status is 0 satisfied, 1 not satisfied, 2 undecided or failed | `repl/analysis.go` `Session.doAnalysis`, `Session.analysisVerdict`, `Session.runAnalysis`, `splitAnalysisArgs`, `parseAnalysisArgs`; `repl/lookup.go` `Session.owningInstance`; `repl/run.go` `Session.RunAnalysis`; `repl/meta.go`; `cmd/sysml/check.go` (the `-analysis` loop), `cmd/sysml/usage.go`; `packaging/man/man1/sysml.1` | `repl/analysis_test.go` (all), `cmd/sysml/run_test.go:TestRunAnalysis`, `cmd/sysml/usage_test.go`, `make man-check` | ✅ Faithful | | `RunAnalysis` RPC: `symbol_id`, optional `subject_symbol_id`, positional `arguments` and `named_arguments`; answers `outputs`, `verdicts` (satisfied / not satisfied / undecided as `holds` + `error`), the subject's `instances`, and a `failure_reason` on refusal (`WRONG_KIND`, `AMBIGUOUS_SUBJECT`, `EVALUATION`); exposed in the Connect adapter, the Go client (`Client.RunAnalysis`, `Subject`, `Arguments`, `Argument`) and the Python client (`Model.run_analysis`, `AnalysisResult`) | `grpc/analysis.go` `Service.RunAnalysis`; `grpc/connect.go`; `client/opensysml/analysis.go`, `client.go`, `inprocess.go`, `remote.go`; `client/python/opensysml/model.py`, `connection.py`, `verdict.py`; `api/proto/sysml.proto` (generated code by `make proto-buf`, `make python-proto`, `make proto-ts`, `make proto-rust`) | `grpc/analysis_test.go` (all), `grpc/capability_test.go`, `conformance/scenarios/13-run-analysis.json` (8 scenarios over grpc, connect-json, pkg, pkg-connect), `client/python/tests/test_analysis_integration.py`, `client/python/tests/test_verification.py` | ✅ Faithful — over the RPC a case owned by a part has no session object to belong to, so it runs against a fresh object of that part; the REPL and CLI run it on the object `%instantiate`/`-instantiate` created; a case owned by a part definition runs on the object `subject_symbol_id` names, which is the object it is a feature of rather than its subject | @@ -51,9 +51,9 @@ is stated as a rule of the language, and none tests for the trade-study library. | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| | A domain library's calc (`TradeStudies::TradeStudyObjective::eval`, `EvaluationFunction`) is executed from its text as a model's calc is; only the semantic libraries' metamodel frame and the Kernel functions the runtime implements natively are realized rather than executed | `runtime/library_functions.go` `Context.frameDeclared`; `symbols/library_tier.go` `LibraryTier.Semantic` | `analysis_trade_study_minimize.sysml`, `analysis_trade_study_maximize_definition.sysml` | ✅ Faithful | -| `in calc :>> eval = evaluationFunction;` binds a calc-typed parameter to a calc held as a value — the case's own `evaluationFunction`, abstract or bodied — and `eval(x)` in an expression the objective inherits applies it; the objective records the calc `eval` is bound to hold (`Objective.Evaluates`) | `runtime/analysis.go` `Context.objectiveOf`, `Context.calcHeldBy`, `Objective.Evaluates`; `runtime/function_value.go` `EvalContext.functionValueOf`, `EvalContext.invokeFunction`; `runtime/invoke_calc.go` `Context.calcInterfaceOf`, `calcShape.Uncomputed` | `analysis_trade_study_minimize.sysml`, `analysis_trade_study_abstract_evaluation.sysml`, `robustness_analysis_test.go:trade_study_with_an_abstract_evaluation_function` | ✅ Faithful | +| `in calc :>> eval = evaluationFunction;` binds a calc-typed parameter to a calc held as a value — the case's own `evaluationFunction`, abstract or bodied — and `eval(x)` in an expression the objective inherits applies it; the objective records the calc `eval` is bound to hold (`Objective.Evaluates`) | `runtime/analysis.go` `Context.objectiveOf`, `Context.calcHeldBy`, `Objective.Evaluates`; `runtime/function_value.go` `EvalContext.functionValueOf`, `EvalContext.invokeFunction`; `runtime/invoke_calc.go` `Context.calcInterfaceOf`, `loadedCalc.Uncomputed` | `analysis_trade_study_minimize.sysml`, `analysis_trade_study_abstract_evaluation.sysml`, `robustness_analysis_test.go:trade_study_with_an_abstract_evaluation_function` | ✅ Faithful | | An inherited expression reads a feature as the running behavior inherits it: through the redefinition where the behavior, or a behavior enclosing it, redefines the feature the library expression names (KerML §7.3.4.5) — `studyAlternatives` and `evaluationFunction` in the library's bindings answer to the model's `subject` and `calc :>> evaluationFunction`, and `tradeStudyObjective(selectedAlternative = a)` in the library's `selectOne` body applies the model's objective usage | `runtime/eval.go` `EvalContext.runningBehavior`, `Context.inheritedFeature`, `EvalContext.evalNameGeneral`, `EvalContext.invocationTarget`; `runtime/frame.go` `frame.running`; `semantics/masking.go` `Model.maskingRedefinedFeatures` (a subject or objective redefining by role masks what it redefines) | `analysis_trade_study_minimize.sysml`, `analysis_trade_study_maximize_definition.sysml`, `analysis_trade_study_case_parameter.sysml` | ✅ Faithful | -| A requirement usage is applied as a predicate: `tradeStudyObjective(selectedAlternative = a)` binds the subject — a case's or requirement's first input parameter (§7.21.2, §7.22.2) — and answers whether its `require` constraints hold, the body's frames flattened so the objective's inherited bindings (`alternatives`, `eval`, `best`) are read in the applying case's run; a call may omit the subject when its declaration, or one it redefines, gives it a default or a multiplicity admitting none | `runtime/invoke_predicate.go` `EvalContext.invokePredicate`, `Context.predicateShapeOf`, `isPredicateDecl`, `flattenFrames`; `semantics/invocation.go` `Model.signatureOf`, `Model.signatureParameterOf`, `Model.OptionalParameter` | `analysis_trade_study_minimize.sysml`, `analysis_trade_study_tie.sysml`, `analysis_trade_study_minimize.trace.golden`, `calc_predicate_defaulted_subject.sysml`, `passes.TestInvocationOverloadDefaultedSubjectIsOptional` | ✅ Faithful | +| A requirement usage is applied as a predicate: `tradeStudyObjective(selectedAlternative = a)` binds the subject — a case's or requirement's first input parameter (§7.21.2, §7.22.2) — and answers whether its `require` constraints hold, the body's frames flattened so the objective's inherited bindings (`alternatives`, `eval`, `best`) are read in the applying case's run; a call may omit the subject when its declaration, or one it redefines, gives it a default or a multiplicity admitting none | `runtime/invoke_predicate.go` `EvalContext.invokePredicate`, `Context.predicateShapeOf`, `flattenFrames`; `lower/calc.go` `IsPredicateDecl`, `ToPredicate`; `semantics/invocation.go` `Model.signatureOf`, `Model.signatureParameterOf`, `Model.OptionalParameter` | `analysis_trade_study_minimize.sysml`, `analysis_trade_study_tie.sysml`, `analysis_trade_study_minimize.trace.golden`, `calc_predicate_defaulted_subject.sysml`, `passes.TestInvocationOverloadDefaultedSubjectIsOptional` | ✅ Faithful | | An objective's inherited `require constraint { eval(selectedAlternative) == best }` is seen and checked as the objective's own condition, its parameters (`eval`, `best`, `alternatives`) visible to the condition through the objective's inherited declarations; the library's condition is reported by the requirement engine as any objective condition is | `runtime/analysis.go` `Context.objectiveConditionsOf`; `runtime/condition.go` `Context.conditionFeatures`, `runtime/shape.go` `Context.parameterFeatures` | `analysis_trade_study_minimize.sysml` (`objective tradeStudyObjective: satisfied`), `analysis_trade_study_maximize_definition.sysml`, `repl/trade_study_test.go:TestAnalysisOfATradeStudyReportsEachAlternative` | ✅ Faithful | | `->minimize {…}`/`->maximize {…}` over a body applying a function value evaluate the body once per element in collection order and answer the least/greatest value; a body answering a feature that holds no value is a typed `NoValueError` naming the feature, not a comparison of nothing | `runtime/collections.go` `EvalContext.applyValueBody`, `EvalContext.extremum`; `runtime/instance.go` `Context.noValueError`, `runtime/errors.go` `NoValueError` | `analysis_trade_study_minimize.sysml`, `analysis_trade_study_maximize_definition.sysml`, `robustness_analysis_test.go:trade_study_whose_alternatives_read_an_unbound_feature` | ✅ Faithful | | `->selectOne {…}` is the library's `select {…}#(1)`: the *first* element the predicate holds for. Two alternatives scoring `best` are both matched by `eval(a) == best`, the first is selected, and the later is reported `tied` so the tie is visible; the library states no tie-breaking beyond `#(1)` and none is invented | `runtime/collections.go` `builtinControlSelectOne`, `EvalContext.filter`; `runtime/evaluation_log.go` `evaluationLog.evaluations` | `analysis_trade_study_tie.sysml`, `cmd/sysml/run_trade_study_test.go:TestRunTradeStudy`, `grpc/case_evaluations_test.go:TestRunAnalysisReportsCaseEvaluations` | ✅ Faithful | @@ -61,7 +61,7 @@ is stated as a rule of the language, and none tests for the trade-study library. | Each application of a case's own calc as a value is reported with the run — `AnalysisResult.Evaluations`, one `AnalysisEvaluation` per distinct argument in first-evaluation order (subject order), with the result or the error, `Selected` for the one whose argument `selectOne` picked and the case returned (a result that merely equals an argument, with no `selectOne` picking it, selects nothing) and `Tied` for a later one computing the same — and the trace shows the same order: the subject bound, each alternative scored under `minimize`, `best` read, then `selectOne` checking each alternative | `runtime/evaluation_log.go` `evaluationLog`, `Context.beginEvaluationLog`, `evaluationLog.record`, `evaluationLog.pick`, `evaluationLog.evaluations`; `runtime/collections.go` `builtinControlSelectOne`; `runtime/analysis_run.go` `AnalysisResult`, `AnalysisEvaluation` | `analysis_trade_study_minimize.trace.golden`, `analysis_trade_study_tie.sysml`, `analysis_evaluations_result_coincides.sysml`, `conformance_test.go` (`evaluations` expectations) | ✅ Faithful | | An evaluation that fails for one alternative — division by zero, a feature with no value, a calc with no return expression — is reported for that alternative with its error, the earlier ones keep their results, nothing is selected, the objective is `undecided` naming the failure and the run fails with it; a swept run keeps the same on its failed row | `runtime/analysis_run.go` `Context.undecidedVerdicts`, `Context.analysisRun`; `repl/sweep.go` `Session.RunSweep`, `sweep/sweep.go` `SweepRow.Evaluations` | `analysis_trade_study_alternative_fails.sysml`, `analysis_trade_study_abstract_evaluation.sysml`, `robustness_analysis_test.go:trade_study_whose_evaluation_fails_for_one_alternative`, `repl/trade_study_test.go:TestSweepOfATradeStudyKeepsEvaluationsOnAFailedRow`, `grpc/case_evaluations_test.go:TestRunSweepKeepsEvaluationsOfFailedRow` | ✅ Faithful | | An output the case cannot compute — a later `out` after an earlier one succeeded — leaves every objective and asserted constraint `undecided` naming that failure, the one reading only the computed output included, while the outputs computed before it and the evaluations made stay reported and none is `selected`; the run fails with the output's error | `runtime/analysis_run.go` `Context.runCase`, `Context.undecidedVerdicts` | `analysis_output_fails_verdicts_undecided.sysml` | ✅ Faithful | -| An application is one evaluation per distinct binding of the calc's parameters, spelled by parameter position — a named argument at its parameter's position, `null` for a parameter the application leaves to the calc before a later one — so the same values bound by name to different parameters are two evaluations and a value bound by position and then by name to the same parameter is one | `runtime/evaluation_log.go` `evaluationLog.record`, `calcShape.argumentsByPosition`, `evaluationLog.seen` | `analysis_evaluations_named_arguments.sysml` | ✅ Faithful | +| An application is one evaluation per distinct binding of the calc's parameters, spelled by parameter position — a named argument at its parameter's position, `null` for a parameter the application leaves to the calc before a later one — so the same values bound by name to different parameters are two evaluations and a value bound by position and then by name to the same parameter is one | `runtime/evaluation_log.go` `evaluationLog.record`, `loadedCalc.argumentsByPosition`, `evaluationLog.seen` | `analysis_evaluations_named_arguments.sysml` | ✅ Faithful | | `%analysis`/`-analysis` print each evaluation beneath the verdicts (`evaluationFunction(Trade::b (object #2)) = 10.0 [selected]`), `-json` carries them as each check's `evaluations`, `%sweep`/`-sweep` as an `evaluations` column and each JSON row's `evaluations`; `RunAnalysisResponse.evaluations` and `SweepRow.evaluations` carry them as `CaseEvaluation` under the `case_evaluations` capability, an alternative as an `instance_id` resolving in `instances`, and the Python (`CaseEvaluation`, `AnalysisResult.evaluations`, `.selected`, `AnalysisRunError.result`), Node, Java and Rust clients read them | `repl/analysis.go` `evaluationOf`; `repl/sweep.go`; `cmd/sysml/report.go` `caseEvaluations`; `grpc/analysis.go` `verifyContext.caseEvaluations`, `verifyContext.runRoots`; `grpc/sweep.go`; `api/proto/sysml.proto` `CaseEvaluation`; `client/python/opensysml/verdict.py`, `client/node/src/core/capabilities.ts`, `client/java/.../Capabilities.java`, `client/rust/opensysml/src/connection.rs` `Connection::call` | `repl/trade_study_test.go`, `cmd/sysml/run_trade_study_test.go:TestRunTradeStudy`, `:TestSweepTradeStudy`, `grpc/case_evaluations_test.go`, `client/python/tests/test_analysis_integration.py`, `client/node/test/client.test.ts`, `client/java/.../ApiIntegrationTest.java`, `client/rust/opensysml/tests/client.rs` | ✅ Faithful | -| The OMG models run through this path: the training corpus's `Trade Study Analysis Example.sysml` (`subject : Engine = (engine4cyl, engine6cyl);`, inheriting `[1..*]`) binds both alternatives and fails at the first with a typed `no value` naming `evaluationFunction::powerRollup`, whose rollup calcs the model leaves bodyless; the pilot's `TradeStudyTest.sysml` declares `evaluationFunction` with no body and fails the same way naming it; both leave the objective `undecided` rather than fabricating a pick | `runtime/invoke_calc.go` `calcShape.Uncomputed` | `cmd/sysml/run_trade_study_corpus_test.go:TestRunTradeStudyTrainingExample`, `:TestRunTradeStudyPilotSimpleTest` | ✅ Faithful — the models state no way to score their alternatives | -| `subject : Engine[1..*] = all engineChoice;` (the pilot's `10b-Trade-off Among Alternative Configurations.sysml`): the subject is the extent of the variation `engineChoice` — its variants `'4cylEngine'` and `'6cylEngine'`, in declaration order — so both alternatives are bound, the first is evaluated and fails with `no value` (`ErrNoValue`) naming `evaluationFunction::powerRollup`, whose rollup calcs the model leaves bodyless (as the training corpus's trade study does, the row above), nothing is selected and the objective is `undecided` naming that failure | `runtime/extent.go` `EvalContext.evalExtent`, `variantValues`; `runtime/invoke_calc.go` `calcShape.Uncomputed` | `cmd/sysml/run_trade_study_corpus_test.go:TestRunTradeStudyPilotTradeOff` | ✅ Faithful — the extent binds and the alternatives are evaluated; the objective stays `undecided` because the model computes no evaluation, not because anything is refused | +| The OMG models run through this path: the training corpus's `Trade Study Analysis Example.sysml` (`subject : Engine = (engine4cyl, engine6cyl);`, inheriting `[1..*]`) binds both alternatives and fails at the first with a typed `no value` naming `evaluationFunction::powerRollup`, whose rollup calcs the model leaves bodyless; the pilot's `TradeStudyTest.sysml` declares `evaluationFunction` with no body and fails the same way naming it; both leave the objective `undecided` rather than fabricating a pick | `runtime/invoke_calc.go` `loadedCalc.Uncomputed` | `cmd/sysml/run_trade_study_corpus_test.go:TestRunTradeStudyTrainingExample`, `:TestRunTradeStudyPilotSimpleTest` | ✅ Faithful — the models state no way to score their alternatives | +| `subject : Engine[1..*] = all engineChoice;` (the pilot's `10b-Trade-off Among Alternative Configurations.sysml`): the subject is the extent of the variation `engineChoice` — its variants `'4cylEngine'` and `'6cylEngine'`, in declaration order — so both alternatives are bound, the first is evaluated and fails with `no value` (`ErrNoValue`) naming `evaluationFunction::powerRollup`, whose rollup calcs the model leaves bodyless (as the training corpus's trade study does, the row above), nothing is selected and the objective is `undecided` naming that failure | `runtime/extent.go` `EvalContext.evalExtent`, `variantValues`; `runtime/invoke_calc.go` `loadedCalc.Uncomputed` | `cmd/sysml/run_trade_study_corpus_test.go:TestRunTradeStudyPilotTradeOff` | ✅ Faithful — the extent binds and the alternatives are evaluated; the objective stays `undecided` because the model computes no evaluation, not because anything is refused | diff --git a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/06-verification-case.md b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/06-verification-case.md index 02c912fe7b..834f9a0c99 100644 --- a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/06-verification-case.md +++ b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/06-verification-case.md @@ -14,9 +14,9 @@ in place of them: a failing verification body does not make a satisfied requirem | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| -| A `verification def`/`verification` usage is run the way an analysis case is — the same lowering of the body over one `ActionGraph`, the same `subject`/`in` binding, the same step execution, the same objective and `assert constraint` checks afterwards — through one shared case path rather than a second executor; a symbol that is not a verification case is a typed refusal | `runtime/analysis_run.go` `Context.RunAnalysis`, `Context.runCase`; `runtime/verification_run.go` `Context.RunVerification`, `IsVerificationCaseSymbol`, `Context.requireVerificationCase`, `runtime/errors.go` `errNotAVerification`; `runtime/invoke_calc.go` `calcShape.isCase`; `runtime/calc_usage.go` `isCalcUsageSymbol`; `lower/case_body.go` `PerformsSteps`, `lower/block_graph.go` `IsCaseNode`, `lower/calc_body.go` `CalcBody` | conformance `verification_verdict_pass` (+ `.trace.golden`), `verification_verdict_fail`, `runtime/verification_verdict_test.go:TestVerificationBodyVerdicts`, `robustness_verification_test.go:verification_of_a_symbol_that_is_not_a_case`, `:verification_with_an_argument_the_case_does_not_take` | ✅ Faithful to the case-execution semantics it reuses | +| A `verification def`/`verification` usage is run the way an analysis case is — the same lowering of the body over one `ActionGraph`, the same `subject`/`in` binding, the same step execution, the same objective and `assert constraint` checks afterwards — through one shared case path rather than a second executor; a symbol that is not a verification case is a typed refusal | `runtime/analysis_run.go` `Context.RunAnalysis`, `Context.runCase`; `runtime/verification_run.go` `Context.RunVerification`, `IsVerificationCaseSymbol`, `Context.requireVerificationCase`, `runtime/errors.go` `errNotAVerification`; `program/calc.go` `Calc.IsCase`; `lower/calc.go` `IsCalcUsageSymbol`; `lower/case_body.go` `PerformsSteps`, `lower/block_graph.go` `IsCaseNode`, `lower/calc_body.go` `CalcBody` | conformance `verification_verdict_pass` (+ `.trace.golden`), `verification_verdict_fail`, `runtime/verification_verdict_test.go:TestVerificationBodyVerdicts`, `robustness_verification_test.go:verification_of_a_symbol_that_is_not_a_case`, `:verification_with_an_argument_the_case_does_not_take` | ✅ Faithful to the case-execution semantics it reuses | | A body whose result is a `VerificationCases::PassIf(...)` call answers `pass` or `fail` as **that library calculation** computes it — the calc is invoked through the ordinary calculation machinery and its `VerdictKind` result read, not a truth table restated in Go | `runtime/verification_run.go` `Context.bodyVerdict`, `Context.verdictOf`, `Context.isVerdictKind`; `runtime/invoke_calc.go` (the calc invocation the body's expression makes); `libs/stdlib/Systems Library/VerificationCases.sysml` `calc def PassIf` | conformance `verification_verdict_pass`, `verification_verdict_fail`, `runtime/verification_verdict_test.go:TestVerificationBodyVerdicts` (`test::passing`, `test::failing`) | ⚠️ Tool-defined — the spec leaves the evaluation non-normative; the pass/fail truth table itself is the library's own text and is executed rather than reimplemented | -| A verification case's objective checks the case's **subject**, not its verdict (§7.24.1: "the objective of a verification case is to verify that the verification subject satisfies one or more specific requirements"; `VerificationCases::VerificationCase::obj` redefines the objective's subject as the case's): the library redefines the objective with a bound, not defaulted, subject — `VerificationCases::VerificationCase::obj :>> Case::obj { subject subj = VerificationCase::subj; }` — and the runtime honours the redefining feature's stated value over the redefined `Case::obj`'s `default Case::result`, as a language rule rather than a verification one: the stated `VerificationCase::subj` is resolved to the run's subject binding through the redefinition index (`subj :>> Case::subj`, the usage's own `subject lander` redefining it). So an objective typed by a requirement def whose subject has another name (`subject lander : Lander` against the library's `subj`) evaluates that requirement against the verification subject with no binding of its own — whether the usage restates the subject without a value (`subject :>> subj;` ahead of its inputs, the shape the pilot accepts since a usage's owned parameters redefine its definition's by position) or leaves it to the library — an objective whose requirement declares no subject is decided the same way, `in limit = limit;` still binds the requirement's parameter, and the objective is decided (`satisfied`/`not satisfied`) beside the body's `pass`/`fail` on every surface — `-analysis`, `-requirement`, `-satisfy`, `%analysis`, `%requirement`, `%satisfy`, gRPC `RunAnalysis`/`VerifyRequirement` — and in every row of a `-sweep`. A requirement subject the verification subject cannot be is `undecided` naming both (`subject rover is bound to the case's subject (VerificationCases::VerificationCase::obj): type mismatch: Lander #1 (scout) is not a Rover`); a verification whose own subject nothing binds is the case's `subjectUnboundError` naming *that* subject; a usage restating the objective's subject (`objective : R { subject lander = other; }`, `subject = other;`, `subject :>> subj = other;`) is refused as overriding the library's fixed `=` value | `runtime/analysis_run.go` `Context.objectiveBindings`, `Context.unboundObjectiveSubject`, `Context.statedSubjectValue`, `Context.statedCaseMember`, `statedAsDefault`, `calcShape.memberRole`; `runtime/calc_usage.go` `Context.calcMemberNames` (redefined features indexed under the run's name), `calcShape.memberName`; `runtime/subsetting.go` `Context.redefinedFeatures`; `runtime/condition.go` `Context.conditionFeatures` (the effective feature values along the chain); `runtime/library_functions.go` `Context.libraryDeclared`; `passes/feature_value_overriding.go` (the `=` refusal) | conformance `verification_objective_subject` (+ `.trace.golden`), `verification_objective_subject_violated`, `verification_objective_subject_unstated`, `verification_objective_subject_unbound`, `analysis_objective_subject_default_over_case_subject`; `robustness_verification_test.go:verification_objective_subject_of_another_type`, `:verification_objective_subject_left_unbound`, `:verification_objective_subject_rebound`; `cmd/sysml/verification_test.go:TestVerificationObjectiveDecidesOnEverySurface`, `cmd/sysml/sweep_test.go:TestSweepVerificationThroughCLI`, `repl/verification_verdict_test.go:TestVerificationObjectiveDecidesAgainstTheCaseSubject`, `grpc/verification_verdict_test.go:TestRunAnalysisDecidesAVerificationObjectiveAgainstTheCaseSubject` | ✅ Faithful | +| A verification case's objective checks the case's **subject**, not its verdict (§7.24.1: "the objective of a verification case is to verify that the verification subject satisfies one or more specific requirements"; `VerificationCases::VerificationCase::obj` redefines the objective's subject as the case's): the library redefines the objective with a bound, not defaulted, subject — `VerificationCases::VerificationCase::obj :>> Case::obj { subject subj = VerificationCase::subj; }` — and the runtime honours the redefining feature's stated value over the redefined `Case::obj`'s `default Case::result`, as a language rule rather than a verification one: the stated `VerificationCase::subj` is resolved to the run's subject binding through the redefinition index (`subj :>> Case::subj`, the usage's own `subject lander` redefining it). So an objective typed by a requirement def whose subject has another name (`subject lander : Lander` against the library's `subj`) evaluates that requirement against the verification subject with no binding of its own — whether the usage restates the subject without a value (`subject :>> subj;` ahead of its inputs, the shape the pilot accepts since a usage's owned parameters redefine its definition's by position) or leaves it to the library — an objective whose requirement declares no subject is decided the same way, `in limit = limit;` still binds the requirement's parameter, and the objective is decided (`satisfied`/`not satisfied`) beside the body's `pass`/`fail` on every surface — `-analysis`, `-requirement`, `-satisfy`, `%analysis`, `%requirement`, `%satisfy`, gRPC `RunAnalysis`/`VerifyRequirement` — and in every row of a `-sweep`. A requirement subject the verification subject cannot be is `undecided` naming both (`subject rover is bound to the case's subject (VerificationCases::VerificationCase::obj): type mismatch: Lander #1 (scout) is not a Rover`); a verification whose own subject nothing binds is the case's `subjectUnboundError` naming *that* subject; a usage restating the objective's subject (`objective : R { subject lander = other; }`, `subject = other;`, `subject :>> subj = other;`) is refused as overriding the library's fixed `=` value | `runtime/analysis_run.go` `Context.objectiveBindings`, `Context.unboundObjectiveSubject`, `Context.statedSubjectValue`, `Context.statedCaseMember`, `statedAsDefault`, `loadedCalc.memberRole`; `runtime/calc_usage.go` `Context.calcMemberNames` (redefined features indexed under the run's name), `loadedCalc.memberName`; `runtime/subsetting.go` `Context.redefinedFeatures`; `runtime/condition.go` `Context.conditionFeatures` (the effective feature values along the chain); `runtime/library_functions.go` `Context.libraryDeclared`; `passes/feature_value_overriding.go` (the `=` refusal) | conformance `verification_objective_subject` (+ `.trace.golden`), `verification_objective_subject_violated`, `verification_objective_subject_unstated`, `verification_objective_subject_unbound`, `analysis_objective_subject_default_over_case_subject`; `robustness_verification_test.go:verification_objective_subject_of_another_type`, `:verification_objective_subject_left_unbound`, `:verification_objective_subject_rebound`; `cmd/sysml/verification_test.go:TestVerificationObjectiveDecidesOnEverySurface`, `cmd/sysml/sweep_test.go:TestSweepVerificationThroughCLI`, `repl/verification_verdict_test.go:TestVerificationObjectiveDecidesAgainstTheCaseSubject`, `grpc/verification_verdict_test.go:TestRunAnalysisDecidesAVerificationObjectiveAgainstTheCaseSubject` | ✅ Faithful | | A body binding `verdict` (or its result) to a `VerdictKind` literal reports that literal, recognized by the enumeration the value belongs to rather than by the spelling of the name. The verdict is read from the case's result parameter only — the name it declares, the library's `verdict`, or an unnamed `result` — so an `out` parameter of the same enumeration is not mistaken for the case's answer | `runtime/verification_run.go` `Context.bodyVerdict`, `verdictOutputNames`, `Context.verdictOf`, `Context.isVerdictKind`, `verdictKindName`, `verdictOutputName`; `runtime/analysis_run.go` `calcRun.resultName` | `runtime/verification_verdict_test.go:TestVerificationBodyVerdicts` (`test::stated`, `test::aux`) | ⚠️ Tool-defined — reported as the library declares the literal | | A body that produces no verdict value — no `PassIf`, no `verdict` binding — is `inconclusive`, saying so, rather than defaulted to a pass or a fail | `runtime/verification_run.go` `Context.bodyVerdict` (`VerdictInconclusive`) | conformance `verification_verdict_inconclusive`, `runtime/verification_verdict_test.go:TestVerificationBodyVerdicts` (`test::silent`) | ⚠️ Tool-defined | | A verification case a part definition owns is a feature of every object of that definition (§7.24; §7.6.3, a usage nested in a definition; the static side is the nested-definition feature accessibility rule): `-validate=`, `-requirement`, `-satisfy` and the gRPC `ValidateInstance`/`VerifyRequirement`/`VerifySatisfaction` run it once per object of the definition the check is about — every object the run holds for a check about none (the gRPC `VerifyConstraint`, `VerifyRequirement` and `RunAnalysis` naming no `subject_symbol_id` evaluate over the population `Instantiate` holds for the model, under its lock, as `VerifySatisfaction` does and as a REPL check is about the objects the session holds: a type-owned constraint is checked on the held object carrying it, a case a definition owns runs as a feature of the held object of that definition, and a sweep that would pass the held-objects bound is `RESOURCE_EXHAUSTED` as `Instantiate` is; a named subject keeps a worker of its own), the objects a multiplicity repeats (`part cheap : Definition [2]`, a `[0..*]` collection's members, a part nested inside a repeated part) each on their own, and the objects one declaration instantiated twice leaves each on their own too — the one the name denotes and the one it displaced, both live objects of the definition (`LiveRoots`): a feature of the definition relates every object in its domain to values (KerML §7.3.4.1) and a verdict is about the subject of one performance (SysML §7.24.1), so the REPL labels the displaced one `on #1 (displaced from test::b)` — so the verdict count is the object count rather than the usage count — with that object as the one it reads its owner's features from, and name the object on the verdict; a `satisfy` without `by` a definition states is checked on the object of that definition the same way. With no object of the definition instantiated the case has nothing to be a feature of, so every surface sweeping it (`-satisfy`, `-requirement`, `-analysis` without an object, the gRPC and JSON surfaces) reports it `inconclusive` — the library's `VerdictKind` for a run that decided nothing, not `error`, nothing being wrong with the model — with the detail `needs an object of : none is instantiated` the definition's `satisfy` line uses; an analysis case a definition owns is refused with the same message (`ErrNeedsObject`). A case a part usage owns is unaffected: it reads the usage's bound values, as a usage's `satisfy` instantiates the usage | `runtime/verification_cases.go` `Context.VerificationVerdictsOn`, `Context.caseCarriers` over `condition.go` `Context.LiveRoots` and `Context.objectsUnder`; `repl/verification.go` `Session.verificationObject`, `Session.displacedLabel`; `grpc/verify.go` `verifyContext.overHeld`, `verifyContext.requirementVerificationRuns` (`VerificationVerdict.Err` classifies the bound), `grpc/analysis.go` `verifyContext.scheduled`, `verifyContext.exhaustedRun`, `grpc/tooldryrun.go` `Service.toolDryRun` (snapshot and runner restored) (every object, where a check's `carriersUnder` keeps one per declaration); `runtime/verification_cases.go` `Context.verificationResultOn`; `runtime/satisfy.go` `Context.SatisfactionSubjects` (every object a type-stated `satisfy` sweeps); `runtime/analysis_run.go` `Context.casePerformer` (`runtime/errors.go` `ErrNeedsObject`); `runtime/verification_run.go` `Context.RunVerification`, `VerificationVerdict.Subject`; `runtime/satisfy.go` `Context.CheckSatisfactionOn`; `repl/verification.go` `Session.withVerifications`, `verificationLine`; `grpc/verify.go` `verifyContext.requirementVerifications` | conformance `verification_definition_owned`, `verification_definition_owned_fail`, `verification_definition_owned_no_object`, `analysis_definition_owned_no_object`; `runtime/definition_owned_case_test.go`, `runtime/robustness_definition_owned_case_test.go`; `repl/definition_owned_case_test.go`; `grpc/definition_owned_case_test.go`, `grpc/held_population_verify_test.go`, `grpc/held_population_preview_test.go:TestToolPreviewLeavesTheHeldPopulationAsItWas`, `grpc/robustness_held_population_test.go:TestGRPCRobustnessHeldPopulation`, gRPC conformance `verify_requirement_held_population`, `verify_requirement_no_object`; `cmd/sysml/verification_test.go:TestRunVerificationOnADefinitionOwnedCase`; conformance `validation_verification_definition_owned` (`_fail`, `_satisfy_holds_verification_fails`, `_satisfy_fails_verification_passes`, `_subject_owner_bound`, `_subject_unbound`), each satisfaction verdict pinned beside the verification runs of its requirement, and `cmd/sysml/verification_test.go:TestValidateAgreesWithConformanceFixtures` holding `-validate` to the same verdicts; conformance `requirement_verification_repeated_subjects` (`[2]` twice, two passing and two failing), `requirement_verification_open_multiplicity_subjects` (an empty `[0..*]` gives no run, a subset one a run per member), `requirement_verification_nested_repeated_subjects` (a bolt in each of four wheels), pinned about no object as `-requirement` reports them, `cmd/sysml/verification_test.go:TestRequirementAgreesWithConformanceFixtures` holding `-requirement` to the same runs, `robustness_verification_test.go:testVerificationRunsOncePerRepeatedObject` (two cars, eight bolts, eight runs), `testVerificationRunsOnEveryObjectOfOneDeclaration`, `repl/displaced_object_sweep_test.go:TestRequirementSweepsTheObjectADisplacedNameLeft`, `grpc/held_population_verify_test.go:TestVerifyRequirementWithoutASubjectSweepsADisplacedObject`, `grpc/definition_owned_case_test.go:TestVerifyRunsADefinitionOwnedCaseOnEveryRepeatedObject` | ✅ Faithful | diff --git a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/07-constraint.md b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/07-constraint.md index 99a0afe794..3f410b7d55 100644 --- a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/07-constraint.md +++ b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/07-constraint.md @@ -33,5 +33,5 @@ | The conditions of a nested constraint (`assert constraint [name] { }`) are the conditions of the member stating it | `parser/behavior.go` `tryParseNestedConstraint`, `condition.go` `appendConditions` | `parser/behavior_require_member_test.go:TestConstraintMemberNestedBody` | ✅ Faithful | | `this` inside an asserted constraint or requirement usage an object's type declares names that object (`Occurrences::Occurrence::this`: the context occurrence, overridden for the owned performances of an object), so a usage typed by a constraint def binds the def's context parameter with `in ref :>> context = this;` and validating the object evaluates the def's result over it; checked about no object (`-constraint` without a subject), the usage reads the type's declared defaults and `this` denotes an exemplar of the type made for the check and abandoned with it | `resolve/this_context.go` `Resolver.ThisContext`, `isBehaviorUsageKind`; `runtime/eval.go` `EvalContext.thisValue`; `runtime/exemplar.go` `Context.exemplarOf` | conformance `instance_validate_constraint_def_context`; `runtime/exemplar_test.go` `TestConstraintAboutNoObjectReadsThisAsExemplar` | ✅ Faithful | | A constraint body that declares parameters (`constraint c { in x : Real; assert x >= 0; }`) states conditions like any other, including conditions that start with a keyword (`assert true`, `assert not false`, `assume null != x`, `assert if …`); a condition missing its expression is a diagnostic | `parser/behavior.go` `atConstraintCondition`, `parser/behavior.go` `exprStartKeywords` | `parse/constraint_parameterised_conditions.golden`, `parser/constraint_condition_test.go`, `parser/negative_test.go:constraint_params_assert_no_condition` | ✅ Faithful | -| A constraint carrying no condition has no value (`ErrNoValue`), neither a verdict nor a vacuous pass: a constraint is a `BooleanEvaluation` whose `result : Boolean[1]` only its body expression binds (KerML 1.1 Beta 2 §7.4.8.2; SysML v2.1 Beta 2 §7.20.2), so with no body nothing binds it — the same unbound-result case a calc with no result expression is, and "no conditions" is not a notion the specification has. The cause stays as the detail (`constraint empty: no value: the constraint declares no condition`; a `return r;` the body never binds carries the calc's hint instead), on every surface that reports a check (REPL, LSP, gRPC `VerifyConstraint`/`VerifyRequirement`, MCP, the solver and `smt` engine, the validation report), and a model-level read of such a constraint's value is undetermined, as any unbound result is. A requirement carrying none is judged by the Requirement rule below instead | `errors.go` `ErrNoValue`, `condition.go` `evaluateConditions`, `conditionCheck.noResultValue`, `conditionCheck.isRequirementCheck`, `invoke_calc.go` `unboundResultHint`, `context.go` `EvaluateConstraintOn`; `solve/translate.go` `requirementSubject`, `Subject.noResultValue`, `unboundConstraintResult` | conformance `condition_less_constraint_def`, `condition_less_constraint_usage`, `read_return_parameters` (`cnegNoCondition`); `runtime/robustness_condition_less_check_test.go`, `runtime/constraint_test.go:TestConstraintWithoutConditionsIsNotAVerdict`, `runtime/owned_constraint_value_test.go:TestOwnedConstraintValueIsNotACondition`, `runtime/robustness_vacuous_requirement_test.go`; `solve/translate_test.go:TestNoConditions`, `solve/asserted_vacuous_test.go` | ✅ Faithful | +| A constraint carrying no condition has no value (`ErrNoValue`), neither a verdict nor a vacuous pass: a constraint is a `BooleanEvaluation` whose `result : Boolean[1]` only its body expression binds (KerML 1.1 Beta 2 §7.4.8.2; SysML v2.1 Beta 2 §7.20.2), so with no body nothing binds it — the same unbound-result case a calc with no result expression is, and "no conditions" is not a notion the specification has. The cause stays as the detail (`constraint empty: no value: the constraint declares no condition`; a `return r;` the body never binds carries the calc's hint instead), on every surface that reports a check (REPL, LSP, gRPC `VerifyConstraint`/`VerifyRequirement`, MCP, the solver and `smt` engine, the validation report), and a model-level read of such a constraint's value is undetermined, as any unbound result is. A requirement carrying none is judged by the Requirement rule below instead | `errors.go` `ErrNoValue`, `condition.go` `evaluateConditions`, `conditionCheck.noResultValue`, `conditionCheck.isRequirementCheck`, `lower/calc.go` `UnboundResultHint`, `context.go` `EvaluateConstraintOn`; `solve/translate.go` `requirementSubject`, `Subject.noResultValue`, `unboundConstraintResult` | conformance `condition_less_constraint_def`, `condition_less_constraint_usage`, `read_return_parameters` (`cnegNoCondition`); `runtime/robustness_condition_less_check_test.go`, `runtime/constraint_test.go:TestConstraintWithoutConditionsIsNotAVerdict`, `runtime/owned_constraint_value_test.go:TestOwnedConstraintValueIsNotACondition`, `runtime/robustness_vacuous_requirement_test.go`; `solve/translate_test.go:TestNoConditions`, `solve/asserted_vacuous_test.go` | ✅ Faithful | | A constraint body's statements (`assign`, `if`, `while`/`loop`/`for` — SysML v2 7.20, whose constraint body is a `CalculationBody`) are steps of one Boolean performance, performed before its conditions are evaluated in the state they left, the ones no `then` between them orders unordered (the library leaves them so; see the unordered-statement row of the action map, which states how `explore`, `-engine check`, replay and seeded runs reach each order); locals live in a fresh frame per check, a body-local `attribute` declared with them is a local too, the constraint's own parameters are copied into the frame so a write binds the copy alone, and `SysML v2 §7.17.9`'s implicit target is the performance itself — so `assign n := …` for a name it holds no feature for (`errConstraintExternalAssignment`) and every effect reaching outside it (a chained or qualified write, `send`, `perform`, `terminate` — `errConstraintEffect`) are refused, as are the stated successions `first`/`fork`/`join`/`merge`/`decide`/`done` and a `then` reaching outside the body (`errStatementNotExecutable`; a `then` between two of the body's own statements orders them), a steps-only body (`ErrNoValue`: nothing binds its result), and a case's own steps, which are its procedure and are not refused | `lower/constraint_body.go` `ConstraintStep`, `condition.go` `appendConditions`/`appendMemberConditions`/`appendOwnedConditions`/`evaluateStepsConditions`/`conditionHoldsAt`, `constraint_statements.go` `constraintStmtHost`/`runConstraintSteps`, `analysis.go` `CaseConditionsOf`, `solve/translate.go` `translator.condition` | `runtime/constraint_test.go:TestConstraintBodyStepsRun`/`TestConstraintBodyPerformIsRefused`/`TestConstraintBodyActionFlowIsRefused`/`TestConstraintBodySuccessionIsRefused`, `runtime/robustness_constraint_body_steps_test.go:TestRuntimeRobustnessConstraintBodySteps`, conformance `constraint_body_steps` + four `constraint_body_steps_*_rejected`, `solve/translate_test.go:TestBodyStatementRefuses`/`TestPerformedActionRefuses`/`TestActionFlowRefuses`, `solve/objective_test.go:TestCaseStepsAreNotBodyStatements` | ⚠️ Approximate (`declared` and `reverse` run the steps in declaration order, a tool-defined linearization, while `explore`, `-engine check`, replay and seeded runs reach every order the library admits; effects outside the performance and stated successions other than `then` between the body's own steps are refused rather than performed) | diff --git a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/12-requirement.md b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/12-requirement.md index 9d480e8730..a6914e03fe 100644 --- a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/12-requirement.md +++ b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/12-requirement.md @@ -13,7 +13,7 @@ | Subject binding evaluation | `context.go` `EvaluateRequirement` (Pass 1) | `requirement_subject.sysml` | ✅ Faithful | | Actor binding evaluation | `context.go` `EvaluateRequirement` (Pass 1) | `requirement_actor.sysml` | ✅ Faithful | | Assume expression evaluation: a requirement's result is `allTrue(assumptions()) implies allTrue(constraints())` (SysML v2 §7.21.2; §9.2.14 `Requirements::RequirementConstraintCheck`), so a subject failing an assumption satisfies the requirement whatever its required constraints read — reported as holding vacuously, naming the assumption (`vacuous: assumption t.enabled of requirement levelOk did not hold`) on every surface that reports the condition-less vacuity — and a denial (`assert not requirement`, `assert not satisfy`) of such a requirement is a violation naming the assumption; an assumption whose body states steps is judged the same, and an assumption declared after the required constraints is judged before them, as `implies` reads its consequent only once its antecedent holds (`conditionCheck.assumptionsFirst`); the solver (`solve/translate.go` `assertedCondition`) and the value read of `result` already encode the implication | `condition.go` `judgeConditions`, `evaluateStepsConditions`, `conditionCheck.decidedBy`, `conditionCheck.failedCondition`, `failedAssumption` | `requirement_assume.sysml`, `requirement_assumption_implies.sysml` (the four cells, assumption × required), `requirement_assumption_false_holds.sysml`, `requirement_assumption_negated.sysml`, `requirement_assumption_negated_violated.sysml`, `requirement_assumption_steps.sysml`, `requirement_assumption_order.sysml`, `robustness_requirement_assumption_test.go`, `repl/satisfy_vacuous_test.go` `TestSatisfyFailedAssumptionHoldsVacuously` | ✅ Faithful | -| A requirement's assume and require constraints are the members of its `assumptions` and `constraints` collections (SysML v2 §7.21.2; §9.2.14 `Requirements::RequirementCheck::assumptions :>> RequirementConstraintCheck::assumptions`, `constraints`, each `[0..*] :> constraintChecks`): a constraint usage subsetting a constraint-typed collection (`assume constraint fullTank :> assumptions { … }`) contributes the object it holds to it, the definition's inherited usages included, and that object's `result` (`Constraints::ConstraintCheck::result`) is the usage's condition evaluated on the object declaring it — `true`, `false`, undetermined where an input is unbound, the error where the evaluation fails, never `false` for either — so `assumptions.result`, `allTrue(assumptions.result)` and the member's own `fullTank.result` read the check. Invoking the collection (`assumptions()`, the library's own `RequirementConstraintCheck::result = allTrue(assumptions()) implies allTrue(constraints())`) is the sequence of the members' results, undetermined as soon as one member's is, `[]` for no member (so `allTrue(assumptions())` over none is `true`), rather than the empty body of the library declaration; an `assume`/`require` constraint invoked by name (`fullTank()`) is the predicate it declares, and a chain invocation naming a constraint or requirement member of an object (`sys.req.assumptions()`, `sys.req()`) applies it on that object. A constraint usage the object enacts reads as its verdict under its bare name in the object's own context as it does through a chain (`req.fullTank`), before and after the collection materialized its object. The requirement's own `result` and its vacuity are the row above's, unchanged. No library name is special: any constraint-typed `[0..*]` collection a constraint usage subsets behaves alike. An `assume`/`require` constraint declaring no type — the reference form `assume c;` included — is the object its owner holds, as an untyped action or state usage is (`untypedPerformanceUsage`), its `result` the named requirement's own check where the reference names one (`referencedRequirement`, `CheckRequirementOn`), so the implicit subsetting (SysML v2 §8.3.20.4, the `checkConstraintUsageRequirementConstraintSpecialization` row) fills the collections for the unadorned spelling too | `runtime/return_parameters.go` `Context.constraintObjectResult` (dispatched from `returnParameterValue`), `Context.constraintResultValue`, `Context.ownObjectOf`, `EvalContext.enactedConstraint`; `runtime/invoke_predicate.go` `isPredicateDecl` (an owned `assume`/`require` constraint), `EvalContext.invokeConstraintCollection`, `Context.featureNamed`, `Context.objectResult`; `runtime/eval.go` `EvalContext.invokeChainOn`, `chainPredicate`, `EvalContext.invokeChainPredicate`, `EvalContext.featureValueOn`; `runtime/instance.go` `untypedPerformanceUsage`, `untypedOccurrenceUsage`; `semantics/shape.go` `Model.withAnonymousShapeFeatures`, `insertByDeclaration`, `AnonymousShapeMember`, `Model.PositionalNameOf` (an anonymous held constraint usage — not an `assert`ed one or an `inv` — is a feature of its owner's shape under its positional name, `Owner::@N`, in declaration order among the named ones, own and inherited alike) | conformance `requirement_check_collections_inherited_assumption_false` (`(-200, -200)`: the inherited assumption false, the requirement vacuously true, the verification inconclusive), `requirement_check_collections_all_true` (`(200, 200)`), `requirement_check_collections_constraint_false` (`(2500, 2500)`: the requirement false, the verification fails), `requirement_check_collections_local_assumption_false` (`(100, 200)`), each asserting `assumptions.result`, `allTrue(assumptions())`, `constraints.result`, the members' `result` and the requirement's `result` beside the verification's verdict, `requirement_check_collections_unadorned` (the same through the implicit subsetting, no `:>`), `requirement_check_collections_referenced` (`assume never;` naming a package-level constraint and `assume inner;` naming the definition's own, each a member whose `result` is the named constraint checked on the requirement), `requirement_check_collections_referenced_requirement` (`assume r;` naming a requirement reads `r`'s own check, implication included: a false required constraint `false`, a false assumption vacuously `true`); `requirement_check_collections_anonymous` (anonymous `assume constraint { … }` and `require constraint { … }` in the definition and in the usage: `constraints.result` is `[true, false]`, the inherited violated one `false` rather than absent, and `allTrue(constraints())` fails the verification); `runtime/robustness_requirement_collections_test.go:TestRuntimeRobustnessRequirementCollections` (a division by zero in an assumption is the error under `result` and under `assumptions()`, an unbound subject undetermined, no assumption vacuous, an argument refused), `runtime/robustness_requirement_anonymous_test.go:TestRuntimeRobustnessAnonymousRequirementConstraints` (the same error and undetermined preservation for an anonymous member), `semantics/nested_test.go:TestImplicitSubsettingsAnonymousRequirementConstraints` (the anonymous constraint's shape feature and its implicit subsetting, own and inherited) | ✅ Faithful: an anonymous `assume constraint { … }` / `require constraint { … }` is a feature of the object's shape under its positional name and so a member of `assumptions`/`constraints` exactly as a named one is. The reporter's package-level form `allTrue(fullTankRequirement.assumptions())` from a verification's return expression depends on reading a package-level requirement usage as an object there (#1854) | +| A requirement's assume and require constraints are the members of its `assumptions` and `constraints` collections (SysML v2 §7.21.2; §9.2.14 `Requirements::RequirementCheck::assumptions :>> RequirementConstraintCheck::assumptions`, `constraints`, each `[0..*] :> constraintChecks`): a constraint usage subsetting a constraint-typed collection (`assume constraint fullTank :> assumptions { … }`) contributes the object it holds to it, the definition's inherited usages included, and that object's `result` (`Constraints::ConstraintCheck::result`) is the usage's condition evaluated on the object declaring it — `true`, `false`, undetermined where an input is unbound, the error where the evaluation fails, never `false` for either — so `assumptions.result`, `allTrue(assumptions.result)` and the member's own `fullTank.result` read the check. Invoking the collection (`assumptions()`, the library's own `RequirementConstraintCheck::result = allTrue(assumptions()) implies allTrue(constraints())`) is the sequence of the members' results, undetermined as soon as one member's is, `[]` for no member (so `allTrue(assumptions())` over none is `true`), rather than the empty body of the library declaration; an `assume`/`require` constraint invoked by name (`fullTank()`) is the predicate it declares, and a chain invocation naming a constraint or requirement member of an object (`sys.req.assumptions()`, `sys.req()`) applies it on that object. A constraint usage the object enacts reads as its verdict under its bare name in the object's own context as it does through a chain (`req.fullTank`), before and after the collection materialized its object. The requirement's own `result` and its vacuity are the row above's, unchanged. No library name is special: any constraint-typed `[0..*]` collection a constraint usage subsets behaves alike. An `assume`/`require` constraint declaring no type — the reference form `assume c;` included — is the object its owner holds, as an untyped action or state usage is (`untypedPerformanceUsage`), its `result` the named requirement's own check where the reference names one (`referencedRequirement`, `CheckRequirementOn`), so the implicit subsetting (SysML v2 §8.3.20.4, the `checkConstraintUsageRequirementConstraintSpecialization` row) fills the collections for the unadorned spelling too | `runtime/return_parameters.go` `Context.constraintObjectResult` (dispatched from `returnParameterValue`), `Context.constraintResultValue`, `Context.ownObjectOf`, `EvalContext.enactedConstraint`; `lower/calc.go` `IsPredicateDecl` (an owned `assume`/`require` constraint); `runtime/invoke_predicate.go` `EvalContext.invokeConstraintCollection`, `Context.featureNamed`, `Context.objectResult`; `runtime/eval.go` `EvalContext.invokeChainOn`, `chainPredicate`, `EvalContext.invokeChainPredicate`, `EvalContext.featureValueOn`; `runtime/instance.go` `untypedPerformanceUsage`, `untypedOccurrenceUsage`; `semantics/shape.go` `Model.withAnonymousShapeFeatures`, `insertByDeclaration`, `AnonymousShapeMember`, `Model.PositionalNameOf` (an anonymous held constraint usage — not an `assert`ed one or an `inv` — is a feature of its owner's shape under its positional name, `Owner::@N`, in declaration order among the named ones, own and inherited alike) | conformance `requirement_check_collections_inherited_assumption_false` (`(-200, -200)`: the inherited assumption false, the requirement vacuously true, the verification inconclusive), `requirement_check_collections_all_true` (`(200, 200)`), `requirement_check_collections_constraint_false` (`(2500, 2500)`: the requirement false, the verification fails), `requirement_check_collections_local_assumption_false` (`(100, 200)`), each asserting `assumptions.result`, `allTrue(assumptions())`, `constraints.result`, the members' `result` and the requirement's `result` beside the verification's verdict, `requirement_check_collections_unadorned` (the same through the implicit subsetting, no `:>`), `requirement_check_collections_referenced` (`assume never;` naming a package-level constraint and `assume inner;` naming the definition's own, each a member whose `result` is the named constraint checked on the requirement), `requirement_check_collections_referenced_requirement` (`assume r;` naming a requirement reads `r`'s own check, implication included: a false required constraint `false`, a false assumption vacuously `true`); `requirement_check_collections_anonymous` (anonymous `assume constraint { … }` and `require constraint { … }` in the definition and in the usage: `constraints.result` is `[true, false]`, the inherited violated one `false` rather than absent, and `allTrue(constraints())` fails the verification); `runtime/robustness_requirement_collections_test.go:TestRuntimeRobustnessRequirementCollections` (a division by zero in an assumption is the error under `result` and under `assumptions()`, an unbound subject undetermined, no assumption vacuous, an argument refused), `runtime/robustness_requirement_anonymous_test.go:TestRuntimeRobustnessAnonymousRequirementConstraints` (the same error and undetermined preservation for an anonymous member), `semantics/nested_test.go:TestImplicitSubsettingsAnonymousRequirementConstraints` (the anonymous constraint's shape feature and its implicit subsetting, own and inherited) | ✅ Faithful: an anonymous `assume constraint { … }` / `require constraint { … }` is a feature of the object's shape under its positional name and so a member of `assumptions`/`constraints` exactly as a named one is. The reporter's package-level form `allTrue(fullTankRequirement.assumptions())` from a verification's return expression depends on reading a package-level requirement usage as an object there (#1854) | | A requirement's `actor` and `stakeholder` parameters — the part usages an ActorMembership or StakeholderMembership owns in a requirement definition or usage, a viewpoint among them — implicitly subset `Requirements::RequirementCheck::actors` and `Requirements::RequirementCheck::stakeholders` beside their kind's `Parts::parts`, and a case's actor (a verification, analysis or use case definition or usage) subsets `Cases::Case::actors` (SysML v2 §8.3.11.3 `checkPartUsageActorSpecialization`, `checkPartUsageStakeholderSpecialization`), as the pilot's ImplicitGeneralizationMap derives them; at runtime the collections, referential as the library declares them (`ref part actors : Part[0..*]`), hold the objects the actor and stakeholder parameters hold — a usage's actor redefining its definition's positionally, the pair holds one member — so `%eval in #1 : actors`, a model's `(sys.r.actors as Person).id` and `%features` once the collection is read list the members (an unread collection filled only by implied subsetting is listed `[]`, as every such collection is); a requirement declaring no actor reads an empty collection, and a referential collection nothing subsets stays unread | `semantics/nested.go` `nestedSubsettingFQN`, `familyNestedFQN`, `ownerInRequirementFamily`, `ownerInCaseFamily`; `runtime/instance.go` `Instance.materializeReferentialCollection` (a referential collection with subsetters holds their values, KerML 1.1 §7.3.4.5; a model-level read stops where a subsetter is open), `Instance.holdContributions` | `semantics/nested_test.go` `TestImplicitSubsettingsRequirementRoles`, `TestImplicitSubsettingCandidatesRequirementRoles`; `runtime/requirement_role_subsetting_test.go` `TestRequirementRolesFillTheRequirementCheckCollections`, `TestReferentialCollectionWithoutSubsettersHoldsNothing`; conformance `requirement_role_collections` (the usage's slots), `requirement_role_collections_reads` (reads through a part, a case's actors, an actorless requirement); `repl/features_role_collections_test.go` `TestRoleCollectionsOfARequirementReadTheirMembers` | ✅ Faithful | | A `frame concern` — the concern usage a FramedConcernMembership owns in a requirement definition or usage, a viewpoint among them — implicitly subsets `Requirements::RequirementCheck::concerns` beside its kind's `Requirements::concernChecks` (SysML v2 §8.3.21.4 `checkConcernUsageFramedConcernSpecialization`), as the pilot's ImplicitGeneralizationMap derives it; a plain `concern` member of a requirement is a subrequirement; at runtime `concerns` — and through the library's `concerns :> concernChecks, subrequirements` the requirement's `subrequirements` and `constraints` — holds the object the framed concern holds | `semantics/nested.go` `nestedSubsettingFQN`, `familyNestedFQN`; `runtime/instance.go` `Instance.materializeComposite` | `semantics/nested_test.go` `TestImplicitSubsettingsRequirementRoles`; `runtime/requirement_role_subsetting_test.go`; conformance `requirement_role_collections`, `requirement_role_collections_reads` | ✅ Faithful | | A `satisfy` usage implicitly subsets `Requirements::satisfiedRequirementChecks`, a negated one (`assert not satisfy`) `Requirements::notSatisfiedRequirementChecks`, in place of its kind's `Requirements::requirementChecks`, which both library usages subset (SysML v2 §8.3.21.10 `checkSatisfyRequirementUsageSpecialization`), as the pilot's ImplicitGeneralizationMap derives it; an objective's `verify` is no satisfy usage and keeps `requirementChecks`. Both library usages are package-level, so no object holds a collection of them | `semantics/implicit.go` `usageRoleBaseFQN`, `Model.implicitBaseFeatures` | `semantics/implicit_audit_test.go` `TestImpliedSpecializationAuditRows` (rows `P::psat::sq`, `P::psat::nsq`); `workspace/model/base_feature_conformance_test.go` | ✅ Faithful | @@ -26,7 +26,7 @@ | An `actor` or `stakeholder` a case, requirement or usage of either declares implicitly redefines the general's effective actor at its position (KerML §7.4.7.3, a parameter after the subject; SysML v2 §8.3.21.2 `ActorMembership`), keeping the inherited name or not, unless it carries a `:>>` clause of its own; a general presents its own actors first, then the inherited ones none of them redefines. The redefining actor takes the redefined one's type and multiplicity, so `requirement r : PilotedLimit { actor pilots = pilot; }` is held to the definition's `actor pilots : Pilot[2]` — one pilot is refused as a multiplicity violation, two buoys as a type mismatch, two pilots satisfy — and an objective binding one such actor is `undecided` with that detail | `semantics/roles.go` `ImplicitRoleRedefinitions`, `positionalActorRedefinitions`, `effectiveActors`; reached from `semantics/model.go` `DirectSupertypes` and `runtime/subsetting.go` `collectRedefinedFeatures`, so `runtime/context.go` `holdBound` folds the declaration the value is checked against | `semantics/roles_test.go:TestActorsRedefineByPosition`, `TestActorsThroughLayeredDiamondsAreLinear`, `runtime/analysis_objective_binding_test.go:TestImplicitActorIsHeldToTheInheritedDeclaration`, conformance `analysis_objective_actor_implicit_redefinition`, `analysis_objective_actor_implicit_refused` | ✅ Faithful | | A view's `render` member implicitly redefines the library `Views::View::viewRendering` and nothing else (SysML v2 §8.3.26 `RenderingUsage`), so `render rendering s : AsTable;` in a view specializing one that declares `render rendering r : AsTree;` leaves `r` inherited and resolvable, and `render r;` in a view whose general also renders `r` redefines the same library feature, so it replaces the inherited `r` rather than duplicating it (KerML §7.3.4.5) | `semantics/roles.go` `ImplicitRoleRedefinitions`, `viewRenderingFQN` | `semantics/roles_test.go:TestRenderMemberRedefinesOnlyTheLibraryViewRendering`, `passes/nameres_test.go:TestRenderOfAnotherNameLeavesInheritedRenderingResolvable`, `TestRenderReferenceToOwnRenderingIsNoConflict` | ✅ Faithful (the pinned validator, `2026-07`, resolves `r.a` in both views and reports nothing) | | Nested requirements | `context.go` `EvaluateRequirement` (recursive); a nested requirement usage is a subrequirement, one of the container's required constraints over the container's subject (`condition.go` `Context.appendSubrequirementConditions`; the nested-usage subject row above) | `requirement_nested.sysml`, conformance `subrequirement_*` | ✅ Faithful | -| A required or assumed requirement (`require ladenLimit { in w = unit; }`) is evaluated through the constraint usage the member declares, so the arguments its body binds (`in w = unit`, read in the requiring requirement's environment, where `unit` is the subject a `satisfy` supplies) are the referenced requirement's parameter values, and an attribute it redefines from that parameter (`attribute :>> actual = w.tare + w.cargo;`) is computed from the bound object; `require r` requires r's required conditions and keeps its assumptions assumptions, `assume r` assumes them all | `condition.go` `appendReferencedConditions`, `referencingConstraintOf`, `setAssumed`, `constraintScope`; `invoke_calc.go` `unwrappedDeclMembers` (a reference-form member's body is the usage's members) | golden `parse/require_reference_body_binding`; conformance `satisfy_referenced_requirement_arguments`, `satisfy_referenced_requirement_assumption`, `requirement_referenced_arguments`, `requirement_referenced_arguments_violated`, `requirement_assumed_reference`; `runtime/robustness_referenced_requirement_binding_test.go` (a binding to an unresolved name, a body binding none of the referenced requirement's parameters, a bodyless reference, a requirement referencing itself); `repl/satisfy_referenced_test.go:TestSatisfyBindsReferencedRequirementArguments`, `cmd/sysml/satisfy_referenced_test.go:TestSatisfyDecidesReferencedRequirementsFromTheSubject` | ✅ Faithful | +| A required or assumed requirement (`require ladenLimit { in w = unit; }`) is evaluated through the constraint usage the member declares, so the arguments its body binds (`in w = unit`, read in the requiring requirement's environment, where `unit` is the subject a `satisfy` supplies) are the referenced requirement's parameter values, and an attribute it redefines from that parameter (`attribute :>> actual = w.tare + w.cargo;`) is computed from the bound object; `require r` requires r's required conditions and keeps its assumptions assumptions, `assume r` assumes them all | `condition.go` `appendReferencedConditions`, `referencingConstraintOf`, `setAssumed`, `constraintScope`; `lower/calc.go` `UnwrappedDeclMembers` (a reference-form member's body is the usage's members) | golden `parse/require_reference_body_binding`; conformance `satisfy_referenced_requirement_arguments`, `satisfy_referenced_requirement_assumption`, `requirement_referenced_arguments`, `requirement_referenced_arguments_violated`, `requirement_assumed_reference`; `runtime/robustness_referenced_requirement_binding_test.go` (a binding to an unresolved name, a body binding none of the referenced requirement's parameters, a bodyless reference, a requirement referencing itself); `repl/satisfy_referenced_test.go:TestSatisfyBindsReferencedRequirementArguments`, `cmd/sysml/satisfy_referenced_test.go:TestSatisfyDecidesReferencedRequirementsFromTheSubject` | ✅ Faithful | | `satisfy ` is an `OwnedReferenceSubsetting` of an existing usage, not a typing (SysML v2 §8.3.21.10 `SatisfyRequirementUsage`) | `parser/defusage.go` `parseDefUsage` (`ast.RelSubsets`) | `tests/parser/testdata/parse/satisfy_reference.golden` | ✅ Faithful | | `referencedFeatureTarget().oclIsKindOf(RequirementUsage)` — satisfy/verify may only reference a requirement usage (incl. viewpoint/concern usages and an `objective`); a feature chain is judged by its last feature, and the referent may be inherited, redefined, aliased or nested | `passes/typecheck.go` `compatMessage`, `referentKindMessage`, `referentKind`, `isRequirementUsageKind`, chains through `typeChecker.checkChainReferenceKind` | `passes/typecheck_test.go` `TestTypeCheckSatisfyRequirementUsageOK`, `TestTypeCheckSatisfyViewpointUsageOK`, `TestTypeCheckSatisfyNonRequirementUsageError`; `passes/typecheck_satisfy_reference_test.go` `TestSatisfyReferenceToRequirementIsSilent`, `TestSatisfyReferenceToNonRequirementRejected` (every referent kind in the census row, refereed against the pinned pilot) | ✅ Faithful | | `validateAssertConstraintUsageReference` (`Must reference a constraint.`, SysML v2 §8.3.20.2 `AssertConstraintUsage`): `assert c`, `assert not c` and `assert constraint c` reference a constraint usage — a requirement usage included — the last feature of a feature chain deciding, an inherited constraint accepted, the reference stated as a usage or as a member of a constraint body alike; a definition, a package or an unresolved name is the earlier tier's error | `passes/typecheck.go` `referenceKindMessage` (the referent-kind query `satisfy` and `assert` share), `checkChainReferenceKind` for a chained referent, `checkBehaviorMember` for a constraint-body member | `passes/typecheck_assert_reference_test.go`; `semantic/s04-assert-references-non-constraint.sysml` is rejected by both tools | ✅ Faithful | diff --git a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/13-action.md b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/13-action.md index b68703e428..9104662069 100644 --- a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/13-action.md +++ b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/13-action.md @@ -52,7 +52,7 @@ | Action execution nodes | `action_executor.go` `stepActionExecutionNode`Node | `action_control_flow.sysml` | ✅ Faithful | | Nested action invocation (`action call : Callee;`, `action call = Callee(3, 4);`, `action call = Callee(a = 3);`, `perform action call : Callee;`) | `runtime/action_frame.go` `bindArguments`, `performInvocation`, `checkInputsBound`; `runtime/state_statements.go` `stateStmtHost.performNode`; `runtime/invoke_action.go` `invocationArguments`, `bindArgumentList`, `Context.actionParametersOf` (over `semantics.Model.BehaviorParametersOf`, so inherited and redefined parameters keep their effective order); `action_executor.go` `stepNestedAction` | `invoke_action_test.go:TestInvokeActionPassesParametersBothWays`, `:TestInvokeActionBindsPositionalArguments`, `:TestInvokeActionBindsNamedArguments`, `:TestInvokeActionRejectsBadArguments`; `conformance/action_node_invocation_positional`, `action_node_invocation_named`, `action_node_typed_body_inputs`, `action_node_inherited_parameters`, `action_node_invocation_empty`, `action_inherited_typed_node_scope`, `action_node_arguments_before_defaults`, `state_block_flow_node_arguments_before_defaults` | ✅ Faithful (arguments bind the callee's inputs by the callee's parameter order and names, never by what the caller happens to name alike; they bind the node's pins before the defaults it declares are evaluated, so a default an argument replaces is never evaluated and one the node keeps reads the argument's value, in an action's flow and in a state's body alike; the callee is resolved where the node was declared, so a node a derived action inherits still finds a callee visible only to the general action; a surplus, missing, unknown or repeated argument is `errActionArity`, `errUnboundParameter`, `errUnknownParameter` or `errDuplicateArgument` before the callee runs — a surplus one is also reported statically by `passes/typecheck_expr.go`, so no conformance fixture states it. The row above states the compatibility by-name fallback a bare usage keeps) | | Assignment statement in a body (`assign x := `) | `lower/action_graph.go` lowerStatement Assign; `runtime/action_statements.go` execStatement | `action_send_accept.sysml`, `lower/action_body_test.go:TestActionBodyLowering` | ✅ Faithful | -| A value written to a feature conforms to that feature's declared type, by the relation that governs binding an initial value: `Actions::AssignmentAction :> FeatureWritePerformance, Action` "updates the `accessedFeature` of its `target` Occurrence with the given `replacementValues`" and `FeatureReferencingPerformances::FeatureWritePerformance` "assigns the values of a feature on an occurrence to the given `replacementValues`", so what is written are values *of that feature* and answer to its type. The values of a feature are instances of all its types (KerML 1.1 §8.3.3.3.4), so a feature holds a value exactly when `istype` would affirm the feature's type of it — the one shared `runtime/classification.go` `classifyValue`, so a write and a classification never judge the same value and type differently. Specialization widens (a subtype value where a supertype is declared), the scalar lattice widens by representation (`Integer ⊑ Rational ⊑ Real ⊑ Complex`: an integer is held by a `Rational` or `Real` feature, a finite real — a `Rational`, §8.4.4.9.2 — by a `Real` one, and neither the whole real `2.0` nor the quotient `4 / 2` by an `Integer` or `Natural` feature, whatever number it is; `RationalFunctions::ToInteger`, `RealFunctions::ToInteger` and `IntegerFunctions::ToNatural` convert), a target the value's types leave open holds it (`Natural = 7`, `Even :> Integer = 4`, `Cost :> Real = 250.0` — the declaration being what states which of the supertype's values it is — while the bounds of `Natural` and `Positive` refuse `-1` and `0`), and an untyped or `Anything`-typed target holds anything. Statically, an expression's type only bounds its values, so the type pass refuses a value only for a disjoint type (`String` to `Integer`), when the value is a literal, whose type is exact (`2.5` to `Natural`), or when it is a quotient, a `Rational` however whole (`IntegerFunctions::'/'`, §9.4.11.1: `Integer = 7 / 2`, `Natural = i / 2`, an argument `add(4 / 2)` to an `Integer` parameter); a call or a `Real` feature bound to an `Integer` is the run time's to judge, since a `Real` feature may hold an integer, and the run time judges every binding it holds a value for: a default a declaration binds (folded ahead of time or evaluated on first read), a value read through a declaration, an argument or default bound to a calc parameter (inherited or redefined), and a calc's result. A calc parameter or result is held in the shape its declared multiplicity gives it, the one decision as for an action parameter: a stated `[0..*]` or `[n]` holds a sequence (one computed value becomes a one-element sequence), a stated `[1]` or `[0..1]` the sole value, and one stating no multiplicity what the body computed; a condition, `==` and `===` read a one-element collection as the value it holds, and a generated Go or C program (`codegen.Held`) holds and reads its parameters and result the same way (`calc_param_multiplicity_many_consumers.sysml`, `TestCompiledCalcsAgreeWithInterpreter`). A value's element count answers to the applicable multiplicity: a non-parameter feature is judged against the multiplicity it states or inherits — its own, the one it redefines, or, abstract, the intersection of what it subsets (the redefinition-and-subsetting row of the Instantiation map) — and against the implicit `[1..1]` where SysML v2 §7.6.3 declares it (`attribute z : Real;` on a part, action or state def refuses `(3.0, 4.0)` whether a nested action, the definition's own body or an entry action writes it); one that states and inherits none has the usual default `0..*` (KerML 1.1 §7.4.12; the default-multiplicity and representative-object rows of the Instantiation map), so a write of several values to it is held on every path — `attribute x : Real :> many;` written `(3.0, 4.0)` holds both whether a part def's nested action writes the part's feature (`Instance.SetFeatureValue` → `Context.checkAdmits`, no count judged where `MultiplicityDefaulted`) or the action or state def declaring it writes its own (`ActionExecutor.setFeature`, `StateExecutor.assignAttribute` → `Context.checkNamedWriteHeld`, whose `writeTarget.countJudged` is the same `featureRange` decision) — and writes and declarations of an action parameter are judged against its effective parameter range (`EffectiveParameterRange`, SysML v2 §7.6.3), so a bare action parameter is `[0..*]` and an explicitly stated `[*]`, `[0..1]` or `[1..2]` parameter holds what it declares — and a count-judged write holds its value in the shape that multiplicity gives it, as the performance occurrence already did (`Context.admitted`): a `[1]` target the sole element, a multi-valued one a sequence (a set where declared), so the frame, the occurrence's slots, the gRPC `FeatureValue`, `%action` and `-json` agree, and the shape no longer depends on the order the body read a pin in (the Action row on pin shape); `action_param_default_scalar_count.sysml` now explicitly declares `[1]`, and `robustness_performed_action_inputs_test.go:testNestedActionParameterMultiplicity` covers bare nested parameters and explicit required inner and outer parameters; a null holds none and so reaches a `[0..1]` parameter's `??` but not a `[1..1]` one — and a Complex is one value (`ValComplex`) against any feature's multiplicity: a Complex feature holds it, a Real feature refuses one off the real axis by type, and a numeric pair `(1.0, 2.0)` is two values whatever the feature's type | `passes/typecheck_assign.go` `exprChecker.checkAssignmentValue` → `passes/typecheck_expr.go` `exprChecker.checkBoundValue` and `bindable` (the initial-value rule, reached with the target's declaration); `runtime/write_conformance.go` `Context.writeTargetIn` → `writeTargetOf` (the target's multiplicity and whether a write's count is judged are `runtime/shape.go` `featureRange`'s, the one decision `effectiveFeature` gives an object's feature and `Context.checkAdmits` judges: a parameter's effective range, a stated or inherited multiplicity or the implicit `[1..1]` judge the count, a defaulted feature's representative range judges none; `writeTarget.stated` carries the declaration rule below), `Context.checkTargetAs` → `shapeForTarget` (the shape a count-judged write is held in) and `shapeDeclared` (a root default binding, `runtime/action_frame.go` `registerRootBindings`), `Context.checkWrite`/`checkWriteType`/`valueConforms`, reached from `runtime/instance.go` `Instance.SetFeatureValue`, `Context.checkDefault` (at `materialize` for a folded default and `materializeFeatureValueIntrinsic` for a deferred one), `runtime/binding.go` `assignBindingEndpoint`, `runtime/eval.go` `EvalContext.declaredValue`, `runtime/write_conformance.go` `Context.checkBodyDeclaration` → `checkTarget` with `admitDeclared` (a declaration local to a calc, action or constraint body, a `{ }` block or a collection-expression body — `lower.Declare` in `runtime/statements.go`, an action node's own feature seeded by `runtime/action_frame.go` `seedDeclaredValues`, and the compiled statement tier's `runtime/compiled_stmts.go` — is judged once when its initial value is bound, by the namespace-level rule: its type with declared admission, so an enumeration-typed local holds the enumerated value; its multiplicity only where the declaration states one, an omitted multiplicity holding the count the initializer has; its uniqueness where it declares more than one value; KerML 1.1 §7.3.4, "the values of a feature are instances of its types"; a local declaring no type holds anything), the statement hosts' `assignData`/`assignOuter`, `ActionExecutor.setFeature` and `StateExecutor.assignAttribute`; `runtime/invoke_calc.go` `bindCalcParameters` and `runtime/calc_statements.go` `calcStmtHost.acceptReturn` and `runtime/calc_usage.go` `calcRun.value` (an output a declaration binds rather than a body assigns, and a result a `bind result = ...` binds, which `calcShape.designatedOutput` keeps the declared result's declaration for) through `calcMemberDecl.check` (the parameter's effective declaration: `calcMemberDecl.redeclaring` keeps the type and multiplicity a redeclaration states none of from the parameter, output or result it redeclares, per KerML 1.1 §7.3.4.5; the compiled calc tier's `runtime/compile.go` `scalarCheck.accepts` decides the same declaration on the lattice, `least` being the `Positive` bound, and hands every value it declines to `calcMemberDecl.check`) and `Context.writeCountRefusal` (`runtime/write_conformance.go`, shared with `checkWrite` and `checkDefault`; `elementCount` in `runtime/collections.go` counts a scalar, a Complex included, without materializing a sequence); `runtime/state_statements.go` `stateStmtHost.assignStateAttribute` for an attribute held in a state's own frame | `passes/typecheck_assign_test.go` (state entry/do bodies, action bodies, calc bodies, transition effects, numeric widening, silence on an unknown type); `passes/typecheck_expr_test.go:TestExprQuotientDoesNotBindToWholeNumberFeature` (`Integer = 4 / 2` refused, `Rational = 7 / 2` and `Integer = RationalFunctions::ToInteger(4 / 2)` accepted), `:TestExprInvocationArgumentNarrowerThanExpression` (`add(7 / 2, 1)` refused, `add(RationalFunctions::ToInteger(7 / 2), w)` accepted), `:TestExprBindWiderTypedReferenceToNarrowerFeatureOK`, `:TestExprBindDecimalLiteralToNaturalRejected`, `:TestExprBindStringToIntegerAttribute`; `runtime/value_conformance_test.go:TestDefaultValueMustConformAtMaterialization` (`whole : Integer = 4 / 2`, `nat : Natural = 4 / 2`, `fromReal : Integer = two` and `computed : Integer = seven - two` are `ErrTypeMismatch`; `wholeRational : Rational = 4 / 2` holds `2.0` and `wholeConverted : Integer = ToInteger(4 / 2)` holds `2`), `:TestRestatedDefaultConformsByItsOwnValue`, `:TestDeclaredValueReadChecksItsType`, `:TestUserDeclaredScalarTypeDefersToTheBinding`, `:TestCalcParameterAndResultMustConform` (`Half(4 / 2)` and `Half(two)` refused, `Half(ToInteger(4 / 2))` accepted), `runtime/compile_test.go:TestCompiledCalcErrorParity` (`Natural1(-1)`, `Positive1(0)` and a `Positive` or `Rank :> Positive` result of `0` are the interpreter's `ErrTypeMismatch` on the compiled tier too), `sweep/sweep_test.go` (a sweep range over a `Natural`/`Positive` parameter admits what the parameter holds, in the interpreter and compiled alike), `conformance/value_classification_shared_rule` (`e : Even = 4`, `q : Rational = 6 / 3`, `none : Integer[0..1]`), a feature typed by an enumeration admits exactly the enumerated values (SysML v2 §8.3.7, the classification row) — `held : Level = three` for `three : Integer = 3` is held as `Level::high` (`checkWriteType` → `holdAsEnumerated`, so `held hastype Level` is `true`) and `= two` is the write-conformance `ErrTypeMismatch`, and a calc parameter, output or result declared by an enumeration holds its argument the same way (`calcMemberDecl.check` → `holdForDeclared`; `runtime/compile.go` `scalarCheckFor` declines an enumeration-typed declaration, so the compiled tier hands it to the same checker), so `asLevel(3) hastype Level` is `true` for `calc def asLevel { in n : Integer; return : Level = n; }`; where the value is constant the checker refuses it at its spelling (`passes/typecheck_value.go` `checkEnumeratedValue`: `Level = 2` is `cannot bind 2 (an Integer) to a feature typed by Level, whose values are Level::low = 1, Level::high = 3`, while a value of another kind stays the scalar lattice's one diagnostic) — `classify_test.go:TestEnumerationTypedFeatureAdmitsOnlyEnumeratedValues`, `robustness_classification_test.go:enumeration_typed_feature_holding_an_unenumerated_value`, `passes/typecheck_value_test.go:TestValueScalarConstantToScalarValuedEnumeration`, `:TestCalcParameterAndResultMultiplicity`, `:TestCalcMultiplicityViolationNamesTheBinding`, `:TestComplexCountsAsOneValue`, conformance `calc_body_local_declared_type` (`BodyOnly(3)` is `3` with `l` held as `Level::high`, `BodyOnly(2)` is the write's `ErrTypeMismatch`, `attribute xs : Integer[2] = (n, n + 1, n + 2)` `ErrMultiplicityViolation`, `attribute xs : Integer[*] = (n, n + 1, n)` `ErrUniquenessViolation`, an untyped local and one stating no multiplicity hold what they are given, a local in an `if` block and in a `->collect` body is judged the same), `action_body_local_declared_type`, `constraint_body_local_declared_type`, `constraint_body_local_count` (a condition reads its body's declaration through `eval.go` `conformBodyDeclared`, a stated multiplicity judging the count, determined or open, and an omitted one keeping it — `undetermined_test.go:TestBodyLocalKeepsOpenInitializerCount`), `constraint_param_scalar_count` (the unchanged constraint-parameter case), `action_param_default_scalar_count.sysml` (now explicitly `[1]`, preserving its two-value `ErrMultiplicityViolation` expectation), `robustness_performed_action_inputs_test.go:testNestedActionParameterMultiplicity` (bare nested action parameters admit no input, while explicit `[1]` inner and outer parameters preserve their typed errors), `constraint_body_local_redefined_type` and `constraint_body_local_redefined_count` (a local inheriting its initializer from the declaration it redefines is held to the redefinition, `shape.go` `statedMultiplicity` walking the same redefinition, positional-parameter and abstract-subsetting chain as the effective multiplicity — `undetermined_test.go:TestBodyLocalIntersectsRedefinedMultiplicities`, `TestBodyLocalInheritsImplicitParameterMultiplicity`), `constraint_body_local_redefined_unique` and `action_body_local_redefined_count` (the inherited bound also gates uniqueness and the lowered `checkBodyDeclaration` path), `robustness_body_locals_test.go:body_local_outside_its_declaration`, `tests/codegen/compiled_calcs_test.go:TestCompiledCalcsAgreeWithInterpreter` (`PosLoc`, `Seq::LocM1`, `Seq::LocN`, `Seq::LocAny2`: the native tier declares the local through `codegen/compile.go` `compileDeclare` → `bind` with the declared range and stated multiplicity, `codegen/ir.go` `Declare.Range`, and declines an enumeration-typed local and one redefining or subsetting another feature — `TestCompileRefusesWhatItCannotCompile` `SubsetLocal` — `not compilable`, rather than answer differently), pilot-exec-diff `body_local_conformance` (the pilot binds a body-local value unchecked, so the accepted case agrees and the refused ones are ours-error against its answer), `invoke_calc_body_test.go:TestCalcForLoopOverParameterSequence` (`in xs[*];`), `conformance/calc_null_coalesce.sysml` (`in n : Integer[0..1];`), `conformance/calc_result_multiplicity_many`, `_named_many`, `_body_many` and `_fixed` (`return : Real[0..*] = x`, `return r : Real[0..*] = x`, a `[0..*]` result the trailing expression binds and `return : Real[2]` hold sequences), `_one` and `_optional` (`[1]` and `[0..1]` hold the bare value), `_unstated` and `_unstated_named` (a bare `return : Real` keeps the computed scalar) and `verification_result_multiplicity_many` (a `VerdictKind[0..*]` verdict is read from its one element — `verification_run.go` `verdictOf` reads through `soleElement`), `conformance/calc_library_feature_imaginary_unit.sysml` (`return : Complex = i;`), pilot-exec-diff `intdiv` (deliberately `ours-error`, now refused by the checker: the reference answers `3.5` for `return : Integer = 7 / 2`, checking nothing against the parameter's type) and `rational-feature-hastype-integer` (`rat : Rational = 4` holds the `Integer` `4`); `conformance/assign_write_conforms_to_target_type.sysml`, `assign_write_violates_target_type.sysml`, `assign_write_through_chain_violates_target_type.sysml`; `robustness_typed_writes_test.go:write_of_a_wrong_typed_value_leaves_the_feature`, `:state_entry_write_of_a_wrong_typed_value`, `:chained_write_of_a_wrong_typed_value`, `robustness_calc_invocation_test.go:calc_output_write_of_a_wrong_typed_value`, `:action_local_write_of_a_wrong_typed_value`, `:action_output_write_of_a_wrong_typed_value`, `:performer_feature_write_of_a_wrong_typed_value`, `:performance_occurrence_write_of_a_wrong_typed_value`, `robustness_calc_usage_test.go:calc_output_binding_violates_declared_type`; `conformance/assign_write_to_state_attribute_violates_target_type.sysml` | ⚠️ Approximate (self-assessed: the pinned pilot implementation executes no action or state machine, so it cannot referee this. A quantity value carries its unit rather than the quantity value type measured in it, so the specialization relation between two quantity value types is not itself decided; the quantity kind such a target declares is judged dimensionally, by the row below. A model declaring its own scalar types gives the run time no value semantics for them, so there a value is refused only for a disjoint type, the binding rule's either-direction conformance standing in for a judgement by value. A Complex is a `Complex` on the real axis or off it — `rect(2.0, 0.0)` is refused by a `Real` or `Integer` feature, `re` of it is the `Real` — since `ComplexFunctions` return `Complex` and the specification fixes no finer classification of such a value. The fixtures that relied on a whole real or a quotient being held by an `Integer` feature were re-adjudicated by this rule rather than relaxed: an `Integer` or `Natural` feature or parameter a quotient or a whole real reaches is declared `Rational` or `Real` where the model computes such a value (`tests/codegen/testdata/compile_calcs.sysml`, `repl/choice_test.go`, `grpc/explore_test.go`, `lsp/library_invocation_test.go`, `passes/typecheck_value_test.go`, `runtime/classify_test.go`), converts with `ToInteger` where it means the integer (`runtime/value_conformance_test.go`, `passes/typecheck_expr_test.go`), or pins the typed error where the write is what the fixture probes (`runtime/value_conformance_test.go`, `robustness_quantities_test.go` coordinate-frame failure modes: a number written where a `ScalarMeasurementReference` or a `TranslationOrRotation` is declared is the write's `ErrTypeMismatch`), each by §8.3.3.3.4 and §9.4.11.1 above) | +| A value written to a feature conforms to that feature's declared type, by the relation that governs binding an initial value: `Actions::AssignmentAction :> FeatureWritePerformance, Action` "updates the `accessedFeature` of its `target` Occurrence with the given `replacementValues`" and `FeatureReferencingPerformances::FeatureWritePerformance` "assigns the values of a feature on an occurrence to the given `replacementValues`", so what is written are values *of that feature* and answer to its type. The values of a feature are instances of all its types (KerML 1.1 §8.3.3.3.4), so a feature holds a value exactly when `istype` would affirm the feature's type of it — the one shared `runtime/classification.go` `classifyValue`, so a write and a classification never judge the same value and type differently. Specialization widens (a subtype value where a supertype is declared), the scalar lattice widens by representation (`Integer ⊑ Rational ⊑ Real ⊑ Complex`: an integer is held by a `Rational` or `Real` feature, a finite real — a `Rational`, §8.4.4.9.2 — by a `Real` one, and neither the whole real `2.0` nor the quotient `4 / 2` by an `Integer` or `Natural` feature, whatever number it is; `RationalFunctions::ToInteger`, `RealFunctions::ToInteger` and `IntegerFunctions::ToNatural` convert), a target the value's types leave open holds it (`Natural = 7`, `Even :> Integer = 4`, `Cost :> Real = 250.0` — the declaration being what states which of the supertype's values it is — while the bounds of `Natural` and `Positive` refuse `-1` and `0`), and an untyped or `Anything`-typed target holds anything. Statically, an expression's type only bounds its values, so the type pass refuses a value only for a disjoint type (`String` to `Integer`), when the value is a literal, whose type is exact (`2.5` to `Natural`), or when it is a quotient, a `Rational` however whole (`IntegerFunctions::'/'`, §9.4.11.1: `Integer = 7 / 2`, `Natural = i / 2`, an argument `add(4 / 2)` to an `Integer` parameter); a call or a `Real` feature bound to an `Integer` is the run time's to judge, since a `Real` feature may hold an integer, and the run time judges every binding it holds a value for: a default a declaration binds (folded ahead of time or evaluated on first read), a value read through a declaration, an argument or default bound to a calc parameter (inherited or redefined), and a calc's result. A calc parameter or result is held in the shape its declared multiplicity gives it, the one decision as for an action parameter: a stated `[0..*]` or `[n]` holds a sequence (one computed value becomes a one-element sequence), a stated `[1]` or `[0..1]` the sole value, and one stating no multiplicity what the body computed; a condition, `==` and `===` read a one-element collection as the value it holds, and a generated Go or C program (`codegen.Held`) holds and reads its parameters and result the same way (`calc_param_multiplicity_many_consumers.sysml`, `TestCompiledCalcsAgreeWithInterpreter`). A value's element count answers to the applicable multiplicity: a non-parameter feature is judged against the multiplicity it states or inherits — its own, the one it redefines, or, abstract, the intersection of what it subsets (the redefinition-and-subsetting row of the Instantiation map) — and against the implicit `[1..1]` where SysML v2 §7.6.3 declares it (`attribute z : Real;` on a part, action or state def refuses `(3.0, 4.0)` whether a nested action, the definition's own body or an entry action writes it); one that states and inherits none has the usual default `0..*` (KerML 1.1 §7.4.12; the default-multiplicity and representative-object rows of the Instantiation map), so a write of several values to it is held on every path — `attribute x : Real :> many;` written `(3.0, 4.0)` holds both whether a part def's nested action writes the part's feature (`Instance.SetFeatureValue` → `Context.checkAdmits`, no count judged where `MultiplicityDefaulted`) or the action or state def declaring it writes its own (`ActionExecutor.setFeature`, `StateExecutor.assignAttribute` → `Context.checkNamedWriteHeld`, whose `writeTarget.countJudged` is the same `featureRange` decision) — and writes and declarations of an action parameter are judged against its effective parameter range (`EffectiveParameterRange`, SysML v2 §7.6.3), so a bare action parameter is `[0..*]` and an explicitly stated `[*]`, `[0..1]` or `[1..2]` parameter holds what it declares — and a count-judged write holds its value in the shape that multiplicity gives it, as the performance occurrence already did (`Context.admitted`): a `[1]` target the sole element, a multi-valued one a sequence (a set where declared), so the frame, the occurrence's slots, the gRPC `FeatureValue`, `%action` and `-json` agree, and the shape no longer depends on the order the body read a pin in (the Action row on pin shape); `action_param_default_scalar_count.sysml` now explicitly declares `[1]`, and `robustness_performed_action_inputs_test.go:testNestedActionParameterMultiplicity` covers bare nested parameters and explicit required inner and outer parameters; a null holds none and so reaches a `[0..1]` parameter's `??` but not a `[1..1]` one — and a Complex is one value (`ValComplex`) against any feature's multiplicity: a Complex feature holds it, a Real feature refuses one off the real axis by type, and a numeric pair `(1.0, 2.0)` is two values whatever the feature's type | `passes/typecheck_assign.go` `exprChecker.checkAssignmentValue` → `passes/typecheck_expr.go` `exprChecker.checkBoundValue` and `bindable` (the initial-value rule, reached with the target's declaration); `runtime/write_conformance.go` `Context.writeTargetIn` → `writeTargetOf` (the target's multiplicity and whether a write's count is judged are `runtime/shape.go` `featureRange`'s, the one decision `effectiveFeature` gives an object's feature and `Context.checkAdmits` judges: a parameter's effective range, a stated or inherited multiplicity or the implicit `[1..1]` judge the count, a defaulted feature's representative range judges none; `writeTarget.stated` carries the declaration rule below), `Context.checkTargetAs` → `shapeForTarget` (the shape a count-judged write is held in) and `shapeDeclared` (a root default binding, `runtime/action_frame.go` `registerRootBindings`), `Context.checkWrite`/`checkWriteType`/`valueConforms`, reached from `runtime/instance.go` `Instance.SetFeatureValue`, `Context.checkDefault` (at `materialize` for a folded default and `materializeFeatureValueIntrinsic` for a deferred one), `runtime/binding.go` `assignBindingEndpoint`, `runtime/eval.go` `EvalContext.declaredValue`, `runtime/write_conformance.go` `Context.checkBodyDeclaration` → `checkTarget` with `admitDeclared` (a declaration local to a calc, action or constraint body, a `{ }` block or a collection-expression body — `lower.Declare` in `runtime/statements.go`, an action node's own feature seeded by `runtime/action_frame.go` `seedDeclaredValues`, and the compiled statement tier's `runtime/compiled_stmts.go` — is judged once when its initial value is bound, by the namespace-level rule: its type with declared admission, so an enumeration-typed local holds the enumerated value; its multiplicity only where the declaration states one, an omitted multiplicity holding the count the initializer has; its uniqueness where it declares more than one value; KerML 1.1 §7.3.4, "the values of a feature are instances of its types"; a local declaring no type holds anything), the statement hosts' `assignData`/`assignOuter`, `ActionExecutor.setFeature` and `StateExecutor.assignAttribute`; `runtime/invoke_calc.go` `bindCalcParameters` and `runtime/calc_statements.go` `calcStmtHost.acceptReturn` and `runtime/calc_usage.go` `calcRun.value` (an output a declaration binds rather than a body assigns, and a result a `bind result = ...` binds, which `runtime/calc_usage.go` `loadedCalc.designatedOutput` keeps the declared result's declaration for) through `calcMemberDecl.check` (the parameter's effective declaration: `calcMemberDecl.redeclaring` keeps the type and multiplicity a redeclaration states none of from the parameter, output or result it redeclares, per KerML 1.1 §7.3.4.5; the compiled calc tier's `runtime/compile.go` `scalarCheck.accepts` decides the same declaration on the lattice, `least` being the `Positive` bound, and hands every value it declines to `calcMemberDecl.check`) and `Context.writeCountRefusal` (`runtime/write_conformance.go`, shared with `checkWrite` and `checkDefault`; `elementCount` in `runtime/collections.go` counts a scalar, a Complex included, without materializing a sequence); `runtime/state_statements.go` `stateStmtHost.assignStateAttribute` for an attribute held in a state's own frame | `passes/typecheck_assign_test.go` (state entry/do bodies, action bodies, calc bodies, transition effects, numeric widening, silence on an unknown type); `passes/typecheck_expr_test.go:TestExprQuotientDoesNotBindToWholeNumberFeature` (`Integer = 4 / 2` refused, `Rational = 7 / 2` and `Integer = RationalFunctions::ToInteger(4 / 2)` accepted), `:TestExprInvocationArgumentNarrowerThanExpression` (`add(7 / 2, 1)` refused, `add(RationalFunctions::ToInteger(7 / 2), w)` accepted), `:TestExprBindWiderTypedReferenceToNarrowerFeatureOK`, `:TestExprBindDecimalLiteralToNaturalRejected`, `:TestExprBindStringToIntegerAttribute`; `runtime/value_conformance_test.go:TestDefaultValueMustConformAtMaterialization` (`whole : Integer = 4 / 2`, `nat : Natural = 4 / 2`, `fromReal : Integer = two` and `computed : Integer = seven - two` are `ErrTypeMismatch`; `wholeRational : Rational = 4 / 2` holds `2.0` and `wholeConverted : Integer = ToInteger(4 / 2)` holds `2`), `:TestRestatedDefaultConformsByItsOwnValue`, `:TestDeclaredValueReadChecksItsType`, `:TestUserDeclaredScalarTypeDefersToTheBinding`, `:TestCalcParameterAndResultMustConform` (`Half(4 / 2)` and `Half(two)` refused, `Half(ToInteger(4 / 2))` accepted), `runtime/compile_test.go:TestCompiledCalcErrorParity` (`Natural1(-1)`, `Positive1(0)` and a `Positive` or `Rank :> Positive` result of `0` are the interpreter's `ErrTypeMismatch` on the compiled tier too), `sweep/sweep_test.go` (a sweep range over a `Natural`/`Positive` parameter admits what the parameter holds, in the interpreter and compiled alike), `conformance/value_classification_shared_rule` (`e : Even = 4`, `q : Rational = 6 / 3`, `none : Integer[0..1]`), a feature typed by an enumeration admits exactly the enumerated values (SysML v2 §8.3.7, the classification row) — `held : Level = three` for `three : Integer = 3` is held as `Level::high` (`checkWriteType` → `holdAsEnumerated`, so `held hastype Level` is `true`) and `= two` is the write-conformance `ErrTypeMismatch`, and a calc parameter, output or result declared by an enumeration holds its argument the same way (`calcMemberDecl.check` → `holdForDeclared`; `runtime/compile.go` `scalarCheckFor` declines an enumeration-typed declaration, so the compiled tier hands it to the same checker), so `asLevel(3) hastype Level` is `true` for `calc def asLevel { in n : Integer; return : Level = n; }`; where the value is constant the checker refuses it at its spelling (`passes/typecheck_value.go` `checkEnumeratedValue`: `Level = 2` is `cannot bind 2 (an Integer) to a feature typed by Level, whose values are Level::low = 1, Level::high = 3`, while a value of another kind stays the scalar lattice's one diagnostic) — `classify_test.go:TestEnumerationTypedFeatureAdmitsOnlyEnumeratedValues`, `robustness_classification_test.go:enumeration_typed_feature_holding_an_unenumerated_value`, `passes/typecheck_value_test.go:TestValueScalarConstantToScalarValuedEnumeration`, `:TestCalcParameterAndResultMultiplicity`, `:TestCalcMultiplicityViolationNamesTheBinding`, `:TestComplexCountsAsOneValue`, conformance `calc_body_local_declared_type` (`BodyOnly(3)` is `3` with `l` held as `Level::high`, `BodyOnly(2)` is the write's `ErrTypeMismatch`, `attribute xs : Integer[2] = (n, n + 1, n + 2)` `ErrMultiplicityViolation`, `attribute xs : Integer[*] = (n, n + 1, n)` `ErrUniquenessViolation`, an untyped local and one stating no multiplicity hold what they are given, a local in an `if` block and in a `->collect` body is judged the same), `action_body_local_declared_type`, `constraint_body_local_declared_type`, `constraint_body_local_count` (a condition reads its body's declaration through `eval.go` `conformBodyDeclared`, a stated multiplicity judging the count, determined or open, and an omitted one keeping it — `undetermined_test.go:TestBodyLocalKeepsOpenInitializerCount`), `constraint_param_scalar_count` (the unchanged constraint-parameter case), `action_param_default_scalar_count.sysml` (now explicitly `[1]`, preserving its two-value `ErrMultiplicityViolation` expectation), `robustness_performed_action_inputs_test.go:testNestedActionParameterMultiplicity` (bare nested action parameters admit no input, while explicit `[1]` inner and outer parameters preserve their typed errors), `constraint_body_local_redefined_type` and `constraint_body_local_redefined_count` (a local inheriting its initializer from the declaration it redefines is held to the redefinition, `shape.go` `statedMultiplicity` walking the same redefinition, positional-parameter and abstract-subsetting chain as the effective multiplicity — `undetermined_test.go:TestBodyLocalIntersectsRedefinedMultiplicities`, `TestBodyLocalInheritsImplicitParameterMultiplicity`), `constraint_body_local_redefined_unique` and `action_body_local_redefined_count` (the inherited bound also gates uniqueness and the lowered `checkBodyDeclaration` path), `robustness_body_locals_test.go:body_local_outside_its_declaration`, `tests/codegen/compiled_calcs_test.go:TestCompiledCalcsAgreeWithInterpreter` (`PosLoc`, `Seq::LocM1`, `Seq::LocN`, `Seq::LocAny2`: the native tier declares the local through `codegen/compile.go` `compileDeclare` → `bind` with the declared range and stated multiplicity, `codegen/ir.go` `Declare.Range`, and declines an enumeration-typed local and one redefining or subsetting another feature — `TestCompileRefusesWhatItCannotCompile` `SubsetLocal` — `not compilable`, rather than answer differently), pilot-exec-diff `body_local_conformance` (the pilot binds a body-local value unchecked, so the accepted case agrees and the refused ones are ours-error against its answer), `invoke_calc_body_test.go:TestCalcForLoopOverParameterSequence` (`in xs[*];`), `conformance/calc_null_coalesce.sysml` (`in n : Integer[0..1];`), `conformance/calc_result_multiplicity_many`, `_named_many`, `_body_many` and `_fixed` (`return : Real[0..*] = x`, `return r : Real[0..*] = x`, a `[0..*]` result the trailing expression binds and `return : Real[2]` hold sequences), `_one` and `_optional` (`[1]` and `[0..1]` hold the bare value), `_unstated` and `_unstated_named` (a bare `return : Real` keeps the computed scalar) and `verification_result_multiplicity_many` (a `VerdictKind[0..*]` verdict is read from its one element — `verification_run.go` `verdictOf` reads through `soleElement`), `conformance/calc_library_feature_imaginary_unit.sysml` (`return : Complex = i;`), pilot-exec-diff `intdiv` (deliberately `ours-error`, now refused by the checker: the reference answers `3.5` for `return : Integer = 7 / 2`, checking nothing against the parameter's type) and `rational-feature-hastype-integer` (`rat : Rational = 4` holds the `Integer` `4`); `conformance/assign_write_conforms_to_target_type.sysml`, `assign_write_violates_target_type.sysml`, `assign_write_through_chain_violates_target_type.sysml`; `robustness_typed_writes_test.go:write_of_a_wrong_typed_value_leaves_the_feature`, `:state_entry_write_of_a_wrong_typed_value`, `:chained_write_of_a_wrong_typed_value`, `robustness_calc_invocation_test.go:calc_output_write_of_a_wrong_typed_value`, `:action_local_write_of_a_wrong_typed_value`, `:action_output_write_of_a_wrong_typed_value`, `:performer_feature_write_of_a_wrong_typed_value`, `:performance_occurrence_write_of_a_wrong_typed_value`, `robustness_calc_usage_test.go:calc_output_binding_violates_declared_type`; `conformance/assign_write_to_state_attribute_violates_target_type.sysml` | ⚠️ Approximate (self-assessed: the pinned pilot implementation executes no action or state machine, so it cannot referee this. A quantity value carries its unit rather than the quantity value type measured in it, so the specialization relation between two quantity value types is not itself decided; the quantity kind such a target declares is judged dimensionally, by the row below. A model declaring its own scalar types gives the run time no value semantics for them, so there a value is refused only for a disjoint type, the binding rule's either-direction conformance standing in for a judgement by value. A Complex is a `Complex` on the real axis or off it — `rect(2.0, 0.0)` is refused by a `Real` or `Integer` feature, `re` of it is the `Real` — since `ComplexFunctions` return `Complex` and the specification fixes no finer classification of such a value. The fixtures that relied on a whole real or a quotient being held by an `Integer` feature were re-adjudicated by this rule rather than relaxed: an `Integer` or `Natural` feature or parameter a quotient or a whole real reaches is declared `Rational` or `Real` where the model computes such a value (`tests/codegen/testdata/compile_calcs.sysml`, `repl/choice_test.go`, `grpc/explore_test.go`, `lsp/library_invocation_test.go`, `passes/typecheck_value_test.go`, `runtime/classify_test.go`), converts with `ToInteger` where it means the integer (`runtime/value_conformance_test.go`, `passes/typecheck_expr_test.go`), or pins the typed error where the write is what the fixture probes (`runtime/value_conformance_test.go`, `robustness_quantities_test.go` coordinate-frame failure modes: a number written where a `ScalarMeasurementReference` or a `TranslationOrRotation` is declared is the write's `ErrTypeMismatch`), each by §8.3.3.3.4 and §9.4.11.1 above) | | A performed action's parameter holds its values in the shape its effective multiplicity gives them from the moment it is bound, on every surface alike (KerML 1.1 §7.3.4 — the values of a feature are a sequence its multiplicity constrains; SysML v2 §7.6.3 — a parameter stating no multiplicity has no implied `1..1`, so a bare pin is `[0..*]`): a `[1]` pin holds its one value bare, a pin written without a multiplicity or `[0..*]` holds a one-element sequence, `[0..*]` holding several or none is that sequence or the empty one, and so for `in`, `out` and `inout` pins, the pins of a state's entry, do and exit actions, a nested performed action at every depth, a pin bound through `:>>` or through the same-position parameter of a specializing action def (which inherits the multiplicity it implicitly redefines unless it states its own), a pin the body reads twice, and the slots of the part performing the action — in the action's results, `-json`, the trace, the performing object's features and the gRPC `FeatureValue`. `bare_parameter_effective_multiplicity_action_bare` accordingly reads its bare `out n` as `[0]`: the count that case pins is unchanged, the shape now follows the multiplicity the case is about rather than being printed as computed. A calculation's `return` and a case's verdict take the same rule (`runtime/invoke_calc.go` `calcMemberDeclFor` resolves a parameter stating no multiplicity to `semantics.EffectiveParameterRange`): a bare `return : Real` is a one-element sequence, `[1]` or `[0..1]` the bare value (SysML v2 §7.6.3 — no implicit `[1..1]` for a reference usage; §7.19.2 — a `return` redefines the `[0..*]` result it inherits; KerML §7.3.4.2 — an unspecified feature has only the default subsetting of `things`), and a `return attribute r : Real` is `[1..1]` by the attribute rule; a calc `out` parameter a body assigns is a body write and takes the rule. The case and analysis consumers of verdicts, selected alternatives, ranges, indexes, conditions and quantities read a one-element sequence as its element (KerML §7.4.8), and a feature reads a value bound from such a result in its own shape (`runtime/set_feature.go` `declaredCollection`) | `runtime/write_conformance.go` `shapeForTarget` (from `checkTargetAs`, so `checkNamedWriteAs` for `ActionExecutor.setFeature`, `checkBodyDeclarationAs` and `calcMemberDecl.check` share it) and `shapeDeclared` (`runtime/action_frame.go` `registerRootBindings`, a default the root binds with no body write); `runtime/instance.go` `Context.heldShape`, the one shape predicate `Context.admitted` applies on the occurrence side and `runtime/set_feature.go` `declaredCollection` applies to a namespace-level read of a declared value (`-e P::xs`, the REPL, gRPC `Evaluate`), so a `[0..*]` attribute valued `= 4` reads `[4]` at model level as it does on an object; the one-value consumers of the sequence row above, `runtime/function_value.go` `boundFunction` and `declaredFunction` among them (a call applies the one function a calc-typed parameter holds) | conformance `performed_pin_shape_scalar`, `performed_pin_shape_bare`, `performed_pin_shape_multi`, `performed_pin_shape_inout`, `performed_pin_shape_nested`, `performed_pin_shape_redefined`, `performed_pin_shape_state_actions` (+ trace), `performed_pin_shape_part_scalar`, `performed_pin_shape_part_bare`; `robustness_pin_shape_test.go` (`TestRuntimeRobustnessPinShape`: a `[1]` pin bound to two values, a `[1]` output assigned from a two-valued pin, two values in a bare pin as an operand, a bare pin holding one object as a send target); `tests/grpc` `execute_action_pin_shape` (a `[1]` output as `int_value`, a bare one as a one-element `sequence`, a `[0..*]` one as the sequence); `client/opensysml` `pin_shape_helpers_test.go` `sole` over the client suites; the trace goldens whose bare-pin reads moved from `-> v` to `-> (v)` and the conformance expectations whose bare pins moved from `v` to `[v]`; `repl/namespace_shape_test.go` `TestEvalNamespaceReadHoldsEffectiveMultiplicity` (`[0..*]`, `[1]`, `[0..1]`, bare, `[2]`, empty and `(4)`-valued features at namespace and object level, `P::xs + 1`, `P::xs == 4`), `tests/grpc` `evaluate_namespace_feature_multiplicity`, `evaluate_object_feature_multiplicity`, `robustness_usage_members_test.go` (a constraint's bare `in limit = 100.0` reads `[100.0]`) | ✅ Faithful | | A constant is a value of the scalar type its representation states and of that type's supertypes, whatever number it holds: an integer is an `Integer` (and so a `Rational`, `Real`, `Complex`, `Number`) whether written `2` or computed as `ToInteger(4 / 2)`; a finite real is a `Rational` — `2.0`, `4 / 2` (which `IntegerFunctions::'/'` declares `Rational`, KerML 1.1 §9.4.11.1) and `3.5` alike, so none is held by an `Integer` feature and nothing is truncated to become one (§8.4.4.9.2: a `LiteralRational` is classified in `Rational`); an infinity is a `Real`; a Complex is a `Complex` on the real axis or off it; a NaN, real or complex, states no type at all and so is of none — `istype`, `hastype`, `@` and `as` answer nothing for it, a `Real` or `Rational` parameter refuses it as `ErrTypeMismatch` on both calc tiers and its direct type is `errUndeterminedValueType`, never `Rational` — while `*` is the `Positive` exceeding every bound (§9.3) and is placed on the lattice as one: a subtype a model declares of `Positive` (or of any type above it) holds it through a declared feature and leaves a bare cast undecided, exactly as `Even :> Integer` treats `4`, while `String`, `Boolean` and their subtypes exclude it; a sequence index is still the position a whole value names (`Value.WholeNumber`), which is arithmetic, not classification | `runtime/classification.go` `representationPrim`, `representationClassifies`, `scalarLibraryType`, `isNaN`; `runtime/eval.go` `directValueType`; `semantics/eval.go` `Value.WholeNumber` (a finite real with no fractional part within the `Integer` range, so NaN, an infinity and `1e19` are no whole numbers); `runtime/write_conformance.go` `valueConforms` over `classifyValue` | `semantics/eval_test.go:TestWholeNumberRefusesFractionsAndNonFinite`; `runtime/value_conformance_test.go`, `:TestNaNIsOfNoScalarType`, `runtime/compile_test.go:TestCompiledCalcErrorParity` (`SameReal(NaN)`, `SameRational(NaN)`), `runtime/infinity_test.go:TestInfinityDirectType`, `:TestUnboundedAgainstDeclaredSubtype`, `:TestComplexCountsAsOneValue`, `runtime/complex_test.go`; `runtime/eval_operator_test.go:TestTypeClassificationOperators` (a model's own `Integer`/`Natural` keep their declared relation); conformance `value_classification_shared_rule`; pilot-exec-diff `scalar_classification.cases` | ✅ Faithful | | A bound or written quantity is measured in the dimension its target's declared quantity value type fixes: `ScalarQuantityValue` declares `attribute :>> mRef: ScalarMeasurementReference`, `ScalarMeasurementReference` declares `attribute quantityDimension: QuantityDimension[1]`, and a quantity kind narrows that reference to one unit definition (`DurationValue :>> mRef: DurationUnit[1]`, whose `durationPF` states `quantity = isq.T`, `exponent = 1`), so the type states the dimension its values are measured in and a unit of another dimension is not one of them. Scale is not part of a dimension, so `5 [min]` and `250 [ms]` conform where `5 [s]` does, and a composed unit is judged by the dimension it reduces to (`10 [m] / 2 [s]` conforms to `SpeedValue`, not to `DurationValue`) | `semantics/dimension.go` `Model.DimensionOfType` (beside `DimensionOfFeature`, sharing `quantityValueTypeIn`), compared with `Model.DimensionOfUnit` by `UnitTerm.Commensurable`; `passes/typecheck_dimension.go` `exprChecker.checkValueDimension`, reached from `passes/typecheck_expr.go` `exprChecker.checkBoundValue` and so from `passes/typecheck_assign.go` `exprChecker.checkAssignmentValue`; `runtime/write_conformance.go` `Context.quantityConforms`, reached from `Context.valueConforms` so every write path inherits it | `semantics/dimension_type_test.go:TestDimensionOfTypeFixesTheDeclaredKind`, `:TestDimensionOfTypeThroughAnAlias`, `:TestDimensionOfTypeMatchesTheFeatureItTypes`; `passes/typecheck_dimension_test.go:TestBoundQuantityOfAnotherDimension`, `:TestBoundQuantityOfTheSameDimensionAtAnotherScale`, `:TestBoundQuantityComposedToItsDimension`, `:TestAssignedQuantityOfAnotherDimension`, `:TestBoundQuantityThroughAnAlias`, `:TestBoundDimensionlessQuantity`; `conformance/action_quantity_dimension_scaled.sysml`, `action_quantity_dimension_inferred_mismatch.sysml`, `instance_quantity_dimension_mismatch.sysml`; `robustness_quantities_test.go:quantity_write_of_another_dimension` | ⚠️ Approximate (self-assessed: the pinned pilot implementation executes no action or state machine, so it cannot referee this. the rule is stated as the existing type-mismatch error with a dimension-specific message rather than a new diagnostic kind, so `-validate`, the REPL and the LSP report it identically) | diff --git a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/18-kerml-function-library.md b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/18-kerml-function-library.md index 78e44cfba2..08556c90d4 100644 --- a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/18-kerml-function-library.md +++ b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/18-kerml-function-library.md @@ -609,7 +609,7 @@ restores library symbols without their AST). | `NumericalFunctions::sum0`/`product1` (the library declares no `Integer`/`Rational`/`Real` specialization of either) fold a collection as `sum`/`product` do, keeping the elements' kind, and answer the identity argument for an empty collection; an identity that is not `isZero`/`isUnit` as the library's own precondition asserts is `ErrTypeMismatch`, and a non-numeric element or an overflow is reported as for `sum` | `runtime/builtins_named.go` `builtinNumericalSum0`, `builtinNumericalProduct1`, `aggregateWithIdentity` | `TestAggregationsWithIdentity`, `TestAggregationsWithIdentityErrors`; conformance `calc_library_aggregation_identity`, `calc_library_sum0_wrong_identity` | ✅ Faithful | | Generic `DataFunctions::max`/`min` and `ScalarFunctions::max`/`min` order two values the way the `<` operator does — numbers keeping their kind as the numeric `max`/`min` do, strings by character, quantities by magnitude — answering the operand chosen; a pair the library declares no ordering for (two Booleans, two sequences, a string and a number) is `ErrTypeMismatch` | `runtime/library_operators.go` `registerGenericExtrema`, `genericExtremum` | `TestOperatorFunctionValues`, `TestOperatorFunctionErrors`; conformance `calc_library_operator_call_forms` (`largest`, `smallest`) | ✅ Faithful | | **Every operator the library declares is callable as a function**, and evaluates by the operator's own evaluator code, never a second arithmetic: `'+'`, `'-'` (with `y` omitted — not given, null or an empty collection — the unary sign, the same evaluation the `+`/`-` operator with an empty second operand reaches, KerML 1.1 §8.4.4.9.6; see the binary-operators row), `'*'`, `'/'`, `'%'`, `'**'`, `'^'`, `'<'`, `'<='`, `'>'`, `'>='` in `DataFunctions`, `ScalarFunctions`, `NumericalFunctions`, `RealFunctions`, `RationalFunctions`, `IntegerFunctions` (`'**'`/`'^'` with a Natural exponent) and `NaturalFunctions`, each package's parameter types imposed on the arguments — a `RealFunctions` parameter is `Real`, so an Integer argument is bound as the Real it equals and the function answers a Real (`RealFunctions::'+'(1, 2)` is `3.0`, `'-'(5)` is `-5.0`, `'*'(2^40, 2^40)` is `2^80` where the Integer product would overflow), as `RealFunctions::max`/`abs` already answer Reals, while `RationalFunctions` bind an Integer as the exact Rational it equals and answer an exact Rational (`RationalFunctions::'+'(1, 2)` is the Rational `3.0`), as `RationalFunctions::abs`/`max`/`min` keep exactness (see [exact-rational-evaluation.md](exact-rational-evaluation.md)); `NaturalFunctions::'/'` is the exception to the shared arithmetic, computing the Natural its declaration returns: `'/'(6, 3)` is the Integer `2`, a quotient no Natural equals (`'/'(7, 2)`) is `semantics.ErrArithmeticDomain` rather than a truncated or Rational answer, and `'/'(6, 0)` stays `ErrDivisionByZero` (the `/` *operator* on Natural operands still answers the Rational the pilot answers, see [omg-issues.md](omg-issues.md)); `'=='` in every package that declares it, each typed package (`Boolean`, `Integer`, `Natural`, `Rational`, `Real`) imposing its operand type on both given operands (`IntegerFunctions::'=='(2, 2.0)` is `ErrTypeMismatch`, where the `==` operator and `BaseFunctions::'=='` answer `true`) while an omitted operand stays admitted as null, and `DataFunctions::'=='`/`'==='` admitting DataValues only (a part or other occurrence is `ErrTypeMismatch` naming `DataValue`; `BaseFunctions::'=='`/`'==='` compare anything), `'!='`, `'==='` and `'!=='` in `BaseFunctions`/`DataFunctions` — every equality and identity form holds its `[0..1]` operands to one value: an empty collection is the same no value as null (`BaseFunctions::'=='((), null)` is `true`, `'!=='((), "")` `true`, as `() == null` is by the operator, `isEmptyValue`), a sole element stands for itself, and two or more values are `ErrMultiplicityViolation` naming the parameter (`singleOperands`); every other operand is declared `[1]` and held to exactly one value before its kind is judged — a one-element sequence or set stands for its element (`IntegerFunctions::'+'(xs->select {in i; i > 2}, 1)` adds that element), while an empty or several-valued operand is `ErrMultiplicityViolation` naming the parameter, never a type mismatch (`soleValue`, `checkOperands`, the generic `max`/`min` and `NaturalFunctions::'/'` included); `'not'`, `'xor'`, `'\|'`, `'&'` in `BooleanFunctions`, `DataFunctions` and `ScalarFunctions` (both operands given, so nothing short-circuits); `'..'` in `DataFunctions`, `ScalarFunctions` and `IntegerFunctions`; `BaseFunctions::'#'` (declared `Positive[1..*]` indexes: one selects from the sequence as `SequenceFunctions::'#'` does, several address an Array and select its element in row-major order as `CollectionFunctions::'array#'` does — over a flat sequence they are `ErrTypeMismatch` — and none is `ErrMultiplicityViolation`, `builtinBaseIndex`; conformance `calc_library_base_index_many`, `calc_library_base_index_many_sequence`, robustness `base_index_with_several_indexes`), `CollectionFunctions::'#'` and `','`; `CollectionFunctions::'=='`. A failure names the function as the model writes it (`function IntegerFunctions::'/': division by zero`) | `runtime/library_operators.go` `registerOperatorFunctions`, `naturalDivision`, `equalityForm` over `runtime/eval.go` `arithmeticValues`, `comparisonValues`, `Context.equalityValues`, `unaryValue`, `combineBooleans`, `valueIdentical`; `runtime/builtins_named.go` `registerNamedOperatorBuiltins`, `runtime/range.go` `rangeBuiltin`; `runtime/library_functions.go` `writtenName` | `TestOperatorFunctionValues`, `TestOperatorFunctionErrors`, `TestOperatorFunctionOperandMultiplicity`, `TestDataOperatorFunctionsRequireDataValues`, `TestSequenceOperatorCallForms`, `TestIndexCallFormErrors`, `TestEval_EqualityNull`; conformance `calc_library_operator_call_forms`, `calc_library_operator_singleton_operands`, `calc_library_operator_many_operands`, `calc_library_empty_equality`, `calc_library_base_index_many`, `calc_library_base_index_many_sequence`; robustness `named_library_call_that_has_no_value`, `data_equality_over_a_part`, `base_index_with_several_indexes` | ✅ Faithful | -| An input parameter whose multiplicity admits no value (`in y: Integer[0..1]`) may go without an argument, and is then null: the static arity check counts it as optional and the runtime binds null to it, so `IntegerFunctions::'-'(5)` and `ControlFunctions::'if'(false, 1)` are accepted and evaluated, and a model's own `calc def Scale { in x: Integer; in factor: Integer[0..1]; ... }` is called as `Scale(5)`. A built-in receives one value per parameter it declares whatever the call wrote, positionally or by name, so a trailing omission is null too: `SequenceFunctions::size()` is `0`, `NumericalFunctions::sum()` `0`, `ControlFunctions::'if'(false)` and `'??'()` null, and `subsequence(seq, 2)` runs to the end as the library's `endIndex` default declares; a required parameter left out, or an argument past the last parameter, is `errCalcArity`. A parameter declaring no multiplicity holds one value and stays required (KerML 1.1 §7.4.7.2) | `semantics/multiplicity.go` `Range.AllowsNone`, `Model.IsOptionalParameter`; `passes/typecheck_expr.go` `checkArguments`; `runtime/invoke_calc.go` `calcParameter.optional`, `EvalContext.bindCalcParameter`; `runtime/builtins_signature.go` `bindBuiltinArgs`, `fillUnbound` | `passes/typecheck_expr_test.go:TestExprInvocationOptionalParameterMayBeOmitted`; conformance `calc_parameter_optional_omitted`, `calc_library_operator_call_forms`, `calc_library_control_functions`, `calc_library_builtin_omitted_arguments`; `builtins_named_test.go:TestControlFunctionCallForms`, `:TestSequenceOperatorCallForms`; robustness `builtin_named_argument_that_binds_nothing` | ✅ Faithful | +| An input parameter whose multiplicity admits no value (`in y: Integer[0..1]`) may go without an argument, and is then null: the static arity check counts it as optional and the runtime binds null to it, so `IntegerFunctions::'-'(5)` and `ControlFunctions::'if'(false, 1)` are accepted and evaluated, and a model's own `calc def Scale { in x: Integer; in factor: Integer[0..1]; ... }` is called as `Scale(5)`. A built-in receives one value per parameter it declares whatever the call wrote, positionally or by name, so a trailing omission is null too: `SequenceFunctions::size()` is `0`, `NumericalFunctions::sum()` `0`, `ControlFunctions::'if'(false)` and `'??'()` null, and `subsequence(seq, 2)` runs to the end as the library's `endIndex` default declares; a required parameter left out, or an argument past the last parameter, is `errCalcArity`. A parameter declaring no multiplicity holds one value and stays required (KerML 1.1 §7.4.7.2) | `semantics/multiplicity.go` `Range.AllowsNone`, `Model.IsOptionalParameter`; `passes/typecheck_expr.go` `checkArguments`; `runtime/invoke_calc.go` `optionalParameter`, `EvalContext.bindCalcParameter`; `runtime/builtins_signature.go` `bindBuiltinArgs`, `fillUnbound` | `passes/typecheck_expr_test.go:TestExprInvocationOptionalParameterMayBeOmitted`; conformance `calc_parameter_optional_omitted`, `calc_library_operator_call_forms`, `calc_library_control_functions`, `calc_library_builtin_omitted_arguments`; `builtins_named_test.go:TestControlFunctionCallForms`, `:TestSequenceOperatorCallForms`; robustness `builtin_named_argument_that_binds_nothing` | ✅ Faithful | | `ControlFunctions::'if'`, `'and'`, `'or'`, `'implies'` and `'??'` as functions keep the library's short-circuit semantics: the `expr` parameters are deferred, so only the branch `'if'` selects, the second operand `'and'`/`'or'`/`'implies'` needs, and the second value `'??'` falls back to are evaluated (`'if'(true, 1, 1 / 0)` is `1`); a body written for one (`{1 + 1}`), or reached through an `expr` parameter of the calling calc (`Pick(test, { base + 1 }, { 0 - base })`), is applied rather than answered as a body, and one declaring a parameter is `ErrBodyArity`. `'if'` with `elseValue` omitted answers null, as the `[0..1]` parameter allows; `'??'` falls back over an empty first operand — null, `()`, an empty set or a collection filtered down to nothing — in the function form and the `??` operator alike (`coalesceNull`, `isEmptyValue`) | `runtime/builtins_named.go` `EvalContext.evalDeferred`, `deferredCount`, `builtinControlIf`, `builtinControlLogical`, `builtinControlNullCoalesce`, `EvalContext.evalDeferred` | `TestControlFunctionCallForms`, `TestControlFunctionCallFormErrors`; conformance `calc_library_control_functions`, `calc_library_control_body_by_reference`; robustness `named_library_call_that_has_no_value`, `body_by_reference_that_cannot_be_applied` | ✅ Faithful | | A call is dispatched by the declaration it resolves to, the model's declarations first: a built-in answers only a symbol the library declares, so a model's own `NumericalFunctions::sum0` or `size` — with a body or without one — is the model's calc, evaluated from its body or reported as `ErrNoValue` (its result is never bound), never computed by the library's implementation of that name; a built-in binds named arguments by its declared parameter names in any order (`sum0(zero = 0, collection = xs)`), and a name it does not declare is `errUnknownParameter`, a name bound twice or mixed with positional arguments `errCalcArity`. A direct invocation through the runtime API (`InvokeCalc`, `invokeCalcNamed`) takes the same path, so it binds and computes as the written call does; a body or expression value it hands to an `expr` parameter is applied only when the control function selects it, any other value is the operand's value | `runtime/eval.go` `invocationTarget`, `evalInvocation`; `runtime/builtins.go` `Context.builtinFor` (`runtime/library_functions.go` `Context.libraryDeclared`); `runtime/builtins_signature.go` `bindBuiltin`, `bindBuiltinArgs`, `bindBuiltinValues`; `runtime/invoke_calc.go` `invokeCalcWithSelf`, `invokeBuiltinValues` | conformance `calc_library_builtin_shadowed_by_model`, `calc_library_builtin_named_arguments`; robustness `bodiless_model_calc_named_as_a_builtin`, `builtin_named_argument_that_binds_nothing`; `TestInvokeCalcDispatchesBuiltins`, `TestInvokeCalcNamedDispatchesBuiltins`, `TestInvokeCalcDefersBodyArguments` | ✅ Faithful | | **Occurrences have a lifetime in the runtime's own execution order** (KerML §9.4.18 `OccurrenceFunctions`, `Occurrences::Occurrence`/`Life`, `Performances::Performance`): every object the runtime materializes, and every action or state performance it runs, is an occurrence with a begin and an end expressed as *activation marks* — the monotone counter the executors already order performances by, never wall-clock time — held in a side table keyed by object identity (`Context.lives`), so no Kernel Semantic Library frame member (`portionOfLife`, `startShot`, `endShot`) is added to an object and `%features` keeps its shape. An object begins when its whole does (a part materialized under a bench began when the bench did), a performed action's or exhibited state's occurrence begins when its performance starts and ends when it completes, and the lifetimes of the objects an evaluation materialized and abandoned are dropped with them. The information is read through `Context.OccurrenceLife(id)` (began, ended, destroyed), `%features` (a destroyed object prints `(destroyed at N, alive since M)` in place of features it no longer holds), `%instances` (`, destroyed`), and the execution trace (`create: T #n`, `destroy: T #n`) | `runtime/lifetimes.go` `life`, `Context.beginLife`, `beginPerformanceLife`, `endPerformanceLife`, `forgetLives`, `OccurrenceLife`; `runtime/instance.go` `materialize`; `runtime/action_executor.go` `initialize`/`retireToken`; `runtime/state_executor.go` `initialize`/`completeIfDone`; `runtime/trace.go` `recordOccurrenceCreated`/`recordOccurrenceDestroyed`; `repl/meta.go` `featureValueWalk.rows` (the `(destroyed at N, alive since M)` and `, destroyed` renderings) | `lifetimes_test.go:TestOccurrenceLifeBeginsWithWhole`, `:TestAbandonedObjectsLoseTheirLives`; conformance `occurrence_is_during_completed_performance`, `occurrence_is_during_in_exhibited_state`, `occurrence_lifecycle_in_performed_action` (+ golden trace); `repl/occurrence_test.go:TestOccurrenceCommandReturnsAtThePrompt`; `library_shape_test.go:TestShapeKeepsSystemsLibraryFeaturesAndLeavesOutTheKernelFrame` passes unchanged | ✅ Faithful | diff --git a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/30-kerml-rules-not-reported.md b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/30-kerml-rules-not-reported.md index 99af22b0b7..7e29d62e1b 100644 --- a/docs/project/spec-compliance/02-detailed-semantic-compliance-map/30-kerml-rules-not-reported.md +++ b/docs/project/spec-compliance/02-detailed-semantic-compliance-map/30-kerml-rules-not-reported.md @@ -19,7 +19,7 @@ reference disagrees about, which is the false-positive result that work wanted. | `validateConnectorRelatedFeatures` (`KerMLValidator.checkConnector`): a concrete connector has at least two related features (**error**), whether its ends are positional (`(x, y)`, `from x to y`), declared `end` features or inherited | `passes/w10b_related_elements.go` `W10BRelatedElementsPass`, counting through `semantics.Model.RelatedFeatureCount`; KerML `connector x to y` without `from` now parses as a two-ended connector (`parser/defusage.go` `atConnectorBinaryEnds`) | `passes/w10b_related_elements_kerml_test.go` `TestW10BKerMLConnectorRelatedFeatures`, `TestW10BSysMLRelatedFeatures`; parser golden `tests/parser/testdata/parse/kerml_binary_connector.kerml` and `parser/negative_test.go` (`binary_connector_*`) | ✅ Faithful | | `validateAssociationBinarySpecialization` / `validateConnectorBinarySpecialization` (`KerMLValidator.checkAssociation`, `checkConnector`; KerML 1.1 §8.3.4.4.2, §8.3.4.5.3): an association or connector that conforms to `Links::BinaryLink` cannot have more than two ends (**error**, `Cannot have more than two ends`), counting positional ends, declared `end` features and inherited ends; positional ends redefine the inherited binary ends by position rather than adding to them | `passes/constraint.go` `checkBinaryConnectorEnds`, over `semantics.Model.BinaryConnectorExcessEnds` and `Model.IsBinaryConnector`; each end past the second is reported at its own span | `passes/binary_connector_ends_test.go` `TestBinaryConnectorPositionalEnds`, `TestBinaryConnectorDeclaredAndInheritedEnds`, `TestBinaryConnectorSysMLShapes`; corpus `semantic/k24-binary-assoc-three-ends.kerml`, `semantic/k26-binary-connector-three-ends.kerml` | ✅ Faithful | | `validateFeatureOwnedReferenceSubsetting` (`KerMLValidator.xtend`): a feature has at most one reference subsetting (**error**), every one after the first reported on its target | `passes/w8c_reference_subsetting.go` `ReferenceSubsettingPass` | `passes/w8c_structural_rules_test.go` (two references fire on the second; one reference and a binding's two ends stay silent) | ✅ Faithful (a binding is exempt: our AST encodes its two ends as reference subsettings) | -| `validateFunctionResultExpressionMembership` / `validateExpressionResultExpressionMembership` (`KerMLValidator.xtend`; KerML 1.1 §8.3.4.8 `Function::result`, `Expression::result`): a function, predicate, expression or boolean expression — and so a SysML calculation, constraint, requirement, concern or viewpoint definition or usage — owns or inherits at most one result expression (**error**, `Only one (owned or inherited) result expression is allowed`); a calculation, function or expression body lists at most one bare expression (`calc def C { in x; x + 1 x + 2 }` states two, where the pilot's `CalculationBodyPart`/`FunctionBodyPart` stop parsing), and a specialization or redefinition of a type that owns a result expression inherits it and may not state a second (`constraint def Sub :> Base { x > 1 }`, `require constraint :>> c { x > 1 }`, `calc def D :> C { x + 2 }`, `calc c : C { … }`, `constraint d ::> c { x > 1 }`), while the conditions of one constraint body are one result, an empty or documentation-only redefinition keeps the inherited one and a nested `assert constraint { … }` is a separate constraint rather than a result expression; reported on the second body stated, on the newly owned body when the type states one over an inherited one, on the declaration when two are inherited | `semantics/result_expression.go` `Model.ResultExpressionsOf`, `Model.ResultExpressionMemberships`, `Model.ResultExpressionConflict` (owned bodies, then those of every member source through `Model.MemberSources` — specialized or reference-subsetted, a diamond counted once; a constraint body's conditions collapse to one membership) consumed by `passes/w8c_result_expression.go` `ResultExpressionPass` (constraint tier, `ElementScoped`); the runtime refuses the same shapes with `errConflictingResultExpressions` instead of choosing a body (`runtime/invoke_calc.go` `calcShapeOf`, `runtime/condition.go` `Condition.Conflict`, `solve/translate.go`) and collects the bodies it does evaluate over the same member sources (`runtime/invoke_calc.go` `calcChain`, `runtime/context.go` `chainMembers`), so a bodiless reference-subsetting calculation or constraint inherits the referenced one | `semantics/result_expression_test.go`, `passes/result_expression_test.go` (specialized definition, redefining, typed and reference-subsetting usages, two inherited generals on a constraint, requirement, concern or viewpoint, two bare expressions in one calculation/function/expression body and a bodiless type inheriting them, KerML predicate/function/expression forms fire; an inherited-only body and many conditions in one constraint body stay silent), `passes/w8c_structural_rules_test.go`, `runtime/robustness_calc_invocation_test.go` `calc_states_second_result`, `calc_body_states_two_results`, `runtime/invoke_calc_body_test.go` `TestReferencedCalcBodyIsInherited`, `runtime/conditions_of_test.go` `TestConditionsOfReferencedConstraint` (bodiless `::>` inherits and evaluates the referenced body, a diamond once), `repl/replaced_result_check_test.go`, `repl/owned_constraint_check_test.go` (empty, braced and nested-assertion redefinitions inherit and execute the redefined check); corpus `semantic/k29-`, `k30-`, `k43-`, `k44-`, `s82-`, `s83-`, `s84-`, `s85-…-result-expression` (both reject at the same positions), `grammar/g69-`, `k20-…-second-result-expression` (the pilot's grammar stops at the second expression where we report it) | ✅ Faithful (the reference's separate `validateResultExpressionMembershipOwningType` (census unknown) owning-type half is grammar-unconstructable in both tools) | +| `validateFunctionResultExpressionMembership` / `validateExpressionResultExpressionMembership` (`KerMLValidator.xtend`; KerML 1.1 §8.3.4.8 `Function::result`, `Expression::result`): a function, predicate, expression or boolean expression — and so a SysML calculation, constraint, requirement, concern or viewpoint definition or usage — owns or inherits at most one result expression (**error**, `Only one (owned or inherited) result expression is allowed`); a calculation, function or expression body lists at most one bare expression (`calc def C { in x; x + 1 x + 2 }` states two, where the pilot's `CalculationBodyPart`/`FunctionBodyPart` stop parsing), and a specialization or redefinition of a type that owns a result expression inherits it and may not state a second (`constraint def Sub :> Base { x > 1 }`, `require constraint :>> c { x > 1 }`, `calc def D :> C { x + 2 }`, `calc c : C { … }`, `constraint d ::> c { x > 1 }`), while the conditions of one constraint body are one result, an empty or documentation-only redefinition keeps the inherited one and a nested `assert constraint { … }` is a separate constraint rather than a result expression; reported on the second body stated, on the newly owned body when the type states one over an inherited one, on the declaration when two are inherited | `semantics/result_expression.go` `Model.ResultExpressionsOf`, `Model.ResultExpressionMemberships`, `Model.ResultExpressionConflict` (owned bodies, then those of every member source through `Model.MemberSources` — specialized or reference-subsetted, a diamond counted once; a constraint body's conditions collapse to one membership) consumed by `passes/w8c_result_expression.go` `ResultExpressionPass` (constraint tier, `ElementScoped`); the runtime refuses the same shapes with `errConflictingResultExpressions` instead of choosing a body (`runtime/invoke_calc.go` `calcShapeOf`, `runtime/condition.go` `Condition.Conflict`, `solve/translate.go`) and collects the bodies it does evaluate over the same member sources (`lower/calc.go` `CalcChain`, `runtime/context.go` `chainMembers`), so a bodiless reference-subsetting calculation or constraint inherits the referenced one | `semantics/result_expression_test.go`, `passes/result_expression_test.go` (specialized definition, redefining, typed and reference-subsetting usages, two inherited generals on a constraint, requirement, concern or viewpoint, two bare expressions in one calculation/function/expression body and a bodiless type inheriting them, KerML predicate/function/expression forms fire; an inherited-only body and many conditions in one constraint body stay silent), `passes/w8c_structural_rules_test.go`, `runtime/robustness_calc_invocation_test.go` `calc_states_second_result`, `calc_body_states_two_results`, `runtime/invoke_calc_body_test.go` `TestReferencedCalcBodyIsInherited`, `runtime/conditions_of_test.go` `TestConditionsOfReferencedConstraint` (bodiless `::>` inherits and evaluates the referenced body, a diamond once), `repl/replaced_result_check_test.go`, `repl/owned_constraint_check_test.go` (empty, braced and nested-assertion redefinitions inherit and execute the redefined check); corpus `semantic/k29-`, `k30-`, `k43-`, `k44-`, `s82-`, `s83-`, `s84-`, `s85-…-result-expression` (both reject at the same positions), `grammar/g69-`, `k20-…-second-result-expression` (the pilot's grammar stops at the second expression where we report it) | ✅ Faithful (the reference's separate `validateResultExpressionMembershipOwningType` (census unknown) owning-type half is grammar-unconstructable in both tools) | | `checkAssociationEndTypes` (`KerMLValidator.xtend`): an association end must have exactly one type (**error**) | `passes/w8c_association_end_types.go` `AssociationEndTypesPass` | `passes/w8c_structural_rules_test.go` (a two-typed end fires; single-typed, untyped and redefining ends stay silent); `tools/referee/reject` `xpect/p02-association-end-two-types.kerml` — `both-reject (ours 1 error(s), pilot 1 error(s))` | ⚠️ Approximate (we report only the two-or-more case: an end with *no* declared or inherited type is accepted, because the reference's own parsing fixtures — `ParsingTests_Associations`, `AssociationTest_EndRedefinitionGoodCase` — supply that type implicitly, and reporting it produced false positives on them) | | `validateImportTopLevelVisibility` (`KerMLValidator.xtend`): an import owned by a root namespace must be private (**error**) | `passes/w8c_import_visibility.go` `TopLevelImportPass` at the name-resolution tier | `passes/w8c_structural_rules_test.go` (a public and a protected root import fire at the import's span; a private root import, an `expose`, and a public import inside a package stay silent); `tools/referee/reject` `xpect/p01-public-root-import.kerml` — `both-reject (ours 1 error(s), pilot 1 error(s))` | ✅ Faithful (deliberately not at the syntax tier: parser-recovered root members in the SysML corpora would otherwise be diagnosed) | | `validateFeatureIsVariable` (`KerMLValidator.xtend`): a `variable` feature must be owned by an occurrence type (**error**) | `passes/w8c_variable_feature.go` `VariableFeaturePass`, resolving `Occurrences::Occurrence` and testing conformance of the owning type | `passes/w8c_structural_rules_test.go` (a variable feature in a datatype fires, one in an occurrence definition stays silent); `tools/referee/reject` `xpect/p03-variable-in-datatype.kerml` — `both-reject (ours 1 error(s), pilot 1 error(s))` | ✅ Faithful | diff --git a/internal/exec/runtime/action_executor.go b/internal/exec/runtime/action_executor.go index 7955d83b0a..de493296a3 100644 --- a/internal/exec/runtime/action_executor.go +++ b/internal/exec/runtime/action_executor.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -3758,7 +3759,7 @@ func (h Held) Features() []lower.Attribute { // Value is the value held under name: its redefinition's when name is redefined. func (h Held) Value(name string) (Value, bool) { - v, ok := h.data[canonical(h.aliases, name)] + v, ok := h.data[program.Canonical(h.aliases, name)] return v, ok } diff --git a/internal/exec/runtime/action_frame.go b/internal/exec/runtime/action_frame.go index 0f9325c456..3f7aad7e6b 100644 --- a/internal/exec/runtime/action_frame.go +++ b/internal/exec/runtime/action_frame.go @@ -8,6 +8,7 @@ import ( "sort" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -447,7 +448,7 @@ func (ctx *Context) aliasRedefinitions(aliases *map[string]string, sym *symbols. continue } seen[redefined] = true - *aliases = aliasRedefined(*aliases, redefined.Name, name) + *aliases = program.AliasRedefined(*aliases, redefined.Name, name) visit(redefined) } } @@ -469,7 +470,7 @@ func (f *actionFrame) declareUnvalued(name string, dir ast.FeatureDirection, isR // key is the name the performance holds name under: its redefinition's, else its own. func (f *actionFrame) key(name string) string { - return canonical(f.aliases, name) + return program.Canonical(f.aliases, name) } // beginPerformance starts a performance of node (of parent's flow, or a block's with locals bound), @@ -777,14 +778,14 @@ type nodePins struct { // declares reports whether the pins hold name, under its own name or as a redefinition. func (p nodePins) declares(name string) bool { - _, ok := p.directions[canonical(p.aliases, name)] + _, ok := p.directions[program.Canonical(p.aliases, name)] return ok } // admitsNoValueAt reports whether the performance's pin admits no value at all: a // flow out of it then carries nothing when the performance wrote it none. func (perf *actionFrame) admitsNoValueAt(pin string) bool { - return perf != nil && perf.optional[canonical(perf.aliases, pin)] + return perf != nil && perf.optional[program.Canonical(perf.aliases, pin)] } // nodePins returns the pins a performance of node holds. A call its arguments' values @@ -1154,7 +1155,7 @@ func (e *performances) deliver(f *actionFrame, flow *lower.ActionGraph, node ast if err := e.ctx.checkNamedWriteAs(flow.Scopes[node], func() string { return nodeDescription(node) }, pin, &value); err != nil { return err } - f.queue(node, canonical(pins.aliases, pin), value) + f.queue(node, program.Canonical(pins.aliases, pin), value) return nil } @@ -1362,7 +1363,7 @@ func (e *performances) streamFlow( if err := e.ctx.checkNamedWriteAs(graph.Scopes[flow.Target], func() string { return nodeDescription(flow.Target) }, flow.TargetPin, &value); err != nil { return fmt.Errorf("%s: %w", flowDescription(flow), err) } - pin := canonical(pins.aliases, flow.TargetPin) + pin := program.Canonical(pins.aliases, flow.TargetPin) from := flow.SourcePin if perf != nil { from = perf.key(from) diff --git a/internal/exec/runtime/analysis.go b/internal/exec/runtime/analysis.go index 8e66933880..20026ddfeb 100644 --- a/internal/exec/runtime/analysis.go +++ b/internal/exec/runtime/analysis.go @@ -329,7 +329,7 @@ func (ctx *Context) readEvalValue(obj *Objective, objSym *symbols.Symbol) bool { return true } body := bodyScope(evalSym, evalSym.OwnerScope) - stmts := lower.CalcBodyWith(evalSym.Decl, unwrappedDeclMembers(evalSym.Decl), body, ctx.Resolver()) + stmts := lower.CalcBodyWith(evalSym.Decl, lower.UnwrappedDeclMembers(evalSym.Decl), body, ctx.Resolver()) if len(stmts) == 1 { if ret, ok := stmts[0].(lower.Return); ok && ret.Value != nil { obj.Value, obj.Scope = ret.Value, ret.Scope @@ -378,7 +378,7 @@ func (ctx *Context) objectiveMember(objSym *symbols.Symbol, name string) *symbol if body == nil { return nil } - for _, node := range unwrappedDeclMembers(objSym.Decl) { + for _, node := range lower.UnwrappedDeclMembers(objSym.Decl) { member := memberSymbol(body, node) if member != nil && restatesFeatureNamed(member, name) { return member @@ -481,7 +481,7 @@ func (ctx *Context) objectiveConditionsOf(sym *symbols.Symbol) (model, library [ if link == nil || ctx.frameDeclared(link) { continue } - for _, node := range unwrappedDeclMembers(link.Decl) { + for _, node := range lower.UnwrappedDeclMembers(link.Decl) { if ctx.libraryDeclared(link) { libraryMembers = append(libraryMembers, scopedMember{node: node, scope: body}) } else { @@ -489,7 +489,7 @@ func (ctx *Context) objectiveConditionsOf(sym *symbols.Symbol) (model, library [ } } } - for _, node := range unwrappedDeclMembers(sym.Decl) { + for _, node := range lower.UnwrappedDeclMembers(sym.Decl) { members = append(members, scopedMember{node: node, scope: body}) } return ctx.conditionsOf(sym, members), ctx.conditionsOf(sym, libraryMembers) diff --git a/internal/exec/runtime/analysis_run.go b/internal/exec/runtime/analysis_run.go index f57f77435e..3ff0cc05f4 100644 --- a/internal/exec/runtime/analysis_run.go +++ b/internal/exec/runtime/analysis_run.go @@ -6,89 +6,13 @@ import ( "slices" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) -// subjectDecl is a case's subject as its body declares it: the object the case -// is about, which binds as the case's first input parameter (SysML v2 §7.21). -type subjectDecl struct { - Name string - Value ast.Node - Node ast.Node -} - -// subjectDeclaration reads a subject declaration, written as a subject member -// (`subject s : T;`, `subject = expr;`) or a subject-kind usage. -func subjectDeclaration(member ast.Node) (subjectDecl, bool) { - switch m := member.(type) { - case *ast.SubjectMember: - name, _ := m.EffectiveName() - return subjectDecl{Name: name, Value: m.BindingExpr, Node: m}, true - case *ast.Usage: - if m.Kind != ast.UsageSubject { - return subjectDecl{}, false - } - name, _ := ast.EffectiveName(m) - return subjectDecl{Name: name, Value: m.Value, Node: m}, true - } - return subjectDecl{}, false -} - -// subjectParameter records link's subject among params: refining the subject an -// earlier link declared, else inserting it first, the position a subject binds by. -func (ctx *Context) subjectParameter( - params []calcParameter, index map[string]int, aliases *map[string]string, - link *symbols.Symbol, member ast.Node, subject subjectDecl, -) []calcParameter { - sym := memberSymbol(DeclScope(link), member) - param := calcParameter{ - Name: subject.Name, Default: subject.Value, Owner: link, IsSubject: true, - Sym: sym, - Decl: ctx.calcMemberDeclOf(link, sym, subject.Name), - } - at := -1 - for i := range params { - if params[i].IsSubject { - at = i - break - } - } - if at < 0 { - if seen, ok := ctx.redeclaredIndex(index, sym, subject.Name); ok { - at = seen - } - } - if at >= 0 { - // A redeclaration binding no value keeps the inherited binding; one - // declaring no name keeps the inherited name. - if param.Default == nil { - param.Default, param.Owner = params[at].Default, params[at].Owner - } - if param.Name == "" { - param.Name = params[at].Name - } - param.Decl = param.Decl.redeclaring(params[at].Decl) - if params[at].Name != param.Name { - *aliases = aliasRedefined(*aliases, params[at].Name, param.Name) - delete(index, params[at].Name) - } - params[at] = param - index[param.Name] = at - return params - } - if param.Name == "" { - param.Name = "subject" - } - for name, i := range index { - index[name] = i + 1 - } - index[param.Name] = 0 - return append([]calcParameter{param}, params...) -} - // subjectParameter is the parameter the case's subject binds to, if it declares one. -func (shape *calcShape) subjectParameter() (*calcParameter, bool) { +func (shape *loadedCalc) subjectParameter() (*program.CalcParam, bool) { for i := range shape.Params { if shape.Params[i].IsSubject { return &shape.Params[i], true @@ -100,7 +24,7 @@ func (shape *calcShape) subjectParameter() (*calcParameter, bool) { // enclosingSubject is the subject of the case whose body declares shape's usage, // which a nested case binding no subject of its own takes (SysML v2 §7.21.2): // read from the environment of the evaluation reading the usage. -func (ctx *Context) enclosingSubject(shape *calcShape, enclosing *EvalContext) (Value, bool, error) { +func (ctx *Context) enclosingSubject(shape *loadedCalc, enclosing *EvalContext) (Value, bool, error) { if enclosing == nil { return Value{}, false, nil } @@ -137,7 +61,7 @@ func enclosingBehavior(sym *symbols.Symbol) *symbols.Symbol { } // unboundSubject reports a case run with no object as its subject. -func (shape *calcShape) unboundSubject(param *calcParameter) error { +func (shape *loadedCalc) unboundSubject(param *program.CalcParam) error { return &subjectUnboundError{Kind: shape.Kind, Element: shape.Name, Subject: param.Name} } @@ -293,7 +217,7 @@ func (ctx *Context) runCase(sym *symbols.Symbol, args AnalysisArgs, scope *symbo } // Arguments the case cannot take refuse the request before anything runs: // the case established nothing, so there is no partial run to report. - asUsage := args.Subject == nil && len(args.Positional) == 0 && len(args.Named) == 0 && isCalcUsageSymbol(sym) + asUsage := args.Subject == nil && len(args.Positional) == 0 && len(args.Named) == 0 && lower.IsCalcUsageSymbol(sym) var calcArgs calcArgs if !asUsage { if calcArgs, err = shape.analysisArgs(args); err != nil { @@ -342,13 +266,13 @@ func (ctx *Context) runCase(sym *symbols.Symbol, args AnalysisArgs, scope *symbo // it); otherwise it is the owner (over one found for the type), binding such a subject too. Given // none, the single object carrying the case, as a type-owned constraint is checked; a definition // with no object at all cannot run its case (ErrNeedsObject), the case being a feature of one. -func (ctx *Context) casePerformer(shape *calcShape, args AnalysisArgs, self *Instance) (*Instance, AnalysisArgs, error) { +func (ctx *Context) casePerformer(shape *loadedCalc, args AnalysisArgs, self *Instance) (*Instance, AnalysisArgs, error) { owner := declaringType(shape.Sym) if owner == nil { return self, args, nil } - subject, declared := shape.subjectParameter() - unbound := declared && subject.Default == nil + subject, declared := shape.SubjectParameter() + unbound := declared && subject.Default == 0 if unbound { _, named := args.Named[subject.Name] unbound = !named @@ -410,7 +334,7 @@ func (ctx *Context) undecidedVerdicts(sym *symbols.Symbol, scope *symbols.Scope, // body once, unmemoized: arguments make it an invocation of its own, not the // evaluation the case's outputs answer from when read as features. deferResults // leaves the results ending the body for a Monte Carlo to evaluate over its sample. -func (ctx *Context) analysisRun(shape *calcShape, reader *EvalContext, calcArgs calcArgs, deferResults bool) (*calcRun, error) { +func (ctx *Context) analysisRun(shape *loadedCalc, reader *EvalContext, calcArgs calcArgs, deferResults bool) (*calcRun, error) { key := calcUsageKey{sym: shape.Sym} if reader.self != nil { key.instance = reader.self.ID @@ -428,23 +352,23 @@ func (ctx *Context) analysisRun(shape *calcShape, reader *EvalContext, calcArgs // the inputs in declaration order — a defaulted one included, as a calc // invocation binds them; the subject is an object, so only Subject or a named // argument binds it. -func (shape *calcShape) analysisArgs(args AnalysisArgs) (calcArgs, error) { +func (shape *loadedCalc) analysisArgs(args AnalysisArgs) (calcArgs, error) { named := make(map[string]Value, len(args.Named)+len(args.Positional)+1) for name, value := range args.Named { - if !shape.hasParameter(name) { + if !shape.HasParameter(name) { return calcArgs{}, fmt.Errorf("%w: %s has no input parameter %q", errUnknownParameter, shape.Label, name) } named[name] = value } if args.Subject != nil { - subject, ok := shape.subjectParameter() + subject, ok := shape.SubjectParameter() if !ok { return calcArgs{}, fmt.Errorf("%w: %s declares no subject to bind an object to", errUnknownParameter, shape.Label) } named[subject.Name] = Value{Kind: ValInstance, Instance: args.Subject.ID} } - open := make([]*calcParameter, 0, len(shape.Params)) + open := make([]*program.CalcParam, 0, len(shape.Params)) for i := range shape.Params { param := &shape.Params[i] if _, bound := named[param.Name]; bound || param.IsSubject || param.IsActor { @@ -465,7 +389,7 @@ func (shape *calcShape) analysisArgs(args AnalysisArgs) (calcArgs, error) { // boundSubject is the object the run's subject parameter holds, nil when the // case declares none or the binding names no object. func (run *calcRun) boundSubject(ctx *Context) *Instance { - param, ok := run.shape.subjectParameter() + param, ok := run.shape.SubjectParameter() if !ok { return nil } @@ -487,7 +411,7 @@ func (run *calcRun) outputValues(ctx *Context) ([]CalcOutputValue, error) { for _, out := range run.shape.Outputs { if out.Name == "" { if run.returned { - values = append(values, CalcOutputValue{Name: resultOutputName, Value: run.result}) + values = append(values, CalcOutputValue{Name: program.ResultOutputName, Value: run.result}) } continue } @@ -498,7 +422,7 @@ func (run *calcRun) outputValues(ctx *Context) ([]CalcOutputValue, error) { values = append(values, CalcOutputValue{Name: out.Name, Value: value}) } if len(run.shape.Outputs) == 0 && run.returned { - values = append(values, CalcOutputValue{Name: resultOutputName, Value: run.result}) + values = append(values, CalcOutputValue{Name: program.ResultOutputName, Value: run.result}) } return values, nil } @@ -584,7 +508,7 @@ func (ctx *Context) unboundObjectiveSubject(obj *symbols.Symbol, members []scope var stated scopedExpr unbound := make(map[string]bool) for _, member := range members { - declared, ok := subjectDeclaration(member.node) + declared, ok := lower.SubjectDeclaration(member.node) if !ok { continue } @@ -666,8 +590,8 @@ func statedAsDefault(decl *symbols.Symbol) bool { // memberRole says what the run's member of that name is to the case, as a diagnostic // names it: its result, its subject, or the member by name. -func (shape *calcShape) memberRole(name string) string { - if out := shape.resultOutput(); (out != nil && out.Name == name) || (out == nil && name == resultOutputName) { +func (shape *loadedCalc) memberRole(name string) string { + if out := shape.ResultOutput(); (out != nil && out.Name == name) || (out == nil && name == program.ResultOutputName) { return "result" } for _, param := range shape.Params { @@ -687,10 +611,10 @@ func (run *calcRun) caseResult(bindings map[string]Value) (Value, bool) { // resultName is the name the run's result reads under: the one the case's result // parameter declares, or `result` for a result it leaves unnamed. func (run *calcRun) resultName() string { - if out := run.shape.resultOutput(); out != nil && out.Name != "" { + if out := run.shape.ResultOutput(); out != nil && out.Name != "" { return out.Name } - return resultOutputName + return program.ResultOutputName } // analysisChecks reports whether the case states an objective or asserts a @@ -755,7 +679,7 @@ func (run *calcRun) bindingsFrame(ctx *Context) frame { f := frame{vars: vars, perf: run.perf, owner: run.shape, run: run.env.run} for _, body := range run.bodyFrames { for name := range body.unvalued { - name = canonical(body.aliases, name) + name = program.Canonical(body.aliases, name) if _, bound := vars[name]; !bound { f.markUnvalued(name) } @@ -772,10 +696,10 @@ func (run *calcRun) bindings(ctx *Context) map[string]Value { run.env.each(func(name string, value Value) { bindings[name] = value }) for _, body := range run.bodyFrames { for name := range body.unvalued { - delete(bindings, canonical(body.aliases, name)) + delete(bindings, program.Canonical(body.aliases, name)) } for name, value := range body.vars { - bindings[canonical(body.aliases, name)] = value + bindings[program.Canonical(body.aliases, name)] = value } } for _, out := range run.shape.Outputs { @@ -788,7 +712,7 @@ func (run *calcRun) bindings(ctx *Context) map[string]Value { } if run.returned { if out, err := run.shape.designatedOutput(); err != nil || out.Name == "" { - bindings[resultOutputName] = run.result + bindings[program.ResultOutputName] = run.result } } return bindings diff --git a/internal/exec/runtime/assign_chain_test.go b/internal/exec/runtime/assign_chain_test.go index 3c08376f67..bda499fb56 100644 --- a/internal/exec/runtime/assign_chain_test.go +++ b/internal/exec/runtime/assign_chain_test.go @@ -4,8 +4,6 @@ import ( "errors" "strings" "testing" - - "github.com/Open-MBEE/OpenSysML/internal/ir/lower" ) // A chained target whose last segment names no feature of the object the chain @@ -157,18 +155,3 @@ func runChainWrite(t *testing.T, body string) error { } return err } - -// TestChainedAssignmentBindsNoCalcOutput locks that a chained target writing a -// feature of another object does not count as the calc computing its own output -// of that name. -func TestChainedAssignmentBindsNoCalcOutput(t *testing.T) { - outputs := []calcOutput{{Name: "x"}} - chained := []lower.Statement{lower.Assign{Target: "x", Chain: &lower.AssignTarget{Text: "foo.x"}}} - if assigned := assignedOutputs(chained, outputs, nil); len(assigned) != 0 { - t.Errorf("assignedOutputs(chained) = %v; want none, since foo.x is another object's feature", assigned) - } - plain := []lower.Statement{lower.Assign{Target: "x"}} - if assigned := assignedOutputs(plain, outputs, nil); !assigned["x"] { - t.Errorf("assignedOutputs(plain) = %v; want x, which the body does bind", assigned) - } -} diff --git a/internal/exec/runtime/binding.go b/internal/exec/runtime/binding.go index 76a2fcf825..ee800a7537 100644 --- a/internal/exec/runtime/binding.go +++ b/internal/exec/runtime/binding.go @@ -167,7 +167,7 @@ func (ctx *Context) connectorOwnedBindings(decl *symbols.Symbol) []lower.Binding ctx.model.hoistingBindings[decl] = true defer delete(ctx.model.hoistingBindings, decl) var out []lower.Binding - for _, member := range unwrappedDeclMembers(decl.Decl) { + for _, member := range lower.UnwrappedDeclMembers(decl.Decl) { usage, ok := member.(*ast.Usage) if !ok || usage.IsVariant { continue diff --git a/internal/exec/runtime/calc_statements.go b/internal/exec/runtime/calc_statements.go index 4318ca23e7..0c4959af05 100644 --- a/internal/exec/runtime/calc_statements.go +++ b/internal/exec/runtime/calc_statements.go @@ -14,7 +14,7 @@ import ( // the case, which flow steps the tokens of; a calc's body performs nothing. type calcStmtHost struct { ctx *Context - shape *calcShape + shape *loadedCalc self *Instance result Value // the value its `return` yielded flow *ActionExecutor @@ -61,11 +61,11 @@ func (o *calcOccurrence) materializeOccurrence() (*Instance, error) { // attachPerformances makes the host perform the steps of a case body as // subperformances of the case, whose performance is the root the body reads through. func (h *calcStmtHost) attachPerformances(engine *stmtEngine) { - if !h.shape.performs() { + if !h.shape.Performs() { return } root := &actionFrame{ - scope: h.shape.bodyScope(), + scope: h.shape.BodyScope(), data: make(map[string]Value), features: make(map[string]ast.FeatureDirection), subactions: make(map[ast.Node]*actionFrame), @@ -141,7 +141,7 @@ func (h *calcStmtHost) assignOuter(env *stmtEnv, name string, value Value, s low return fmt.Errorf("%w: %s is not declared by the calculation", errCalcExternalAssignment, name) } out, _ := h.shape.output(name) - if out.Value != nil && !out.IsInitial { + if out.Value != 0 && !out.IsInitial { return fmt.Errorf( "%w: output %s of %s is both given a value by its declaration and assigned in its body", errConflictingOutput, name, h.shape.Label, @@ -169,9 +169,9 @@ func (h *calcStmtHost) assignData(env *stmtEnv, name string, value Value, s lowe // hold: a qualified write lands the way the body's own assignments do — checked // against the declaration, bound in the frame, and mirrored into the run's // occurrence when `this` materialized and holds the feature. -func calcFeatureWriter(ctx *Context, shape *calcShape, occ *calcOccurrence) func(frame, string, Value) error { +func calcFeatureWriter(ctx *Context, shape *loadedCalc, occ *calcOccurrence) func(frame, string, Value) error { return func(f frame, name string, value Value) error { - if err := ctx.checkNamedWriteComputed(shape.bodyScope(), calcBodyDescription, name, &value); err != nil { + if err := ctx.checkNamedWriteComputed(shape.BodyScope(), calcBodyDescription, name, &value); err != nil { return err } f.set(name, value) @@ -234,7 +234,7 @@ func (h *calcStmtHost) statementFootprint([]lower.Statement, int) lower.Footprin } func (h *calcStmtHost) statementOrder(stmts []lower.Statement) *lower.StatementOrder { - if h.shape.performs() || len(stmts) < 2 { + if h.shape.Performs() || len(stmts) < 2 { return nil } key := &stmts[0] @@ -245,7 +245,7 @@ func (h *calcStmtHost) statementOrder(stmts []lower.Statement) *lower.StatementO } return nil } - order := lower.CalcBodyStatementOrder(h.shape.bodyScope(), nil, stmts) + order := lower.CalcBodyStatementOrder(h.shape.BodyScope(), nil, stmts) actual, _ := h.shape.statementOrders.LoadOrStore(key, order) order = actual.(*lower.StatementOrder) if h.ctx.scheduling().ordersStatements() || order.HasReversePrecedence() || order.HasSkipped() { @@ -396,7 +396,7 @@ func (h *calcStmtHost) assignAround(name string, value Value) (bool, error) { return true, nil } if h.declaredOutput(name) || h.env.data.has(name) { - if err := h.ctx.checkNamedWriteComputed(h.shape.bodyScope(), h.describe(), name, &value); err != nil { + if err := h.ctx.checkNamedWriteComputed(h.shape.BodyScope(), h.describe(), name, &value); err != nil { return true, err } h.env.data.setBody(h.ctx, name, value) @@ -409,7 +409,7 @@ func (h *calcStmtHost) assignAround(name string, value Value) (bool, error) { func (h *calcStmtHost) returnAround(name string, value Value) (bool, error) { if h.declaredOutput(name) || h.env.data.has(name) { what := func() string { return fmt.Sprintf("%s: output %s returned", h.describe(), name) } - if err := h.ctx.checkMutable(h.shape.bodyScope(), what, name); err != nil { + if err := h.ctx.checkMutable(h.shape.BodyScope(), what, name); err != nil { return true, err } } diff --git a/internal/exec/runtime/calc_usage.go b/internal/exec/runtime/calc_usage.go index b7c4c5e1f8..16a91a9eb9 100644 --- a/internal/exec/runtime/calc_usage.go +++ b/internal/exec/runtime/calc_usage.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -25,191 +26,54 @@ import ( // the same lowered statement engine an invocation uses, and its outputs are // evaluated on demand against the environment that computation left behind. -// calcOutput is one output feature of a calc: an `out` parameter, or the result -// parameter a `return` declares. Its value is a feature of the calc, read by -// name, rather than a statement of the calc's body. -type calcOutput struct { - Name string // feature name the output answers to, empty for an anonymous result parameter - Value ast.Node // value-binding expression, nil when the calc gives the output no value - Owner *symbols.Symbol // the calc declaring the binding, whose scope it is written in - IsResult bool // declared with `return`: the result an invocation of the calc yields - // IsInOut marks an `inout` parameter: an output feature (KerML 7.4.9) bound by - // the invocation rather than by a declaration or an assignment. - IsInOut bool - // IsInitial marks a value declared with `:=`: what the output holds when the - // body starts, which the body's assignments may then replace. - IsInitial bool - Decl calcMemberDecl // the declaration, closest to the invoked calc, the output's value answers to -} - -// calcOutputs flattens the output features declared along chain (most general -// first), as calcParameters does the inputs, recording renamed ones in aliases. -func (ctx *Context) calcOutputs(chain []*symbols.Symbol, aliases *map[string]string) []calcOutput { - var outs []calcOutput - index := make(map[string]int) - - for _, link := range chain { - for _, member := range unwrappedDeclMembers(link.Decl) { - usage, ok := member.(*ast.Usage) - if !ok { - continue - } - if usage.Direction != ast.DirOut && usage.Direction != ast.DirInOut && !usage.IsResult { - continue - } - // An output written as a redefinition names the one it overrides. - name, _ := ast.EffectiveName(usage) - sym := memberSymbol(DeclScope(link), usage) - out := calcOutput{Name: name, Value: usage.Value, Owner: link, IsResult: usage.IsResult, - IsInitial: usage.Value != nil && usage.ValueIsInitial, Decl: ctx.calcMemberDeclOf(link, sym, name)} - if usage.Direction == ast.DirInOut { - // The value an `inout` declares is the default of the parameter, not a - // binding of the output: the invocation binds it either way. - out.IsInOut, out.Value = true, nil - } - - var at int - var seen bool - if name != "" { - at, seen = ctx.redeclaredIndex(index, sym, name) - } else { - // An anonymous result parameter has no name to redeclare by, so - // it refines the anonymous result it inherits. - at, seen = indexOfAnonymousResult(outs) - } - if seen { - if out.Value == nil { - out.Value = outs[at].Value - out.Owner = outs[at].Owner - out.IsInitial = outs[at].IsInitial - } - out.Decl = out.Decl.redeclaring(outs[at].Decl) - if name != "" { - *aliases = aliasRedefined(*aliases, outs[at].Name, name) - index[name] = at - } - outs[at] = out - continue - } - if name != "" { - index[name] = len(outs) - } - outs = append(outs, out) - } - } - return outs -} - -// resultOutput returns the result parameter a `return` declares, if the calc has one. -func (shape *calcShape) resultOutput() *calcOutput { - for i := range shape.Outputs { - if shape.Outputs[i].IsResult { - return &shape.Outputs[i] - } - } - return nil -} - -// hasInitialOutput reports whether some output starts the body holding a value -// (`:=`), which the calc yields when the body leaves it as it is. -func (shape *calcShape) hasInitialOutput() bool { - for _, out := range shape.Outputs { - if out.IsInitial { - return true - } - } - return false -} - -// indexOfAnonymousResult finds the unnamed result parameter among outs, which is -// the one an unnamed redeclaration refines. -func indexOfAnonymousResult(outs []calcOutput) (int, bool) { - for i, out := range outs { - if out.Name == "" { - return i, true - } - } - return 0, false -} - -// assignedOutputs are the outputs the body's statements assign, on any path -// through it: what the calc computes, whichever way an execution branches. -func assignedOutputs(stmts []lower.Statement, outputs []calcOutput, aliases map[string]string) map[string]bool { - declared := make(map[string]string, len(outputs)+len(aliases)) - for _, out := range outputs { - if out.Name != "" && !out.IsInOut { - declared[out.Name] = out.Name - } - } - for alias, name := range aliases { - if _, ok := declared[name]; ok { - declared[alias] = name - } - } - assigned := make(map[string]bool) - collectAssignedOutputs(stmts, declared, assigned) - return assigned +// declaredOutput is an output of a loaded calc with the declaration, closest to +// the invoked calc, its value answers to. +type declaredOutput struct { + program.CalcOutput + Decl calcMemberDecl } -// collectAssignedOutputs walks the statements, and the blocks they carry, for -// assignments to a declared output, by any name declared maps to it. A chained -// target writes a feature of another object, so it binds no output of the calc. -func collectAssignedOutputs(stmts []lower.Statement, declared map[string]string, assigned map[string]bool) { - for _, stmt := range stmts { - switch s := stmt.(type) { - case lower.Assign: - if name, ok := declared[s.Target]; ok && s.Chain == nil { - assigned[name] = true - } - case lower.Block: - collectAssignedOutputs(s.Steps(), declared, assigned) - case lower.Loop: - collectAssignedOutputs(s.Body.Steps(), declared, assigned) - case lower.If: - collectAssignedOutputs(s.Then.Steps(), declared, assigned) - if s.Else != nil { - collectAssignedOutputs(s.Else.Steps(), declared, assigned) - } - } - } +// declaredOutput is the output at position i of Outputs with its declaration. +func (shape *loadedCalc) declaredOutput(i int) declaredOutput { + return declaredOutput{CalcOutput: shape.Outputs[i], Decl: shape.outputDecls[i]} } // output finds the output feature of that name. -func (shape *calcShape) output(name string) (calcOutput, bool) { +func (shape *loadedCalc) output(name string) (declaredOutput, bool) { if name == "" { - return calcOutput{}, false + return declaredOutput{}, false } - name = canonical(shape.Aliases, name) - for _, out := range shape.Outputs { - if out.Name == name { - return out, true + name = program.Canonical(shape.Aliases, name) + for i := range shape.Outputs { + if shape.Outputs[i].Name == name { + return shape.declaredOutput(i), true } } - if name == resultOutputName { + if name == program.ResultOutputName { return shape.anonymousResult() } - return calcOutput{}, false + return declaredOutput{}, false } -// resultOutputName is the name an unnamed result parameter answers to. -const resultOutputName = "result" - // anonymousResult is the unnamed result a `return`, a trailing result expression // or a body returning a value produces, which reads under the name `result`. -func (shape *calcShape) anonymousResult() (calcOutput, bool) { - for _, out := range shape.Outputs { - if out.IsResult && out.Name == "" { - return calcOutput{Name: resultOutputName, Value: out.Value, Owner: out.Owner, Decl: out.Decl, IsResult: true}, true +func (shape *loadedCalc) anonymousResult() (declaredOutput, bool) { + for i := range shape.Outputs { + if out := &shape.Outputs[i]; out.IsResult && out.Name == "" { + return declaredOutput{ + CalcOutput: program.CalcOutput{Name: program.ResultOutputName, Value: out.Value, Owner: out.Owner, IsResult: true}, + Decl: shape.outputDecls[i], + }, true } } - if shape.ResultExpr != nil { - return calcOutput{Name: resultOutputName, Value: shape.ResultExpr, Owner: shape.Sym, IsResult: true}, true + if shape.Result != 0 { + return declaredOutput{CalcOutput: program.CalcOutput{Name: program.ResultOutputName, Value: shape.Result, Owner: shape.Sym, IsResult: true}}, true } - if shape.resultOutput() == nil && lower.Returns(shape.Steps) { + if shape.ResultOutput() == nil && shape.StepsReturn { // The body's `return` binds the result, so the run holds it and nothing evaluates it. - return calcOutput{Name: resultOutputName, Owner: shape.BodyOwner, IsResult: true}, true + return declaredOutput{CalcOutput: program.CalcOutput{Name: program.ResultOutputName, Owner: shape.BodyOwner, IsResult: true}}, true } - return calcOutput{}, false + return declaredOutput{}, false } // returnsResult reports a calc usage whose value is the unnamed result it @@ -220,7 +84,7 @@ func (ctx *Context) returnsResult(sym *symbols.Symbol) bool { return false } for _, out := range shape.Outputs { - if out.Name == resultOutputName { + if out.Name == program.ResultOutputName { return false } } @@ -232,7 +96,7 @@ func (ctx *Context) returnsResult(sym *symbols.Symbol) bool { // parameter, output or local its chain declares, a feature one of those redefines // (`VerificationCase::subj` as the case's subject), or an inherited result parameter // (`Case::result`) as the result the calc designates. -func (shape *calcShape) memberName(ctx *Context, sym *symbols.Symbol) (string, bool) { +func (shape *loadedCalc) memberName(ctx *Context, sym *symbols.Symbol) (string, bool) { if shape.members == nil { shape.members = ctx.calcMemberNames(shape) } @@ -240,11 +104,11 @@ func (shape *calcShape) memberName(ctx *Context, sym *symbols.Symbol) (string, b return name, true } if usage, ok := sym.Decl.(*ast.Usage); ok && usage.IsResult && ctx.libraryDeclared(sym) { - if out := shape.resultOutput(); out != nil && out.Name != "" { + if out := shape.ResultOutput(); out != nil && out.Name != "" { return out.Name, true } if _, ok := shape.anonymousResult(); ok { - return resultOutputName, true + return program.ResultOutputName, true } } return "", false @@ -252,7 +116,7 @@ func (shape *calcShape) memberName(ctx *Context, sym *symbols.Symbol) (string, b // qualifiedBy reports whether a name qualified by qualifier (`MassCase::result`, // `Cases::Case::result`) denotes this calc's run: the calc itself or one it specializes. -func (shape *calcShape) qualifiedBy(ctx *Context, qualifier *symbols.Symbol) bool { +func (shape *loadedCalc) qualifiedBy(ctx *Context, qualifier *symbols.Symbol) bool { return ctx.isOrSpecializes(shape.Sym, qualifier) } @@ -274,13 +138,13 @@ func (ctx *Context) isOrSpecializes(sym, general *symbols.Symbol) bool { // calcMemberNames indexes the named members declared along shape's chain, and the features // they redefine (KerML §7.3.4.5, one feature), by the name the run binds each under. -func (ctx *Context) calcMemberNames(shape *calcShape) map[*symbols.Symbol]string { +func (ctx *Context) calcMemberNames(shape *loadedCalc) map[*symbols.Symbol]string { members := make(map[*symbols.Symbol]string) redefined := make(map[*symbols.Symbol]string) - for _, link := range ctx.calcChain(shape.Sym) { - for _, member := range unwrappedDeclMembers(link.Decl) { + for _, link := range shape.Chain { + for _, member := range lower.UnwrappedDeclMembers(link.Decl) { var name string - if subject, ok := subjectDeclaration(member); ok { + if subject, ok := lower.SubjectDeclaration(member); ok { name = subject.Name } else if usage, ok := member.(*ast.Usage); ok { name, _ = ast.EffectiveName(usage) @@ -289,7 +153,7 @@ func (ctx *Context) calcMemberNames(shape *calcShape) map[*symbols.Symbol]string if name == "" || sym == nil { continue } - name = canonical(shape.Aliases, name) + name = program.Canonical(shape.Aliases, name) members[sym] = name for _, feature := range ctx.redefinedFeatures(sym, shape.Sym) { redefined[feature] = name @@ -305,26 +169,11 @@ func (ctx *Context) calcMemberNames(shape *calcShape) map[*symbols.Symbol]string } // resultSegments is the member path reading the unnamed result of a usage. -var resultSegments = []ast.NameSegment{{Text: resultOutputName}} - -// outputNames renders the output features the calc declares, for a diagnostic -// about one it does not. -func (shape *calcShape) outputNames() string { - names := make([]string, 0, len(shape.Outputs)) - for _, out := range shape.Outputs { - if out.Name != "" { - names = append(names, out.Name) - } - } - if len(names) == 0 { - return "none" - } - return strings.Join(names, ", ") -} +var resultSegments = []ast.NameSegment{{Text: program.ResultOutputName}} // declaresUsage reports whether the calc's body declares a calc or analysis // usage named name among its own members, which a read from outside may name. -func (shape *calcShape) declaresUsage(name string) bool { +func (shape *loadedCalc) declaresUsage(name string) bool { for _, stmt := range shape.Body { if decl, ok := stmt.(lower.DeclareUsage); ok && decl.Name == name { return true @@ -334,7 +183,7 @@ func (shape *calcShape) declaresUsage(name string) bool { } // declaresLocal reports whether the calc's body declares a feature of that name. -func (shape *calcShape) declaresLocal(name string) bool { +func (shape *loadedCalc) declaresLocal(name string) bool { for _, stmt := range shape.Body { if decl, ok := stmt.(lower.Declare); ok && decl.Name == name { return true @@ -345,7 +194,7 @@ func (shape *calcShape) declaresLocal(name string) bool { // memberNames lists the features a usage of the calc has: its parameters, the // features its body declares and its outputs, in that order. -func (shape *calcShape) memberNames() string { +func (shape *loadedCalc) memberNames() string { var names []string for _, param := range shape.Params { names = append(names, param.Name) @@ -374,27 +223,28 @@ func (shape *calcShape) memberNames() string { // output the calc binds a value to. A calc binding several of them designates // none, so its values are read from a calc usage's output features instead of // being narrowed to whichever output comes first. -func (shape *calcShape) designatedOutput() (calcOutput, error) { - if shape.ResultExpr != nil { +func (shape *loadedCalc) designatedOutput() (declaredOutput, error) { + if shape.Result != 0 { // The bound value is the declared result's, so it answers to that declaration. - out := calcOutput{Name: "result", Value: shape.ResultExpr, Owner: shape.Sym, IsResult: true} - if declared := shape.resultOutput(); declared != nil { - out.Decl = declared.Decl + out := declaredOutput{CalcOutput: program.CalcOutput{Name: program.ResultOutputName, Value: shape.Result, Owner: shape.Sym, IsResult: true}} + if at := shape.ResultOutputIndex(); at >= 0 { + out.Decl = shape.outputDecls[at] } return out, nil } - var valued []calcOutput - for _, out := range shape.Outputs { + var valued []declaredOutput + for i := range shape.Outputs { + out := &shape.Outputs[i] if out.IsResult { - if out.Value == nil { + if out.Value == 0 { break } - return out, nil + return shape.declaredOutput(i), nil } // An output the body assigns is computed as much as one a declaration binds, // so it is a candidate result of an invocation just the same. - if out.Value != nil || shape.BodyOutputs[out.Name] { - valued = append(valued, out) + if out.Value != 0 || shape.BodyOutputs[out.Name] { + valued = append(valued, shape.declaredOutput(i)) } } @@ -402,7 +252,7 @@ func (shape *calcShape) designatedOutput() (calcOutput, error) { case 0: // The calc computed nothing to hand back: a body that fell off its end, // or outputs none of which states a value, so the result has no value. - return calcOutput{}, fmt.Errorf("%w: %s ended without a return", ErrNoValue, shape.Label) + return declaredOutput{}, fmt.Errorf("%w: %s ended without a return", ErrNoValue, shape.Label) case 1: return valued[0], nil default: @@ -410,28 +260,13 @@ func (shape *calcShape) designatedOutput() (calcOutput, error) { for _, out := range valued { names = append(names, out.Name) } - return calcOutput{}, fmt.Errorf( + return declaredOutput{}, fmt.Errorf( "%w: %s computes %d output features (%s) and designates no result; read them from a usage instead: %s", - errAmbiguousResult, shape.Label, len(valued), strings.Join(names, ", "), shape.usageSpelling(valued[0].Name), + errAmbiguousResult, shape.Label, len(valued), strings.Join(names, ", "), shape.UsageSpelling(valued[0].Name), ) } } -// usageSpelling writes the calc usage a modeler declares to read the outputs of -// a calc that designates no result, with the inputs it has to bind. -func (shape *calcShape) usageSpelling(output string) string { - var b strings.Builder - fmt.Fprintf(&b, "%s c : %s {", shape.Kind, shape.Name) - for _, param := range shape.Params { - if param.IsActor { - continue - } - fmt.Fprintf(&b, " in %s = ...;", param.Name) - } - fmt.Fprintf(&b, " } then read c.%s", output) - return b.String() -} - // calcUsageKey identifies one evaluation of one calc usage within an activation: // the usage and the object whose state it reads. The bound input values are not // part of it, so a read after an assignment to a feature an input names cannot @@ -446,7 +281,7 @@ type calcUsageKey struct { // the run is created, so reading several outputs of a calc usage does not run // its body again. type calcRun struct { - shape *calcShape + shape *loadedCalc scope *symbols.Scope // scope of whoever reads the usage, for a calc that owns none self *Instance // object the usage is a feature of, nil when unbound env frame @@ -500,7 +335,7 @@ func (run *calcRun) lookup(name string) (Value, bool) { // boundInputs are the values each non-subject input parameter of shape was // bound to in env, in declaration order. -func boundInputs(shape *calcShape, env frame) []InputBinding { +func boundInputs(shape *loadedCalc, env frame) []InputBinding { var inputs []InputBinding for i := range shape.Params { param := &shape.Params[i] @@ -520,7 +355,7 @@ func (run *calcRun) inputs() []InputBinding { } // newCalcRun holds the environment one evaluation of a calc computed. -func newCalcRun(shape *calcShape, scope *symbols.Scope, self *Instance, env frame) *calcRun { +func newCalcRun(shape *loadedCalc, scope *symbols.Scope, self *Instance, env frame) *calcRun { return &calcRun{ shape: shape, scope: scope, @@ -636,7 +471,7 @@ func (ctx *Context) caseReadWaits(start *calcUsageStart) error { // calcUsageStart is an evaluation of a calc usage under way: its inputs bound and // its body about to run, or paused part-way for a wait, as a case's steps may. type calcUsageStart struct { - shape *calcShape + shape *loadedCalc key calcUsageKey ec, nested, reader *EvalContext env frame @@ -659,7 +494,7 @@ type calcUsageStart struct { // run is answered instead, with no start, where this activation evaluated the // usage already. func (ctx *Context) beginCalcUsage(reader *EvalContext, sym *symbols.Symbol) (*calcUsageStart, *calcRun, error) { - if !isCalcUsageSymbol(sym) { + if !lower.IsCalcUsageSymbol(sym) { return nil, nil, fmt.Errorf( "%w: %s does not evaluate output features", errNotACalcUsage, ctx.QualifiedName(sym), ) @@ -698,7 +533,7 @@ func (ctx *Context) beginCalcUsage(reader *EvalContext, sym *symbols.Symbol) (*c } // startCalcUsage binds the usage's inputs from args and makes its body ready to run. -func (ctx *Context) startCalcUsage(shape *calcShape, key calcUsageKey, reader *EvalContext, args calcArgs) (*calcUsageStart, error) { +func (ctx *Context) startCalcUsage(shape *loadedCalc, key calcUsageKey, reader *EvalContext, args calcArgs) (*calcUsageStart, error) { leave, err := ctx.enterCalcUsageRun(shape, key) if err != nil { return nil, err @@ -781,7 +616,7 @@ func (ctx *Context) startCalcUsage(shape *calcShape, key calcUsageKey, reader *E // enterCalcUsageRun spends the calc depth on the usage and marks it running for the // call under way (binding its inputs, or running its body until it ends or pauses). -func (ctx *Context) enterCalcUsageRun(shape *calcShape, key calcUsageKey) (leave func(), err error) { +func (ctx *Context) enterCalcUsageRun(shape *loadedCalc, key calcUsageKey) (leave func(), err error) { if err := ctx.enterCalc(shape.Name); err != nil { return nil, err } @@ -815,7 +650,7 @@ func (ctx *Context) finishCalcUsage(start *calcUsageStart) (*calcRun, error) { // enterCalcUsage marks the usage's body as running until leave is called; a usage // read while its own body runs would run itself without end, so that is an error. -func (ctx *Context) enterCalcUsage(shape *calcShape, key calcUsageKey) (leave func(), err error) { +func (ctx *Context) enterCalcUsage(shape *loadedCalc, key calcUsageKey) (leave func(), err error) { if running := ctx.calcUsageRunning[key]; running != nil && running.reads(shape.Sym) { return nil, fmt.Errorf("%w: %s reads itself while its body runs", errCalcUsageRecursion, shape.Label) } @@ -826,8 +661,8 @@ func (ctx *Context) enterCalcUsage(shape *calcShape, key calcUsageKey) (leave fu // reads reports whether sym, read from within the body of the usage this is the // shape of, is that usage itself rather than a same-named usage its body declares // (a `calc next : Down` inside Down, read while a `next` runs, is the next level down). -func (shape *calcShape) reads(sym *symbols.Symbol) bool { - return shape.Sym == sym && sym.OwnerScope != shape.bodyScope() +func (shape *loadedCalc) reads(sym *symbols.Symbol) bool { + return shape.Sym == sym && sym.OwnerScope != shape.BodyScope() } // bodyUsageSymbol resolves the usage a body-local declaration declares, in the @@ -837,7 +672,7 @@ func (ctx *Context) bodyUsageSymbol(stmt lower.DeclareUsage) (*symbols.Symbol, e return nil, fmt.Errorf("%w: calc usage %s needs a resolved model", errNotACalcUsage, stmt.Name) } sym, ok := ctx.lookupName(stmt.Scope, stmt.Name) - if !ok || !isCalcUsageSymbol(sym) { + if !ok || !lower.IsCalcUsageSymbol(sym) { return nil, fmt.Errorf( "%w: calc usage %s is declared in this body but is not resolved to one", errNotACalcUsage, stmt.Name, @@ -867,7 +702,7 @@ func (ctx *Context) forgetCalcUsage(activation int64, sym *symbols.Symbol) { // bindCalcUsage binds a calc usage's inputs from args and its own declarations, // answering with the environment the usage's body runs in, the environment // reading it (null unless it is nested in a calc), and the bindings themselves. -func (ctx *Context) bindCalcUsage(shape *calcShape, reader *EvalContext, args calcArgs, occurrence *calcOccurrence) (*EvalContext, *EvalContext, frame, map[string]bool, error) { +func (ctx *Context) bindCalcUsage(shape *loadedCalc, reader *EvalContext, args calcArgs, occurrence *calcOccurrence) (*EvalContext, *EvalContext, frame, map[string]bool, error) { ec := NewEvalContextIn(ctx, ctx.calcScope(shape.BodyOwner, shape.Sym, reader.scope), reader.self) if ec.trace != nil { ec.trace.recordCalculationEnter(shape.Kind, shape.Name) @@ -930,7 +765,7 @@ func holdsRunningValues(sym *symbols.Symbol) bool { // bodyEnclosing is the part of enclosing, the bindings of the behavior body the // calc is declared in, its body reads: all of it for a body written inside that // behavior, none for a body inherited from a calc declared elsewhere. -func (shape *calcShape) bodyEnclosing(enclosing []frame) []frame { +func (shape *loadedCalc) bodyEnclosing(enclosing []frame) []frame { if len(enclosing) == 0 || !declaredWithin(shape.BodyOwner, enclosingBehavior(shape.Sym)) { return nil } @@ -953,7 +788,7 @@ func runOf(ctx *Context, frames []frame, behavior *symbols.Symbol) []frame { // closesOverBody reports the calc reading the bindings of the behavior body it is // declared in: through a body written there, or a default it declares there. -func (shape *calcShape) closesOverBody() bool { +func (shape *loadedCalc) closesOverBody() bool { if !enclosedByBehaviorBody(shape.Sym) { return false } @@ -962,7 +797,7 @@ func (shape *calcShape) closesOverBody() bool { return true } for i := range shape.Params { - if param := &shape.Params[i]; param.Default != nil && declaredWithin(param.Owner, behavior) { + if param := &shape.Params[i]; param.Default != 0 && declaredWithin(param.Owner, behavior) { return true } } @@ -1090,10 +925,10 @@ func (ctx *Context) runCalcUsage(start *calcUsageStart) (*calcRun, error) { // The returned value is the result parameter's, read under its name or as // `result`; every other output states its own value, never the returned one. if returned { - if out := shape.resultOutput(); out != nil && out.Name != "" { + if out := shape.ResultOutput(); out != nil && out.Name != "" { run.outputs[out.Name] = result } else if _, anonymous := shape.anonymousResult(); anonymous { - run.outputs[resultOutputName] = result + run.outputs[program.ResultOutputName] = result } } kept = true @@ -1117,7 +952,7 @@ func (run *calcRun) endOccurrence(ctx *Context) { // its binding the first time it is read. func (run *calcRun) output(ctx *Context, name string) (Value, error) { if value, ok := run.outputs[name]; ok { - if out, found := run.shape.output(name); found && (out.Value == nil || out.IsInitial) { + if out, found := run.shape.output(name); found && (out.Value == 0 || out.IsInitial) { if _, held := run.env.lookup(out.Name); held { return run.readBodyOutput(ctx, out.Name) } @@ -1160,7 +995,7 @@ func (run *calcRun) inheritedResult(ctx *Context, name string) (Value, bool, err if !found || !resultParameter(sym) { return Value{}, false, nil } - if out := run.shape.resultOutput(); out != nil && out.Name != "" && out.Name != name { + if out := run.shape.ResultOutput(); out != nil && out.Name != "" && out.Name != name { value, err := run.output(ctx, out.Name) return value, true, err } @@ -1187,7 +1022,7 @@ func (run *calcRun) readBodyOutput(ctx *Context, name string) (Value, error) { // declares it, with the calc's parameters and locals in scope, and memoizes it // for this evaluation. An output that names another output has it evaluated on // demand; one that names itself, directly or through others, is a cycle. -func (run *calcRun) value(ctx *Context, out calcOutput) (Value, error) { +func (run *calcRun) value(ctx *Context, out declaredOutput) (Value, error) { if value, ok := run.outputs[out.Name]; ok && out.Name != "" { return value, nil } @@ -1197,7 +1032,7 @@ func (run *calcRun) value(ctx *Context, out calcOutput) (Value, error) { return Value{}, &ToolError{Tool: run.shape.Tool.tool, Kind: ToolMissingOutput, Detail: fmt.Sprintf("%s (%s of %s) was not answered", out.Name, run.outputDescription(out), run.shape.Label)} } - if out.Value == nil || out.IsInitial { + if out.Value == 0 || out.IsInitial { // An output the body assigned, and an `inout` the invocation bound, are values // the activation left behind rather than bindings to evaluate. if _, ok := run.env.lookup(out.Name); ok && out.Name != "" { @@ -1209,7 +1044,7 @@ func (run *calcRun) value(ctx *Context, out calcOutput) (Value, error) { return value, nil } } - if out.Value == nil { + if out.Value == 0 { return Value{}, &unassignedOutputError{Output: run.outputDescription(out), Calc: run.shape.Label} } if run.computing[out.Name] { @@ -1229,7 +1064,7 @@ func (run *calcRun) value(ctx *Context, out calcOutput) (Value, error) { } defer leave() - value, err := run.bindingEnv(ctx, out.Owner).Eval(out.Value) + value, err := run.bindingEnv(ctx, out.Owner).Eval(run.shape.Expr(out.Value)) if err != nil { return Value{}, calcFrame(run.shape.Kind, run.shape.Name, fmt.Errorf("output %s: %w", run.outputDescription(out), err)) } @@ -1288,7 +1123,7 @@ func (run *calcRun) bindingEnv(ctx *Context, owner *symbols.Symbol) *EvalContext // outputDescription names an output for a diagnostic, saying what an anonymous // result parameter is rather than naming it with an empty string. -func (run *calcRun) outputDescription(out calcOutput) string { +func (run *calcRun) outputDescription(out declaredOutput) string { if out.Name != "" { return out.Name } @@ -1310,22 +1145,6 @@ func (run *calcRun) lookupOutput(ctx *Context, name string) (Value, bool, error) return value, true, err } -// isCalcUsageSymbol reports whether sym declares a calc usage or a case usage — -// analysis or verification — the forms that carry an evaluation whose outputs are -// features. A definition is a type: it is invoked, not read. -func isCalcUsageSymbol(sym *symbols.Symbol) bool { - if sym == nil { - return false - } - if sym.Decl != nil { - usage, ok := sym.Decl.(*ast.Usage) - return ok && (usage.Kind == ast.UsageCalc || usage.Kind == ast.UsageAnalysisCase || - usage.Kind == ast.UsageVerificationCase) - } - return sym.Kind == symbols.SymbolCalcUsage || sym.Kind == symbols.SymbolAnalysisCaseUsage || - sym.Kind == symbols.SymbolVerificationCaseUsage -} - // evalCalcUsageMembers reads a name written after a calc usage: its first part // is an output feature of the usage, evaluated from one run of the usage's body, // or a usage the body declares, whose own members the parts after it read; any @@ -1359,7 +1178,7 @@ func (ec *EvalContext) evalCalcUsageMembers(sym *symbols.Symbol, parts []ast.Nam if len(parts) == 0 { return Value{}, fmt.Errorf( "%w: %s computes output features (%s); read one of them", - ErrNoValue, run.shape.Label, run.shape.outputNames(), + ErrNoValue, run.shape.Label, run.shape.OutputNames(), ) } } @@ -1388,7 +1207,7 @@ func (run *calcRun) memberValue(ctx *Context, name string) (Value, bool, error) if _, isOutput := run.shape.output(name); isOutput { return Value{}, false, nil } - param := run.shape.parameter(name) + param := run.shape.Parameter(name) if param == nil && !run.shape.declaresUsage(name) { if value, ok := run.lookupBody(name); ok { return value, true, nil @@ -1426,7 +1245,7 @@ func (start *calcUsageStart) openInput() string { } for i := range shape.Params { param := &shape.Params[i] - if start.unsupplied[param.Name] && shape.opensRun(param, start.ec.ctx.model.semantics) { + if start.unsupplied[param.Name] && shape.opensRun(i, start.ec.ctx.model.semantics) { return param.Name } } @@ -1435,24 +1254,24 @@ func (start *calcUsageStart) openInput() string { // opensRun reports whether leaving this parameter unsupplied stops the usage computing: a // case still runs without a subject, an actor or an input whose range admits none. -func (shape *calcShape) opensRun(param *calcParameter, m *semantics.Model) bool { - if shape.isCase() && (param.IsSubject || param.IsActor || param.optional(m)) { +func (shape *loadedCalc) opensRun(i int, m *semantics.Model) bool { + if param := &shape.Params[i]; shape.IsCase() && (param.IsSubject || param.IsActor || optionalParameter(param, m)) { return false } - return !param.declaredOptional() + return !shape.declaredOptional(i) } // readsOpen reports a calc or case usage read as a feature, whose unsupplied inputs // are left open: one passed no arguments (an invocation binds its parameters, KerML // §7.4.9.4) and not written in a behavior body (nested reads it), which binds its // inputs from that body's evaluation. -func (shape *calcShape) readsOpen(nested *EvalContext, args calcArgs) bool { +func (shape *loadedCalc) readsOpen(nested *EvalContext, args calcArgs) bool { sym := shape.Sym - if !isCalcUsageSymbol(sym) || nested != nil || len(args.positional) > 0 || len(args.named) > 0 { + if !lower.IsCalcUsageSymbol(sym) || nested != nil || len(args.positional) > 0 || len(args.named) > 0 { return false } owner := sym.Owner() - return owner == nil || !(isBehaviorDefKind(owner.Kind) || isCalcUsageSymbol(owner)) + return owner == nil || !(isBehaviorDefKind(owner.Kind) || lower.IsCalcUsageSymbol(owner)) } // openValue is the undetermined value an output or body local of an open run reads: @@ -1464,7 +1283,7 @@ func (run *calcRun) openValue(name string, count semantics.Range, feature *symbo // unsuppliedParameters names the parameters args and the declared defaults leave // unbound; the subject binds through its own rule and is never among them. -func (shape *calcShape) unsuppliedParameters(args calcArgs) map[string]bool { +func (shape *loadedCalc) unsuppliedParameters(args calcArgs) map[string]bool { var out map[string]bool positional := 0 for i := range shape.Params { @@ -1475,7 +1294,7 @@ func (shape *calcShape) unsuppliedParameters(args calcArgs) map[string]bool { supplied = positional < len(args.positional) positional++ } - if param.Default != nil || param.IsSubject || supplied { + if param.Default != 0 || param.IsSubject || supplied { continue } if _, named := args.named[param.Name]; named { @@ -1510,7 +1329,7 @@ func (ctx *Context) unboundSubjectRead(sym *symbols.Symbol, name string, err err if shapeErr != nil || unbound.Element != shape.Name { return Value{}, false } - param, ok := shape.subjectParameter() + param, ok := shape.SubjectParameter() if !ok || param.Name != name { return Value{}, false } @@ -1571,7 +1390,7 @@ func (run *calcRun) nestedUsage(ctx *Context, name string) (*calcRun, bool, erro } scope := ctx.calcScope(run.shape.BodyOwner, run.shape.Sym, run.scope) sym, ok := ctx.lookupName(scope, name) - if !ok || !isCalcUsageSymbol(sym) { + if !ok || !lower.IsCalcUsageSymbol(sym) { return nil, false, nil } nested, err := ctx.calcUsageRun(run.bindingEnv(ctx, run.shape.BodyOwner), sym) @@ -1600,7 +1419,7 @@ func (ec *EvalContext) calcUsageOperand(operand ast.Node) (*symbols.Symbol, bool } } sym, ok := ec.ctx.resolveQualified(ec.scope, ref.Name) - if !ok || !isCalcUsageSymbol(sym) { + if !ok || !lower.IsCalcUsageSymbol(sym) { return nil, false, nil } return sym, true, nil @@ -1684,8 +1503,8 @@ func (ec *EvalContext) calcUsageMemberValue(sym *symbols.Symbol, self *Instance, // about reading the usage itself rather than one of its outputs. func (ctx *Context) calcUsageOutputSummary(sym *symbols.Symbol) string { names := make([]string, 0) - var aliases map[string]string - for _, out := range ctx.calcOutputs(ctx.calcChain(sym), &aliases) { + calc, _ := lower.ToCalc(sym, ctx.calcEnv()) + for _, out := range calc.Outputs { if out.Name != "" { names = append(names, out.Name) } diff --git a/internal/exec/runtime/carry_test.go b/internal/exec/runtime/carry_test.go index 8f6e538309..58e4c60d6f 100644 --- a/internal/exec/runtime/carry_test.go +++ b/internal/exec/runtime/carry_test.go @@ -4,6 +4,7 @@ import ( "errors" "testing" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" ) @@ -135,7 +136,7 @@ func TestCarryRefusesWhatNoOtherContextHolds(t *testing.T) { sym := &symbols.Symbol{Name: "inBody"} fn := Value{Kind: ValFunction, ref: &functionValue{ - shape: &calcShape{Sym: sym, Name: "inBody"}, + shape: &loadedCalc{Calc: &program.Calc{Sym: sym, Name: "inBody"}}, enclosing: []frame{{vars: map[string]Value{"k": integerValue(1)}, run: 1}}, }} var notPortable *notPortableError diff --git a/internal/exec/runtime/case_step.go b/internal/exec/runtime/case_step.go index 59d621e830..7b71929b39 100644 --- a/internal/exec/runtime/case_step.go +++ b/internal/exec/runtime/case_step.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -33,7 +34,7 @@ func stepSymbol(flow *lower.ActionGraph, node ast.Node) (*symbols.Symbol, *symbo // caseStepSymbol resolves the case a nested usage node declares, in the body that declares it. func caseStepSymbol(flow *lower.ActionGraph, node ast.Node) (*symbols.Symbol, *symbols.Scope, error) { sym, declaring := stepSymbol(flow, node) - if sym == nil || !isCalcUsageSymbol(sym) { + if sym == nil || !lower.IsCalcUsageSymbol(sym) { return nil, nil, fmt.Errorf("%w: %s is not resolved to a case", errNotACalcUsage, nodeDescription(node)) } return sym, declaring, nil @@ -101,10 +102,10 @@ func (e *performances) performCase(perf *actionFrame) error { perf.features[out.Name] = ast.DirOut e.ctx.writeBodyValue(perf.cells, perf.data, perf.key(out.Name), out.Value) } - if out := run.shape.resultOutput(); out != nil { + if out := run.shape.ResultOutput(); out != nil { perf.result = out.Name if perf.result == "" { - perf.result = resultOutputName + perf.result = program.ResultOutputName } } return nil diff --git a/internal/exec/runtime/chain_declared.go b/internal/exec/runtime/chain_declared.go index 8e9bb6f99f..6d7780b33a 100644 --- a/internal/exec/runtime/chain_declared.go +++ b/internal/exec/runtime/chain_declared.go @@ -5,6 +5,7 @@ import ( "slices" "strings" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -82,7 +83,7 @@ func (ec *EvalContext) readsDeclaredMembers(sym *symbols.Symbol) bool { if sym.OwnerScope == nil || !namespaceScope(sym.OwnerScope) { return false } - if _, usage := sym.Decl.(*ast.Usage); !usage || declaresType(sym) || isCalcUsageSymbol(sym) { + if _, usage := sym.Decl.(*ast.Usage); !usage || declaresType(sym) || lower.IsCalcUsageSymbol(sym) { return false } if ec.ctx.extractDefaultValue(sym) != nil || ec.ctx.model.semantics.IsVariationFeature(sym) { diff --git a/internal/exec/runtime/compile.go b/internal/exec/runtime/compile.go index abed1046d9..efcf46e096 100644 --- a/internal/exec/runtime/compile.go +++ b/internal/exec/runtime/compile.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -197,7 +198,7 @@ func (c *scalarCheck) refuse(ctx *Context, v scalar, what func() string) error { // compiledCalcOf answers the compiled body of shape, compiling it and every // calc it invokes on first ask, or nil when the shape is ineligible. -func (ctx *Context) compiledCalcOf(shape *calcShape) *compiledCalc { +func (ctx *Context) compiledCalcOf(shape *loadedCalc) *compiledCalc { switch shape.compileState { case compileEligible: return shape.compiled @@ -218,12 +219,15 @@ func (ctx *Context) compiledCalcOf(shape *calcShape) *compiledCalc { // trust, so the batch settles every call once each shape has an answer. type compileBatch struct { ctx *Context - // callees records the shapes each member compiled a call to. - callees map[*calcShape][]*calcShape + // callees records the calcs each member compiled a call to; held is the + // runtime's record of every calc the batch decided or called. + callees map[*program.Calc][]*program.Calc + held map[*program.Calc]*loadedCalc } // compile decides shape, compiling its callees first. -func (b *compileBatch) compile(shape *calcShape) { +func (b *compileBatch) compile(shape *loadedCalc) { + b.hold(shape) // A tool-computed calc has no body to compile; callers settle to the evaluator. if shape.Tool != nil { shape.withdraw("computed by tool " + shape.Tool.tool) @@ -240,18 +244,28 @@ func (b *compileBatch) compile(shape *calcShape) { } // withdraw keeps shape on the evaluator for good, for the reason given. -func (shape *calcShape) withdraw(why string) { +func (shape *loadedCalc) withdraw(why string) { shape.compileState = compileIneligible shape.compiled = nil shape.ineligibleWhy = why } +// hold records the runtime's record of a calc the batch decides or calls. +func (b *compileBatch) hold(shape *loadedCalc) { + if b.held == nil { + b.held = map[*program.Calc]*loadedCalc{} + } + b.held[shape.Calc] = shape +} + // call records that member compiled a call to callee. -func (b *compileBatch) call(member, callee *calcShape) { +func (b *compileBatch) call(member, callee *loadedCalc) { if b.callees == nil { - b.callees = map[*calcShape][]*calcShape{} + b.callees = map[*program.Calc][]*program.Calc{} } - b.callees[member] = append(b.callees[member], callee) + b.hold(member) + b.hold(callee) + b.callees[member.Calc] = append(b.callees[member.Calc], callee.Calc) } // settle withdraws eligibility from every member calling an ineligible shape, @@ -259,11 +273,13 @@ func (b *compileBatch) call(member, callee *calcShape) { func (b *compileBatch) settle() { for changed := true; changed; { changed = false - for member, callees := range b.callees { + for calc, callees := range b.callees { + member := b.held[calc] if member.compileState != compileEligible { continue } - for _, callee := range callees { + for _, called := range callees { + callee := b.held[called] if callee.compileState != compileEligible { member.withdraw("callee " + callee.Name + " is ineligible") changed = true @@ -286,7 +302,7 @@ func ineligible(why string) error { return errors.New(why) } type calcCompiler struct { batch *compileBatch ctx *Context - shape *calcShape + shape *loadedCalc cell *compiledCalc } @@ -312,20 +328,20 @@ func (c *calcCompiler) compile(cell *compiledCalc) error { if param.IsActor { return ineligible(fmt.Sprintf("parameter %q is an actor, bound by name only", param.Name)) } - if param.Decl.statesCollection() { + if shape.paramDecls[i].statesCollection() { return ineligible(fmt.Sprintf("parameter %q declares a multiplicity above one", param.Name)) } - check, ok := c.scalarCheckFor(¶m.Decl) + check, ok := c.scalarCheckFor(&shape.paramDecls[i]) if !ok { return ineligible(fmt.Sprintf("parameter %q declares a type outside the scalar lattice", param.Name)) } cell.params[i] = compiledParam{name: param.Name, check: check} - if param.Default == nil { + if param.Default == 0 { cell.required.add(i) continue } scope := c.ctx.calcScope(param.Owner, shape.Sym, nil) - dflt, err := c.compileNode(param.Default, scope, newFrameLayout(shape.ParamNames, shape.Aliases, i)) + dflt, err := c.compileNode(shape.Expr(param.Default), scope, newFrameLayout(shape.ParamNames, shape.Aliases, i)) if err != nil { return err } @@ -335,11 +351,11 @@ func (c *calcCompiler) compile(cell *compiledCalc) error { cell.params[i].dflt = dflt.expr() } var result *scalarCheck - if out := shape.resultOutput(); out != nil { - if out.Decl.statesCollection() { + if at := shape.ResultOutputIndex(); at >= 0 { + if shape.outputDecls[at].statesCollection() { return ineligible("result declares a multiplicity above one") } - check, ok := c.scalarCheckFor(&out.Decl) + check, ok := c.scalarCheckFor(&shape.outputDecls[at]) if !ok { return ineligible("result declares a type outside the scalar lattice") } @@ -364,14 +380,14 @@ func (c *calcCompiler) compile(cell *compiledCalc) error { // the result parameter, whose errors the evaluator words as an output feature's. func (c *calcCompiler) compileResultBinding(cell *compiledCalc, layout *frameLayout, result *scalarCheck) error { shape := c.shape - if shape.ResultExpr == nil { + if shape.resultExpr() == nil { return ineligible("body without a result expression") } scope := c.ctx.calcScope(shape.Sym, shape.Sym, nil) if err := plainScope(scope); err != nil { return err } - body, err := c.compileNode(shape.ResultExpr, scope, layout) + body, err := c.compileNode(shape.resultExpr(), scope, layout) if err != nil { return err } @@ -563,7 +579,7 @@ func (c *calcCompiler) compileQualifiedName(qn *ast.QualifiedName, scope *symbol return nil, ineligible(fmt.Sprintf("qualified name %s does not resolve", qualifiedNameToString(qn))) } for _, part := range qn.Parts[1:] { - if isCalcUsageSymbol(sym) { + if lower.IsCalcUsageSymbol(sym) { return nil, ineligible(fmt.Sprintf("qualified name %s reads a calc usage", qualifiedNameToString(qn))) } next, found := c.ctx.model.semantics.LookupMember(sym, part.Text) @@ -780,7 +796,7 @@ func (c *calcCompiler) compileInvocation(n *ast.InvocationExpr, scope *symbols.S // compileCalcCall compiles a call of another eligible calc, the arguments // placed in its slots and its arity checked here rather than per call. -func (c *calcCompiler) compileCalcCall(callee *calcShape, args *callArguments, scope *symbols.Scope, layout *frameLayout) (*cnode, error) { +func (c *calcCompiler) compileCalcCall(callee *loadedCalc, args *callArguments, scope *symbols.Scope, layout *frameLayout) (*cnode, error) { if callee.compileState == compileUndecided { c.batch.compile(callee) } @@ -796,7 +812,7 @@ func (c *calcCompiler) compileCalcCall(callee *calcShape, args *callArguments, s return nil, ineligible(fmt.Sprintf("%s: %v", callee.Name, err)) } for i := range callee.Params { - if !bound.has(i) && callee.Params[i].Default == nil { + if !bound.has(i) && callee.Params[i].Default == 0 { return nil, ineligible(fmt.Sprintf("%s called without an argument for %q", callee.Name, callee.Params[i].Name)) } } diff --git a/internal/exec/runtime/compile_differential_test.go b/internal/exec/runtime/compile_differential_test.go index 2d54038ec3..f72aab0ba5 100644 --- a/internal/exec/runtime/compile_differential_test.go +++ b/internal/exec/runtime/compile_differential_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -184,7 +185,7 @@ func calcSymbolsUnder(scope *symbols.Scope) []*symbols.Symbol { var walk func(*symbols.Scope) walk = func(s *symbols.Scope) { for _, name := range s.MemberNames() { - if sym, ok := s.LookupLocal(name); ok && sym != nil && isCalcDecl(sym.Decl) { + if sym, ok := s.LookupLocal(name); ok && sym != nil && lower.IsCalcDecl(sym.Decl) { calcs = append(calcs, sym) } } diff --git a/internal/exec/runtime/condition.go b/internal/exec/runtime/condition.go index 1810ecc567..16abaa01ce 100644 --- a/internal/exec/runtime/condition.go +++ b/internal/exec/runtime/condition.go @@ -781,7 +781,7 @@ func (c conditionCheck) vacuous(why string) (conditionVerdict, error) { // noResultValue is the error for a constraint whose result nothing binds (KerML 1.1 // §7.4.8.2): no value, not a verdict, with the unbound-return hint or else cause. func (check conditionCheck) noResultValue(cause string) error { - if hint := unboundResultHint([]*symbols.Symbol{check.sym}); hint != "" { + if hint := lower.UnboundResultHint([]*symbols.Symbol{check.sym}); hint != "" { return fmt.Errorf("%s %s: %w%s", check.kind, check.name(), ErrNoValue, hint) } return fmt.Errorf("%s %s: %w: %s", check.kind, check.name(), ErrNoValue, cause) diff --git a/internal/exec/runtime/conformance_test.go b/internal/exec/runtime/conformance_test.go index 1b37c66d38..e98e0dace8 100644 --- a/internal/exec/runtime/conformance_test.go +++ b/internal/exec/runtime/conformance_test.go @@ -14,6 +14,7 @@ import ( "strings" "testing" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -1567,11 +1568,11 @@ func checkCaseRun(t *testing.T, ctx *Context, idx *symbols.Index, result Analysi validateValue(t, ctx, name, expectedVal, actual) } if expected.Result != nil { - actual, ok := values[resultOutputName] + actual, ok := values[program.ResultOutputName] if !ok { t.Errorf("the case returned no result; its outputs are %v", outputNames(result.Outputs)) } else { - validateValue(t, ctx, resultOutputName, *expected.Result, actual) + validateValue(t, ctx, program.ResultOutputName, *expected.Result, actual) } } diff --git a/internal/exec/runtime/connector.go b/internal/exec/runtime/connector.go index 4f6c64e3d0..32bed8e627 100644 --- a/internal/exec/runtime/connector.go +++ b/internal/exec/runtime/connector.go @@ -6,6 +6,7 @@ import ( "slices" "strings" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -727,7 +728,7 @@ func (ctx *Context) anonymousConnectors(typeSym *symbols.Symbol) []*symbols.Symb if decl != typeSym && ctx.libraryDeclared(decl) { continue } - for _, member := range unwrappedDeclMembers(decl.Decl) { + for _, member := range lower.UnwrappedDeclMembers(decl.Decl) { usage, ok := member.(*ast.Usage) if !ok || usage.Ident.Name != "" || usage.Ident.ShortName != "" { continue diff --git a/internal/exec/runtime/context.go b/internal/exec/runtime/context.go index 62bef1f43e..03352de4ef 100644 --- a/internal/exec/runtime/context.go +++ b/internal/exec/runtime/context.go @@ -78,7 +78,7 @@ type Context struct { // calcUsageRunning holds the calc usages whose bodies are running, so a body // reading its own usage is a recursion rather than a nested evaluation. - calcUsageRunning map[calcUsageKey]*calcShape + calcUsageRunning map[calcUsageKey]*loadedCalc // activations numbers the body activations begun in this context: a calc // invocation, a block entry, a loop iteration, a body application. @@ -442,7 +442,7 @@ func NewContext(model *Model, maxSteps int64) *Context { run: &runState{ calcUsageRuns: make(map[int64]map[calcUsageKey]*calcRun), }, - calcUsageRunning: make(map[calcUsageKey]*calcShape), + calcUsageRunning: make(map[calcUsageKey]*loadedCalc), maxActionSteps: DefaultMaxActionSteps, maxStateEvents: DefaultMaxStateEvents, @@ -1647,11 +1647,11 @@ func (ctx *Context) chainMembers(sym *symbols.Symbol, scope *symbols.Scope) []sc if link == nil || ctx.frameDeclared(link) { continue } - for _, node := range unwrappedDeclMembers(link.Decl) { + for _, node := range lower.UnwrappedDeclMembers(link.Decl) { out = append(out, scopedMember{node: node, scope: bodyScope(link, link.OwnerScope)}) } } - for _, node := range unwrappedDeclMembers(sym.Decl) { + for _, node := range lower.UnwrappedDeclMembers(sym.Decl) { out = append(out, scopedMember{node: node, scope: bodyScope(sym, scope)}) } return out diff --git a/internal/exec/runtime/eval.go b/internal/exec/runtime/eval.go index 48ce59fad3..152cfc6c16 100644 --- a/internal/exec/runtime/eval.go +++ b/internal/exec/runtime/eval.go @@ -484,7 +484,7 @@ func (ctx *Context) EvalDeclaredValue(sym *symbols.Symbol) (Value, error) { return val, err } // A calc usage returning one unnamed result is read as that result. - if isCalcUsageSymbol(sym) && ctx.returnsResult(sym) { + if lower.IsCalcUsageSymbol(sym) && ctx.returnsResult(sym) { return newEvalContext(ctx, sym.OwnerScope).evalCalcUsageMembers(sym, resultSegments) } // A namespace-level object usage reads as the objects it denotes for the run. @@ -875,7 +875,7 @@ func (ec *EvalContext) evalNameGeneral(qn *ast.QualifiedName, unset *symbols.Sym return val, err } // A calc usage returning one unnamed result is read as that result. - if isCalcUsageSymbol(sym) && ec.ctx.returnsResult(sym) { + if lower.IsCalcUsageSymbol(sym) && ec.ctx.returnsResult(sym) { return ec.evalCalcUsageMembers(sym, resultSegments) } // A feature declared with no value, whose multiplicity admits none, @@ -938,7 +938,7 @@ func (ec *EvalContext) evalNameGeneral(qn *ast.QualifiedName, unset *symbols.Sym if !resolved { break } - if isCalcUsageSymbol(part) { + if lower.IsCalcUsageSymbol(part) { return ec.evalCalcUsageMembers(part, qn.Parts[i+1:]) } } @@ -1007,11 +1007,11 @@ func (ec *EvalContext) evalNameGeneral(qn *ast.QualifiedName, unset *symbols.Sym return val, err } // A calc usage returning one unnamed result is read as that result. - if isCalcUsageSymbol(currentSym) && ec.ctx.returnsResult(currentSym) { + if lower.IsCalcUsageSymbol(currentSym) && ec.ctx.returnsResult(currentSym) { return ec.evalCalcUsageMembers(currentSym, resultSegments) } // A calc usage or a KerML type is never the empty sequence, whatever it admits. - if isCalcUsageSymbol(currentSym) || declaresType(currentSym) { + if lower.IsCalcUsageSymbol(currentSym) || declaresType(currentSym) { return ec.resolvedWithoutValue(currentSym, qn) } // A valueless feature admitting nothing is the empty sequence, however spelled. @@ -1141,7 +1141,7 @@ func (ec *EvalContext) withoutValue(sym *symbols.Symbol, spelled string, qn *ast // A calc usage is an evaluation, not a value: it is read through the output // features it computes, since a name it does not designate a result for has // no one value. - if isCalcUsageSymbol(sym) { + if lower.IsCalcUsageSymbol(sym) { return Value{}, fmt.Errorf( "%w: calc usage %s computes output features (%s); read one of them", ErrNoValue, spelled, ec.ctx.calcUsageOutputSummary(sym), @@ -1998,7 +1998,7 @@ func (ec *EvalContext) chainThroughStructured(value Value, parts []ast.NameSegme func (ec *EvalContext) chainUnheldMember(inst *Instance, name string, rest []ast.NameSegment, from string) (Value, error) { // A calc usage is an evaluation rather than a feature value, so its outputs are // read from a run of it against this object. - if sym, found := ec.ctx.model.semantics.LookupMember(inst.Type, name); found && isCalcUsageSymbol(sym) { + if sym, found := ec.ctx.model.semantics.LookupMember(inst.Type, name); found && lower.IsCalcUsageSymbol(sym) { return ec.calcUsageMemberValue(sym, inst, rest) } if ec.ctx.derivesKernelCollection(inst, name) { @@ -3450,7 +3450,7 @@ type invocationTarget struct { builtin builtinFunc // the built-in the name denotes: the library declaration calc is builtinName string // the built-in's registered name, keying its declared signature library *libraryFunction // the library function the name denotes: the library declaration calc is - shape *calcShape // calc's invocation interface, nil when it has none + shape *loadedCalc // calc's invocation interface, nil when it has none predicate bool // calc is a constraint or requirement, applied as a predicate names []string // the parameter each named argument binds, as calc's signature spells it unbound []error // per named argument, why calc has no parameter for it; nil when it binds @@ -3585,7 +3585,7 @@ func (ctx *Context) implementInvocation(target *invocationTarget, sym *symbols.S target.library = fn } else if shape, err := ctx.calcShapeOf(sym); err == nil { target.shape = shape - } else if isPredicateDecl(sym.Decl) { + } else if lower.IsPredicateDecl(sym.Decl) { target.predicate = true } } @@ -3714,7 +3714,7 @@ func (ec *EvalContext) applyInvocation(target *invocationTarget, callArgs calcAr // enclosingRun is the environment a nested calc closes over here: the frames through // the innermost run of the behavior it is declared in, none when no such run is active. -func (ec *EvalContext) enclosingRun(shape *calcShape) []frame { +func (ec *EvalContext) enclosingRun(shape *loadedCalc) []frame { return runOf(ec.ctx, ec.frames, enclosingBehavior(shape.Sym)) } @@ -3762,7 +3762,7 @@ func (ec *EvalContext) invokeChainOn(n *ast.InvocationExpr, name, callee string, if id, isObject := receiver.Object(); isObject { if inst, ok := ec.ctx.instances[id]; ok { sym, found := ec.ctx.model.semantics.LookupMember(inst.Type, name) - if _, held := inst.FeatureValues[name]; !held && found && isCalcUsageSymbol(sym) { + if _, held := inst.FeatureValues[name]; !held && found && lower.IsCalcUsageSymbol(sym) { fn, err := NewEvalContextIn(ec.ctx, sym.OwnerScope, inst).functionValueOf(sym) if err != nil { return Value{}, err @@ -3771,7 +3771,7 @@ func (ec *EvalContext) invokeChainOn(n *ast.InvocationExpr, name, callee string, } // A constraint or requirement member is a predicate about the object, invoked on it // (`req.assumptions()`, `sys.req()`), as its own evaluation invokes the bare name. - if found && isPredicateDecl(sym.Decl) { + if found && lower.IsPredicateDecl(sym.Decl) { return ec.invokeChainPredicate(n, callee, &chainPredicate{self: inst, sym: sym}) } } @@ -4116,7 +4116,7 @@ func (target *invocationTarget) namedBinding(qualName string, i int, named map[s // invokeCalcShapeStacked evaluates exprs onto the context's argument stack and // invokes shape with them, popping them however the invocation ends. -func (ec *EvalContext) invokeCalcShapeStacked(shape *calcShape, exprs []ast.Node, enclosing []frame) (Value, error) { +func (ec *EvalContext) invokeCalcShapeStacked(shape *loadedCalc, exprs []ast.Node, enclosing []frame) (Value, error) { ctx := ec.ctx base := len(ctx.argStack) for _, arg := range exprs { diff --git a/internal/exec/runtime/evaluation_log.go b/internal/exec/runtime/evaluation_log.go index b9428fc288..f3f96bfd08 100644 --- a/internal/exec/runtime/evaluation_log.go +++ b/internal/exec/runtime/evaluation_log.go @@ -45,8 +45,8 @@ func (log *evaluationLog) record(ctx *Context, fn *functionValue, args calcArgs, } arguments := fn.shape.argumentsByPosition(args) for i := range arguments { - if i < len(fn.shape.Params) && fn.shape.Params[i].Decl.Target != nil { - if ctx.holdAsReal(&arguments[i], fn.shape.Params[i].Decl.Target.typ) != nil { + if i < len(fn.shape.Params) && fn.shape.paramDecls[i].Target != nil { + if ctx.holdAsReal(&arguments[i], fn.shape.paramDecls[i].Target.typ) != nil { arguments[i] = fn.shape.argumentsByPosition(args)[i] } } @@ -71,9 +71,9 @@ func (log *evaluationLog) pick(element Value) { // argumentsByPosition lists an application's arguments by parameter position, // null where the application binds none before a later one; unknown names follow, sorted. -func (shape *calcShape) argumentsByPosition(args calcArgs) []Value { +func (shape *loadedCalc) argumentsByPosition(args calcArgs) []Value { arguments := make([]Value, 0, len(shape.ParamNames)) - indexes := shape.positionalIndexes() + indexes := shape.PositionalIndexes() for k, value := range args.positional { position := len(arguments) if k < len(indexes) { diff --git a/internal/exec/runtime/feature_identity.go b/internal/exec/runtime/feature_identity.go index f7fcf3d0c6..432740b2fb 100644 --- a/internal/exec/runtime/feature_identity.go +++ b/internal/exec/runtime/feature_identity.go @@ -2,6 +2,7 @@ package runtime import ( "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -135,7 +136,7 @@ func (ec *EvalContext) lookupNamed(name string, unset *symbols.Symbol) (Value, b // holdsAnother reports whether f binds name as a feature other than sym. func (f *frame) holdsAnother(name string, sym *symbols.Symbol) bool { - name = canonical(f.aliases, name) + name = program.Canonical(f.aliases, name) for held, bound := range f.held { if bound == name && held != sym { return true diff --git a/internal/exec/runtime/frame.go b/internal/exec/runtime/frame.go index 3ea6d9c7aa..70a1ba2a59 100644 --- a/internal/exec/runtime/frame.go +++ b/internal/exec/runtime/frame.go @@ -4,6 +4,7 @@ import ( "maps" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" ) @@ -32,7 +33,7 @@ type frame struct { perf *actionFrame // owner is the calc whose parameters, locals and outputs the frame binds, so a // qualified name of one of its members (`MassCase::result`) reads the binding. - owner *calcShape + owner *loadedCalc // lexical marks local statement bindings, which are not qualified members. lexical bool // write is the run's own write path for a feature the frame binds — checked @@ -65,40 +66,13 @@ type firing struct { payload map[string]Value } -// canonical is the name aliases bind name under: its redefinition's, else its own. -func canonical(aliases map[string]string, name string) string { - if alias, ok := aliases[name]; ok { - return alias - } - return name -} - -// aliasRedefined records that name now binds the feature redefined was known by. -// Aliases already pointing at redefined follow it to name. -func aliasRedefined(aliases map[string]string, redefined, name string) map[string]string { - if redefined == "" || name == "" || redefined == name { - return aliases - } - if aliases == nil { - aliases = make(map[string]string) - } - for alias, target := range aliases { - if target == redefined { - aliases[alias] = name - } - } - aliases[redefined] = name - delete(aliases, name) - return aliases -} - // mapFrame is a frame holding vars alone. func mapFrame(vars map[string]Value) frame { return frame{vars: vars} } // ownedFrame is a frame holding the values a run of owner bound, by name. -func ownedFrame(owner *calcShape, vars map[string]Value) frame { +func ownedFrame(owner *loadedCalc, vars map[string]Value) frame { return frame{vars: vars, owner: owner} } @@ -146,7 +120,7 @@ func (f frame) withVars(vars map[string]Value) frame { // lookup finds name in the frame: a slot binding it, else the map. func (f *frame) lookup(name string) (Value, bool) { - name = canonical(f.aliases, name) + name = program.Canonical(f.aliases, name) if f.visible != nil && !f.visible[name] { return Value{}, false } @@ -164,7 +138,7 @@ func (f *frame) lookup(name string) (Value, bool) { // read resolves a name and derives its body cell when the stored value is stale. func (f frame) read(ctx *Context, name string) (Value, bool, error) { - name = canonical(f.aliases, name) + name = program.Canonical(f.aliases, name) if f.masked[name] || (f.visible != nil && !f.visible[name]) { return Value{}, false, nil } @@ -230,7 +204,7 @@ func (f *frame) occurrenceValue(name string, valued bool) (Value, bool) { // hides reports whether the frame masks name or keeps it out of sight, so a read // of it passes to the frames outside. func (f *frame) hides(name string) bool { - name = canonical(f.aliases, name) + name = program.Canonical(f.aliases, name) return f.masked[name] || (f.visible != nil && !f.visible[name]) } @@ -254,7 +228,7 @@ func (f *frame) markUnvalued(name string) { // set binds name: in its slot when the frame has one for it, else in the map. func (f frame) set(name string, value Value) { - name = canonical(f.aliases, name) + name = program.Canonical(f.aliases, name) delete(f.unvalued, name) if f.slots != nil && f.slots.set(name, value) { return @@ -264,7 +238,7 @@ func (f frame) set(name string, value Value) { // setBody writes through the frame's dependency cell when one is present. func (f frame) setBody(ctx *Context, name string, value Value) { - name = canonical(f.aliases, name) + name = program.Canonical(f.aliases, name) delete(f.unvalued, name) if f.slots != nil && f.slots.set(name, value) { return @@ -278,7 +252,7 @@ func (f frame) setBody(ctx *Context, name string, value Value) { // bindParam binds parameter i, named name: by position in the slots, else by name. func (f frame) bindParam(i int, name string, value Value) { - name = canonical(f.aliases, name) + name = program.Canonical(f.aliases, name) delete(f.unvalued, name) if f.slots != nil { f.slots.bind(i, value) @@ -291,14 +265,14 @@ func (f frame) bindParam(i int, name string, value Value) { func (f frame) each(fn func(name string, value Value)) { if f.slots != nil { f.slots.each(func(name string, value Value) { - name = canonical(f.aliases, name) + name = program.Canonical(f.aliases, name) if !f.unvalued[name] && (f.visible == nil || f.visible[name]) { fn(name, value) } }) } for name, value := range f.vars { - name = canonical(f.aliases, name) + name = program.Canonical(f.aliases, name) if !f.unvalued[name] && (f.visible == nil || f.visible[name]) { fn(name, value) } @@ -308,14 +282,14 @@ func (f frame) each(fn func(name string, value Value)) { // visibleNames returns the visible names bound by the frame and its cells. func (f frame) visibleNames() map[string]bool { names := make(map[string]bool, len(f.vars)) - f.each(func(name string, _ Value) { names[canonical(f.aliases, name)] = true }) + f.each(func(name string, _ Value) { names[program.Canonical(f.aliases, name)] = true }) if f.cells != nil { for _, name := range f.cells.order { - names[canonical(f.aliases, name)] = true + names[program.Canonical(f.aliases, name)] = true } } for name := range f.unvalued { - name = canonical(f.aliases, name) + name = program.Canonical(f.aliases, name) if f.visible == nil || f.visible[name] { names[name] = true } diff --git a/internal/exec/runtime/function_value.go b/internal/exec/runtime/function_value.go index 772873ab49..59a6b31aa4 100644 --- a/internal/exec/runtime/function_value.go +++ b/internal/exec/runtime/function_value.go @@ -3,6 +3,8 @@ package runtime import ( "fmt" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -11,7 +13,7 @@ import ( // the environment it was read in, which an invocation of the value runs it against. // A library calc the runtime implements natively carries that implementation instead. type functionValue struct { - shape *calcShape + shape *loadedCalc library *libraryFunction scope *symbols.Scope // names the calc's defaults and body resolve against self *Instance // object the calc's feature names resolve against, nil for none @@ -27,9 +29,9 @@ func (fn *functionValue) arity() (low, high int) { if fn.library != nil { return fn.library.leastPositional(), len(fn.library.params) } - indexes := fn.shape.positionalIndexes() + indexes := fn.shape.PositionalIndexes() for k := len(indexes) - 1; k >= 0; k-- { - if fn.shape.Params[indexes[k]].Default == nil { + if fn.shape.Params[indexes[k]].Default == 0 { return k + 1, len(indexes) } } @@ -122,7 +124,7 @@ func (ctx *Context) readsAsFunction(sym *symbols.Symbol) bool { if isCalcDefSymbol(sym) { return true } - if !isCalcUsageSymbol(sym) { + if !lower.IsCalcUsageSymbol(sym) { return false } shape, err := ctx.calcInterfaceOf(sym) @@ -131,10 +133,10 @@ func (ctx *Context) readsAsFunction(sym *symbols.Symbol) bool { // hasUnsuppliedInput reports an input parameter no read of the calc could bind: // neither an argument, a default nor the enclosing case supplies one. -func (shape *calcShape) hasUnsuppliedInput() bool { +func (shape *loadedCalc) hasUnsuppliedInput() bool { for i := range shape.Params { param := &shape.Params[i] - if param.Default == nil && !param.IsSubject && !param.IsActor && !param.declaredOptional() { + if param.Default == 0 && !param.IsSubject && !param.IsActor && !shape.declaredOptional(i) { return true } } @@ -166,7 +168,7 @@ func (ec *EvalContext) calcAsValue(sym *symbols.Symbol) (Value, bool, error) { // environment — a parameter bound by argument, or a feature of the bound object — // which an invocation of callee applies; false when nothing here binds it. func (ec *EvalContext) boundFunction(callee *symbols.Symbol, qn *ast.QualifiedName) (Value, bool, error) { - if qn == nil || len(qn.Parts) == 0 || !isCalcUsageSymbol(callee) { + if qn == nil || len(qn.Parts) == 0 || !lower.IsCalcUsageSymbol(callee) { return Value{}, false, nil } if len(qn.Parts) > 1 { @@ -211,7 +213,7 @@ func (ec *EvalContext) declaredFunction(callee *symbols.Symbol) (Value, bool, er return Value{}, false, nil } named, ok := ec.ctx.model.resolver.ResolveQualified(callee.OwnerScope, ref.Name) - if !ok || named == nil || !isCalcDecl(named.Decl) { + if !ok || named == nil || !lower.IsCalcDecl(named.Decl) { return Value{}, false, nil } val, err := ec.declaredValue(callee, ref) @@ -238,7 +240,7 @@ func (ec *EvalContext) qualifiedBoundFunction(callee *symbols.Symbol, qn *ast.Qu // checkFunction refuses a value bound to a calc usage parameter that is no // function; an omitted optional parameter holds null. -func (param *calcParameter) checkFunction(value *Value, what func() string) error { +func checkFunction(param *program.CalcParam, value *Value, what func() string) error { held := soleElement(*value) if !param.IsCalc || held.Kind == ValFunction || held.Kind == ValNull { return nil diff --git a/internal/exec/runtime/held_image_test.go b/internal/exec/runtime/held_image_test.go index 5ba62ab1bf..bb6a96b88c 100644 --- a/internal/exec/runtime/held_image_test.go +++ b/internal/exec/runtime/held_image_test.go @@ -9,6 +9,7 @@ import ( "sync" "testing" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -1100,7 +1101,7 @@ func TestHeldImageRefusesWhatItCannotCarry(t *testing.T) { // A value closed over the run that made it names what no other context holds. inBody := Value{Kind: ValFunction, ref: &functionValue{ - shape: &calcShape{Sym: &symbols.Symbol{Name: "inBody"}, Name: "inBody"}, + shape: &loadedCalc{Calc: &program.Calc{Sym: &symbols.Symbol{Name: "inBody"}, Name: "inBody"}}, enclosing: []frame{{vars: map[string]Value{"k": integerValue(1)}, run: 1}}, }} src.instances[bulb.ID].FeatureValues["lamp"].Value = inBody @@ -1163,7 +1164,7 @@ func TestHeldImageOfABehaviorlessClosureLeavesTheBusBe(t *testing.T) { t.Fatalf("Instantiate(Plain): %v", err) } inBody := Value{Kind: ValFunction, ref: &functionValue{ - shape: &calcShape{Sym: &symbols.Symbol{Name: "inBody"}, Name: "inBody"}, + shape: &loadedCalc{Calc: &program.Calc{Sym: &symbols.Symbol{Name: "inBody"}, Name: "inBody"}}, enclosing: []frame{{vars: map[string]Value{"k": integerValue(1)}, run: 1}}, }} src.PostMessage(Message{SignalType: "go", Payload: map[string]Value{"k": inBody}}) @@ -1208,7 +1209,7 @@ func TestHeldImageMaterializeFailsWhole(t *testing.T) { } sound := img.messages inBody := Value{Kind: ValFunction, ref: &functionValue{ - shape: &calcShape{Sym: &symbols.Symbol{Name: "inBody"}, Name: "inBody"}, + shape: &loadedCalc{Calc: &program.Calc{Sym: &symbols.Symbol{Name: "inBody"}, Name: "inBody"}}, enclosing: []frame{{vars: map[string]Value{"k": integerValue(1)}, run: 1}}, }} img.messages = append(slices.Clone(sound), Message{Object: bulb.ID, SignalType: "go", Payload: map[string]Value{"k": inBody}}) diff --git a/internal/exec/runtime/holders.go b/internal/exec/runtime/holders.go index 87cf032c53..b079bb5bc4 100644 --- a/internal/exec/runtime/holders.go +++ b/internal/exec/runtime/holders.go @@ -275,7 +275,7 @@ type returnedAnalysis struct { // of: those its return expressions pass on, a `for` variable standing for its collection. // Memoized per shape; a call within a cycle reads the set as far as it is known, and the // cycle's root iterates every shape of it until none grows. -func (ctx *Context) returnedParameters(shape *calcShape) map[string]bool { +func (ctx *Context) returnedParameters(shape *loadedCalc) map[string]bool { a, ok := ctx.model.returnedParams[shape] if !ok { a = &returnedAnalysis{passed: make(map[string]bool), active: -1} @@ -328,7 +328,7 @@ type valueSource struct { // collectReturnedParameters adds to passed the parameters of shape its returns and result // binding pass on, directly or through the locals its body writes them to. -func (ctx *Context) collectReturnedParameters(shape *calcShape, passed, params map[string]bool) { +func (ctx *Context) collectReturnedParameters(shape *loadedCalc, passed, params map[string]bool) { var returns []valueSource locals := make(map[*symbols.Symbol][]valueSource) written := func(in *symbols.Scope, name string, source valueSource) { diff --git a/internal/exec/runtime/instance.go b/internal/exec/runtime/instance.go index 99081fa148..8b775822f1 100644 --- a/internal/exec/runtime/instance.go +++ b/internal/exec/runtime/instance.go @@ -5,6 +5,7 @@ import ( "fmt" "slices" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -1735,7 +1736,7 @@ func isReferenceUsage(sym *symbols.Symbol) bool { // declaresFeatures reports whether a usage's own body restates or adds features, // which the object it materializes has to carry. func declaresFeatures(sym *symbols.Symbol) bool { - for member := range eachUnwrappedDeclMember(sym.Decl) { + for member := range lower.EachUnwrappedDeclMember(sym.Decl) { if memberDeclaresFeature(member) { return true } @@ -1850,7 +1851,7 @@ func (ctx *Context) restatedValueInBody(sym, typ *symbols.Symbol) string { for _, f := range ctx.FeaturesOf(typ) { inherited[f.Name] = true } - for _, member := range unwrappedDeclMembers(sym.Decl) { + for _, member := range lower.UnwrappedDeclMembers(sym.Decl) { usage, ok := member.(*ast.Usage) if !ok || !valuesAFeature(usage) { continue @@ -1923,7 +1924,7 @@ func valuesAFeature(usage *ast.Usage) bool { if usage.Value != nil { return true } - for _, member := range unwrappedDeclMembers(usage) { + for _, member := range lower.UnwrappedDeclMembers(usage) { if nested, ok := member.(*ast.Usage); ok && valuesAFeature(nested) { return true } diff --git a/internal/exec/runtime/invocation_statement_order.go b/internal/exec/runtime/invocation_statement_order.go index fe6a678f88..40436435f5 100644 --- a/internal/exec/runtime/invocation_statement_order.go +++ b/internal/exec/runtime/invocation_statement_order.go @@ -11,7 +11,7 @@ import ( ) type invocationOrderKey struct { - shape *calcShape + shape *loadedCalc self int64 arguments string } @@ -33,7 +33,7 @@ type invocationOrderMemo struct { const resultOrderWherePrefix = "result of " func (ctx *Context) invokeWithStatementOrderResults( - shape *calcShape, + shape *loadedCalc, args calcArgs, self *Instance, memoize bool, @@ -157,7 +157,7 @@ func (ctx *Context) invokeWithStatementOrderResults( return value, err } -func canonicalInvocationArguments(shape *calcShape, args calcArgs) string { +func canonicalInvocationArguments(shape *loadedCalc, args calcArgs) string { var b strings.Builder // Positional arguments fall on the non-actor parameters in declaration order, // as bindCalcParameters binds them. @@ -185,7 +185,7 @@ func canonicalInvocationArguments(shape *calcShape, args calcArgs) string { } b.WriteByte(';') } - if arity := shape.positionalArity(); len(args.positional) > arity { + if arity := shape.PositionalArity(); len(args.positional) > arity { b.WriteString("extra-pos:") for _, value := range args.positional[arity:] { fmt.Fprintf(&b, "%d:%s;", value.Kind, strconv.Quote(FormatValue(value))) @@ -197,7 +197,7 @@ func canonicalInvocationArguments(shape *calcShape, args calcArgs) string { } sort.Strings(names) for _, name := range names { - if !shape.hasParameter(name) { + if !shape.HasParameter(name) { value := args.named[name] fmt.Fprintf(&b, "unknown:%s=%d:%s;", strconv.Quote(name), value.Kind, strconv.Quote(FormatValue(value))) continue @@ -245,7 +245,7 @@ func orderedInvocationResults(results []invocationOrderResult) []invocationOrder } func (ctx *Context) invokeOrderedPredicate( - shape *calcShape, + shape *loadedCalc, args calcArgs, self *Instance, invoke func() (Value, error), diff --git a/internal/exec/runtime/invoke_calc.go b/internal/exec/runtime/invoke_calc.go index 4e6b9813a7..05da15df91 100644 --- a/internal/exec/runtime/invoke_calc.go +++ b/internal/exec/runtime/invoke_calc.go @@ -2,35 +2,17 @@ package runtime import ( "fmt" - "iter" - "slices" "sort" "strings" "sync" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" - "github.com/Open-MBEE/OpenSysML/internal/syntax/source" ) -// calcParameter is one input parameter of a calc, in positional order. -type calcParameter struct { - Name string // parameter name arguments bind to - Default ast.Node // value-binding expression used when no argument is passed (nil if none) - Owner *symbols.Symbol // the calc that declares the default (a supertype for an inherited one) - Sym *symbols.Symbol // the parameter's own symbol, which its effective multiplicity is read off - Decl calcMemberDecl // the declaration, closest to the invoked calc, a bound value answers to - // IsSubject marks a case's subject parameter, the object the case is about. - IsSubject bool - // IsActor marks an actor parameter (SysML v2 §7.19.2, §7.24.2): bound by name - // like the subject, never by position, and left unvalued when nothing binds it. - IsActor bool - // IsCalc marks a calc usage parameter (`in calc f`), which a function value binds. - IsCalc bool -} - // calcMemberDecl is the type and multiplicity a calc's parameter or output // declares, and the calc whose declaration states them. type calcMemberDecl struct { @@ -146,14 +128,6 @@ func (ctx *Context) boundMemberDecl(owner *symbols.Symbol, features []*symbols.S return decl } -// calcMemberDeclOf resolves what a member of link declares for a value bound to it. -func (ctx *Context) calcMemberDeclOf(link *symbols.Symbol, sym *symbols.Symbol, name string) calcMemberDecl { - if sym == nil { - return calcMemberDecl{} - } - return ctx.calcMemberDeclFor(link, sym, name) -} - // calcMemberDeclFor is what the member sym of link declares for a value bound to it. func (ctx *Context) calcMemberDeclFor(link, sym *symbols.Symbol, name string) calcMemberDecl { mult, stated := ctx.extractMultiplicity(sym) @@ -171,46 +145,19 @@ func (ctx *Context) calcMemberDeclFor(link, sym *symbols.Symbol, name string) ca } } -// calcShape is a calc's invocation interface: the input parameters it binds, in -// positional order, the output features it computes, and the expression its body -// returns. All are resolved across the specialization chain, so a usage typed by -// a calc definition inherits that definition's parameters, outputs and result. -type calcShape struct { - Sym *symbols.Symbol - Name string // qualified name, for diagnostics and traces - // Kind is the notation keyword of the behavior (`calc` or `analysis`), and - // Label is Kind followed by Name, as diagnostics name the behavior. - Kind string - Label string - Params []calcParameter - // ParamNames are the Params' names by position: the slots an invocation binds. - ParamNames []string - // Aliases map the name of a parameter or output redeclared under a new name - // (`in g :>> x`) to that name, which an inherited body reads it by. - Aliases map[string]string - // Outputs are the calc's output features — its `out` parameters and the - // result parameter a `return` declares — in declaration order. - Outputs []calcOutput - // Body is the computation the calc states, lowered in the scope of the calc - // that declares it: local declarations, assignments, conditionals, loops and - // returns, in declaration order. - Body []lower.Statement - BodyOwner *symbols.Symbol // the calc whose body declares Body - // Steps is Body without the bindings of its `out` features, which are - // evaluated when those features are read rather than run as statements. - Steps []lower.Statement - statementOrders sync.Map - // Nodes are the action nodes the body's flow performs as the steps of a case. - Nodes []ast.Node - // BodyOutputs are the output features some statement of the body assigns, - // whatever path the execution takes. A calc that binds its outputs this way - // computes them though it returns nothing, so it states a computation. - BodyOutputs map[string]bool - Bindings []lower.Binding - ResultExpr ast.Node +// loadedCalc is the runtime's hold on a lowered calc: its program.Calc, the +// trees its body and expressions still are, and what the runtime builds per calc. +type loadedCalc struct { + *program.Calc + *lower.CalcTrees + // paramDecls and outputDecls are what a value bound to each parameter and + // output answers to, folded along the declarations the record lists for it. + paramDecls []calcMemberDecl + outputDecls []calcMemberDecl // ResultDecl is what the result answers to: its own `return`, else the result // parameter it inherits (SysML v2 §7.19.2), ultimately Evaluation::result [0..*]. - ResultDecl calcMemberDecl + ResultDecl calcMemberDecl + statementOrders sync.Map // Uncomputed says why the calc computes nothing (no body, no bound output); // the calc is still a function value, and invoking it reports this. Uncomputed error @@ -227,10 +174,20 @@ type calcShape struct { members map[*symbols.Symbol]string } +// resultExpr is the expression a binding connector binds to `result`, nil for none. +func (shape *loadedCalc) resultExpr() ast.Node { + return shape.Expr(shape.Result) +} + +// calcEnv is what lowering a calc reads of this context's model. +func (ctx *Context) calcEnv() lower.CalcEnv { + return lower.CalcEnv{Semantics: ctx.model.semantics, Resolver: ctx.model.resolver, Realized: ctx.frameDeclared} +} + // calcShapeOf resolves the invocation interface of a calc symbol: its // positional input parameters (own and inherited, with defaults) and its result // expression. A calc computing nothing is an error. The result is memoized per symbol. -func (ctx *Context) calcShapeOf(sym *symbols.Symbol) (*calcShape, error) { +func (ctx *Context) calcShapeOf(sym *symbols.Symbol) (*loadedCalc, error) { shape, err := ctx.calcInterfaceOf(sym) if err != nil { return nil, err @@ -243,7 +200,7 @@ func (ctx *Context) calcShapeOf(sym *symbols.Symbol) (*calcShape, error) { // calcInterfaceOf is calcShapeOf for a calc that may compute nothing — an // abstract calc, or one awaiting a body — whose shape records why in Uncomputed. -func (ctx *Context) calcInterfaceOf(sym *symbols.Symbol) (*calcShape, error) { +func (ctx *Context) calcInterfaceOf(sym *symbols.Symbol) (*loadedCalc, error) { if sym == nil || sym.Decl == nil { return nil, fmt.Errorf("%w: invalid symbol", ErrNotACalc) } @@ -252,14 +209,10 @@ func (ctx *Context) calcInterfaceOf(sym *symbols.Symbol) (*calcShape, error) { } name := ctx.QualifiedName(sym) - if !isCalcDecl(sym.Decl) && !ctx.calcTypedFeature(sym) { + if !lower.IsCalcDecl(sym.Decl) && !ctx.calcTypedFeature(sym) { return nil, fmt.Errorf("%w: %s is %s, not a calc definition or usage", ErrNotACalc, name, describeDecl(sym.Decl)) } - kind, label := calcKindLabel(sym, name) - - // Most general first, so an inherited parameter keeps the position it has in - // the calc that declares it and a redeclaration refines it in place. - chain := ctx.calcChain(sym) + label := lower.CalcKind(sym) + " " + name if conflict := ctx.model.semantics.ResultExpressionConflict(sym); conflict != nil { if conflict.Stated > 1 { return nil, fmt.Errorf("%w: %s states %d result expressions", @@ -272,44 +225,22 @@ func (ctx *Context) calcInterfaceOf(sym *symbols.Symbol) (*calcShape, error) { return nil, fmt.Errorf("%w: %s states or inherits a result expression from each of %s", errConflictingResultExpressions, label, strings.Join(names, ", ")) } - body, bodyOrder, bodyOwner := ctx.calcBodyWithOrder(chain) - shape := &calcShape{ - Sym: sym, - Name: name, - Kind: kind, - Label: label, - Body: body, - BodyOwner: bodyOwner, - Steps: lower.CalcSteps(body), - } - if len(shape.Steps) > 0 && bodyOrder != nil { - shape.statementOrders.Store(&shape.Steps[0], bodyOrder) - } - indexCalcStatementOrders(&shape.statementOrders, shape.Steps) - shape.Nodes = lower.BlockNodes(shape.Steps) - shape.Params = ctx.calcParameters(chain, &shape.Aliases) - shape.Outputs = ctx.calcOutputs(chain, &shape.Aliases) - shape.ParamNames = make([]string, len(shape.Params)) - for i, param := range shape.Params { - shape.ParamNames[i] = param.Name - } - shape.BodyOutputs = assignedOutputs(shape.Steps, shape.Outputs, shape.Aliases) - shape.Bindings = calcBindings(chain) - shape.ResultExpr = resultBindingExpr(shape.Bindings) - if out := shape.resultOutput(); out != nil { - shape.ResultDecl = out.Decl + calc, trees := lower.ToCalc(sym, ctx.calcEnv()) + shape := ctx.loadCalc(calc, trees) + if at := calc.ResultOutputIndex(); at >= 0 { + shape.ResultDecl = shape.outputDecls[at] } else if inherited := ctx.model.semantics.ResultParameterOf(sym); inherited != nil { - shape.ResultDecl = ctx.calcMemberDeclFor(sym, inherited, resultOutputName) + shape.ResultDecl = ctx.calcMemberDeclFor(sym, inherited, program.ResultOutputName) if ctx.model.semantics.IsKindBaseParameter(sym, inherited) { // The kind base's result (`Evaluation::result : Anything`) types nothing. shape.ResultDecl.Target.typ = nil } } else { // Nothing to inherit (no library): the effective default is `[0..*]`. - shape.ResultDecl = calcMemberDecl{Target: &writeTarget{name: resultOutputName, mult: semantics.UnboundedRange()}, Owner: sym, effective: true} + shape.ResultDecl = calcMemberDecl{Target: &writeTarget{name: program.ResultOutputName, mult: semantics.UnboundedRange()}, Owner: sym, effective: true} } // Only a calc computes by tool; a case always runs its body. - if kind == "calc" { + if calc.Kind == "calc" { tool, err := ctx.toolExecutionOf(sym) if err != nil { return nil, err @@ -320,22 +251,51 @@ func (ctx *Context) calcInterfaceOf(sym *symbols.Symbol) (*calcShape, error) { // computes through its steps, or answers with its verdicts alone, and a // library function the runtime implements natively computes through that. // One that computes nothing leaves its result parameter without a value. - performs := shape.isCase() && (len(shape.Nodes) > 0 || ctx.analysisChecks(sym)) + performs := calc.IsCase() && (calc.StepCount > 0 || ctx.analysisChecks(sym)) _, native := ctx.libraryFunctionFor(sym) - computes := lower.Returns(shape.Body) || len(shape.BodyOutputs) > 0 || shape.ResultExpr != nil || shape.hasInitialOutput() || performs || native + computes := calc.Returns || len(calc.BodyOutputs) > 0 || calc.Result != 0 || calc.HasInitialOutput() || performs || native switch { case computes || shape.Tool != nil: - case len(shape.Outputs) > 0 && shape.resultOutput() == nil: + case len(calc.Outputs) > 0 && calc.ResultOutput() == nil: shape.Uncomputed = fmt.Errorf("%w: %s binds none of its outputs (%s)", - ErrNoValue, label, shape.outputNames()) + ErrNoValue, label, calc.OutputNames()) default: - shape.Uncomputed = fmt.Errorf("%w: %s has no return expression%s", ErrNoValue, label, unboundResultHint(chain)) + shape.Uncomputed = fmt.Errorf("%w: %s has no return expression%s", ErrNoValue, label, lower.UnboundResultHint(calc.Chain)) } ctx.model.calcShapes[sym] = shape return shape, nil } +// loadCalc takes a lowered calc into the runtime: the declarations its members +// answer to, folded, and the statement orders its body states. +func (ctx *Context) loadCalc(calc *program.Calc, trees *lower.CalcTrees) *loadedCalc { + shape := &loadedCalc{Calc: calc, CalcTrees: trees} + shape.paramDecls = make([]calcMemberDecl, len(calc.Params)) + for i := range calc.Params { + shape.paramDecls[i] = ctx.memberDecl(calc.Params[i].Declarations) + } + shape.outputDecls = make([]calcMemberDecl, len(calc.Outputs)) + for i := range calc.Outputs { + shape.outputDecls[i] = ctx.memberDecl(calc.Outputs[i].Declarations) + } + if len(shape.Steps) > 0 && trees.BodyOrder != nil { + shape.statementOrders.Store(&shape.Steps[0], trees.BodyOrder) + } + indexCalcStatementOrders(&shape.statementOrders, shape.Steps) + return shape +} + +// memberDecl folds what a member declares along its declarations, most general +// first: a redeclaration stating no type or multiplicity keeps the redeclared feature's. +func (ctx *Context) memberDecl(members []program.CalcMember) calcMemberDecl { + var decl calcMemberDecl + for _, m := range members { + decl = ctx.calcMemberDeclFor(m.Owner, m.Feature, m.Name).redeclaring(decl) + } + return decl +} + func indexCalcStatementOrders(orders *sync.Map, stmts []lower.Statement) { for _, stmt := range stmts { switch s := stmt.(type) { @@ -384,202 +344,6 @@ func indexCalcGraphOrders(orders *sync.Map, graph *lower.ActionGraph, seen map[* } } -func calcBindings(chain []*symbols.Symbol) []lower.Binding { - var out []lower.Binding - for _, link := range chain { - if link != nil { - out = append(out, lower.ToBindings(link.Decl, DeclScope(link))...) - } - } - return out -} - -func resultBindingExpr(bindings []lower.Binding) ast.Node { - var result ast.Node - for _, binding := range bindings { - for i := range binding.Ends { - if binding.Ends[i].Path != "result" { - continue - } - other := binding.Ends[1-i] - if other.Expr != nil { - result = other.Expr - } - } - } - return result -} - -// calcChain returns the calcs sym takes members from (its supertypes and the calc -// it references), most general first, then sym. Non-calc links and the library's -// frame contribute nothing; a domain library's calc contributes as a model's does. -func (ctx *Context) calcChain(sym *symbols.Symbol) []*symbols.Symbol { - supers := ctx.model.semantics.MemberSources(sym) - chain := make([]*symbols.Symbol, 0, len(supers)+1) - for i := len(supers) - 1; i >= 0; i-- { - if supers[i] != nil && isCalcDecl(supers[i].Decl) && !ctx.frameDeclared(supers[i]) { - chain = append(chain, supers[i]) - } - } - return append(chain, sym) -} - -// calcParameters flattens the input parameters declared along chain (most general -// first): a redeclaration keeps its position and default, a renamed one is aliased. -func (ctx *Context) calcParameters(chain []*symbols.Symbol, aliases *map[string]string) []calcParameter { - var params []calcParameter - index := make(map[string]int) - - for _, link := range chain { - for _, member := range unwrappedDeclMembers(link.Decl) { - if subject, ok := subjectDeclaration(member); ok { - params = ctx.subjectParameter(params, index, aliases, link, member, subject) - continue - } - usage, ok := member.(*ast.Usage) - if !ok { - continue - } - name, _ := ast.EffectiveName(usage) - if name == "" { - continue - } - actor := usage.Kind == ast.UsageActor - if !actor && usage.Direction != ast.DirIn && usage.Direction != ast.DirInOut { - continue - } - sym := memberSymbol(DeclScope(link), usage) - param := calcParameter{ - Name: name, Default: usage.Value, Owner: link, - Sym: sym, IsActor: actor, - Decl: ctx.calcMemberDeclOf(link, sym, name), IsCalc: isCalcUsageSymbol(sym), - } - if at, seen := ctx.redeclaredIndex(index, sym, name); seen { - // A redeclaration binding no value keeps the inherited default, - // which is written in the scope of the calc that stated it. - if param.Default == nil { - param.Default = params[at].Default - param.Owner = params[at].Owner - } - param.Decl = param.Decl.redeclaring(params[at].Decl) - *aliases = aliasRedefined(*aliases, params[at].Name, name) - params[at] = param - index[name] = at - continue - } - index[param.Name] = len(params) - params = append(params, param) - } - } - return params -} - -// redeclaredIndex finds the flattened member the member sym, named name, -// redeclares: the one of its own name, one it redefines (`in y :>> x`), else -// the one its role redefines by position (an actor over the general's actor). -func (ctx *Context) redeclaredIndex(index map[string]int, sym *symbols.Symbol, name string) (int, bool) { - if at, seen := index[name]; seen { - return at, true - } - for _, redefined := range ctx.model.semantics.RedefinedFeatures(sym) { - if at, seen := index[redefined.Name]; seen { - return at, true - } - } - for _, redefined := range ctx.model.semantics.ImplicitRoleRedefinitions(sym) { - if at, seen := index[redefined.Name]; seen { - return at, true - } - } - return 0, false -} - -// calcBody returns the computation the invoked calc runs — its own body if that -// states one, otherwise the closest inherited one — with the calc that declares -// it, whose scope the body's statements are written in. -func (ctx *Context) calcBody(chain []*symbols.Symbol) ([]lower.Statement, *symbols.Symbol) { - body, _, owner := ctx.calcBodyWithOrder(chain) - return body, owner -} - -func (ctx *Context) calcBodyWithOrder(chain []*symbols.Symbol) ([]lower.Statement, *lower.StatementOrder, *symbols.Symbol) { - var stated []lower.Statement - var statedOrder *lower.StatementOrder - var owner *symbols.Symbol - for i := len(chain) - 1; i >= 0; i-- { - link := chain[i] - stmts, order := lower.CalcBodyWithOrder(link.Decl, unwrappedDeclMembers(link.Decl), link.Scope, ctx.Resolver()) - if lower.Returns(stmts) { - return stmts, order, link - } - // A body that computes but returns nothing leaves an inherited result in - // force, so keep looking up the chain before settling for it. - if stated == nil && len(stmts) > 0 { - stated, statedOrder, owner = stmts, order, link - } - } - return stated, statedOrder, owner -} - -// unboundResultHint explains a `return` that declares a result parameter without -// binding it (`return h;` declares h, it does not return the member h). -func unboundResultHint(chain []*symbols.Symbol) string { - for i := len(chain) - 1; i >= 0; i-- { - if chain[i] == nil { - continue - } - members := unwrappedDeclMembers(chain[i].Decl) - result := unboundResultParameter(members) - if result == nil { - continue - } - name, _ := ast.EffectiveName(result) - who, trailing, expr := "the result parameter", "of the body", "" - typ := lower.TypeText(result) - if name != "" { - spelled := source.NameText(name) - who = "result parameter " + spelled - if sibling := valuedMemberNamed(members, name, result); sibling != nil { - trailing, expr = "`"+spelled+"`", spelled - if typ == "" { - typ = lower.TypeText(sibling) - } - } - } - if typ == "" { - typ = "" - } - return fmt.Sprintf(": %s binds no value; write the result as the trailing expression %s, or bind it with `return : %s = %s;`", - who, trailing, typ, expr) - } - return "" -} - -// unboundResultParameter returns the body's `return` member that binds no value. -func unboundResultParameter(members []ast.Node) *ast.Usage { - for _, member := range members { - if u, ok := member.(*ast.Usage); ok && u.IsResult && u.Value == nil { - return u - } - } - return nil -} - -// valuedMemberNamed returns the body member called name, other than except, -// whose declaration binds a value; an unbound one would not compute a result. -func valuedMemberNamed(members []ast.Node, name string, except *ast.Usage) *ast.Usage { - for _, member := range members { - u, ok := member.(*ast.Usage) - if !ok || u == except || u.Value == nil { - continue - } - if actual, _ := ast.EffectiveName(u); actual == name { - return u - } - } - return nil -} - // calcArgs are the arguments of one calc invocation. The notation keeps the two // forms mutually exclusive: positional arguments bind in parameter order, named // arguments by parameter name. @@ -642,7 +406,7 @@ func (ctx *Context) invokeCalcNamed(sym *symbols.Symbol, args map[string]Value, return ctx.invokeCalc(sym, calcArgs{named: args}, scope) } -func (ctx *Context) invokeCalcNamedShapeOn(shape *calcShape, args map[string]Value, scope *symbols.Scope, self *Instance) (Value, error) { +func (ctx *Context) invokeCalcNamedShapeOn(shape *loadedCalc, args map[string]Value, scope *symbols.Scope, self *Instance) (Value, error) { return ctx.invokeCalcShape(shape, calcArgs{named: args}, scope, self) } @@ -699,8 +463,8 @@ type invocationFrame struct { bindings map[string]Value cells *bodyCells aliases map[string]string - owner *calcShape // the calc invoked, whose members the locals bind - run int64 // the run this invocation is (Context.newRun) + owner *loadedCalc // the calc invoked, whose members the locals bind + run int64 // the run this invocation is (Context.newRun) host calcStmtHost env stmtEnv engine stmtEngine @@ -756,13 +520,13 @@ func (ctx *Context) releaseInvocationFrame(frame *invocationFrame) { // invokeCalcShape binds arguments and evaluates the calc body. Binding happens // in the calc's own environment: defaults and the result expression see the // parameters and the calc's lexical scope, never the caller's frames. -func (ctx *Context) invokeCalcShape(shape *calcShape, args calcArgs, callerScope *symbols.Scope, self *Instance) (Value, error) { +func (ctx *Context) invokeCalcShape(shape *loadedCalc, args calcArgs, callerScope *symbols.Scope, self *Instance) (Value, error) { return ctx.invokeCalcShapeIn(shape, args, callerScope, self, nil) } // invokeCalcShapeIn is invokeCalcShape for a calc declared in a behavior body: // enclosing holds that body's bindings, outermost first, which the calc's own shadow. -func (ctx *Context) invokeCalcShapeIn(shape *calcShape, args calcArgs, callerScope *symbols.Scope, self *Instance, enclosing []frame) (Value, error) { +func (ctx *Context) invokeCalcShapeIn(shape *loadedCalc, args calcArgs, callerScope *symbols.Scope, self *Instance, enclosing []frame) (Value, error) { if shape != nil && ctx.scheduling().ordersStatements() && ctx.reordersTransitively(shape.Sym) && ctx.pureTransitively(shape.Sym) { return ctx.invokeWithStatementOrderResults(shape, args, self, len(enclosing) == 0, func() (Value, error) { @@ -772,7 +536,7 @@ func (ctx *Context) invokeCalcShapeIn(shape *calcShape, args calcArgs, callerSco return ctx.invokeCalcShapeDirect(shape, args, callerScope, self, enclosing) } -func (ctx *Context) invokeCalcShapeDirect(shape *calcShape, args calcArgs, callerScope *symbols.Scope, self *Instance, enclosing []frame) (Value, error) { +func (ctx *Context) invokeCalcShapeDirect(shape *loadedCalc, args calcArgs, callerScope *symbols.Scope, self *Instance, enclosing []frame) (Value, error) { if shape.Uncomputed != nil { return Value{}, shape.Uncomputed } @@ -869,7 +633,7 @@ func (ctx *Context) invokeCalcShapeDirect(shape *calcShape, args calcArgs, calle // computeCalcResultByTool is the result of a tool-annotated calc: the result // parameter's value, else the one its outputs settle. -func (ctx *Context) computeCalcResultByTool(shape *calcShape, scope *symbols.Scope, held func(string) (Value, bool)) (Value, error) { +func (ctx *Context) computeCalcResultByTool(shape *loadedCalc, scope *symbols.Scope, held func(string) (Value, bool)) (Value, error) { result, returned, outputs, err := ctx.computeCalcByTool(shape, scope, held) if err != nil || returned { return result, err @@ -879,7 +643,7 @@ func (ctx *Context) computeCalcResultByTool(shape *calcShape, scope *symbols.Sco // calcOccurrenceMaterializer materializes the occurrence a definition's `this` // denotes once, seeded with the parameters bound so far. -func (ctx *Context) calcOccurrenceMaterializer(shape *calcShape, locals frame, activation int64, occurrence *calcOccurrence) func() (*Instance, error) { +func (ctx *Context) calcOccurrenceMaterializer(shape *loadedCalc, locals frame, activation int64, occurrence *calcOccurrence) func() (*Instance, error) { return func() (*Instance, error) { if occurrence.inst != nil { return occurrence.inst, nil @@ -941,7 +705,7 @@ type calcBindingSite struct { // declared closest to the invoked calc, evaluated in the scope of the calc // declaring it. ec supplies the environment defaults are evaluated in. func (ctx *Context) bindCalcParameters( - shape *calcShape, + shape *loadedCalc, ec *EvalContext, args calcArgs, callerScope *symbols.Scope, @@ -959,7 +723,7 @@ func (ctx *Context) bindCalcParameters( position = positional positional++ } - if param.IsActor && param.Default == nil { + if param.IsActor && param.Default == 0 { // An actor nothing binds is a feature without a value, not a missing argument. if _, supplied := args.named[param.Name]; !supplied { continue @@ -972,7 +736,7 @@ func (ctx *Context) bindCalcParameters( } var value Value var source string - if open[param.Name] && shape.opensRun(param, ctx.model.semantics) { + if open[param.Name] && shape.opensRun(i, ctx.model.semantics) { // A feature read passes no arguments: an input whose absence stops the run binds // undetermined (a default reading it is open with it); an optional one binds empty. var count semantics.Range @@ -991,7 +755,7 @@ func (ctx *Context) bindCalcParameters( what := func() string { return fmt.Sprintf("%s: %s for parameter %q", shape.Label, source, param.Name) } - decl := param.Decl + decl := shape.paramDecls[i] // An actor states no direction, so the parameter chain does not reach what it // redefines; its range is the one the shape folded along the redeclarations. if decl.Target != nil && param.Sym != nil && !param.IsActor { @@ -1002,7 +766,7 @@ func (ctx *Context) bindCalcParameters( if err := decl.check(ctx, &value, what); err != nil { return err } - if err := param.checkFunction(&value, what); err != nil { + if err := checkFunction(param, &value, what); err != nil { return err } } @@ -1027,7 +791,7 @@ func (ctx *Context) bindCalcParameters( // the invocation yields: what the body returned, or, for a body that returns // nothing, the calc's designated output feature, evaluated in the invocation's // activation, which the caller ends after it. -func (ctx *Context) runCalcBody(shape *calcShape, frame *invocationFrame, callerScope *symbols.Scope, self *Instance, activation int64, enclosing []frame, occurrence *calcOccurrence) (Value, error) { +func (ctx *Context) runCalcBody(shape *loadedCalc, frame *invocationFrame, callerScope *symbols.Scope, self *Instance, activation int64, enclosing []frame, occurrence *calcOccurrence) (Value, error) { frame.host = calcStmtHost{ctx: ctx, shape: shape, self: self, occ: occurrence} frame.env = stmtEnv{ data: frame.locals(), enclosing: shape.bodyEnclosing(enclosing), locals: make(map[string]Value), @@ -1083,14 +847,14 @@ func (ctx *Context) runCalcBody(shape *calcShape, frame *invocationFrame, caller // parameter the tool answered wins outright, the one output the tool bound wins // alone, and several or none fail as designatedOutput fails. The tool's answers // are the whole computation; no body binding is evaluated. -func (shape *calcShape) toolCalcResult(outputs map[string]Value) (Value, error) { +func (shape *loadedCalc) toolCalcResult(outputs map[string]Value) (Value, error) { out, err := shape.designatedToolOutput(outputs) if err != nil { return Value{}, err } key := out.Name if key == "" { - key = resultOutputName + key = program.ResultOutputName } return outputs[key], nil } @@ -1098,25 +862,26 @@ func (shape *calcShape) toolCalcResult(outputs map[string]Value) (Value, error) // designatedToolOutput returns the output an invocation of a tool-computed calc // yields, in the spirit of designatedOutput: the candidates are the outputs the // tool answered, a result parameter winning outright over the rest. -func (shape *calcShape) designatedToolOutput(outputs map[string]Value) (calcOutput, error) { - var valued []calcOutput - for _, out := range shape.Outputs { +func (shape *loadedCalc) designatedToolOutput(outputs map[string]Value) (declaredOutput, error) { + var valued []declaredOutput + for i := range shape.Outputs { + out := &shape.Outputs[i] key := out.Name if out.IsResult && key == "" { - key = resultOutputName + key = program.ResultOutputName } if _, answered := outputs[key]; !answered { continue } if out.IsResult { - return out, nil + return shape.declaredOutput(i), nil } - valued = append(valued, out) + valued = append(valued, shape.declaredOutput(i)) } switch len(valued) { case 0: - return calcOutput{}, fmt.Errorf("%w: %s ended without a return", ErrNoValue, shape.Label) + return declaredOutput{}, fmt.Errorf("%w: %s ended without a return", ErrNoValue, shape.Label) case 1: return valued[0], nil default: @@ -1124,9 +889,9 @@ func (shape *calcShape) designatedToolOutput(outputs map[string]Value) (calcOutp for _, out := range valued { names = append(names, out.Name) } - return calcOutput{}, fmt.Errorf( + return declaredOutput{}, fmt.Errorf( "%w: %s computes %d output features (%s) and designates no result; read them from a usage instead: %s", - errAmbiguousResult, shape.Label, len(valued), strings.Join(names, ", "), shape.usageSpelling(valued[0].Name), + errAmbiguousResult, shape.Label, len(valued), strings.Join(names, ", "), shape.UsageSpelling(valued[0].Name), ) } } @@ -1147,7 +912,7 @@ func runCalcSteps(engine *stmtEngine, host *calcStmtHost, steps []lower.Statemen // observationSteps splits Steps at the results ending them (lower.IsResult): the // steps a run of a Monte Carlo case performs, then the results run over its sample. -func (shape *calcShape) observationSteps() (steps, results []lower.Statement) { +func (shape *loadedCalc) observationSteps() (steps, results []lower.Statement) { end := len(shape.Steps) for end > 0 && lower.IsResult(shape.Steps[end-1]) { end-- @@ -1157,8 +922,8 @@ func (shape *calcShape) observationSteps() (steps, results []lower.Statement) { // checkArgs rejects an argument list that cannot bind to the parameters at all: // more positional arguments than parameters, or a name that is not one. -func (shape *calcShape) checkArgs(args calcArgs) error { - if arity := shape.positionalArity(); len(args.positional) > arity { +func (shape *loadedCalc) checkArgs(args calcArgs) error { + if arity := shape.PositionalArity(); len(args.positional) > arity { return fmt.Errorf( "%w: %s takes %d argument(s), got %d", errCalcArity, shape.Label, arity, len(args.positional), @@ -1173,7 +938,7 @@ func (shape *calcShape) checkArgs(args calcArgs) error { sort.Strings(names) for _, name := range names { - if !shape.hasParameter(name) { + if !shape.HasParameter(name) { return fmt.Errorf( "%w: %s has no input parameter %q", errUnknownParameter, shape.Label, name, @@ -1183,69 +948,17 @@ func (shape *calcShape) checkArgs(args calcArgs) error { return nil } -// positionalIndexes are the indexes in Params of the parameters a positional -// argument can bind, in declaration order: every parameter but the actors, -// which bind by name only. -func (shape *calcShape) positionalIndexes() []int { - indexes := make([]int, 0, len(shape.Params)) - for i := range shape.Params { - if !shape.Params[i].IsActor { - indexes = append(indexes, i) - } - } - return indexes -} - -// positionalArity counts the parameters a positional argument can bind. -func (shape *calcShape) positionalArity() int { - return len(shape.positionalIndexes()) -} - -// optional reports whether the parameter may go without an argument: its +// optionalParameter reports whether the parameter may go without an argument: its // effective multiplicity admits no value, as `[0..1]` does. -func (param *calcParameter) optional(m *semantics.Model) bool { +func optionalParameter(param *program.CalcParam, m *semantics.Model) bool { return param.Sym != nil && m.EffectiveParameterRange(param.Sym).AllowsNone() } -// declaredOptional answers the read rule — whether the parameter as written +// declaredOptional answers the read rule — whether parameter i as written // admits no value — not invocation, where a bare parameter is optional anyway. -func (param *calcParameter) declaredOptional() bool { - return param.Decl.multStated && param.Decl.Target != nil && param.Decl.Target.mult.AllowsNone() -} - -// hasParameter reports whether the calc declares an input parameter of that name. -func (shape *calcShape) hasParameter(name string) bool { - return shape.parameter(name) != nil -} - -// parameter is the calc's parameter of that name, nil when it declares none. -func (shape *calcShape) parameter(name string) *calcParameter { - for i := range shape.Params { - if shape.Params[i].Name == name { - return &shape.Params[i] - } - } - return nil -} - -// performs reports whether the body's action nodes are performed as steps: a case -// is an action, so its body is; a calc's body performs nothing. -func (shape *calcShape) performs() bool { - return shape.isCase() -} - -// isCase reports whether the shape is a case — an analysis or a verification — -// rather than a plain calc. -func (shape *calcShape) isCase() bool { - return shape.Kind == "analysis" || shape.Kind == "verification" -} - -// bodyScope is the namespace the body's names resolve in. -func (shape *calcShape) bodyScope() *symbols.Scope { - if shape.BodyOwner != nil { - return shape.BodyOwner.Scope - } - return shape.Sym.Scope +func (shape *loadedCalc) declaredOptional(i int) bool { + decl := &shape.paramDecls[i] + return decl.multStated && decl.Target != nil && decl.Target.mult.AllowsNone() } // bindCalcParameter resolves the value of one parameter: its argument (by @@ -1255,8 +968,8 @@ func (shape *calcShape) bodyScope() *symbols.Scope { // these takes the enclosing case's subject, read from enclosing. A position // below zero is a parameter no positional argument binds, as an actor is. func (ec *EvalContext) bindCalcParameter( - shape *calcShape, - param *calcParameter, + shape *loadedCalc, + param *program.CalcParam, args calcArgs, position int, enclosing *EvalContext, @@ -1267,7 +980,7 @@ func (ec *EvalContext) bindCalcParameter( if value, ok := args.named[param.Name]; ok { return value, "argument", nil } - if param.Default == nil { + if param.Default == 0 { if param.IsSubject { value, ok, err := ec.ctx.enclosingSubject(shape, enclosing) if err != nil { @@ -1278,12 +991,12 @@ func (ec *EvalContext) bindCalcParameter( } return Value{}, "", shape.unboundSubject(param) } - if param.optional(ec.ctx.model.semantics) { + if optionalParameter(param, ec.ctx.model.semantics) { return nullValue(), "omitted", nil } return Value{}, "", shape.unboundParameter(param.Name) } - value, err := ec.Eval(param.Default) + value, err := ec.Eval(shape.Expr(param.Default)) if err != nil { return Value{}, "", calcDefaultError(shape.Kind, shape.Name, param.Name, err) } @@ -1292,7 +1005,7 @@ func (ec *EvalContext) bindCalcParameter( // unboundParameter is the error of a parameter an invocation or a run neither // binds nor defaults: the request cannot proceed without it. -func (shape *calcShape) unboundParameter(name string) error { +func (shape *loadedCalc) unboundParameter(name string) error { return fmt.Errorf("%w: %s parameter %q has no argument and no default", errUnboundParameter, shape.Label, name) } @@ -1308,23 +1021,14 @@ func (ctx *Context) calcScope(declarer, invoked *symbols.Symbol, callerScope *sy return callerScope } -// calcTypedFeature reports whether sym is a feature typed by a calc — a step, which an -// invocation performs as the calc it is typed by. -func (ctx *Context) calcTypedFeature(sym *symbols.Symbol) bool { - if _, ok := sym.Decl.(*ast.Usage); !ok { - return false - } - return len(ctx.calcChain(sym)) > 1 -} - // libraryPerformance is how a call of a model calc applies an inherited library // function: the library declaration implementing it and, when the model calcs // redeclare inputs, the effective signature the call's arguments bind through. type libraryPerformance struct { lib *symbols.Symbol - signature *calcShape // the effective inputs, bodiless; nil when the library's own bind as written - positions []int // per signature parameter, the library input it redefines; -1 for none - arity int // the library declaration's input parameter count + signature *loadedCalc // the effective inputs, bodiless; nil when the library's own bind as written + positions []int // per signature parameter, the library input it redefines; -1 for none + arity int // the library declaration's input parameter count } // libraryCalcPerformed returns the library function whose implementation a call of @@ -1342,12 +1046,22 @@ func (ctx *Context) libraryCalcPerformed(sym *symbols.Symbol) *libraryPerformanc return perf } +// calcTypedFeature reports whether sym is a feature typed by a calc — a step, which an +// invocation performs as the calc it is typed by. +func (ctx *Context) calcTypedFeature(sym *symbols.Symbol) bool { + if _, ok := sym.Decl.(*ast.Usage); !ok { + return false + } + return len(lower.CalcChain(sym, ctx.calcEnv())) > 1 +} + func (ctx *Context) resolveLibraryPerformance(sym *symbols.Symbol) *libraryPerformance { if usage, ok := sym.Decl.(*ast.Usage); ok && usage.Value != nil { return nil } - chain := ctx.calcChain(sym) - if ctx.calcComputes(chain) { + env := ctx.calcEnv() + chain := lower.CalcChain(sym, env) + if lower.CalcStatesComputation(chain, env) { return nil } // A tool-computed calc answers from its tool, not the library's implementation; @@ -1364,49 +1078,16 @@ func (ctx *Context) resolveLibraryPerformance(sym *symbols.Symbol) *libraryPerfo if !ctx.redeclaresInputs(chain) { return perf } - perf.signature = &calcShape{Sym: sym, Name: ctx.QualifiedName(sym)} - perf.signature.Kind, perf.signature.Label = calcKindLabel(sym, perf.signature.Name) - for _, p := range ctx.model.semantics.BehaviorParametersOf(sym) { - if p.IsResult || (p.Direction != ast.DirIn && p.Direction != ast.DirInOut) { - continue - } - param, position := ctx.effectiveParameter(p.Symbol, libInputs) - perf.signature.Params = append(perf.signature.Params, param) - perf.signature.ParamNames = append(perf.signature.ParamNames, param.Name) - perf.positions = append(perf.positions, position) - } + calc, trees, positions := lower.ToCalcSignature(sym, libInputs, env) + perf.signature, perf.positions = ctx.loadCalc(calc, trees), positions return perf } -// effectiveParameter is the input parameter sym as a call binds it — named as -// written, declared and defaulted by the nearest of its redefinitions the model -// states — with the position of the library input it redefines, -1 for none. -func (ctx *Context) effectiveParameter(sym *symbols.Symbol, libInputs []*symbols.Symbol) (calcParameter, int) { - param := calcParameter{Name: sym.Name, Sym: sym, IsCalc: isCalcUsageSymbol(sym)} - if effective, _ := ast.EffectiveName(sym.Decl.(*ast.Usage)); effective != "" { - param.Name = effective - } - for _, link := range ctx.model.semantics.ParameterRedefinitionChain(sym) { - owner := link.OwnerScope.Owner() - param.Decl = param.Decl.redeclaring(ctx.calcMemberDeclFor(owner, link, param.Name)) - if at := slices.Index(libInputs, link); at >= 0 { - return param, at - } - if ctx.libraryDeclared(link) { - break - } - if value := ctx.extractDefaultValue(link); param.Default == nil && value != nil { - param.Default, param.Owner = value, owner - } - } - return param, -1 -} - // implementedLibraryCalc returns the nearest library calc sym specializes that // this runtime implements, or nil when none is. func (ctx *Context) implementedLibraryCalc(sym *symbols.Symbol) *symbols.Symbol { for _, super := range ctx.model.semantics.AllSupertypes(sym) { - if super == nil || !isCalcDecl(super.Decl) || !ctx.libraryDeclared(super) { + if super == nil || !lower.IsCalcDecl(super.Decl) || !ctx.libraryDeclared(super) { continue } if _, ok := ctx.builtinFor(super); ok { @@ -1434,7 +1115,7 @@ func (ctx *Context) redeclaresInputs(chain []*symbols.Symbol) bool { // owns, in declaration order. func (ctx *Context) ownedInputSymbols(sym *symbols.Symbol) []*symbols.Symbol { var inputs []*symbols.Symbol - for _, member := range unwrappedDeclMembers(sym.Decl) { + for _, member := range lower.UnwrappedDeclMembers(sym.Decl) { usage, ok := member.(*ast.Usage) if !ok || (usage.Direction != ast.DirIn && usage.Direction != ast.DirInOut) { continue @@ -1514,45 +1195,6 @@ func (ctx *Context) applyLibraryPerformance(perf *libraryPerformance, args calcA return fn.invoke(ctx, libArgs) } -// calcComputes reports whether a calc chain states a computation: a body that -// returns or assigns an output, or a binding of the result. -func (ctx *Context) calcComputes(chain []*symbols.Symbol) bool { - body, _ := ctx.calcBody(chain) - if lower.Returns(body) || resultBindingExpr(calcBindings(chain)) != nil { - return true - } - var aliases map[string]string - return len(assignedOutputs(lower.CalcSteps(body), ctx.calcOutputs(chain, &aliases), aliases)) > 0 -} - -// isCalcDecl reports whether a declaration is a calc definition or usage, or an -// analysis or verification case, each a calculation (SysML v2 §7.22, §7.23): -// each is invoked the same way. -func isCalcDecl(decl ast.Node) bool { - switch d := decl.(type) { - case *ast.Definition: - return d.Kind == ast.DefCalc || d.Kind == ast.DefAnalysisCase || d.Kind == ast.DefVerificationCase - case *ast.Usage: - return d.Kind == ast.UsageCalc || d.Kind == ast.UsageAnalysisCase || d.Kind == ast.UsageVerificationCase - default: - return false - } -} - -// calcKindLabel names what sym declares — a calc, an analysis case or a -// verification case — and how diagnostics about it refer to it. -func calcKindLabel(sym *symbols.Symbol, name string) (kind, label string) { - switch { - case IsVerificationCaseSymbol(sym): - kind = "verification" - case lower.PerformsSteps(sym.Decl): - kind = "analysis" - default: - kind = "calc" - } - return kind, kind + " " + name -} - // isCalcSymbol reports whether sym declares a calc, reading its declaration when // it has one and its symbol kind otherwise. func isCalcSymbol(sym *symbols.Symbol) bool { @@ -1560,7 +1202,7 @@ func isCalcSymbol(sym *symbols.Symbol) bool { return false } if sym.Decl != nil { - return isCalcDecl(sym.Decl) + return lower.IsCalcDecl(sym.Decl) } switch sym.Kind { case symbols.SymbolCalcDef, symbols.SymbolCalcUsage, @@ -1609,59 +1251,6 @@ func isStateSymbol(sym *symbols.Symbol) bool { return sym.Kind == symbols.SymbolStateDef || sym.Kind == symbols.SymbolStateUsage } -// unwrappedDeclMembers is ast.DeclMembers extended to named owned constraints, -// with the Membership wrappers the parser produces unwrapped. -func unwrappedDeclMembers(decl ast.Node) []ast.Node { - if body, ok := constraintMemberBody(decl); ok { - return body - } - members := ast.DeclMembers(decl) - if members == nil { - return nil - } - - out := make([]ast.Node, 0, len(members)) - for member := range eachUnwrappedDeclMember(decl) { - out = append(out, member) - } - return out -} - -// constraintMemberBody returns the nested members of an assume or require member -// in either of its forms, and false for every other declaration. -func constraintMemberBody(decl ast.Node) ([]ast.Node, bool) { - switch m := decl.(type) { - case *ast.AssumeMember: - return m.Body, true - case *ast.RequireMember: - return m.Body, true - } - return nil, false -} - -// eachUnwrappedDeclMember yields what unwrappedDeclMembers lists without building the list, -// for the readers that stop at the first member of interest. -func eachUnwrappedDeclMember(decl ast.Node) iter.Seq[ast.Node] { - return func(yield func(ast.Node) bool) { - if body, ok := constraintMemberBody(decl); ok { - for _, member := range body { - if !yield(member) { - return - } - } - return - } - for _, member := range ast.DeclMembers(decl) { - if membership, ok := member.(*ast.Membership); ok { - member = membership.Member - } - if !yield(member) { - return - } - } - } -} - // QualifiedName renders a symbol's qualified name, falling back to its // local name when no index is reachable. func (ctx *Context) QualifiedName(sym *symbols.Symbol) string { diff --git a/internal/exec/runtime/invoke_operation.go b/internal/exec/runtime/invoke_operation.go index 3905b13988..3f17296efe 100644 --- a/internal/exec/runtime/invoke_operation.go +++ b/internal/exec/runtime/invoke_operation.go @@ -125,7 +125,7 @@ func (ctx *Context) InvokeOperationWith(inst *Instance, name string, args Operat // resultName is the name an invocation returns the calc's result under: its // named result parameter, else `result`. -func (shape *calcShape) resultName() string { +func (shape *loadedCalc) resultName() string { for _, output := range shape.Outputs { if output.IsResult && output.Name != "" { return output.Name diff --git a/internal/exec/runtime/invoke_predicate.go b/internal/exec/runtime/invoke_predicate.go index 0b5e079f19..b5ecc83b2d 100644 --- a/internal/exec/runtime/invoke_predicate.go +++ b/internal/exec/runtime/invoke_predicate.go @@ -5,8 +5,8 @@ import ( "fmt" "slices" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" - "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) // A constraint or requirement is a predicate (KerML §7.4.9; SysML v2 §7.19, §7.21): @@ -14,26 +14,11 @@ import ( // arguments bound to its subject and input parameters. A parameter left unbound // takes its default, evaluated where the predicate is declared. -// isPredicateDecl reports a declaration an invocation applies as a predicate: a -// constraint or requirement definition or usage, an objective among the latter. -func isPredicateDecl(decl ast.Node) bool { - if _, ok := ast.OwnedConstraintOf(decl); ok { - return true - } - switch d := decl.(type) { - case *ast.Definition: - return d.Kind == ast.DefConstraint || d.Kind == ast.DefRequirement - case *ast.Usage: - return d.Kind == ast.UsageConstraint || d.Kind == ast.UsageRequirement || d.Kind == ast.UsageObjective - } - return false -} - // InvokePredicate applies a constraint or requirement to positional arguments // and reports whether its conditions hold. func (ctx *Context) InvokePredicate(sym *symbols.Symbol, args []Value, scope *symbols.Scope) (bool, error) { defer ctx.beginRun()() - if sym == nil || !isPredicateDecl(sym.Decl) { + if sym == nil || !lower.IsPredicateDecl(sym.Decl) { name := "" if sym != nil { name = ctx.QualifiedName(sym) @@ -51,49 +36,14 @@ func (ctx *Context) InvokePredicate(sym *symbols.Symbol, args []Value, scope *sy return result.Const.Bool, nil } -// predicateKind names the kind of predicate sym declares, as verdicts name it. -func predicateKind(sym *symbols.Symbol) string { - switch d := sym.Decl.(type) { - case *ast.Definition: - if d.Kind == ast.DefConstraint { - return "constraint" - } - case *ast.Usage: - switch d.Kind { - case ast.UsageConstraint: - return "constraint" - case ast.UsageObjective: - return "objective" - } - } - return "requirement" -} - // predicateShapeOf is the memoized invocation interface of the predicate sym: // its parameters, the subject first; its conditions stand for a body. -func (ctx *Context) predicateShapeOf(sym *symbols.Symbol) *calcShape { +func (ctx *Context) predicateShapeOf(sym *symbols.Symbol) *loadedCalc { if cached, ok := ctx.model.predicateShapes[sym]; ok { return cached } - supers := ctx.model.semantics.MemberSources(sym) - chain := make([]*symbols.Symbol, 0, len(supers)+1) - for i := len(supers) - 1; i >= 0; i-- { - if supers[i] != nil && isPredicateDecl(supers[i].Decl) && !ctx.frameDeclared(supers[i]) { - chain = append(chain, supers[i]) - } - } - chain = append(chain, sym) - name := ctx.QualifiedName(sym) - if sym.Name == "" { - // An anonymous usage answers to the name it redefines (`objective : MinimizeObjective;`). - name += ctx.model.semantics.EffectiveNameOf(sym) - } - shape := &calcShape{Sym: sym, Name: name, Kind: predicateKind(sym), Label: predicateKind(sym) + " " + name} - shape.Params = ctx.calcParameters(chain, &shape.Aliases) - shape.ParamNames = make([]string, len(shape.Params)) - for i, param := range shape.Params { - shape.ParamNames[i] = param.Name - } + calc, trees := lower.ToPredicate(sym, ctx.calcEnv()) + shape := ctx.loadCalc(calc, trees) ctx.model.predicateShapes[sym] = shape return shape } diff --git a/internal/exec/runtime/library_functions_test.go b/internal/exec/runtime/library_functions_test.go index c80410ddad..4505995566 100644 --- a/internal/exec/runtime/library_functions_test.go +++ b/internal/exec/runtime/library_functions_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -1317,10 +1318,12 @@ func TestVendoredFunctionsAreAllDispatchable(t *testing.T) { func checkLibrarySignature(t *testing.T, ctx *Context, fqn string, sym *symbols.Symbol, fn *libraryFunction) { t.Helper() var declared []declaredParam - for _, param := range ctx.calcParameters(ctx.calcChain(sym), new(map[string]string)) { + calc, _ := lower.ToCalc(sym, ctx.calcEnv()) + for i := range calc.Params { + param := &calc.Params[i] declared = append(declared, declaredParam{ name: param.Name, - optional: param.Default != nil || param.optional(ctx.model.semantics), + optional: param.Default != 0 || optionalParameter(param, ctx.model.semantics), }) } // An implementation may pin a parameter the declaration leaves optional; @@ -1350,7 +1353,7 @@ func checkBuiltinSignature(t *testing.T, ctx *Context, fqn string, sym *symbols. return } var declared []declaredParam - for _, member := range unwrappedDeclMembers(sym.Decl) { + for _, member := range lower.UnwrappedDeclMembers(sym.Decl) { usage, ok := member.(*ast.Usage) if !ok || (usage.Direction != ast.DirIn && usage.Direction != ast.DirInOut) { continue diff --git a/internal/exec/runtime/library_provenance_test.go b/internal/exec/runtime/library_provenance_test.go index 9e70cac85b..a03c5445cf 100644 --- a/internal/exec/runtime/library_provenance_test.go +++ b/internal/exec/runtime/library_provenance_test.go @@ -3,6 +3,7 @@ package runtime import ( "testing" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -94,7 +95,7 @@ func TestCalcChainLeavesOutLibraryDeclaredLinks(t *testing.T) { } }`) - chain := ctx.calcChain(lookupOne(t, idx, "M::Double")) + chain := lower.CalcChain(lookupOne(t, idx, "M::Double"), ctx.calcEnv()) if len(chain) != 1 || symbols.FQNOf(chain[0]) != "M::Double" { t.Fatalf("calcChain(M::Double) = %v, want the model's calc alone", chain) } diff --git a/internal/exec/runtime/model.go b/internal/exec/runtime/model.go index daa7720a66..303fae57d9 100644 --- a/internal/exec/runtime/model.go +++ b/internal/exec/runtime/model.go @@ -68,7 +68,7 @@ type Model struct { // returnedParams memoizes, per calc shape, the parameters its result passes on; // returnedStack is the shapes under analysis, returnedProvisional those awaiting // the root of their call cycle. - returnedParams map[*calcShape]*returnedAnalysis + returnedParams map[*loadedCalc]*returnedAnalysis returnedStack []*returnedAnalysis returnedProvisional []*returnedAnalysis @@ -84,7 +84,7 @@ type Model struct { // calcShapes memoizes resolved calc invocation interfaces (parameters, // defaults, result expression) per calc symbol. - calcShapes map[*symbols.Symbol]*calcShape + calcShapes map[*symbols.Symbol]*loadedCalc // constraintSteps memoizes the step each member of a constraint body lowers // to, one lowering per member node however often a check reads the body. @@ -112,7 +112,7 @@ type Model struct { // predicateShapes memoizes the invocation interfaces of constraints and // requirements applied as predicates. - predicateShapes map[*symbols.Symbol]*calcShape + predicateShapes map[*symbols.Symbol]*loadedCalc // librarySymbols memoizes the library declaration each qualified name denotes; // see librarySymbol. @@ -260,12 +260,12 @@ func NewModel(sem *semantics.Model, resolver *resolve.Resolver) *Model { chainHosts: make(map[*symbols.Symbol]bool), denotedFeatures: make(map[*symbols.Symbol]map[*symbols.Symbol]string), holders: make(map[*symbols.Symbol]map[string][]string), - returnedParams: make(map[*calcShape]*returnedAnalysis), + returnedParams: make(map[*loadedCalc]*returnedAnalysis), redefined: make(map[featureOfType][]*symbols.Symbol), chainDeclarations: make(map[chainDeclarationsKey]*chainDecls), writeTargets: make(map[writeTargetKey]*writeTarget), - calcShapes: make(map[*symbols.Symbol]*calcShape), - predicateShapes: make(map[*symbols.Symbol]*calcShape), + calcShapes: make(map[*symbols.Symbol]*loadedCalc), + predicateShapes: make(map[*symbols.Symbol]*loadedCalc), constraintSteps: make(map[ast.Node]lower.Statement), activityOperands: make(map[*ast.FeatureChainExpr]ast.Node), activitySplits: make(map[activitySplitKey]activitySplit), diff --git a/internal/exec/runtime/montecarlo_case.go b/internal/exec/runtime/montecarlo_case.go index e1e4a5820d..31b5537fa0 100644 --- a/internal/exec/runtime/montecarlo_case.go +++ b/internal/exec/runtime/montecarlo_case.go @@ -6,6 +6,7 @@ import ( "maps" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" ) @@ -107,7 +108,7 @@ func (ctx *Context) ObserveMonteCarlo(sym *symbols.Symbol, args AnalysisArgs, sc if err != nil { return nil, err } - asUsage := args.Subject == nil && len(args.Positional) == 0 && len(args.Named) == 0 && isCalcUsageSymbol(sym) + asUsage := args.Subject == nil && len(args.Positional) == 0 && len(args.Named) == 0 && lower.IsCalcUsageSymbol(sym) var calcArgs calcArgs if !asUsage { if calcArgs, err = shape.analysisArgs(args); err != nil { @@ -242,7 +243,7 @@ func (ctx *Context) monteCarloObserved(run *calcRun) (Value, error) { } // monteCarloMember is the name the case's run binds the library feature under. -func (ctx *Context) monteCarloMember(shape *calcShape, feature string) (string, bool) { +func (ctx *Context) monteCarloMember(shape *loadedCalc, feature string) (string, bool) { if ctx.model.resolver == nil || ctx.model.resolver.Index() == nil { return "", false } @@ -546,10 +547,10 @@ func (r *MonteCarloRun) returnResults() error { if !returned { return nil } - if out := run.shape.resultOutput(); out != nil && out.Name != "" { + if out := run.shape.ResultOutput(); out != nil && out.Name != "" { run.outputs[out.Name] = result } else if _, anonymous := run.shape.anonymousResult(); anonymous { - run.outputs[resultOutputName] = result + run.outputs[program.ResultOutputName] = result } run.result, run.returned = result, true return nil diff --git a/internal/exec/runtime/order_analysis.go b/internal/exec/runtime/order_analysis.go index d05567f433..16648a0d2c 100644 --- a/internal/exec/runtime/order_analysis.go +++ b/internal/exec/runtime/order_analysis.go @@ -55,20 +55,20 @@ func (ctx *Context) analyzeBodyOrderNode(sym *symbols.Symbol) *bodyOrderAnalysis return true }) ctx.analyzeOrderStatements(shape.Steps, analysis) - if shape.performs() { + if shape.Performs() { analysis.effects = true } for _, param := range shape.Params { - ctx.analyzeOrderExpression(param.Default, ctx.calcScope(param.Owner, shape.Sym, nil), analysis) + ctx.analyzeOrderExpression(shape.Expr(param.Default), ctx.calcScope(param.Owner, shape.Sym, nil), analysis) } for _, binding := range shape.Bindings { for _, end := range binding.Ends { ctx.analyzeOrderExpression(end.Expr, binding.Scope, analysis) } } - ctx.analyzeOrderExpression(shape.ResultExpr, shape.bodyScope(), analysis) + ctx.analyzeOrderExpression(shape.resultExpr(), shape.BodyScope(), analysis) } - case isPredicateDecl(sym.Decl): + case lower.IsPredicateDecl(sym.Decl): conditions := ctx.ConditionsOf(sym, sym.Scope) ctx.analyzeOrderConditions(conditions, analysis) for _, feature := range ctx.conditionFeatures(sym) { diff --git a/internal/exec/runtime/parameters.go b/internal/exec/runtime/parameters.go index 97f0bc963e..8969308056 100644 --- a/internal/exec/runtime/parameters.go +++ b/internal/exec/runtime/parameters.go @@ -60,7 +60,7 @@ func (ctx *Context) Parameters(sym *symbols.Symbol) ([]Parameter, error) { params := make([]Parameter, len(shape.Params)) for i := range shape.Params { p := &shape.Params[i] - params[i] = Parameter{Name: p.Name, IsSubject: p.IsSubject, decl: p.Decl} + params[i] = Parameter{Name: p.Name, IsSubject: p.IsSubject, decl: shape.paramDecls[i]} } return params, nil } diff --git a/internal/exec/runtime/robustness_toolcalc_test.go b/internal/exec/runtime/robustness_toolcalc_test.go index c5e033fce3..d7edb69ec7 100644 --- a/internal/exec/runtime/robustness_toolcalc_test.go +++ b/internal/exec/runtime/robustness_toolcalc_test.go @@ -2,6 +2,7 @@ package runtime import ( "errors" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "strings" "testing" ) @@ -44,7 +45,7 @@ func TestRuntimeRobustnessToolCalc(t *testing.T) { if _, err := ctx.CalcUsageOutputs(sym, scope, nil); err != nil { t.Fatalf("CalcUsageOutputs = %v, want no error for an input nothing supplies", err) } - result, err := ctx.calcUsageOutput(sym, resultOutputName, scope, nil) + result, err := ctx.calcUsageOutput(sym, program.ResultOutputName, scope, nil) if err != nil || result.Kind != ValUndetermined || !strings.Contains(result.Undetermined().reason, `parameter "m"`) { t.Fatalf("result of tbare = %s, %v; want undetermined naming the input nothing supplies", FormatValue(result), err) } diff --git a/internal/exec/runtime/satisfy.go b/internal/exec/runtime/satisfy.go index b33640f51a..19f454435f 100644 --- a/internal/exec/runtime/satisfy.go +++ b/internal/exec/runtime/satisfy.go @@ -6,6 +6,7 @@ import ( "slices" "strings" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -683,7 +684,7 @@ func (ctx *Context) chainSubject(a *SatisfyAssertion) (*Instance, error) { // which is how a `satisfy requirement r by p { require ... }` form carries one // without referencing another requirement. func (ctx *Context) declaresConditions(sym *symbols.Symbol) bool { - for _, node := range unwrappedDeclMembers(sym.Decl) { + for _, node := range lower.UnwrappedDeclMembers(sym.Decl) { if len(ctx.appendConditions(nil, node, nil, true, false, nil)) > 0 { return true } diff --git a/internal/exec/runtime/set_feature_test.go b/internal/exec/runtime/set_feature_test.go index 540803dac4..fdd97b92f4 100644 --- a/internal/exec/runtime/set_feature_test.go +++ b/internal/exec/runtime/set_feature_test.go @@ -5,6 +5,7 @@ import ( "math" "testing" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" ) @@ -466,9 +467,9 @@ func TestSetIsNotASequenceAsAMember(t *testing.T) { // runs, or two declarations of one name — take one position each, so equal // sets of functions enumerate alike whatever order they were written in. func TestFunctionsRenderedAlikeOrderByIdentity(t *testing.T) { - shape := &calcShape{Sym: &symbols.Symbol{Name: "scale"}, Name: "P::scale"} - twin := &calcShape{Sym: &symbols.Symbol{Name: "scale", DocName: "other"}, Name: "P::scale"} - fn := func(shape *calcShape, self *Instance, run int64) Value { + shape := &loadedCalc{Calc: &program.Calc{Sym: &symbols.Symbol{Name: "scale"}, Name: "P::scale"}} + twin := &loadedCalc{Calc: &program.Calc{Sym: &symbols.Symbol{Name: "scale", DocName: "other"}, Name: "P::scale"}} + fn := func(shape *loadedCalc, self *Instance, run int64) Value { f := &functionValue{shape: shape, self: self} if run != 0 { f.enclosing = []frame{{vars: map[string]Value{"k": integerValue(1)}, run: run}} diff --git a/internal/exec/runtime/shape.go b/internal/exec/runtime/shape.go index 7eeb7c5fc2..1573c80447 100644 --- a/internal/exec/runtime/shape.go +++ b/internal/exec/runtime/shape.go @@ -1,6 +1,7 @@ package runtime import ( + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -436,16 +437,7 @@ func knownBound(plain, scoped semantics.Bound) semantics.Bound { // extractDefaultValue returns the default-value expression for a feature (nil if none). func (ctx *Context) extractDefaultValue(featureSym *symbols.Symbol) ast.Node { - if oc, ok := ast.OwnedConstraintOf(featureSym.Decl); ok { - return oc.Value - } - switch decl := featureSym.Decl.(type) { - case *ast.Usage: - return decl.Value // nil if no default - case *ast.SubjectMember: - return decl.BindingExpr - } - return nil + return lower.DefaultValueOf(featureSym) } // featureMultiplicity is the multiplicity a feature has on owner: a parameter's @@ -606,7 +598,7 @@ func (ctx *Context) roleRedefiningDefault(sym, typeSym *symbols.Symbol) (ast.Nod return nil, nil } scope := bodyScope(typeSym, typeSym.OwnerScope) - for node := range eachUnwrappedDeclMember(typeSym.Decl) { + for node := range lower.EachUnwrappedDeclMember(typeSym.Decl) { member := memberSymbol(scope, node) if member == nil || member == sym || member.Name != "" { continue diff --git a/internal/exec/runtime/shared_default_test.go b/internal/exec/runtime/shared_default_test.go index c7b7959a6b..9834211d73 100644 --- a/internal/exec/runtime/shared_default_test.go +++ b/internal/exec/runtime/shared_default_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" ) @@ -490,7 +491,7 @@ func TestSharedDefaultRecordsUndoneWithFailedImage(t *testing.T) { } sound := img.messages inBody := Value{Kind: ValFunction, ref: &functionValue{ - shape: &calcShape{Sym: &symbols.Symbol{Name: "inBody"}, Name: "inBody"}, + shape: &loadedCalc{Calc: &program.Calc{Sym: &symbols.Symbol{Name: "inBody"}, Name: "inBody"}}, enclosing: []frame{{vars: map[string]Value{"k": integerValue(1)}, run: 1}}, }} img.messages = append(slices.Clone(sound), Message{Object: fleet.ID, SignalType: "go", Payload: map[string]Value{"k": inBody}}) diff --git a/internal/exec/runtime/shared_verdict.go b/internal/exec/runtime/shared_verdict.go index f23623a6a4..5dbc4f47cf 100644 --- a/internal/exec/runtime/shared_verdict.go +++ b/internal/exec/runtime/shared_verdict.go @@ -3,6 +3,7 @@ package runtime import ( "strings" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" ) @@ -238,7 +239,7 @@ func (ctx *Context) declaredAlongAll(inst *Instance, paths [][]string) bool { // the requirement it references when the assertion states nothing of its own, so // two assertions of it about objects of one shape share; else the assertion. func sharedElement(a *SatisfyAssertion) *symbols.Symbol { - if a.Requirement != nil && !a.Negated && len(unwrappedDeclMembers(a.Symbol.Decl)) == 0 { + if a.Requirement != nil && !a.Negated && len(lower.UnwrappedDeclMembers(a.Symbol.Decl)) == 0 { return a.Requirement } return a.Symbol diff --git a/internal/exec/runtime/tool_calc.go b/internal/exec/runtime/tool_calc.go index f5a7390527..5fddd128b6 100644 --- a/internal/exec/runtime/tool_calc.go +++ b/internal/exec/runtime/tool_calc.go @@ -4,6 +4,7 @@ import ( "fmt" "sort" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -22,7 +23,7 @@ import ( // unbound non-optional input is errUnboundParameter, a ToolVariable name two parameters // carry is a ToolError. The name the result is bound under is returned with the call, // empty when the calc declares no result parameter — an out-only calc asks for none. -func (ctx *Context) calcToolCall(shape *calcShape, scope *symbols.Scope, held func(param string) (Value, bool)) (*ToolCall, string, error) { +func (ctx *Context) calcToolCall(shape *loadedCalc, scope *symbols.Scope, held func(param string) (Value, bool)) (*ToolCall, string, error) { execution := shape.Tool tool := execution.tool call := &ToolCall{Action: execution.on, ToolName: tool, URI: execution.uri, ctx: ctx, scope: scope} @@ -48,8 +49,8 @@ func (ctx *Context) calcToolCall(shape *calcShape, scope *symbols.Scope, held fu } var resultKey string if result != nil { - resultKey = resultOutputName - variable := resultOutputName + resultKey = program.ResultOutputName + variable := program.ResultOutputName if result.Name != "" { resultKey, variable = result.Name, result.Name } @@ -71,7 +72,7 @@ func (ctx *Context) calcToolCall(shape *calcShape, scope *symbols.Scope, held fu // addToolCallParameter adds a ToolVariable-named parameter to the call: an // input for the value held, an output for a parameter the tool writes. -func (ctx *Context) addToolCallParameter(call *ToolCall, shape *calcShape, param semantics.BehaviorParameter, namedBy map[string]string, held func(param string) (Value, bool)) error { +func (ctx *Context) addToolCallParameter(call *ToolCall, shape *loadedCalc, param semantics.BehaviorParameter, namedBy map[string]string, held func(param string) (Value, bool)) error { tool := call.ToolName variable, named, err := ctx.toolVariableOf(param.Symbol) if err != nil { @@ -115,7 +116,7 @@ func (ctx *Context) addToolCallParameter(call *ToolCall, shape *calcShape, param // declaring no result parameter, as a body that returns nothing reports. The failures // of a performance's tool apply unchanged: an annotation naming no tool, or a context // with no runner, is not registered and the body never stands in. -func (ctx *Context) computeCalcByTool(shape *calcShape, scope *symbols.Scope, held func(param string) (Value, bool)) (result Value, returned bool, outputs map[string]Value, err error) { +func (ctx *Context) computeCalcByTool(shape *loadedCalc, scope *symbols.Scope, held func(param string) (Value, bool)) (result Value, returned bool, outputs map[string]Value, err error) { tool := shape.Tool.tool if tool == "" { return Value{}, false, nil, &ToolNotRegisteredError{Tool: tool} diff --git a/internal/exec/runtime/undetermined.go b/internal/exec/runtime/undetermined.go index f49262e204..c01729911f 100644 --- a/internal/exec/runtime/undetermined.go +++ b/internal/exec/runtime/undetermined.go @@ -453,15 +453,15 @@ func (ctx *Context) openArgumentsOf(name string, args []Value) error { return nil } for i, arg := range args { - if arg.Kind != ValUndetermined || i >= len(shape.Params) || shape.Params[i].Decl.Target == nil { + if arg.Kind != ValUndetermined || i >= len(shape.Params) || shape.paramDecls[i].Target == nil { continue } - param := shape.Params[i] - if !ctx.openValueMayBe(arg, param.Decl.Target.typ) { + param, target := shape.Params[i], shape.paramDecls[i].Target + if !ctx.openValueMayBe(arg, target.typ) { return fmt.Errorf("%w: function %s parameter %q requires a %s value, got %s", - ErrTypeMismatch, writtenName(name), param.Name, param.Decl.Target.typ.Name, ctx.describeOpenOperand(arg)) + ErrTypeMismatch, writtenName(name), param.Name, target.typ.Name, ctx.describeOpenOperand(arg)) } - if msg := ctx.writeCountRefusal(param.Decl.Target, &arg); msg != "" { + if msg := ctx.writeCountRefusal(target, &arg); msg != "" { return fmt.Errorf("%w: function %s parameter %q: %s", ErrMultiplicityViolation, writtenName(name), param.Name, msg) } @@ -498,8 +498,8 @@ func (ctx *Context) libraryResultCount(fqn string) semantics.Range { if err != nil { return openRange() } - if result := shape.resultOutput(); result != nil && result.Decl.Target != nil { - return result.Decl.Target.mult + if at := shape.ResultOutputIndex(); at >= 0 && shape.outputDecls[at].Target != nil { + return shape.outputDecls[at].Target.mult } return openRange() } diff --git a/internal/exec/runtime/value_kinds_test.go b/internal/exec/runtime/value_kinds_test.go index ca4913f738..a269f5e7bd 100644 --- a/internal/exec/runtime/value_kinds_test.go +++ b/internal/exec/runtime/value_kinds_test.go @@ -4,6 +4,7 @@ import ( "errors" "testing" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -36,7 +37,7 @@ func kindSamples() map[ValueKind][2]Value { } spatial, temporal := frameOf("spatial", metre, metre), frameOf("temporal", second, second) functionOf := func(name string, sym *symbols.Symbol) Value { - return Value{Kind: ValFunction, ref: &functionValue{shape: &calcShape{Sym: sym, Name: name}}} + return Value{Kind: ValFunction, ref: &functionValue{shape: &loadedCalc{Calc: &program.Calc{Sym: sym, Name: name}}}} } placementOf := func(source, target *CoordinateFrame, origin int64) *CoordinateTransformation { return &CoordinateTransformation{Source: source, Target: target, Placement: &FramePlacement{ @@ -86,7 +87,7 @@ func kindSamples() map[ValueKind][2]Value { // two runs bind the names it closes over differently. func TestFunctionValueIdentity(t *testing.T) { sym := &symbols.Symbol{Name: "inner"} - shape := &calcShape{Sym: sym, Name: "inner"} + shape := &loadedCalc{Calc: &program.Calc{Sym: sym, Name: "inner"}} self := &Instance{ID: 7} bare := func() Value { return Value{Kind: ValFunction, ref: &functionValue{shape: shape, self: self}} } closing := func(run int64) Value { diff --git a/internal/exec/runtime/verification_run.go b/internal/exec/runtime/verification_run.go index fb8ec49737..3154925617 100644 --- a/internal/exec/runtime/verification_run.go +++ b/internal/exec/runtime/verification_run.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -235,7 +236,7 @@ func (ctx *Context) subcaseVerdicts(run *calcRun) []VerificationVerdict { } var out []VerificationVerdict for _, node := range run.shape.Nodes { - sym := memberSymbol(run.shape.bodyScope(), node) + sym := memberSymbol(run.shape.BodyScope(), node) if !IsVerificationCaseSymbol(sym) { continue } @@ -276,5 +277,5 @@ func verdictOutputNames(result string) []string { if result != "" { names = append(names, result) } - return append(names, verdictOutputName, resultOutputName) + return append(names, verdictOutputName, program.ResultOutputName) } diff --git a/internal/ir/lower/calc.go b/internal/ir/lower/calc.go new file mode 100644 index 0000000000..875c52ae0c --- /dev/null +++ b/internal/ir/lower/calc.go @@ -0,0 +1,787 @@ +package lower + +import ( + "iter" + "slices" + + "github.com/Open-MBEE/OpenSysML/internal/ir/program" + "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" +) + +// CalcEnv is what lowering a calc reads beyond the syntax tree: the semantic +// model and resolver, and which library calcs the runtime realizes itself. +type CalcEnv struct { + Semantics *semantics.Model + Resolver *resolve.Resolver + // Realized reports a library calc the runtime realizes rather than runs as + // written; it contributes no members to the chain of a calc specializing it. + Realized func(*symbols.Symbol) bool +} + +// qualifiedName renders a symbol's qualified name, falling back to its local +// name when no index is reachable. +func (env CalcEnv) qualifiedName(sym *symbols.Symbol) string { + if sym == nil { + return "" + } + if env.Resolver != nil && env.Resolver.Index() != nil { + if fqn := env.Resolver.Index().GetFQN(sym); fqn != "" { + return fqn + } + } + return sym.Name +} + +// CalcTrees is what a lowered calc still keeps as syntax trees beside its program.Calc, +// until expressions are lowered: the body, the bindings and the table its ExprIDs index. +type CalcTrees struct { + // Body is the computation the calc states, lowered in the scope of the calc that + // declares it, in declaration order; BodyOrder is its calculation-statement order. + Body []Statement + BodyOrder *StatementOrder + // Steps is Body without the bindings of its `out` features, which are + // evaluated when those features are read rather than run as statements. + Steps []Statement + // Nodes are the action nodes the body's flow performs as the steps of a case. + Nodes []ast.Node + Bindings []Binding + exprs []ast.Node +} + +// Expr is the expression behind an id, nil for none. +func (t *CalcTrees) Expr(id program.ExprID) ast.Node { + if t == nil || id == 0 || int(id) >= len(t.exprs) { + return nil + } + return t.exprs[id] +} + +// intern gives node an id, 0 for nil. +func (t *CalcTrees) intern(node ast.Node) program.ExprID { + if node == nil { + return 0 + } + if len(t.exprs) == 0 { + t.exprs = make([]ast.Node, 1, 8) + } + t.exprs = append(t.exprs, node) + return program.ExprID(tableIndex(len(t.exprs) - 1)) +} + +// IsCalcDecl reports whether a declaration is a calc definition or usage, or an +// analysis or verification case, each a calculation (SysML v2 §7.22, §7.23). +func IsCalcDecl(decl ast.Node) bool { + switch d := decl.(type) { + case *ast.Definition: + return d.Kind == ast.DefCalc || d.Kind == ast.DefAnalysisCase || d.Kind == ast.DefVerificationCase + case *ast.Usage: + return d.Kind == ast.UsageCalc || d.Kind == ast.UsageAnalysisCase || d.Kind == ast.UsageVerificationCase + default: + return false + } +} + +// IsPredicateDecl reports a declaration an invocation applies as a predicate: a +// constraint or requirement definition or usage, an objective among the latter. +func IsPredicateDecl(decl ast.Node) bool { + if _, ok := ast.OwnedConstraintOf(decl); ok { + return true + } + switch d := decl.(type) { + case *ast.Definition: + return d.Kind == ast.DefConstraint || d.Kind == ast.DefRequirement + case *ast.Usage: + return d.Kind == ast.UsageConstraint || d.Kind == ast.UsageRequirement || d.Kind == ast.UsageObjective + } + return false +} + +// PredicateKind names the kind of predicate sym declares, as verdicts name it. +func PredicateKind(sym *symbols.Symbol) string { + switch d := sym.Decl.(type) { + case *ast.Definition: + if d.Kind == ast.DefConstraint { + return "constraint" + } + case *ast.Usage: + switch d.Kind { + case ast.UsageConstraint: + return "constraint" + case ast.UsageObjective: + return "objective" + } + } + return "requirement" +} + +// isVerificationCase reports whether sym declares a verification case, reading +// its declaration when it has one and its symbol kind otherwise. +func isVerificationCase(sym *symbols.Symbol) bool { + switch d := sym.Decl.(type) { + case *ast.Definition: + return d.Kind == ast.DefVerificationCase + case *ast.Usage: + return d.Kind == ast.UsageVerificationCase + } + if sym.Decl != nil { + return false + } + return sym.Kind == symbols.SymbolVerificationCaseDef || sym.Kind == symbols.SymbolVerificationCaseUsage +} + +// CalcKind names what sym declares — a calc, an analysis case or a verification +// case — as diagnostics refer to it. +func CalcKind(sym *symbols.Symbol) string { + switch { + case isVerificationCase(sym): + return "verification" + case PerformsSteps(sym.Decl): + return "analysis" + default: + return "calc" + } +} + +// UnwrappedDeclMembers is ast.DeclMembers extended to named owned constraints, +// with the Membership wrappers the parser produces unwrapped. +func UnwrappedDeclMembers(decl ast.Node) []ast.Node { + if body, ok := constraintMemberBody(decl); ok { + return body + } + members := ast.DeclMembers(decl) + if members == nil { + return nil + } + out := make([]ast.Node, 0, len(members)) + for member := range EachUnwrappedDeclMember(decl) { + out = append(out, member) + } + return out +} + +// EachUnwrappedDeclMember yields what UnwrappedDeclMembers lists without building the list. +func EachUnwrappedDeclMember(decl ast.Node) iter.Seq[ast.Node] { + return func(yield func(ast.Node) bool) { + if body, ok := constraintMemberBody(decl); ok { + for _, member := range body { + if !yield(member) { + return + } + } + return + } + for _, member := range ast.DeclMembers(decl) { + if !yield(unwrapMembership(member)) { + return + } + } + } +} + +// constraintMemberBody returns the nested members of an assume or require member +// in either of its forms, and false for every other declaration. +func constraintMemberBody(decl ast.Node) ([]ast.Node, bool) { + switch m := decl.(type) { + case *ast.AssumeMember: + return m.Body, true + case *ast.RequireMember: + return m.Body, true + } + return nil, false +} + +// declScope is the scope a symbol's members are declared in: its own, else its owner's. +func declScope(sym *symbols.Symbol) *symbols.Scope { + if sym == nil { + return nil + } + if sym.Scope != nil { + return sym.Scope + } + return sym.OwnerScope +} + +// memberSymbol returns the symbol scope declares for node, anonymous ones +// included, or nil when there is none. +func memberSymbol(scope *symbols.Scope, node ast.Node) *symbols.Symbol { + if scope == nil || node == nil { + return nil + } + return scope.MemberDeclaring(node) +} + +// SubjectDecl is a case's subject as declared: its name, its binding and the node. +type SubjectDecl struct { + Name string + Value ast.Node + Node ast.Node +} + +// SubjectDeclaration reads a subject declaration, written as a subject member +// (`subject s : T;`, `subject = expr;`) or a subject-kind usage. +func SubjectDeclaration(member ast.Node) (SubjectDecl, bool) { + switch m := member.(type) { + case *ast.SubjectMember: + name, _ := m.EffectiveName() + return SubjectDecl{Name: name, Value: m.BindingExpr, Node: m}, true + case *ast.Usage: + if m.Kind != ast.UsageSubject { + return SubjectDecl{}, false + } + name, _ := ast.EffectiveName(m) + return SubjectDecl{Name: name, Value: m.Value, Node: m}, true + } + return SubjectDecl{}, false +} + +// CalcChain returns the calcs sym takes members from, most general first, then sym; +// non-calc links and the library's frame contribute nothing, a domain library's calc does. +func CalcChain(sym *symbols.Symbol, env CalcEnv) []*symbols.Symbol { + return memberChain(sym, env, IsCalcDecl) +} + +func memberChain(sym *symbols.Symbol, env CalcEnv, declares func(ast.Node) bool) []*symbols.Symbol { + supers := env.Semantics.MemberSources(sym) + chain := make([]*symbols.Symbol, 0, len(supers)+1) + for i := len(supers) - 1; i >= 0; i-- { + if supers[i] != nil && declares(supers[i].Decl) && (env.Realized == nil || !env.Realized(supers[i])) { + chain = append(chain, supers[i]) + } + } + return append(chain, sym) +} + +// calcOrigin names the calc the way a message does. +func calcOrigin(sym *symbols.Symbol, name, kind string) program.Origin { + var span source.Span + if sym.Decl != nil { + span = sym.Decl.Span() + } + return program.Origin{ + Doc: symbols.DocNameOf(declScope(sym)), + Span: span, + Path: name, + Kind: kind, + Name: name, + } +} + +// ToCalc lowers the invocation interface of the calc sym — parameters, outputs, body and +// result, each resolved across the chain; the runtime judges from it whether the calc computes. +func ToCalc(sym *symbols.Symbol, env CalcEnv) (*program.Calc, *CalcTrees) { + name := env.qualifiedName(sym) + kind := CalcKind(sym) + chain := CalcChain(sym, env) + calc := &program.Calc{ + Sym: sym, + Origin: calcOrigin(sym, name, kind), + Name: name, + Kind: kind, + Label: kind + " " + name, + Chain: chain, + } + trees := &CalcTrees{} + trees.Body, trees.BodyOrder, calc.BodyOwner = calcBodyWithOrder(chain, env.Resolver) + trees.Steps = CalcSteps(trees.Body) + trees.Nodes = BlockNodes(trees.Steps) + calc.StepCount = len(trees.Nodes) + calc.Params = calcParameters(chain, env, trees, &calc.Aliases) + calc.Outputs = calcOutputs(chain, env, trees, &calc.Aliases) + calc.ParamNames = paramNames(calc.Params) + calc.BodyOutputs = assignedOutputs(trees.Steps, calc.Outputs, calc.Aliases) + trees.Bindings = calcBindings(chain) + calc.Result = trees.intern(resultBindingExpr(trees.Bindings)) + calc.Returns = Returns(trees.Body) + calc.StepsReturn = Returns(trees.Steps) + return calc, trees +} + +// ToPredicate lowers the invocation interface of the predicate sym: its +// parameters, the subject first; its conditions stand for a body. +func ToPredicate(sym *symbols.Symbol, env CalcEnv) (*program.Calc, *CalcTrees) { + chain := memberChain(sym, env, IsPredicateDecl) + name := env.qualifiedName(sym) + if sym.Name == "" { + // An anonymous usage answers to the name it redefines (`objective : MinimizeObjective;`). + name += env.Semantics.EffectiveNameOf(sym) + } + kind := PredicateKind(sym) + calc := &program.Calc{ + Sym: sym, + Origin: calcOrigin(sym, name, kind), + Name: name, + Kind: kind, + Label: kind + " " + name, + Chain: chain, + } + trees := &CalcTrees{} + calc.Params = calcParameters(chain, env, trees, &calc.Aliases) + calc.ParamNames = paramNames(calc.Params) + return calc, trees +} + +// ToCalcSignature lowers the bodiless effective inputs of a model calc applying an inherited +// library function, with, per parameter, the index in libInputs of the input it redefines (-1: none). +func ToCalcSignature(sym *symbols.Symbol, libInputs []*symbols.Symbol, env CalcEnv) (*program.Calc, *CalcTrees, []int) { + name := env.qualifiedName(sym) + kind := CalcKind(sym) + calc := &program.Calc{ + Sym: sym, + Origin: calcOrigin(sym, name, kind), + Name: name, + Kind: kind, + Label: kind + " " + name, + } + trees := &CalcTrees{} + var positions []int + for _, p := range env.Semantics.BehaviorParametersOf(sym) { + if p.IsResult || (p.Direction != ast.DirIn && p.Direction != ast.DirInOut) { + continue + } + param, position := effectiveParameter(p.Symbol, libInputs, env, trees) + calc.Params = append(calc.Params, param) + positions = append(positions, position) + } + calc.ParamNames = paramNames(calc.Params) + return calc, trees, positions +} + +// effectiveParameter is the input parameter sym as a call binds it, with the +// position of the library input it redefines, -1 for none. +func effectiveParameter(sym *symbols.Symbol, libInputs []*symbols.Symbol, env CalcEnv, trees *CalcTrees) (program.CalcParam, int) { + param := program.CalcParam{Name: sym.Name, Sym: sym, IsCalc: IsCalcUsageSymbol(sym)} + if effective, _ := ast.EffectiveName(sym.Decl.(*ast.Usage)); effective != "" { + param.Name = effective + } + var dflt ast.Node + for _, link := range env.Semantics.ParameterRedefinitionChain(sym) { + owner := link.OwnerScope.Owner() + // The chain runs nearest first, so each declaration is more general than the recorded ones. + param.Declarations = slices.Insert(param.Declarations, 0, program.CalcMember{Feature: link, Owner: owner, Name: param.Name}) + if at := slices.Index(libInputs, link); at >= 0 { + param.Default = trees.intern(dflt) + return param, at + } + if libraryDeclared(link, env) { + break + } + if value := DefaultValueOf(link); dflt == nil && value != nil { + dflt, param.Owner = value, owner + } + } + param.Default = trees.intern(dflt) + return param, -1 +} + +// libraryDeclared reports whether sym was declared by one of the library +// documents rather than by the model under evaluation. +func libraryDeclared(sym *symbols.Symbol, env CalcEnv) bool { + if env.Resolver == nil { + return false + } + idx := env.Resolver.Index() + return idx != nil && idx.Library(sym) +} + +// DefaultValueOf is the value a feature's declaration binds: a usage's value, a +// subject's binding or an owned constraint's; nil for none. +func DefaultValueOf(featureSym *symbols.Symbol) ast.Node { + if oc, ok := ast.OwnedConstraintOf(featureSym.Decl); ok { + return oc.Value + } + switch decl := featureSym.Decl.(type) { + case *ast.Usage: + return decl.Value + case *ast.SubjectMember: + return decl.BindingExpr + } + return nil +} + +// IsCalcUsageSymbol reports whether sym declares a calc or case usage, the forms whose +// outputs are features; a definition is a type, invoked rather than read. +func IsCalcUsageSymbol(sym *symbols.Symbol) bool { + if sym == nil { + return false + } + if sym.Decl != nil { + usage, ok := sym.Decl.(*ast.Usage) + return ok && (usage.Kind == ast.UsageCalc || usage.Kind == ast.UsageAnalysisCase || + usage.Kind == ast.UsageVerificationCase) + } + return sym.Kind == symbols.SymbolCalcUsage || sym.Kind == symbols.SymbolAnalysisCaseUsage || + sym.Kind == symbols.SymbolVerificationCaseUsage +} + +func paramNames(params []program.CalcParam) []string { + names := make([]string, len(params)) + for i, param := range params { + names[i] = param.Name + } + return names +} + +// calcParameters flattens the input parameters declared along chain (most general +// first): a redeclaration keeps its position and default, a renamed one is aliased. +func calcParameters(chain []*symbols.Symbol, env CalcEnv, trees *CalcTrees, aliases *map[string]string) []program.CalcParam { + var params []program.CalcParam + index := make(map[string]int) + + for _, link := range chain { + for _, member := range UnwrappedDeclMembers(link.Decl) { + if subject, ok := SubjectDeclaration(member); ok { + params = subjectParameter(params, index, aliases, link, member, subject, env, trees) + continue + } + usage, ok := member.(*ast.Usage) + if !ok { + continue + } + name, _ := ast.EffectiveName(usage) + if name == "" { + continue + } + actor := usage.Kind == ast.UsageActor + if !actor && usage.Direction != ast.DirIn && usage.Direction != ast.DirInOut { + continue + } + sym := memberSymbol(declScope(link), usage) + param := program.CalcParam{ + Name: name, Default: trees.intern(usage.Value), Owner: link, + Sym: sym, IsActor: actor, + Declarations: declarationsOf(nil, link, sym, name), IsCalc: IsCalcUsageSymbol(sym), + } + if at, seen := redeclaredIndex(index, sym, name, env); seen { + // A redeclaration binding no value keeps the inherited default, + // which is written in the scope of the calc that stated it. + if param.Default == 0 { + param.Default = params[at].Default + param.Owner = params[at].Owner + } + param.Declarations = declarationsOf(params[at].Declarations, link, sym, name) + *aliases = program.AliasRedefined(*aliases, params[at].Name, name) + params[at] = param + index[name] = at + continue + } + index[param.Name] = len(params) + params = append(params, param) + } + } + return params +} + +// declarationsOf appends the declaration of sym by link, under name, to those it +// redeclares; a member link declares no symbol for states nothing. +func declarationsOf(redeclared []program.CalcMember, link, sym *symbols.Symbol, name string) []program.CalcMember { + if sym == nil { + return redeclared + } + out := make([]program.CalcMember, len(redeclared), len(redeclared)+1) + copy(out, redeclared) + return append(out, program.CalcMember{Feature: sym, Owner: link, Name: name}) +} + +// redeclaredIndex finds the flattened member sym (named name) redeclares: by its own +// name, by a redefinition (`in y :>> x`), else by its role's position (actor over actor). +func redeclaredIndex(index map[string]int, sym *symbols.Symbol, name string, env CalcEnv) (int, bool) { + if at, seen := index[name]; seen { + return at, true + } + for _, redefined := range env.Semantics.RedefinedFeatures(sym) { + if at, seen := index[redefined.Name]; seen { + return at, true + } + } + for _, redefined := range env.Semantics.ImplicitRoleRedefinitions(sym) { + if at, seen := index[redefined.Name]; seen { + return at, true + } + } + return 0, false +} + +// subjectParameter records link's subject among params: refining the subject an +// earlier link declared, else inserting it first, the position a subject binds by. +func subjectParameter( + params []program.CalcParam, index map[string]int, aliases *map[string]string, + link *symbols.Symbol, member ast.Node, subject SubjectDecl, env CalcEnv, trees *CalcTrees, +) []program.CalcParam { + sym := memberSymbol(declScope(link), member) + param := program.CalcParam{ + Name: subject.Name, Default: trees.intern(subject.Value), Owner: link, IsSubject: true, + Sym: sym, + } + at := -1 + for i := range params { + if params[i].IsSubject { + at = i + break + } + } + if at < 0 { + if seen, ok := redeclaredIndex(index, sym, subject.Name, env); ok { + at = seen + } + } + if at >= 0 { + // A redeclaration binding no value keeps the inherited binding; one + // declaring no name keeps the inherited name. + if param.Default == 0 { + param.Default, param.Owner = params[at].Default, params[at].Owner + } + if param.Name == "" { + param.Name = params[at].Name + } + param.Declarations = declarationsOf(params[at].Declarations, link, sym, subject.Name) + if params[at].Name != param.Name { + *aliases = program.AliasRedefined(*aliases, params[at].Name, param.Name) + delete(index, params[at].Name) + } + params[at] = param + index[param.Name] = at + return params + } + param.Declarations = declarationsOf(nil, link, sym, subject.Name) + if param.Name == "" { + param.Name = "subject" + } + for name, i := range index { + index[name] = i + 1 + } + index[param.Name] = 0 + return append([]program.CalcParam{param}, params...) +} + +// calcOutputs flattens the output features declared along chain (most general +// first), as calcParameters does the inputs, recording renamed ones in aliases. +func calcOutputs(chain []*symbols.Symbol, env CalcEnv, trees *CalcTrees, aliases *map[string]string) []program.CalcOutput { + var outs []program.CalcOutput + index := make(map[string]int) + + for _, link := range chain { + for _, member := range UnwrappedDeclMembers(link.Decl) { + usage, ok := member.(*ast.Usage) + if !ok { + continue + } + if usage.Direction != ast.DirOut && usage.Direction != ast.DirInOut && !usage.IsResult { + continue + } + // An output written as a redefinition names the one it overrides. + name, _ := ast.EffectiveName(usage) + sym := memberSymbol(declScope(link), usage) + out := program.CalcOutput{Name: name, Value: trees.intern(usage.Value), Owner: link, IsResult: usage.IsResult, + IsInitial: usage.Value != nil && usage.ValueIsInitial, Declarations: declarationsOf(nil, link, sym, name)} + if usage.Direction == ast.DirInOut { + // The value an `inout` declares is the default of the parameter, not a + // binding of the output: the invocation binds it either way. + out.IsInOut, out.Value = true, 0 + } + + var at int + var seen bool + if name != "" { + at, seen = redeclaredIndex(index, sym, name, env) + } else { + // An anonymous result parameter has no name to redeclare by, so + // it refines the anonymous result it inherits. + at, seen = indexOfAnonymousResult(outs) + } + if seen { + if out.Value == 0 { + out.Value = outs[at].Value + out.Owner = outs[at].Owner + out.IsInitial = outs[at].IsInitial + } + out.Declarations = declarationsOf(outs[at].Declarations, link, sym, name) + if name != "" { + *aliases = program.AliasRedefined(*aliases, outs[at].Name, name) + index[name] = at + } + outs[at] = out + continue + } + if name != "" { + index[name] = len(outs) + } + outs = append(outs, out) + } + } + return outs +} + +// indexOfAnonymousResult finds the unnamed result parameter among outs, which is +// the one an unnamed redeclaration refines. +func indexOfAnonymousResult(outs []program.CalcOutput) (int, bool) { + for i, out := range outs { + if out.Name == "" { + return i, true + } + } + return 0, false +} + +// assignedOutputs are the outputs the body's statements assign, on any path +// through it: what the calc computes, whichever way an execution branches. +func assignedOutputs(stmts []Statement, outputs []program.CalcOutput, aliases map[string]string) map[string]bool { + declared := make(map[string]string, len(outputs)+len(aliases)) + for _, out := range outputs { + if out.Name != "" && !out.IsInOut { + declared[out.Name] = out.Name + } + } + for alias, name := range aliases { + if _, ok := declared[name]; ok { + declared[alias] = name + } + } + assigned := make(map[string]bool) + collectAssignedOutputs(stmts, declared, assigned) + return assigned +} + +// collectAssignedOutputs walks the statements and their blocks for assignments to a +// declared output by any of its names; a chained target writes another object's feature. +func collectAssignedOutputs(stmts []Statement, declared map[string]string, assigned map[string]bool) { + for _, stmt := range stmts { + switch s := stmt.(type) { + case Assign: + if name, ok := declared[s.Target]; ok && s.Chain == nil { + assigned[name] = true + } + case Block: + collectAssignedOutputs(s.Steps(), declared, assigned) + case Loop: + collectAssignedOutputs(s.Body.Steps(), declared, assigned) + case If: + collectAssignedOutputs(s.Then.Steps(), declared, assigned) + if s.Else != nil { + collectAssignedOutputs(s.Else.Steps(), declared, assigned) + } + } + } +} + +// calcBodyWithOrder returns the body the invoked calc runs — its own, else the closest +// inherited one — with its statement order and the calc whose scope it is written in. +func calcBodyWithOrder(chain []*symbols.Symbol, resolver *resolve.Resolver) ([]Statement, *StatementOrder, *symbols.Symbol) { + var stated []Statement + var statedOrder *StatementOrder + var owner *symbols.Symbol + for i := len(chain) - 1; i >= 0; i-- { + link := chain[i] + stmts, order := CalcBodyWithOrder(link.Decl, UnwrappedDeclMembers(link.Decl), link.Scope, resolver) + if Returns(stmts) { + return stmts, order, link + } + // A body that computes but returns nothing leaves an inherited result in + // force, so keep looking up the chain before settling for it. + if stated == nil && len(stmts) > 0 { + stated, statedOrder, owner = stmts, order, link + } + } + return stated, statedOrder, owner +} + +func calcBindings(chain []*symbols.Symbol) []Binding { + var out []Binding + for _, link := range chain { + if link != nil { + out = append(out, ToBindings(link.Decl, declScope(link))...) + } + } + return out +} + +// resultBindingExpr is the expression a binding connector binds to `result`, nil for none. +func resultBindingExpr(bindings []Binding) ast.Node { + var result ast.Node + for _, binding := range bindings { + for i := range binding.Ends { + if binding.Ends[i].Path != "result" { + continue + } + other := binding.Ends[1-i] + if other.Expr != nil { + result = other.Expr + } + } + } + return result +} + +// CalcStatesComputation reports whether a calc chain states a computation: a body +// that returns or assigns an output, or a binding of the result. +func CalcStatesComputation(chain []*symbols.Symbol, env CalcEnv) bool { + body, _, _ := calcBodyWithOrder(chain, env.Resolver) + if Returns(body) || resultBindingExpr(calcBindings(chain)) != nil { + return true + } + var aliases map[string]string + trees := &CalcTrees{} + return len(assignedOutputs(CalcSteps(body), calcOutputs(chain, env, trees, &aliases), aliases)) > 0 +} + +// UnboundResultHint explains a `return` that declares a result parameter without +// binding it (`return h;` declares h, it does not return the member h). +func UnboundResultHint(chain []*symbols.Symbol) string { + for i := len(chain) - 1; i >= 0; i-- { + if chain[i] == nil { + continue + } + members := UnwrappedDeclMembers(chain[i].Decl) + result := unboundResultParameter(members) + if result == nil { + continue + } + name, _ := ast.EffectiveName(result) + who, trailing, expr := "the result parameter", "of the body", "" + typ := TypeText(result) + if name != "" { + spelled := source.NameText(name) + who = "result parameter " + spelled + if sibling := valuedMemberNamed(members, name, result); sibling != nil { + trailing, expr = "`"+spelled+"`", spelled + if typ == "" { + typ = TypeText(sibling) + } + } + } + if typ == "" { + typ = "" + } + return ": " + who + " binds no value; write the result as the trailing expression " + trailing + + ", or bind it with `return : " + typ + " = " + expr + ";`" + } + return "" +} + +// unboundResultParameter returns the body's `return` member that binds no value. +func unboundResultParameter(members []ast.Node) *ast.Usage { + for _, member := range members { + if u, ok := member.(*ast.Usage); ok && u.IsResult && u.Value == nil { + return u + } + } + return nil +} + +// valuedMemberNamed returns the body member called name, other than except, +// whose declaration binds a value; an unbound one would not compute a result. +func valuedMemberNamed(members []ast.Node, name string, except *ast.Usage) *ast.Usage { + for _, member := range members { + u, ok := member.(*ast.Usage) + if !ok || u == except || u.Value == nil { + continue + } + if actual, _ := ast.EffectiveName(u); actual == name { + return u + } + } + return nil +} diff --git a/internal/ir/lower/calc_test.go b/internal/ir/lower/calc_test.go new file mode 100644 index 0000000000..af7115c12c --- /dev/null +++ b/internal/ir/lower/calc_test.go @@ -0,0 +1,132 @@ +package lower + +import ( + "reflect" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/ir/program" + "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/parser" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" +) + +// TestChainedAssignmentBindsNoCalcOutput locks that a chained target writing another +// object's feature does not count as the calc computing its own output of that name. +func TestChainedAssignmentBindsNoCalcOutput(t *testing.T) { + outputs := []program.CalcOutput{{Name: "x"}} + chained := []Statement{Assign{Target: "x", Chain: &AssignTarget{Text: "foo.x"}}} + if assigned := assignedOutputs(chained, outputs, nil); len(assigned) != 0 { + t.Errorf("assignedOutputs(chained) = %v; want none, since foo.x is another object's feature", assigned) + } + plain := []Statement{Assign{Target: "x"}} + if assigned := assignedOutputs(plain, outputs, nil); !assigned["x"] { + t.Errorf("assignedOutputs(plain) = %v; want x, which the body does bind", assigned) + } +} + +const calcIdentitySrc = ` + package P { + calc def Base { in a : Real = 1.0; in b : Real; return : Real = a + b; } + calc def Derived :> Base { in c : Real = 2.0; out d : Real = a * c; } + analysis def Study { subject s = 3; in n = 4; return : Real = n; } + } +` + +// identityCalc lowers the member of P named name in src, with the environment a +// runtime would hand the lowering. +func identityCalc(t *testing.T, src, name string) (*program.Calc, *CalcTrees) { + t.Helper() + p := parser.New(source.New("calc.sysml", []byte(src))) + root := p.ParseFile() + if len(p.Diagnostics) > 0 { + t.Fatalf("parse errors: %v", p.Diagnostics) + } + idx := symbols.NewIndexFromDoc("calc.sysml", root) + resolver := resolve.New(idx) + sym, _ := idx.DocumentRoot("calc.sysml").ChildFor(root.Members[0].(*ast.Membership).Member).LookupLocal(name) + if sym == nil { + t.Fatalf("P::%s not indexed", name) + } + return ToCalc(sym, CalcEnv{Semantics: semantics.NewModel(resolver), Resolver: resolver}) +} + +// TestToCalcResolvesTheChain pins what ToCalc records for an inherited interface: +// parameters most general first, redeclared outputs, the result and the body owner. +func TestToCalcResolvesTheChain(t *testing.T) { + calc, trees := identityCalc(t, calcIdentitySrc, "Derived") + if calc.Name != "P::Derived" || calc.Kind != "calc" || calc.Label != "calc P::Derived" { + t.Fatalf("identity = %q %q %q", calc.Name, calc.Kind, calc.Label) + } + if calc.Origin.Path != "P::Derived" || calc.Origin.Doc != "calc.sysml" { + t.Fatalf("origin = %+v", calc.Origin) + } + if got := calc.ParamNames; !reflect.DeepEqual(got, []string{"a", "b", "c"}) { + t.Fatalf("ParamNames = %v", got) + } + if len(calc.Chain) != 2 || calc.Chain[1] != calc.Sym { + t.Fatalf("Chain = %v", calc.Chain) + } + if calc.Params[0].Default == 0 || calc.Params[1].Default != 0 || calc.Params[2].Default == 0 { + t.Fatalf("defaults = %v %v %v", calc.Params[0].Default, calc.Params[1].Default, calc.Params[2].Default) + } + if trees.Expr(calc.Params[0].Default) == nil || trees.Expr(calc.Params[1].Default) != nil { + t.Fatal("Expr does not answer the defaults' ids") + } + if len(calc.Params[0].Declarations) != 1 || calc.Params[0].Declarations[0].Owner != calc.Chain[0] { + t.Fatalf("Declarations of a = %+v", calc.Params[0].Declarations) + } + if at := calc.ResultOutputIndex(); at < 0 || calc.Outputs[at].Value == 0 || calc.Outputs[at].Owner != calc.Chain[0] { + t.Fatalf("result output = %d %+v", at, calc.Outputs) + } + // The `out d = a * c` binding is a return of the body, not a step the invocation runs. + if !calc.Returns || calc.StepsReturn || calc.Result != 0 || calc.BodyOwner != calc.Sym { + t.Fatalf("Returns=%v StepsReturn=%v Result=%v BodyOwner=%v", calc.Returns, calc.StepsReturn, calc.Result, calc.BodyOwner) + } +} + +// TestToCalcIDsDenseAndDeterministic pins the expression table: ids dense from 1 +// in the order the record states them, the same across two lowerings. +func TestToCalcIDsDenseAndDeterministic(t *testing.T) { + first, trees := identityCalc(t, calcIdentitySrc, "Study") + second, _ := identityCalc(t, calcIdentitySrc, "Study") + var ids []program.ExprID + for _, p := range first.Params { + ids = append(ids, p.Default) + } + for _, out := range first.Outputs { + ids = append(ids, out.Value) + } + ids = append(ids, first.Result) + seen := map[program.ExprID]bool{} + for _, id := range ids { + if id == 0 { + continue + } + if seen[id] || int(id) >= len(trees.exprs) || trees.exprs[id] == nil { + t.Fatalf("id %d is not a distinct table index: %v", id, ids) + } + seen[id] = true + } + if len(seen) != len(trees.exprs)-1 { + t.Fatalf("%d ids for %d expressions", len(seen), len(trees.exprs)-1) + } + if first.Kind != "analysis" || !first.Params[0].IsSubject || first.ParamNames[0] != "s" { + t.Fatalf("subject not first: %+v", first.ParamNames) + } + first.Sym, second.Sym, first.Chain, second.Chain = nil, nil, nil, nil + for i := range first.Params { + first.Params[i].Sym, first.Params[i].Owner, first.Params[i].Declarations = nil, nil, nil + second.Params[i].Sym, second.Params[i].Owner, second.Params[i].Declarations = nil, nil, nil + } + for i := range first.Outputs { + first.Outputs[i].Owner, first.Outputs[i].Declarations = nil, nil + second.Outputs[i].Owner, second.Outputs[i].Declarations = nil, nil + } + first.BodyOwner, second.BodyOwner = nil, nil + if !reflect.DeepEqual(first, second) { + t.Fatalf("two lowerings differ:\n%+v\n%+v", first, second) + } +} diff --git a/internal/ir/program/calc.go b/internal/ir/program/calc.go new file mode 100644 index 0000000000..e823d2a05e --- /dev/null +++ b/internal/ir/program/calc.go @@ -0,0 +1,244 @@ +package program + +import ( + "strings" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" +) + +// Calc is a calculation's lowered invocation interface: its positional inputs, its outputs +// and its result, each resolved across the specialization chain the usage inherits from. +type Calc struct { + Sym *symbols.Symbol + // Origin names the calc the way a message does: Path and Name are its + // qualified name, Kind the word diagnostics name the behavior by. + Origin Origin + // Name is the qualified name; Kind the behavior's keyword (`calc`, `analysis`, + // `verification`, a predicate's kind); Label is Kind then Name, as diagnostics say it. + Name string + Kind string + Label string + // Chain is the calcs the invoked one takes members from, most general first, + // then the invoked calc itself. + Chain []*symbols.Symbol + Params []CalcParam + // ParamNames are the Params' names by position: the slots an invocation binds. + ParamNames []string + // Aliases map the name of a parameter or output redeclared under a new name + // (`in g :>> x`) to that name, which an inherited body reads it by. + Aliases map[string]string + // Outputs are the calc's output features — its `out` parameters and the + // result parameter a `return` declares — in declaration order. + Outputs []CalcOutput + // BodyOwner is the calc whose body the invocation runs: the invoked calc's own + // when it states one, else the closest inherited body. + BodyOwner *symbols.Symbol + // BodyOutputs are the output features some statement of the body assigns on any + // path: a calc binding its outputs this way states a computation without returning. + BodyOutputs map[string]bool + // Result is the expression a binding connector binds to `result`, 0 when none. + Result ExprID + // Returns reports a body that returns a value on some path; StepsReturn says + // the same of its steps alone, the bindings of its outputs left out. + Returns bool + StepsReturn bool + // StepCount counts the action nodes a case's body performs as its steps. + StepCount int +} + +// CalcParam is one input parameter of a calc, in positional order. +type CalcParam struct { + Name string // parameter name arguments bind to + // Default is the value-binding expression used when no argument is passed, 0 if none. + Default ExprID + Owner *symbols.Symbol // the calc that declares the default (a supertype for an inherited one) + Sym *symbols.Symbol // the parameter's own symbol, which its effective multiplicity is read off + // Declarations are the parameter's declarations along the chain, most general + // first: what a bound value answers to, the closest to the invoked calc winning. + Declarations []CalcMember + // IsSubject marks a case's subject parameter, the object the case is about. + IsSubject bool + // IsActor marks an actor parameter (SysML v2 §7.19.2, §7.24.2): bound by name + // like the subject, never by position, and left unvalued when nothing binds it. + IsActor bool + // IsCalc marks a calc usage parameter (`in calc f`), which a function value binds. + IsCalc bool +} + +// CalcOutput is one output feature of a calc — an `out` parameter, or the result +// parameter a `return` declares — read by name rather than run as a statement. +type CalcOutput struct { + Name string // feature name the output answers to, empty for an anonymous result parameter + // Value is the value-binding expression, 0 when the calc gives the output no value. + Value ExprID + Owner *symbols.Symbol // the calc declaring the binding, whose scope it is written in + // Declarations are the output's declarations along the chain, most general first. + Declarations []CalcMember + IsResult bool // declared with `return`: the result an invocation of the calc yields + // IsInOut marks an `inout` parameter: an output feature (KerML 7.4.9) bound by + // the invocation rather than by a declaration or an assignment. + IsInOut bool + // IsInitial marks a value declared with `:=`: what the output holds when the + // body starts, which the body's assignments may then replace. + IsInitial bool +} + +// CalcMember is one declaration of a parameter or output: the feature declared +// and the calc declaring it, under the name the declaration binds. +type CalcMember struct { + Feature *symbols.Symbol + Owner *symbols.Symbol + Name string +} + +// ResultOutputName is the name an unnamed result parameter answers to. +const ResultOutputName = "result" + +// ResultOutputIndex is the position in Outputs of the result parameter a +// `return` declares, -1 when the calc declares none. +func (c *Calc) ResultOutputIndex() int { + for i := range c.Outputs { + if c.Outputs[i].IsResult { + return i + } + } + return -1 +} + +// ResultOutput returns the result parameter a `return` declares, if the calc has one. +func (c *Calc) ResultOutput() *CalcOutput { + if i := c.ResultOutputIndex(); i >= 0 { + return &c.Outputs[i] + } + return nil +} + +// HasInitialOutput reports whether some output starts the body holding a value +// (`:=`), which the calc yields when the body leaves it as it is. +func (c *Calc) HasInitialOutput() bool { + for _, out := range c.Outputs { + if out.IsInitial { + return true + } + } + return false +} + +// IsCase reports whether the calc is a case — an analysis or a verification — +// rather than a plain calc. +func (c *Calc) IsCase() bool { + return c.Kind == "analysis" || c.Kind == "verification" +} + +// Performs reports whether the body's action nodes are performed as steps: a case +// is an action, so its body is; a calc's body performs nothing. +func (c *Calc) Performs() bool { + return c.IsCase() +} + +// BodyScope is the namespace the body's names resolve in. +func (c *Calc) BodyScope() *symbols.Scope { + if c.BodyOwner != nil { + return c.BodyOwner.Scope + } + return c.Sym.Scope +} + +// Parameter is the calc's parameter of that name, nil when it declares none. +func (c *Calc) Parameter(name string) *CalcParam { + for i := range c.Params { + if c.Params[i].Name == name { + return &c.Params[i] + } + } + return nil +} + +// HasParameter reports whether the calc declares an input parameter of that name. +func (c *Calc) HasParameter(name string) bool { + return c.Parameter(name) != nil +} + +// SubjectParameter is the parameter the case's subject binds to, if it declares one. +func (c *Calc) SubjectParameter() (*CalcParam, bool) { + for i := range c.Params { + if c.Params[i].IsSubject { + return &c.Params[i], true + } + } + return nil, false +} + +// PositionalIndexes are the indexes in Params a positional argument can bind, in +// declaration order: every parameter but the actors, which bind by name only. +func (c *Calc) PositionalIndexes() []int { + indexes := make([]int, 0, len(c.Params)) + for i := range c.Params { + if !c.Params[i].IsActor { + indexes = append(indexes, i) + } + } + return indexes +} + +// PositionalArity counts the parameters a positional argument can bind. +func (c *Calc) PositionalArity() int { + return len(c.PositionalIndexes()) +} + +// OutputNames renders the output features the calc declares, for a diagnostic +// about one it does not. +func (c *Calc) OutputNames() string { + names := make([]string, 0, len(c.Outputs)) + for _, out := range c.Outputs { + if out.Name != "" { + names = append(names, out.Name) + } + } + if len(names) == 0 { + return "none" + } + return strings.Join(names, ", ") +} + +// UsageSpelling writes the calc usage a modeler declares to read the outputs of +// a calc that designates no result, with the inputs it has to bind. +func (c *Calc) UsageSpelling(output string) string { + var b strings.Builder + b.WriteString(c.Kind + " c : " + c.Name + " {") + for _, param := range c.Params { + if param.IsActor { + continue + } + b.WriteString(" in " + param.Name + " = ...;") + } + b.WriteString(" } then read c." + output) + return b.String() +} + +// Canonical is the name the run binds name under: the alias recorded for it, else itself. +func Canonical(aliases map[string]string, name string) string { + if alias, ok := aliases[name]; ok { + return alias + } + return name +} + +// AliasRedefined records that name now binds the feature redefined was known by. +// Aliases already pointing at redefined follow it to name. +func AliasRedefined(aliases map[string]string, redefined, name string) map[string]string { + if redefined == "" || name == "" || redefined == name { + return aliases + } + if aliases == nil { + aliases = make(map[string]string) + } + for alias, target := range aliases { + if target == redefined { + aliases[alias] = name + } + } + aliases[redefined] = name + delete(aliases, name) + return aliases +}