Repository navigation
fix(runtime): evaluate binary + and - with an empty second operand as the unary sign - #1538
Merged
Merged
Conversation
… the unary sign KerML 1.1 §8.4.4.9.6 makes a binary OperatorExpression the invocation of the library's '+'/'-', whose second parameter y is [0..1]: 1 + null is '+'(1, null) and answers 1, 3 - null is '-'(3, null) and answers -3. Both forms now reach the same arithmeticValues, which applies the unary sign when y is omitted; the first parameter and every operand of *, /, %, **, ^ and the comparisons stay [1] and refused when empty. Co-Authored-By: jason.han <hanhuijun@gmail.com>
Contributor
Author
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
…ry sign The interpreter now evaluates a binary + or - whose second operand is empty as the unary sign (KerML 1.1 §8.4.4.9.6, y is [0..1]); generated Go and C code decides the same at run time over a collection operand, evaluating it once and spending the interpreter's steps, and compiles a literal null operand to the sign directly. The arithmeticValues emptiness test no longer allocates per evaluation. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…taking the sign A literal null or a sequence of nulls on the right of + or - is now retyped to the left operand's collection type and takes the same run-time path as any collection operand, so its elements' steps are spent as the interpreter spends them; retype carries the collection type into the elements of a null sequence, and the left operand is held in a temporary as the shared operand path requires. Code comments shortened to the repository's two-line limit. Co-Authored-By: jason.han <hanhuijun@gmail.com>
This was referenced Oct 26, 2026
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
A binary
+or-written with an empty second operand was refused (1 + null→type mismatch: operator '+' is not defined for an Integer and null) while the function form of the same invocation evaluated ('+'(1, null)→1,'-'(3, null)→-3). KerML 1.1 Beta 2 §8.4.4.9.6 makes the operator notation the invocation of the library function, so the two forms must agree.Context.arithmeticValues(the one evaluation both the operator notation and the library's'+'/'-'forms reach) now applies the unary sign (Context.unaryValueofsignOperator) whenyis omitted — null,(), or a feature holding no value — for every operand but a String (StringFunctions::'+'declares both parameters[1], so"a" + nullstays refused).arithmeticForm(the function form) no longer carries its own unary branch; it binds an omittedyas null and calls the samearithmeticValues.-v,v - null) moved fromevalUnaryintounaryValue, so the operator, the function form and the omitted-ypath share it.internal/translate/codegen/compile.gosignOrArithmeticcompiles a+/-whose right operand is a collection — a literalnullor a sequence of nulls is retyped to the left operand's collection type first — to a run-time choice between the sign ofx(signOf, the unary case's code) and the operation over the one value the collection holds; both operands are evaluated once (held in temporaries when impure) and spend the interpreter's steps, the elements of a null sequence included (retypecarries the collection type into the elements of aSeqLit). The arithmetic case's body becamearithmetic, andbinaryOperandssplits intohoistedOperands+ the scalar checks so the+/-case can see the collection before it is held to one value.compile_test.go:TestCompiledCalcErrorParity:Add(1)againstin b : Integer(a reference usage,[0..*], sobis empty — the adjudication of the unbound-parameter row) now answers1on both tiers; the too-few refusal is asserted onMul(1)instead, whose parameters are[1].Behaviour before → after (Integer, Natural, Rational, Real, quantities and vectors alike):
1 + null,'+'(1, null)OperandTypeError/11/13 - null,3 - (),'-'(3, null)OperandTypeError/ — /-3-3/-3/-31.5 - null,2 [m] - null,v - nullOperandTypeError-1.5,-2 [m],⟨-1.0, -2.0⟩x - ywithin y : Integer[0..1]emptyOperandTypeError-xnull + 1(xis[1])OperandTypeErrorOperandTypeError; the function form'+'(null, 1)isErrMultiplicityViolationnamingx)2 * null,2 / (),7 % null,2 ** null,1 < null,"a" + nullOperandTypeError[1]Specification basis
KerML 1.1 Beta 2 §8.4.4.9.6 Operator Expressions: a binary
OperatorExpressionexpr_1 + expr_2is theInvocationExpressionDataFunctions::'+'(expr_1, expr_2);+and-are the only operators with a unary and a binary usage, and the library declares their second parameterywith multiplicity0..1inDataFunctions,ScalarFunctions,NumericalFunctions,NaturalFunctions,IntegerFunctions,RationalFunctions,RealFunctions,ComplexFunctionsandVectorFunctions(internal/workspace/libs/stdlib);xis[1]everywhere, as are both operands of'*','/','%','**','^', the comparisons andStringFunctions::'+'. The library body of'-'withyomitted is the negation, which is what'-'(3, null)already answered; the operator form now reads the same body.docs/project/spec-compliance.md: the binary-operators row moves'-'withyomitted) and the unbound-parameter row (F(1)againstin x; in y;, now1) are updated to match.make docs-countsreports the committed counts current.Pilot: the pinned pilot leaves both the operator form (
OperatorExpression +) and the function form unevaluated, so it referees neither side; the clause and the library's multiplicities decide.Pilot-exec referee movement
New
tools/referee/exec/testdata/cases/empty_second_operand.cases(modelempty_second_operand.sysml), ten cases, every onepilot-unevaluatedwith our answer recorded beside the unevaluated expression — no existing case moved:op-plus-null1 + null1fn-plus-null'+'(1, null)1op-minus-null3 - null-3fn-minus-null'-'(3, null)-3op-minus-empty3 - ()-3op-real-minus-null1.5 - null-1.5op-null-plusnull + 1OperandTypeErrorfn-null-plus'+'(null, 1)ErrMultiplicityViolation(parameterx)op-times-null2 * nullOperandTypeErrorop-less-null1 < nullOperandTypeErrorBucket counts (
go run -C tools ./cmd/pilot-exec-diff, pilot artifact 0.62.0): 500 → 510 cases,pilot-unevaluated157 → 167, every other bucket unchanged (agree 228 · kind-only 1 · order-only 0 · disagree 37 · differs-by-design 5 · pilot-silent 24 · pilot-error 4 · ours-error 10 · ours-undetermined 17 · both-error 17 · nondeterministic 0).docs/project/pilot-execution-referee.mdand the referee skill's reference line record the new denominator and bucket.How it was verified
calc_operator_empty_second_operand(.sysml+.expected.json): both forms side by side over Integer, Natural, Rational and Real, a[0..1]input parameter holding no value, and the refusals (null + 1,'+'(null, 1),2 * null,2 / null,7 % null,2 ** null,1 < null).robustness_operator_empty_operand_test.go(TestRuntimeRobustnessOperatorEmptyOperand):empty_second_operand_is_the_unary_sign(operator form equals function form for null,(), an empty feature, an emptied sequence, a Real, a quantity),empty_required_operand_is_refused(typedOperandTypeErrorfor every[1]operand,ErrMultiplicityViolationfor the function form).tests/codegenTestCompiledCalcsAgreeWithInterpreterandTestCompiledStepBudgetMatchesInterpreter(Go and C): new fixture calcsSeq::SubS2(3 - x),SubR2(1 - xoverReal[0..*]),AddNull(x + null),AddNulls(x + (null, null)),SubNull(x - null),MulS2(2 * x, refused when empty) beside the existingAddS2(1 + x), each overnull, a one-element and a two-element collection — same value or same error text, and the same step budget, as the interpreter.1 + nullalready parsed).bin/sysml: the reproducer answers1,1,-3,-3;v - nulloverVectorFunctions::VectorOf((1.0, 2.0))is⟨-1.0, -2.0⟩,2 [m] - nullis-2 [m],open + nullover an unboundattribute open : Integer;stays<undetermined>,"a" + nullstays refused.origin/develop(ab1a9cf9aat the first two pushes,e5f9c3fcaat the third) and againstchore/pilot-pin-2026-09(PR chore(pilot): pin the OMG pilot implementation to release 2026-09 (0.63.0) #1529;8a100b31, then9ca600a59): every merge clean, no conflicts; on both merged treesTestDocumentedCaseCount,TestShippedCaseFilesResolve,TestExecutionConformance,TestRuntimeRobustnessOperatorEmptyOperand,TestCompiledCalcErrorParity,TestOperatorFunction*, the loop-allocation footprint tests,internal/translate/codegen,tools/referee/diffandtools/census/doccountspass.Gate (corpora fetched: training examples, pilot corpora, spec examples, pilot library XMI, PSSM suite;
OPENSYSML_REQUIRE_*=1for each):The third commit answers the review: a null-typed right operand (
x + (null, null)) no longer drops the sequence's element steps — it takes the same run-time path as a collection operand (TestCompiledStepBudgetMatchesInterpreternow coversSeq::AddNulls), and the new code comments are within two lines.The first push failed the runtime CI shard on the loop-allocation footprint tests (
arithmeticValuestested emptiness throughelementsOf, which allocates for a scalar); the second commit tests throughisEmptyValue, which does not, and compiles the generated-code path.Not verified: the Reference-FMUs gate (
tests/fmi, needsfmpy) skipped; the referee was run with the pinned 0.62.0 evaluator, not the 0.63.0 artifact PR #1529 moves to.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 changelogLink to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/693818cb0e46445da1c4d5d3211107ec
Open in Devin Desktop: https://nasa-jpl-demo.devinenterprise.com/desktop/session/693818cb0e46445da1c4d5d3211107ec?variant=devin
Requested by: @HuiJun