Skip to content

feat(runtime): evaluate the unbounded value * and elem.metadata - #113

Merged
HuiJun merged 11 commits into
mainfrom
feature/infinity-and-metadata-values
Sep 8, 2026
Merged

feat(runtime): evaluate the unbounded value * and elem.metadata#113
HuiJun merged 11 commits into
mainfrom
feature/infinity-and-metadata-values

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What and why

Two value forms the runtime could not produce are now evaluated.

* as a value. Expression-position * was parsed but not evaluable, so a body stating it
was reported as an unevaluable expression. It now evaluates to a dedicated unbounded scalar,
semantics.Value{Kind: ValInfinity}, carried as a ValConst runtime value. It is a semantic
kind of its own rather than an IEEE infinity, so it can never be mistaken for a number:

  • ordering: greater than every finite Integer, Real and Natural, equal to itself, so <, <=,
    > and >= all answer consistently (semantics.UnboundedOrder / OrderSatisfies);
  • printing: * everywhere — value formatting, trace formatting, the REPL's document queries and
    the solver's pinned values, which previously spelled it , a spelling no SysML source uses;
  • arithmetic: refused before any numeric conversion with a typed ErrTypeMismatch naming the
    operation, for +, -, *, /, %, ** and both unary forms, e.g.
    type mismatch: operator '+' is not defined for the unbounded value '*': * + 1. No panic, no
    NaN, no silently narrowed finite answer;
  • the wire: its own Value oneof arm (bool infinity = 16) rather than the string "*", which
    stays an ordinary string. The arm is advertised as the infinity_value capability and behaves
    like the other value-arm capabilities — withheld as an unsupported null in a response, and
    refused with UNIMPLEMENTED in an input — with the Go, Java, Python, Node and Rust clients
    and the wire contract updated for it.

The checker types the literal as ScalarValues::Positive, which the standard library declares as
the natural number exceeding every other, so conformance of attribute n : Natural = *; follows
from the library rather than from a runtime special case.

elem.metadata. ref.metadata now parses as a metadata access rather than a feature chain
through a member named metadata, and evaluates from the annotation side table the @@
classification already reads — no re-derivation and no reparsing of declarations. The semantic
layer materializes that table into semantics.ElementMetadata (type, node, scope, and the
features bound in the annotation body, in declaration order); the runtime turns each entry into
a metadata instance through the existing instance materialization, so a feature the body binds
answers that value and one it does not answers the metadata type's default. An element with no
metadata answers the empty sequence. A non-element operand (1.metadata) is an
ErrTypeMismatch, an unresolved name an ErrUnresolvedReference, and a body binding naming a
feature the metadata type does not have an ErrTypeMismatch naming both — never a dropped
value.

Specification basis

  • * in expression position: KerML 8.4.4.6 (unbounded natural), typed through
    ScalarValues::Positive as the library declares it.
  • Metadata access: KerML 8.4.4.10 (metadata access expressions), whose result is the metadata
    features of the referenced element.

Both add rows to the Expression Evaluation table in docs/project/spec-compliance.md (✅), and
the new wire arm is documented in docs/reference/wire-contract.md and
docs/reference/service-transports.md.

How it was verified

New coverage:

  • conformance: value_unbounded_comparison, value_unbounded_arithmetic_refused (every refused
    operation) and metadata_access_annotations (bound body values, defaults, about metadata,
    several annotations in order, and an element with none), plus an Infinity expected-value type
    in the conformance schema and its README;
  • golden AST: internal/core/parser/testdata/parse/unbounded_and_metadata_access.sysml;
  • robustness: arithmetic over *, * compared with a string, .metadata on a value, and
    .metadata on an unresolved name — each a typed error;
  • unit: internal/core/runtime/infinity_test.go, metadata_test.go and
    internal/grpc/convert_infinity_test.go (round trip of the arm, of * nested in a sequence,
    the withheld null and the refused input without the capability, and "*" still an ordinary
    string).

One existing test changed intentionally. TestUnevaluableResultIsNotReportedAsMissing used
Foo.metadata and * as its two expressions the evaluator has no value kind for; both are now
evaluable, so the model states ~n and n as Foo instead — two forms
unimplementedOperators still declares unevaluated. The test's contract (an unevaluable result
is reported as such, not as a missing result) is unchanged. internal/repl/runtime_commands_test.go
moved from to * with the formatter.

A follow-up commit fixes a difference the prompt showed against the same document over gRPC:
the about index keys the annotated element by symbol, while the prompt evaluates against a
scope tree re-indexed from the same declarations, so its twin of the element reached none of the
annotations stated about it — elem.metadata and a package's metadata read as empty at the
prompt while gRPC answered them. The index now keys those annotations by the annotated element's
declaration as well, which identifies it across both trees
(internal/repl/metadata_about_eval_test.go).

$ gofmt -l .            # empty
$ go build ./...        # ok
$ go vet ./...          # ok
$ go test ./...         # ok
$ python3 scripts/changelog.py check   # ok
$ OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 \
    go test -count=1 ./internal/core/model -run 'TestTrainingExamples|TestPilotCorpora'
ok      github.com/Open-MBEE/OpenSysML/internal/core/model      16.315s

Known limitation: * is a value and an ordering bound only — no arithmetic over it is defined,
by design, since KerML gives none.

Checklist

  • make test and make lint pass locally
  • Tests added or updated for the change
  • Documentation extended where it already covers the surface (see CONTRIBUTING.md)
  • Changelog entry added as changes/unreleased/<slug>.<section>.md, not as an edit to CHANGELOG.md
  • baselines regenerated and make docs-counts run if a gate count moved (compliance rows need nothing: the census is counted at docs build)
  • No internal work-item labels (waves, slices, F4, K5) in the body, docs, or changelog

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Runtime-verified by hand through the REPL and through the gRPC service with the Python client, beside the suite.

Verified: * prints as * and never as , Inf or NaN; the comparisons against Integer, Real and Natural, in sequences and through both conditional branches; every binary operator with the unbounded value on either side and both unary forms refused with a type mismatch naming the operation, the prompt still evaluating 1 + 1 afterwards; the value spelled * in a document query's upper multiplicity, in a calculation trace and in a solver pin diagnostic, which refuses an infinite magnitude rather than solving it; inline metadata order, bound values, type defaults, the empty sequence of an unannotated element and the typed non-element and unresolved-name errors; the Value.infinity arm distinct from the ordinary string "*", retained inside sequences, advertised as infinity_value by GetServerInfo, and round-tripped through an action.

gRPC: the unbounded arm, the ordinary string and the metadata sequence

That pass found the prompt answering elem.metadata without the annotations stated about the element while the same document answered them over gRPC — a symbol-identity difference in the about index, fixed in the follow-up commit and locked by internal/repl/metadata_about_eval_test.go.

Not exercised by hand: the Java, Node and Rust clients, and the capability-withholding path (both covered by tests only). Ordering note: mixed inline and about annotations answer inline-first, which is the order the annotation side table an element filter classifies by already used, and which the compliance row states.

@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review September 8, 2026 01:03
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration
devin-ai-integration Bot force-pushed the feature/infinity-and-metadata-values branch from 5f77ae5 to f1e7e7e Compare September 8, 2026 04:28
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 6 commits September 8, 2026 05:01
`*` in expression position now evaluates to a dedicated unbounded scalar
that orders above every finite number, equals itself, prints as `*` in
values, traces and solver output, crosses the wire in its own Value arm
behind the infinity_value capability, and is refused with a typed error
naming the operation in every arithmetic form.

`elem.metadata` reads the annotation side table and answers an ordered
sequence of materialized metadata instances, including features bound in
an annotation body and the metadata type's own defaults, an empty
sequence for an unannotated element, and a typed error for a non-element
operand.

Co-Authored-By: jason.han <[email protected]>
The `about` index keys the annotated element by symbol, but the prompt
evaluates against a scope tree re-indexed from the same declarations, so
its twin of the element missed every annotation stated about it —
`elem.metadata` and package metadata read as empty there while the same
document answered them over gRPC. The index now also keys the
annotations by the annotated element's declaration, which identifies it
across both trees.

Co-Authored-By: jason.han <[email protected]>
…ty arm

Review found two paths. A metadata access that failed part-way left the
objects of the annotations already read registered in the context, so a
retried access accumulated unreachable objects; one access now
materializes every annotation or none. And the infinity arm arriving as
false read as the unbounded value, letting a malformed request execute
with a value it never supplied; the arm is the value itself, so only
true carries it and false is refused.

Co-Authored-By: jason.han <[email protected]>
A metadata annotation body can read an object whose classifier behavior
starts, so abandoning only the objects left that behavior attached to the
context, able to run after the object it belongs to is gone. The access
now rolls back from the behavior boundary as well.

Co-Authored-By: jason.han <[email protected]>
…ded value

Metadata body values resolve in the body's own scope, nested body
declarations bind through the object their feature holds, a ValInfinity
constant answers ScalarValues::Positive as its direct type, and every
client decoder refuses an infinity arm that is not asserted.

Co-Authored-By: jason.han <[email protected]>
devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration
devin-ai-integration Bot force-pushed the feature/infinity-and-metadata-values branch from f145243 to 0b78ad5 Compare September 8, 2026 05:04
A body declaration with a name of its own writes to the feature it redefines,
so `severity :>> level = 3` binds level rather than being refused. A body value
naming a sibling feature reads it off the object being bound.

Co-Authored-By: jason.han <[email protected]>
devin-ai-integration[bot]

This comment was marked as resolved.

… access

An annotation denotes one object, so a second `elem.metadata` answers what the
first made rather than a fresh identity. The access is bracketed by a journal,
so a failing annotation also undoes writes made while reading the ones before
it, not only the objects and behaviors they left.

Co-Authored-By: jason.han <[email protected]>
devin-ai-integration[bot]

This comment was marked as resolved.

A metadata annotation is named by the element it annotates and its place among
that element's annotations rather than by its syntax node, so a carry-over rebinds
it and a later read of a carried metadata object answers that object instead of
making a second one.

Co-Authored-By: jason.han <[email protected]>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 2 commits September 8, 2026 05:34
…read

A carry-over compares what the annotation at that place states, type and text,
before taking over the object made for it, so an annotation edited, reordered or
retyped between analyses is read again rather than answered with a stale object.

Co-Authored-By: jason.han <[email protected]>
An `about` annotation is stated away from the element it annotates, so its
digest now reads the annotating document rather than the annotated element's,
which an edit there did not change and an unrelated edit here did.

Co-Authored-By: jason.han <[email protected]>
@HuiJun
HuiJun merged commit 3bae1e8 into main Sep 8, 2026
12 checks passed
@HuiJun
HuiJun deleted the feature/infinity-and-metadata-values branch September 8, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant