feat(runtime): calc definitions, usages and calc parameters as function values - #122
Merged
Conversation
…on values A calc definition, a calc usage awaiting an input, or an `in calc` parameter read as a value is a function value: the lowered calc together with the scope, object and enclosing body frames it was read in, invoked through the calc invocation path. Calc-typed parameters accept one by position or name, `f(a)` invokes it, `SampledFunctions::Sample` samples a user calc, and a natively implemented library function is a value too. Function values cross the wire as `Value.function` (`calc_id`, optional `self_id`) under the `function_values` capability, exposed by the Go, Python, Node, Rust and Java clients. Native compilation refuses a calc that binds or applies a function value with a typed error. Co-Authored-By: jason.han <[email protected]>
…that body is written A calc usage nested in a behavior body keeps the enclosing frames only when the body it runs is declared inside that behavior; a body inherited from a calc declared elsewhere reads none of them. A case performing itself as a step no longer nests the caller's frames at every level of the recursion, which multiplied the frames cloned per step. Co-Authored-By: jason.han <[email protected]>
Co-Authored-By: jason.han <[email protected]>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Co-Authored-By: jason.han <[email protected]>
…nformance Co-Authored-By: jason.han <[email protected]>
…object-bound wire functions A function value read off a nested calc now captures the enclosing body's bindings only when the calc reads them — through a body written in that behavior or a default it declares there — so a usage inheriting its body from a calc declared elsewhere no longer carries frames it cannot read, and is no longer refused on the wire as closing over a body. A calc-typed parameter invoked by a qualified name (`Apply::f(a)`, or through the calc a run specializes and a redeclaration) applies what the run bound it to, found through the active frames' owner qualification, as the bare name does. A request function carrying a non-zero self_id is refused: every call instantiates the model afresh, so the object it names lived only within the response that sent it and another call's object may carry the same number. The wire contract, proto comment, clients and conformance scenario say so. Co-Authored-By: jason.han <[email protected]>
…ng behavior A calc declared in a behavior body used to close over every frame on the evaluation stack, so a caller's same-named parameter could satisfy a name in the nested body. Frames are now selected by ownership: a nested calc closes over the frames through the innermost active run of the behavior it is declared in (a calc invocation or usage of that behavior or one specializing it, or an action performance typed by it), and over nothing when no such run is active. Direct invocation, function-value snapshots and nested calc usages all apply the rule. Co-Authored-By: jason.han <[email protected]>
Regenerates the protobuf artifacts from the merged schema, which carries both the function value arm and the parameter sweep RPC, and binds sweep arguments against the run's runtime so a function bound by every row of a sweep applies as it does in an analysis run. Co-Authored-By: jason.han <[email protected]>
A function value returned from a specialized run snapshots the frames it closes over; the snapshot now carries the frames' redefinition aliases, so a nested calc reading an inherited parameter name a subclass renamed resolves it instead of failing. A call through a feature chain, holder.scale(3.0), is checked statically as a direct call is: the chain's terminal calc feature is resolved, its positional and named arguments are held to that feature's effective inputs, 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. Co-Authored-By: jason.han <[email protected]>
Co-Authored-By: jason.han <[email protected]>
…ders A frame snapshot copied an action performance's bindings but not which action it was a run of, so a calc captured from an action body could no longer find the performance when it applied a second body-local calc. The snapshot now records the action it was copied from (frame.performed), which frame.runs answers for as it does for the live performance. The holder analysis prepended every invocation operand to the positional arguments, so a call through a feature chain (picker.pick(lead, trail)) was read with the chain as its first argument and its callee unresolved. It now resolves the chain to the calc applied and maps the written arguments against that calc's returned parameters, sharing ChainCallee/InvocationArgs with the checker. Co-Authored-By: jason.han <[email protected]>
…puts A chain call such as holder.scaled() now denotes the calc feature applied over the receiver's object directly, instead of first reading the chain as a value: a calc every input of which a default supplies, or one with no input, reads bare as its result and so could not be called. Co-Authored-By: jason.han <[email protected]>
Two reads of a calc declared in a behavior body within one run of that body are one function: valueEqual and valueKeyFunc compare the run each read closed over (a Context-numbered identity every invocation, usage evaluation and performance frame carries into its snapshots) rather than the pointer of the read, so `inner == inner` holds and a set of both reads holds one value, while functions returned by two runs stay distinct. Co-Authored-By: jason.han <[email protected]>
Resolve the Value oneof collision by keeping infinity at field 16 and moving function to field 17; regenerate protobuf stubs and keep both the function_values and infinity_value capabilities across service, docs and clients. Co-Authored-By: jason.han <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
On
main, naming a calc where a value is expected fails (Fn(Sq, 3.0)→cannot evaluate definition Sq), so a calc cannot be passed to another calc, bound to anin calcparameter, or handed toSampledFunctions::Sample. This PR makes a calc definition, a calc usage with an unsupplied input, and anin calcparameter/feature read as a function value, and invokes it through the same path a calc usage invocation takes.Value kind:
ValFunctionbesideValExpr, not generalizing itValExpr(BodyExpr) is a closure over one expression body that a collection operation evaluates per element — it has no parameters, nocalcShape, and is never invoked by argument binding. A function value is the opposite: a reference to a lowered calc (calcShape, the invocation interfaceinvokeCalcShapeInalready consumes) plus the environment it was read in. Folding one into the other would have meant either givingValExpra parameter list it never binds or turning function values into statement closures, which is exactly what the design must not do. SoValFunctionis a distinct kind andValExprkeeps its one job; nothing was bridged and noValExprcaller changed.Paths covered: read of a calc def / calc usage /
in calcparameter; binding to a calc-typed parameter (type-checked:ErrNotAFunction), positional and named arguments;f(a),p.f(a),f(f(a)); a calc bound as an action input;SampledFunctions::Sampleapplying a user calc; a calc read off an object; a calc declared in a behavior body closing over that body's bindings and a function returned from a calc; equality/set keying; adoption between runtimes; typed errorsErrNotAFunction,ErrCalcArity,ErrUnboundParameter,ErrFunctionUnbound,ErrFunctionNeedsRuntime.A nested calc closes over its enclosing body only where the body it runs, or a default it declares, is written inside that behavior: a body inherited from a calc declared elsewhere (
calc again : RecursiveStep;) reads none of the enclosing frames (calcShape.bodyEnclosing). Without that rule a case performing itself as a step nested the caller's frames at every level of the recursion. The frames it does close over are those of the innermost active run of the behavior that lexically owns it (EvalContext.enclosingRun,frame.runs) — a calc frame owned by that calc def or a usage specializing it, or an action performance typed by it — never a caller's; when no such run is active it closes over nothing, so a caller's same-named parameter cannot satisfy a nested calc's reference.Wire
Value.function = 17(field 16 isinfinityonmain) withFunction { string calc_id = 1; int64 self_id = 2; }— the calc's qualified name and the response-local id of the object it was read off (0 for none). Because every call instantiates the model afresh, a request function carrying a non-zeroself_idis refused in band rather than matched to whichever object the call numbered the same; a calc is applied over an object within the call that holds it (EvaluateofF::apply(F::holder.scale, 3.0)). Advertised as thefunction_valuescapability. A function closing over a behavior body's bindings cannot be named by calc and object, so it crosses asunsupported: function <calc>.buf breakingagainstmainis clean (a new arm only). Go, Python, Node, Rust and Java clients expose it as a typed value and refuse to send one to a service without the capability; the public Go package and the conformance runner round-trip it.Codegen
Native compilation refuses a calc that binds or applies a function value with a typed error rather than miscompiling it; recorded as such in
spec-compliance.md.Known limitation, documented
SampledFunctions::SamplePairstill fails, onmainand here alike, withtype mismatch: operator '-' is not defined for a Real and a sequence: reading an inherited[0..*]member yields a singleton sequence where the library body expects a scalar. That is a pre-existing multiplicity issue independent of function dispatch, marked ❌ with the reproducer inspec-compliance.mdrather than worked around.Specification basis
KerML 1.1 §7.4.4 (a Function is a Behavior with a
result; an Expression is a Step typed by a Function; a feature reference to either denotes it), §8.3.4.8Function::result/FeatureReferenceExpression; SysML v2 §7.17 and §8.3.16 (a calc def is a Function, a calc usage an Expression,CalculationUsageas a parameter). Adds rows to the calculation and API sections ofdocs/project/spec-compliance.md(✅ for the paths above, ❌ forSamplePair, the codegen refusal stated).How it was verified
New coverage (the four-layer contract):
action_calc_parameter.sysml(in calcon an action);function_value_probe(+ trace golden),_read,_calc_usage,_named_args,_chain_call,_action_parameter,_feature_closure,_body_closure,_library,_sampled,_sampled_closure;value_kinds_test.go(identity/keying, every-kind dispatch),adopt_test.go,eval_no_value_test.go,repl/evalin_test.go;grpc/convert_function_test.go,client/opensysml/function_test.go, Python/Node/Java/Rust client tests, conformance scenarios01-server-info,04-evaluate,10-evaluate-calc(function.sysml) over gRPC, Connect protobuf and Connect JSON, Rust and Java runners.Gates, all clean on the final commit:
internal/core/model/testdata/training_examples_expected.txtis unchanged. Java verification was run against a local Maven mirror because Maven Central rate-limited the build; nothing of that is in the tree.Checklist
make testandmake lintpass locallychanges/unreleased/<slug>.<section>.md, not as an edit toCHANGELOG.mdmake docs-countsrun if a gate count moved (compliance rows need nothing: the census is counted at docs build)F4,K5) in the body, docs, or changelog