From 923ec81f06f4bbcd178d44509e36bb1b744c1fd8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:20:10 +0000 Subject: [PATCH 01/26] feat(semantics): evaluate KerML Rationals exactly Represent Rational values as normalized exact fractions with a compact small-fraction path and a size budget, parse decimal literals exactly, and carry exact Rationals through runtime, solver replay, gRPC, JSON, the Go client and compiled calculations. Co-Authored-By: jason.han --- api/proto/sysml.pb.go | 812 ++++++++++-------- api/proto/sysml.proto | 15 + .../opensysml/proto/DocumentValue.java | 268 +++++- .../proto/DocumentValueOrBuilder.java | 27 + .../openmbee/opensysml/proto/Quantity.java | 286 +++++- .../opensysml/proto/QuantityOrBuilder.java | 27 + .../openmbee/opensysml/proto/Rational.java | 650 ++++++++++++++ .../opensysml/proto/RationalOrBuilder.java | 36 + .../org/openmbee/opensysml/proto/Sysml.java | 387 +++++---- .../org/openmbee/opensysml/proto/Value.java | 268 ++++++ .../opensysml/proto/ValueOrBuilder.java | 30 + client/node/src/generated/sysml_pb.ts | 139 ++- client/opensysml/client.go | 30 + client/opensysml/convert.go | 40 + client/opensysml/documents.go | 26 + client/opensysml/edges_test.go | 8 +- client/opensysml/equal.go | 51 +- client/opensysml/types.go | 1 + client/opensysml/value.go | 42 +- client/python/opensysml/proto/sysml_pb2.py | 202 ++--- client/python/opensysml/proto/sysml_pb2.pyi | 26 +- .../python/opensysml/proto/sysml_pb2_grpc.py | 2 +- .../rust/conformance/sysml.descriptor.binpb | Bin 151114 -> 152233 bytes .../rust/opensysml/src/proto/sysml/sysml.rs | 26 +- internal/check/passes/typecheck_expr.go | 2 +- .../passes/typecheck_metadata_body_test.go | 2 +- internal/check/passes/typecheck_value.go | 22 +- internal/doc/docir/evaluate.go | 6 + internal/doc/docrender/markdown.go | 3 + internal/doc/queryexec/computed.go | 51 +- internal/doc/queryexec/derived.go | 2 + internal/doc/queryexec/events.go | 2 + internal/doc/queryexec/execute.go | 2 + internal/doc/queryexec/memberpath_test.go | 4 + internal/doc/queryexec/operations.go | 27 +- internal/doc/queryexec/quantity_test.go | 6 +- internal/doc/queryexec/value.go | 27 +- internal/exec/analysis/solve_test.go | 5 +- internal/exec/analysis/tool.go | 7 + internal/exec/fmi/runner.go | 5 +- .../exec/runtime/action_activation_test.go | 2 +- .../runtime/action_body_local_usage_test.go | 4 +- internal/exec/runtime/action_executor.go | 16 + .../exec/runtime/action_results_parts_test.go | 6 +- internal/exec/runtime/body_scope_test.go | 8 +- internal/exec/runtime/budget.go | 2 +- internal/exec/runtime/builtins.go | 4 +- internal/exec/runtime/builtins_named.go | 2 +- internal/exec/runtime/builtins_named_test.go | 2 +- .../runtime/calc_usage_body_local_test.go | 16 +- .../exec/runtime/calc_usage_nested_test.go | 4 +- .../exec/runtime/calc_usage_snapshot_test.go | 20 +- internal/exec/runtime/calc_usage_step_test.go | 4 +- internal/exec/runtime/classification.go | 4 +- .../exec/runtime/classifier_behavior_test.go | 2 +- internal/exec/runtime/clock.go | 4 +- internal/exec/runtime/clock_step_test.go | 2 +- internal/exec/runtime/collections.go | 132 ++- internal/exec/runtime/compile.go | 52 +- .../exec/runtime/compile_constructs_test.go | 2 +- internal/exec/runtime/compile_test.go | 8 +- internal/exec/runtime/compiled_ops.go | 19 +- internal/exec/runtime/compiled_stmts.go | 21 +- internal/exec/runtime/conformance_test.go | 37 +- internal/exec/runtime/connector_test.go | 2 +- internal/exec/runtime/declared_test.go | 2 +- internal/exec/runtime/division_test.go | 48 +- internal/exec/runtime/draw_policy_test.go | 2 +- internal/exec/runtime/eval.go | 52 +- internal/exec/runtime/eval_cache_test.go | 22 +- .../exec/runtime/eval_literal_arith_test.go | 4 +- internal/exec/runtime/eval_test.go | 16 +- internal/exec/runtime/evaluation_log.go | 13 +- internal/exec/runtime/exponentiation_test.go | 4 +- internal/exec/runtime/function_value.go | 2 +- internal/exec/runtime/instance_test.go | 8 +- .../exec/runtime/integer_promotion_test.go | 30 +- .../runtime/library_census_probes_test.go | 57 +- internal/exec/runtime/library_conversions.go | 76 +- .../exec/runtime/library_conversions_test.go | 47 +- internal/exec/runtime/library_functions.go | 61 +- .../exec/runtime/library_functions_test.go | 43 +- internal/exec/runtime/library_operators.go | 19 +- .../exec/runtime/library_operators_test.go | 8 +- internal/exec/runtime/library_shape_test.go | 2 +- internal/exec/runtime/measurement_ref_test.go | 2 +- internal/exec/runtime/metaobject.go | 3 + internal/exec/runtime/model.go | 4 +- internal/exec/runtime/modeled.go | 5 +- internal/exec/runtime/montecarlo_test.go | 2 +- .../exec/runtime/optional_feature_test.go | 2 +- .../exec/runtime/part_feature_chain_test.go | 6 +- internal/exec/runtime/quantity_functions.go | 24 +- .../exec/runtime/quantity_functions_test.go | 4 +- internal/exec/runtime/quantity_test.go | 32 +- ...robustness_performed_action_inputs_test.go | 4 +- internal/exec/runtime/robustness_test.go | 40 +- internal/exec/runtime/run_state_test.go | 2 +- .../exec/runtime/runtime_integration_test.go | 4 +- internal/exec/runtime/satisfy_test.go | 3 +- internal/exec/runtime/set_order.go | 14 +- internal/exec/runtime/signal_test.go | 2 +- internal/exec/runtime/stochastic_test.go | 26 +- internal/exec/runtime/subject_report_test.go | 2 +- internal/exec/runtime/sweep_test.go | 8 +- ...cept_binds_signal_occurrence.expected.json | 2 +- .../action_block_flow_if_branch.expected.json | 2 +- .../action_body_declarer_scope.expected.json | 2 +- ...action_body_local_calc_usage.expected.json | 6 +- .../action_body_package_member.expected.json | 2 +- ...body_shadows_enclosing_scope.expected.json | 8 +- ...hoice_chained_write_conflict.expected.json | 4 +- .../conformance/action_for_loop.expected.json | 4 +- ...n_for_over_a_part_collection.expected.json | 2 +- ...ction_guard_reads_calc_usage.expected.json | 2 +- ...n_part_attributes_in_outcome.expected.json | 2 +- ...on_unbraced_body_edges_first.expected.json | 2 +- ...on_unbraced_body_successions.expected.json | 2 +- .../assign_chain_depth_two.expected.json | 2 +- ...sign_chain_inherited_feature.expected.json | 2 +- ...sign_chain_state_do_and_exit.expected.json | 4 +- ...hain_state_entry_guard_reads.expected.json | 2 +- .../assign_chain_through_port.expected.json | 2 +- ...sign_chain_transition_effect.expected.json | 2 +- ...rite_conforms_to_target_type.expected.json | 2 +- .../calc_cast_composed_operands.expected.json | 4 +- ...lc_cast_sequence_elementwise.expected.json | 4 +- ...library_aggregation_identity.expected.json | 4 +- .../calc_library_conversions.expected.json | 7 +- .../calc_library_conversions.sysml | 3 +- ..._library_operator_call_forms.expected.json | 6 +- ...ry_overload_by_argument_type.expected.json | 4 +- ...nal_denominator_beyond_int64.expected.json | 2 +- ...ry_rational_denominator_beyond_int64.sysml | 6 +- .../calc_library_rational_terms.expected.json | 21 +- .../calc_library_rational_terms.sysml | 11 +- ...ustom_specialization_element.expected.json | 2 +- ...ary_vector_quantity_num_type.expected.json | 2 +- ...alization_fixed_element_type.expected.json | 2 +- .../calc_library_vector_sum.expected.json | 6 +- ...ary_vector_sum_flat_sequence.expected.json | 2 +- .../calc_loop_until_body.expected.json | 4 +- .../calc_natural_quotient.expected.json | 2 +- .../calc_return_parameter.expected.json | 2 +- .../calc_rk4_lunar_descent.expected.json | 2 +- .../calc_sample_pair_arithmetic.expected.json | 6 +- ...valued_member_refused_scalar.expected.json | 2 +- .../calc_usage_instance_slots.expected.json | 2 +- ...lc_usage_outputs_one_binding.expected.json | 2 +- .../instance_constraint_binding.expected.json | 2 +- .../instance_derived_slots.expected.json | 8 +- ...nstance_inherited_constraint.expected.json | 2 +- .../instance_nested_usage_body.expected.json | 4 +- ...stance_quantity_calculations.expected.json | 4 +- ...ance_quantity_coherent_units.expected.json | 2 +- ..._redefined_attribute_default.expected.json | 6 +- .../instance_tensor_quantity.expected.json | 2 +- .../instance_tensor_rank_three.expected.json | 32 +- ...e_tensor_rank_three_failures.expected.json | 2 +- ...nstance_unnamed_redefinition.expected.json | 2 +- ...ltiplicity_default_composite.expected.json | 2 +- ...plicity_default_redefinition.expected.json | 6 +- .../nested_redefinition_chain.expected.json | 6 +- ...ted_redefinition_chain_equiv.expected.json | 12 +- ...sted_redefinition_precedence.expected.json | 4 +- ...ew_redefined_payload_feature.expected.json | 4 +- .../send_new_through_alias.expected.json | 2 +- ...d_via_owner_nested_port_path.expected.json | 2 +- ..._owner_port_from_nested_part.expected.json | 2 +- ...hot_succession_portion_reads.expected.json | 6 +- ...te_space_discrete_difference.expected.json | 6 +- ...tate_target_transition_guard.expected.json | 2 +- internal/exec/runtime/that_test.go | 6 +- internal/exec/runtime/trace.go | 2 + internal/exec/runtime/value_equality.go | 9 +- internal/exec/runtime/vector_functions.go | 71 +- internal/exec/runtime/write_conformance.go | 51 +- internal/exec/smt/encode.go | 9 +- internal/exec/smt/state.go | 3 +- internal/exec/solve/agreement_test.go | 40 +- internal/exec/solve/pin.go | 10 + internal/exec/solve/query.go | 4 + internal/exec/solve/reference.go | 3 +- internal/exec/solve/replay.go | 202 ++--- internal/exec/solve/replay_test.go | 59 +- internal/exec/solve/rounded_census_test.go | 126 +-- internal/exec/solve/rounding.go | 81 +- internal/exec/solve/rounding_test.go | 64 +- internal/exec/solve/term.go | 19 +- internal/exec/solve/translate.go | 2 + internal/frontend/core/core.go | 8 + internal/frontend/core/json.go | 8 + internal/frontend/engine/json.go | 16 +- internal/frontend/engine/values.go | 47 + internal/frontend/grpc/capability_response.go | 9 +- .../frontend/grpc/convert_big_integer_test.go | 4 +- internal/frontend/grpc/describe_test.go | 5 + internal/frontend/grpc/docquery.go | 32 + internal/frontend/grpc/quantity_test.go | 29 +- internal/frontend/grpc/service.go | 14 +- internal/frontend/grpc/typefacts_test.go | 3 +- internal/frontend/protoconv/convert.go | 100 +++ internal/frontend/repl/check_test.go | 31 +- internal/frontend/repl/compile_test.go | 7 + internal/frontend/repl/docquery.go | 3 + internal/frontend/repl/evalin_test.go | 2 +- internal/frontend/repl/explain_test.go | 9 +- .../repl/testdata/compile_calcs.sysml | 16 +- internal/frontend/symbolfacts/facts.go | 2 + internal/ir/docplan/compiler.go | 18 +- internal/ir/docplan/compiler_test.go | 6 +- internal/ir/docplan/plan.go | 9 +- internal/semantic/semantics/annotations.go | 29 +- .../semantic/semantics/coherent_quantity.go | 9 +- internal/semantic/semantics/eval.go | 101 ++- internal/semantic/semantics/eval_test.go | 17 +- internal/semantic/semantics/filter.go | 37 +- internal/semantic/semantics/integer.go | 60 +- internal/semantic/semantics/layout.go | 7 +- internal/semantic/semantics/pow_test.go | 12 +- internal/semantic/semantics/quantity.go | 2 + internal/semantic/semantics/quantity_eval.go | 94 +- internal/semantic/semantics/rational.go | 635 ++++++++++++++ internal/semantic/semantics/units.go | 94 +- internal/semantic/symbols/filter.go | 10 +- internal/translate/codegen/compile.go | 38 +- internal/translate/codegen/compile_fn.go | 3 + internal/translate/codegen/compile_seq.go | 18 +- internal/translate/codegen/emit_c.go | 74 +- internal/translate/codegen/emit_go.go | 85 +- internal/translate/codegen/integer.go | 14 + internal/translate/codegen/ir.go | 22 +- internal/translate/codegen/rational.go | 398 +++++++++ internal/workspace/libs/stdlib.snapshot | Bin 3766519 -> 3766519 bytes tests/grpc/conformance_test.go | 5 + tests/semantics/quantity_eval_test.go | 10 +- 236 files changed, 6035 insertions(+), 1814 deletions(-) create mode 100644 client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Rational.java create mode 100644 client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/RationalOrBuilder.java create mode 100644 internal/semantic/semantics/rational.go create mode 100644 internal/translate/codegen/rational.go diff --git a/api/proto/sysml.pb.go b/api/proto/sysml.pb.go index e02d7f5622..c243faed57 100644 --- a/api/proto/sysml.pb.go +++ b/api/proto/sysml.pb.go @@ -7516,6 +7516,7 @@ type Value struct { // *Value_Metaobject // *Value_Undetermined // *Value_BigIntValue + // *Value_RationalValue Kind isValue_Kind `protobuf_oneof:"kind"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -7756,6 +7757,15 @@ func (x *Value) GetBigIntValue() string { return "" } +func (x *Value) GetRationalValue() *Rational { + if x != nil { + if x, ok := x.Kind.(*Value_RationalValue); ok { + return x.RationalValue + } + } + return nil +} + type isValue_Kind interface { isValue_Kind() } @@ -7857,6 +7867,12 @@ type Value_BigIntValue struct { BigIntValue string `protobuf:"bytes,22,opt,name=big_int_value,json=bigIntValue,proto3,oneof"` } +type Value_RationalValue struct { + // An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1` + // or `1 / 3` evaluates; one a double holds exactly crosses as real_value. + RationalValue *Rational `protobuf:"bytes,23,opt,name=rational_value,json=rationalValue,proto3,oneof"` +} + func (*Value_IntValue) isValue_Kind() {} func (*Value_RealValue) isValue_Kind() {} @@ -7901,6 +7917,63 @@ func (*Value_Undetermined) isValue_Kind() {} func (*Value_BigIntValue) isValue_Kind() {} +func (*Value_RationalValue) isValue_Kind() {} + +// Rational is an exact rational number in lowest terms: numerator over a +// positive denominator, each written as big_int_value is. A decoder rejects one +// not in lowest terms or one a double holds exactly, so no number has two spellings. +type Rational struct { + state protoimpl.MessageState `protogen:"open.v1"` + Numerator string `protobuf:"bytes,1,opt,name=numerator,proto3" json:"numerator,omitempty"` + Denominator string `protobuf:"bytes,2,opt,name=denominator,proto3" json:"denominator,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Rational) Reset() { + *x = Rational{} + mi := &file_sysml_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Rational) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Rational) ProtoMessage() {} + +func (x *Rational) ProtoReflect() protoreflect.Message { + mi := &file_sysml_proto_msgTypes[80] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Rational.ProtoReflect.Descriptor instead. +func (*Rational) Descriptor() ([]byte, []int) { + return file_sysml_proto_rawDescGZIP(), []int{80} +} + +func (x *Rational) GetNumerator() string { + if x != nil { + return x.Numerator + } + return "" +} + +func (x *Rational) GetDenominator() string { + if x != nil { + return x.Denominator + } + return "" +} + // Metaobject is an element of the model held as an instance of its reflective // metaclass: what `x meta KerML::Feature`, or the last element of // `x.metadata`, evaluates to (KerML 7.4.9.2, 8.3.4.8.15). It crosses as the @@ -7923,7 +7996,7 @@ type Metaobject struct { func (x *Metaobject) Reset() { *x = Metaobject{} - mi := &file_sysml_proto_msgTypes[80] + mi := &file_sysml_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7935,7 +8008,7 @@ func (x *Metaobject) String() string { func (*Metaobject) ProtoMessage() {} func (x *Metaobject) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[80] + mi := &file_sysml_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7948,7 +8021,7 @@ func (x *Metaobject) ProtoReflect() protoreflect.Message { // Deprecated: Use Metaobject.ProtoReflect.Descriptor instead. func (*Metaobject) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{80} + return file_sysml_proto_rawDescGZIP(), []int{81} } func (x *Metaobject) GetElementId() string { @@ -7985,7 +8058,7 @@ type Undetermined struct { func (x *Undetermined) Reset() { *x = Undetermined{} - mi := &file_sysml_proto_msgTypes[81] + mi := &file_sysml_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7997,7 +8070,7 @@ func (x *Undetermined) String() string { func (*Undetermined) ProtoMessage() {} func (x *Undetermined) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[81] + mi := &file_sysml_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8010,7 +8083,7 @@ func (x *Undetermined) ProtoReflect() protoreflect.Message { // Deprecated: Use Undetermined.ProtoReflect.Descriptor instead. func (*Undetermined) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{81} + return file_sysml_proto_rawDescGZIP(), []int{82} } func (x *Undetermined) GetReason() string { @@ -8049,7 +8122,7 @@ type Function struct { func (x *Function) Reset() { *x = Function{} - mi := &file_sysml_proto_msgTypes[82] + mi := &file_sysml_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8061,7 +8134,7 @@ func (x *Function) String() string { func (*Function) ProtoMessage() {} func (x *Function) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[82] + mi := &file_sysml_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8074,7 +8147,7 @@ func (x *Function) ProtoReflect() protoreflect.Message { // Deprecated: Use Function.ProtoReflect.Descriptor instead. func (*Function) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{82} + return file_sysml_proto_rawDescGZIP(), []int{83} } func (x *Function) GetCalcId() string { @@ -8108,7 +8181,7 @@ type ValueSet struct { func (x *ValueSet) Reset() { *x = ValueSet{} - mi := &file_sysml_proto_msgTypes[83] + mi := &file_sysml_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8120,7 +8193,7 @@ func (x *ValueSet) String() string { func (*ValueSet) ProtoMessage() {} func (x *ValueSet) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[83] + mi := &file_sysml_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8133,7 +8206,7 @@ func (x *ValueSet) ProtoReflect() protoreflect.Message { // Deprecated: Use ValueSet.ProtoReflect.Descriptor instead. func (*ValueSet) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{83} + return file_sysml_proto_rawDescGZIP(), []int{84} } func (x *ValueSet) GetElements() []*Value { @@ -8160,7 +8233,7 @@ type TensorQuantity struct { func (x *TensorQuantity) Reset() { *x = TensorQuantity{} - mi := &file_sysml_proto_msgTypes[84] + mi := &file_sysml_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8172,7 +8245,7 @@ func (x *TensorQuantity) String() string { func (*TensorQuantity) ProtoMessage() {} func (x *TensorQuantity) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[84] + mi := &file_sysml_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8185,7 +8258,7 @@ func (x *TensorQuantity) ProtoReflect() protoreflect.Message { // Deprecated: Use TensorQuantity.ProtoReflect.Descriptor instead. func (*TensorQuantity) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{84} + return file_sysml_proto_rawDescGZIP(), []int{85} } func (x *TensorQuantity) GetDimensions() []int64 { @@ -8217,7 +8290,7 @@ type Array struct { func (x *Array) Reset() { *x = Array{} - mi := &file_sysml_proto_msgTypes[85] + mi := &file_sysml_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8229,7 +8302,7 @@ func (x *Array) String() string { func (*Array) ProtoMessage() {} func (x *Array) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[85] + mi := &file_sysml_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8242,7 +8315,7 @@ func (x *Array) ProtoReflect() protoreflect.Message { // Deprecated: Use Array.ProtoReflect.Descriptor instead. func (*Array) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{85} + return file_sysml_proto_rawDescGZIP(), []int{86} } func (x *Array) GetDimensions() []int64 { @@ -8272,7 +8345,7 @@ type Vector struct { func (x *Vector) Reset() { *x = Vector{} - mi := &file_sysml_proto_msgTypes[86] + mi := &file_sysml_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8284,7 +8357,7 @@ func (x *Vector) String() string { func (*Vector) ProtoMessage() {} func (x *Vector) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[86] + mi := &file_sysml_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8297,7 +8370,7 @@ func (x *Vector) ProtoReflect() protoreflect.Message { // Deprecated: Use Vector.ProtoReflect.Descriptor instead. func (*Vector) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{86} + return file_sysml_proto_rawDescGZIP(), []int{87} } func (x *Vector) GetComponents() []*Value { @@ -8320,7 +8393,7 @@ type VectorQuantity struct { func (x *VectorQuantity) Reset() { *x = VectorQuantity{} - mi := &file_sysml_proto_msgTypes[87] + mi := &file_sysml_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8332,7 +8405,7 @@ func (x *VectorQuantity) String() string { func (*VectorQuantity) ProtoMessage() {} func (x *VectorQuantity) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[87] + mi := &file_sysml_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8345,7 +8418,7 @@ func (x *VectorQuantity) ProtoReflect() protoreflect.Message { // Deprecated: Use VectorQuantity.ProtoReflect.Descriptor instead. func (*VectorQuantity) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{87} + return file_sysml_proto_rawDescGZIP(), []int{88} } func (x *VectorQuantity) GetComponents() []*Quantity { @@ -8367,7 +8440,7 @@ type Complex struct { func (x *Complex) Reset() { *x = Complex{} - mi := &file_sysml_proto_msgTypes[88] + mi := &file_sysml_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8379,7 +8452,7 @@ func (x *Complex) String() string { func (*Complex) ProtoMessage() {} func (x *Complex) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[88] + mi := &file_sysml_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8392,7 +8465,7 @@ func (x *Complex) ProtoReflect() protoreflect.Message { // Deprecated: Use Complex.ProtoReflect.Descriptor instead. func (*Complex) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{88} + return file_sysml_proto_rawDescGZIP(), []int{89} } func (x *Complex) GetReal() float64 { @@ -8429,7 +8502,7 @@ type EnumLiteral struct { func (x *EnumLiteral) Reset() { *x = EnumLiteral{} - mi := &file_sysml_proto_msgTypes[89] + mi := &file_sysml_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8441,7 +8514,7 @@ func (x *EnumLiteral) String() string { func (*EnumLiteral) ProtoMessage() {} func (x *EnumLiteral) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[89] + mi := &file_sysml_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8454,7 +8527,7 @@ func (x *EnumLiteral) ProtoReflect() protoreflect.Message { // Deprecated: Use EnumLiteral.ProtoReflect.Descriptor instead. func (*EnumLiteral) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{89} + return file_sysml_proto_rawDescGZIP(), []int{90} } func (x *EnumLiteral) GetLiteralId() string { @@ -8494,7 +8567,7 @@ type ValueSequence struct { func (x *ValueSequence) Reset() { *x = ValueSequence{} - mi := &file_sysml_proto_msgTypes[90] + mi := &file_sysml_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8506,7 +8579,7 @@ func (x *ValueSequence) String() string { func (*ValueSequence) ProtoMessage() {} func (x *ValueSequence) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[90] + mi := &file_sysml_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8519,7 +8592,7 @@ func (x *ValueSequence) ProtoReflect() protoreflect.Message { // Deprecated: Use ValueSequence.ProtoReflect.Descriptor instead. func (*ValueSequence) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{90} + return file_sysml_proto_rawDescGZIP(), []int{91} } func (x *ValueSequence) GetElements() []*Value { @@ -8540,6 +8613,7 @@ type Quantity struct { // *Quantity_IntMagnitude // *Quantity_RealMagnitude // *Quantity_BigIntMagnitude + // *Quantity_RationalMagnitude Magnitude isQuantity_Magnitude `protobuf_oneof:"magnitude"` // Unit as written ("km/h") or as an operation composed it ("m/s"); empty for // one never written down, described by unit_term alone. @@ -8554,7 +8628,7 @@ type Quantity struct { func (x *Quantity) Reset() { *x = Quantity{} - mi := &file_sysml_proto_msgTypes[91] + mi := &file_sysml_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8566,7 +8640,7 @@ func (x *Quantity) String() string { func (*Quantity) ProtoMessage() {} func (x *Quantity) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[91] + mi := &file_sysml_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8579,7 +8653,7 @@ func (x *Quantity) ProtoReflect() protoreflect.Message { // Deprecated: Use Quantity.ProtoReflect.Descriptor instead. func (*Quantity) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{91} + return file_sysml_proto_rawDescGZIP(), []int{92} } func (x *Quantity) GetMagnitude() isQuantity_Magnitude { @@ -8616,6 +8690,15 @@ func (x *Quantity) GetBigIntMagnitude() string { return "" } +func (x *Quantity) GetRationalMagnitude() *Rational { + if x != nil { + if x, ok := x.Magnitude.(*Quantity_RationalMagnitude); ok { + return x.RationalMagnitude + } + } + return nil +} + func (x *Quantity) GetUnit() string { if x != nil { return x.Unit @@ -8647,12 +8730,19 @@ type Quantity_BigIntMagnitude struct { BigIntMagnitude string `protobuf:"bytes,5,opt,name=big_int_magnitude,json=bigIntMagnitude,proto3,oneof"` } +type Quantity_RationalMagnitude struct { + // An exact Rational magnitude no double holds exactly, as Value.rational_value. + RationalMagnitude *Rational `protobuf:"bytes,6,opt,name=rational_magnitude,json=rationalMagnitude,proto3,oneof"` +} + func (*Quantity_IntMagnitude) isQuantity_Magnitude() {} func (*Quantity_RealMagnitude) isQuantity_Magnitude() {} func (*Quantity_BigIntMagnitude) isQuantity_Magnitude() {} +func (*Quantity_RationalMagnitude) isQuantity_Magnitude() {} + // MeasurementRef is a MeasurementReferences::ScalarMeasurementReference held as // a value: a unit by itself — `SI::m`, `km`, or `m / s` as an operation composed // it — as distinct from a Quantity expressed in one. It carries what the runtime @@ -8681,7 +8771,7 @@ type MeasurementRef struct { func (x *MeasurementRef) Reset() { *x = MeasurementRef{} - mi := &file_sysml_proto_msgTypes[92] + mi := &file_sysml_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8693,7 +8783,7 @@ func (x *MeasurementRef) String() string { func (*MeasurementRef) ProtoMessage() {} func (x *MeasurementRef) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[92] + mi := &file_sysml_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8706,7 +8796,7 @@ func (x *MeasurementRef) ProtoReflect() protoreflect.Message { // Deprecated: Use MeasurementRef.ProtoReflect.Descriptor instead. func (*MeasurementRef) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{92} + return file_sysml_proto_rawDescGZIP(), []int{93} } func (x *MeasurementRef) GetUnit() string { @@ -8745,7 +8835,7 @@ type UnitTerm struct { func (x *UnitTerm) Reset() { *x = UnitTerm{} - mi := &file_sysml_proto_msgTypes[93] + mi := &file_sysml_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8757,7 +8847,7 @@ func (x *UnitTerm) String() string { func (*UnitTerm) ProtoMessage() {} func (x *UnitTerm) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[93] + mi := &file_sysml_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8770,7 +8860,7 @@ func (x *UnitTerm) ProtoReflect() protoreflect.Message { // Deprecated: Use UnitTerm.ProtoReflect.Descriptor instead. func (*UnitTerm) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{93} + return file_sysml_proto_rawDescGZIP(), []int{94} } func (x *UnitTerm) GetScaleNum() float64 { @@ -8806,7 +8896,7 @@ type UnitFactor struct { func (x *UnitFactor) Reset() { *x = UnitFactor{} - mi := &file_sysml_proto_msgTypes[94] + mi := &file_sysml_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8818,7 +8908,7 @@ func (x *UnitFactor) String() string { func (*UnitFactor) ProtoMessage() {} func (x *UnitFactor) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[94] + mi := &file_sysml_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8831,7 +8921,7 @@ func (x *UnitFactor) ProtoReflect() protoreflect.Message { // Deprecated: Use UnitFactor.ProtoReflect.Descriptor instead. func (*UnitFactor) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{94} + return file_sysml_proto_rawDescGZIP(), []int{95} } func (x *UnitFactor) GetUnitId() string { @@ -8863,7 +8953,7 @@ type Diagnostic struct { func (x *Diagnostic) Reset() { *x = Diagnostic{} - mi := &file_sysml_proto_msgTypes[95] + mi := &file_sysml_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8875,7 +8965,7 @@ func (x *Diagnostic) String() string { func (*Diagnostic) ProtoMessage() {} func (x *Diagnostic) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[95] + mi := &file_sysml_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8888,7 +8978,7 @@ func (x *Diagnostic) ProtoReflect() protoreflect.Message { // Deprecated: Use Diagnostic.ProtoReflect.Descriptor instead. func (*Diagnostic) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{95} + return file_sysml_proto_rawDescGZIP(), []int{96} } func (x *Diagnostic) GetSeverity() string { @@ -8933,7 +9023,7 @@ type Span struct { func (x *Span) Reset() { *x = Span{} - mi := &file_sysml_proto_msgTypes[96] + mi := &file_sysml_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8945,7 +9035,7 @@ func (x *Span) String() string { func (*Span) ProtoMessage() {} func (x *Span) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[96] + mi := &file_sysml_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8958,7 +9048,7 @@ func (x *Span) ProtoReflect() protoreflect.Message { // Deprecated: Use Span.ProtoReflect.Descriptor instead. func (*Span) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{96} + return file_sysml_proto_rawDescGZIP(), []int{97} } func (x *Span) GetFile() string { @@ -9006,7 +9096,7 @@ type ServerInfoRequest struct { func (x *ServerInfoRequest) Reset() { *x = ServerInfoRequest{} - mi := &file_sysml_proto_msgTypes[97] + mi := &file_sysml_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9018,7 +9108,7 @@ func (x *ServerInfoRequest) String() string { func (*ServerInfoRequest) ProtoMessage() {} func (x *ServerInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[97] + mi := &file_sysml_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9031,7 +9121,7 @@ func (x *ServerInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerInfoRequest.ProtoReflect.Descriptor instead. func (*ServerInfoRequest) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{97} + return file_sysml_proto_rawDescGZIP(), []int{98} } // ServerInfoResponse describes the running service. @@ -9162,7 +9252,7 @@ type ServerInfoResponse struct { func (x *ServerInfoResponse) Reset() { *x = ServerInfoResponse{} - mi := &file_sysml_proto_msgTypes[98] + mi := &file_sysml_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9174,7 +9264,7 @@ func (x *ServerInfoResponse) String() string { func (*ServerInfoResponse) ProtoMessage() {} func (x *ServerInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[98] + mi := &file_sysml_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9187,7 +9277,7 @@ func (x *ServerInfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerInfoResponse.ProtoReflect.Descriptor instead. func (*ServerInfoResponse) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{98} + return file_sysml_proto_rawDescGZIP(), []int{99} } func (x *ServerInfoResponse) GetVersion() string { @@ -9217,7 +9307,7 @@ type QueryRequest struct { func (x *QueryRequest) Reset() { *x = QueryRequest{} - mi := &file_sysml_proto_msgTypes[99] + mi := &file_sysml_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9229,7 +9319,7 @@ func (x *QueryRequest) String() string { func (*QueryRequest) ProtoMessage() {} func (x *QueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[99] + mi := &file_sysml_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9242,7 +9332,7 @@ func (x *QueryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. func (*QueryRequest) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{99} + return file_sysml_proto_rawDescGZIP(), []int{100} } func (x *QueryRequest) GetModelHash() string { @@ -9279,7 +9369,7 @@ type QueryResponse struct { func (x *QueryResponse) Reset() { *x = QueryResponse{} - mi := &file_sysml_proto_msgTypes[100] + mi := &file_sysml_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9291,7 +9381,7 @@ func (x *QueryResponse) String() string { func (*QueryResponse) ProtoMessage() {} func (x *QueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[100] + mi := &file_sysml_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9304,7 +9394,7 @@ func (x *QueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryResponse.ProtoReflect.Descriptor instead. func (*QueryResponse) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{100} + return file_sysml_proto_rawDescGZIP(), []int{101} } func (x *QueryResponse) GetElements() []*QueryResultElement { @@ -9334,7 +9424,7 @@ type Query struct { func (x *Query) Reset() { *x = Query{} - mi := &file_sysml_proto_msgTypes[101] + mi := &file_sysml_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9346,7 +9436,7 @@ func (x *Query) String() string { func (*Query) ProtoMessage() {} func (x *Query) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[101] + mi := &file_sysml_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9359,7 +9449,7 @@ func (x *Query) ProtoReflect() protoreflect.Message { // Deprecated: Use Query.ProtoReflect.Descriptor instead. func (*Query) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{101} + return file_sysml_proto_rawDescGZIP(), []int{102} } func (x *Query) GetScope() []string { @@ -9398,7 +9488,7 @@ type Constraint struct { func (x *Constraint) Reset() { *x = Constraint{} - mi := &file_sysml_proto_msgTypes[102] + mi := &file_sysml_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9410,7 +9500,7 @@ func (x *Constraint) String() string { func (*Constraint) ProtoMessage() {} func (x *Constraint) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[102] + mi := &file_sysml_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9423,7 +9513,7 @@ func (x *Constraint) ProtoReflect() protoreflect.Message { // Deprecated: Use Constraint.ProtoReflect.Descriptor instead. func (*Constraint) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{102} + return file_sysml_proto_rawDescGZIP(), []int{103} } func (x *Constraint) GetConstraint() isConstraint_Constraint { @@ -9486,7 +9576,7 @@ type PrimitiveConstraint struct { func (x *PrimitiveConstraint) Reset() { *x = PrimitiveConstraint{} - mi := &file_sysml_proto_msgTypes[103] + mi := &file_sysml_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9498,7 +9588,7 @@ func (x *PrimitiveConstraint) String() string { func (*PrimitiveConstraint) ProtoMessage() {} func (x *PrimitiveConstraint) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[103] + mi := &file_sysml_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9511,7 +9601,7 @@ func (x *PrimitiveConstraint) ProtoReflect() protoreflect.Message { // Deprecated: Use PrimitiveConstraint.ProtoReflect.Descriptor instead. func (*PrimitiveConstraint) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{103} + return file_sysml_proto_rawDescGZIP(), []int{104} } func (x *PrimitiveConstraint) GetInverse() bool { @@ -9554,7 +9644,7 @@ type CompositeConstraint struct { func (x *CompositeConstraint) Reset() { *x = CompositeConstraint{} - mi := &file_sysml_proto_msgTypes[104] + mi := &file_sysml_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9566,7 +9656,7 @@ func (x *CompositeConstraint) String() string { func (*CompositeConstraint) ProtoMessage() {} func (x *CompositeConstraint) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[104] + mi := &file_sysml_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9579,7 +9669,7 @@ func (x *CompositeConstraint) ProtoReflect() protoreflect.Message { // Deprecated: Use CompositeConstraint.ProtoReflect.Descriptor instead. func (*CompositeConstraint) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{104} + return file_sysml_proto_rawDescGZIP(), []int{105} } func (x *CompositeConstraint) GetOperator() CompositeOperator { @@ -9611,7 +9701,7 @@ type QueryResultElement struct { func (x *QueryResultElement) Reset() { *x = QueryResultElement{} - mi := &file_sysml_proto_msgTypes[105] + mi := &file_sysml_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9623,7 +9713,7 @@ func (x *QueryResultElement) String() string { func (*QueryResultElement) ProtoMessage() {} func (x *QueryResultElement) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[105] + mi := &file_sysml_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9636,7 +9726,7 @@ func (x *QueryResultElement) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryResultElement.ProtoReflect.Descriptor instead. func (*QueryResultElement) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{105} + return file_sysml_proto_rawDescGZIP(), []int{106} } func (x *QueryResultElement) GetId() string { @@ -9681,7 +9771,7 @@ type SweepRange struct { func (x *SweepRange) Reset() { *x = SweepRange{} - mi := &file_sysml_proto_msgTypes[106] + mi := &file_sysml_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9693,7 +9783,7 @@ func (x *SweepRange) String() string { func (*SweepRange) ProtoMessage() {} func (x *SweepRange) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[106] + mi := &file_sysml_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9706,7 +9796,7 @@ func (x *SweepRange) ProtoReflect() protoreflect.Message { // Deprecated: Use SweepRange.ProtoReflect.Descriptor instead. func (*SweepRange) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{106} + return file_sysml_proto_rawDescGZIP(), []int{107} } func (x *SweepRange) GetParameter() string { @@ -9771,7 +9861,7 @@ type RunSweepRequest struct { func (x *RunSweepRequest) Reset() { *x = RunSweepRequest{} - mi := &file_sysml_proto_msgTypes[107] + mi := &file_sysml_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9783,7 +9873,7 @@ func (x *RunSweepRequest) String() string { func (*RunSweepRequest) ProtoMessage() {} func (x *RunSweepRequest) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[107] + mi := &file_sysml_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9796,7 +9886,7 @@ func (x *RunSweepRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RunSweepRequest.ProtoReflect.Descriptor instead. func (*RunSweepRequest) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{107} + return file_sysml_proto_rawDescGZIP(), []int{108} } func (x *RunSweepRequest) GetModelHash() string { @@ -9890,7 +9980,7 @@ type SweepRow struct { func (x *SweepRow) Reset() { *x = SweepRow{} - mi := &file_sysml_proto_msgTypes[108] + mi := &file_sysml_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9902,7 +9992,7 @@ func (x *SweepRow) String() string { func (*SweepRow) ProtoMessage() {} func (x *SweepRow) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[108] + mi := &file_sysml_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9915,7 +10005,7 @@ func (x *SweepRow) ProtoReflect() protoreflect.Message { // Deprecated: Use SweepRow.ProtoReflect.Descriptor instead. func (*SweepRow) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{108} + return file_sysml_proto_rawDescGZIP(), []int{109} } func (x *SweepRow) GetInputs() []*CalcOutput { @@ -10001,7 +10091,7 @@ type RunSweepResponse struct { func (x *RunSweepResponse) Reset() { *x = RunSweepResponse{} - mi := &file_sysml_proto_msgTypes[109] + mi := &file_sysml_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10013,7 +10103,7 @@ func (x *RunSweepResponse) String() string { func (*RunSweepResponse) ProtoMessage() {} func (x *RunSweepResponse) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[109] + mi := &file_sysml_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10026,7 +10116,7 @@ func (x *RunSweepResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunSweepResponse.ProtoReflect.Descriptor instead. func (*RunSweepResponse) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{109} + return file_sysml_proto_rawDescGZIP(), []int{110} } func (x *RunSweepResponse) GetRows() []*SweepRow { @@ -10127,7 +10217,7 @@ type RunDocumentQueryRequest struct { func (x *RunDocumentQueryRequest) Reset() { *x = RunDocumentQueryRequest{} - mi := &file_sysml_proto_msgTypes[110] + mi := &file_sysml_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10139,7 +10229,7 @@ func (x *RunDocumentQueryRequest) String() string { func (*RunDocumentQueryRequest) ProtoMessage() {} func (x *RunDocumentQueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[110] + mi := &file_sysml_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10152,7 +10242,7 @@ func (x *RunDocumentQueryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RunDocumentQueryRequest.ProtoReflect.Descriptor instead. func (*RunDocumentQueryRequest) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{110} + return file_sysml_proto_rawDescGZIP(), []int{111} } func (x *RunDocumentQueryRequest) GetModelHash() string { @@ -10187,7 +10277,7 @@ type DocumentQueryBinding struct { func (x *DocumentQueryBinding) Reset() { *x = DocumentQueryBinding{} - mi := &file_sysml_proto_msgTypes[111] + mi := &file_sysml_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10199,7 +10289,7 @@ func (x *DocumentQueryBinding) String() string { func (*DocumentQueryBinding) ProtoMessage() {} func (x *DocumentQueryBinding) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[111] + mi := &file_sysml_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10212,7 +10302,7 @@ func (x *DocumentQueryBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentQueryBinding.ProtoReflect.Descriptor instead. func (*DocumentQueryBinding) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{111} + return file_sysml_proto_rawDescGZIP(), []int{112} } func (x *DocumentQueryBinding) GetParameter() string { @@ -10249,6 +10339,7 @@ type DocumentValue struct { // *DocumentValue_State // *DocumentValue_Event // *DocumentValue_BigIntValue + // *DocumentValue_RationalValue Kind isDocumentValue_Kind `protobuf_oneof:"kind"` // Metamodel type of element_id ("PartUsage", ...); answered, ignored when bound. ElementType string `protobuf:"bytes,7,opt,name=element_type,json=elementType,proto3" json:"element_type,omitempty"` @@ -10258,7 +10349,7 @@ type DocumentValue struct { func (x *DocumentValue) Reset() { *x = DocumentValue{} - mi := &file_sysml_proto_msgTypes[112] + mi := &file_sysml_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10270,7 +10361,7 @@ func (x *DocumentValue) String() string { func (*DocumentValue) ProtoMessage() {} func (x *DocumentValue) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[112] + mi := &file_sysml_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10283,7 +10374,7 @@ func (x *DocumentValue) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentValue.ProtoReflect.Descriptor instead. func (*DocumentValue) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{112} + return file_sysml_proto_rawDescGZIP(), []int{113} } func (x *DocumentValue) GetKind() isDocumentValue_Kind { @@ -10401,6 +10492,15 @@ func (x *DocumentValue) GetBigIntValue() string { return "" } +func (x *DocumentValue) GetRationalValue() *Rational { + if x != nil { + if x, ok := x.Kind.(*DocumentValue_RationalValue); ok { + return x.RationalValue + } + } + return nil +} + func (x *DocumentValue) GetElementType() string { if x != nil { return x.ElementType @@ -10461,6 +10561,11 @@ type DocumentValue_BigIntValue struct { BigIntValue string `protobuf:"bytes,13,opt,name=big_int_value,json=bigIntValue,proto3,oneof"` } +type DocumentValue_RationalValue struct { + // An exact Rational no double holds exactly, as Value.rational_value. + RationalValue *Rational `protobuf:"bytes,14,opt,name=rational_value,json=rationalValue,proto3,oneof"` +} + func (*DocumentValue_ElementId) isDocumentValue_Kind() {} func (*DocumentValue_StringValue) isDocumentValue_Kind() {} @@ -10485,6 +10590,8 @@ func (*DocumentValue_Event) isDocumentValue_Kind() {} func (*DocumentValue_BigIntValue) isDocumentValue_Kind() {} +func (*DocumentValue_RationalValue) isDocumentValue_Kind() {} + // DocumentObject is an object the service holds for the model, created by // Instantiate, as a query binds and answers it. A request names it by path // when path is set, and by instance_id otherwise; a request setting both must @@ -10512,7 +10619,7 @@ type DocumentObject struct { func (x *DocumentObject) Reset() { *x = DocumentObject{} - mi := &file_sysml_proto_msgTypes[113] + mi := &file_sysml_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10524,7 +10631,7 @@ func (x *DocumentObject) String() string { func (*DocumentObject) ProtoMessage() {} func (x *DocumentObject) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[113] + mi := &file_sysml_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10537,7 +10644,7 @@ func (x *DocumentObject) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentObject.ProtoReflect.Descriptor instead. func (*DocumentObject) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{113} + return file_sysml_proto_rawDescGZIP(), []int{114} } func (x *DocumentObject) GetInstanceId() int64 { @@ -10592,7 +10699,7 @@ type DocumentVerdict struct { func (x *DocumentVerdict) Reset() { *x = DocumentVerdict{} - mi := &file_sysml_proto_msgTypes[114] + mi := &file_sysml_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10604,7 +10711,7 @@ func (x *DocumentVerdict) String() string { func (*DocumentVerdict) ProtoMessage() {} func (x *DocumentVerdict) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[114] + mi := &file_sysml_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10617,7 +10724,7 @@ func (x *DocumentVerdict) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentVerdict.ProtoReflect.Descriptor instead. func (*DocumentVerdict) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{114} + return file_sysml_proto_rawDescGZIP(), []int{115} } func (x *DocumentVerdict) GetAssertion() *DocumentValue { @@ -10703,7 +10810,7 @@ type DocumentState struct { func (x *DocumentState) Reset() { *x = DocumentState{} - mi := &file_sysml_proto_msgTypes[115] + mi := &file_sysml_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10715,7 +10822,7 @@ func (x *DocumentState) String() string { func (*DocumentState) ProtoMessage() {} func (x *DocumentState) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[115] + mi := &file_sysml_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10728,7 +10835,7 @@ func (x *DocumentState) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentState.ProtoReflect.Descriptor instead. func (*DocumentState) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{115} + return file_sysml_proto_rawDescGZIP(), []int{116} } func (x *DocumentState) GetObject() *DocumentObject { @@ -10818,7 +10925,7 @@ type DocumentEvent struct { func (x *DocumentEvent) Reset() { *x = DocumentEvent{} - mi := &file_sysml_proto_msgTypes[116] + mi := &file_sysml_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10830,7 +10937,7 @@ func (x *DocumentEvent) String() string { func (*DocumentEvent) ProtoMessage() {} func (x *DocumentEvent) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[116] + mi := &file_sysml_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10843,7 +10950,7 @@ func (x *DocumentEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentEvent.ProtoReflect.Descriptor instead. func (*DocumentEvent) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{116} + return file_sysml_proto_rawDescGZIP(), []int{117} } func (x *DocumentEvent) GetKind() string { @@ -10947,7 +11054,7 @@ type DocumentQueryColumn struct { func (x *DocumentQueryColumn) Reset() { *x = DocumentQueryColumn{} - mi := &file_sysml_proto_msgTypes[117] + mi := &file_sysml_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10959,7 +11066,7 @@ func (x *DocumentQueryColumn) String() string { func (*DocumentQueryColumn) ProtoMessage() {} func (x *DocumentQueryColumn) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[117] + mi := &file_sysml_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10972,7 +11079,7 @@ func (x *DocumentQueryColumn) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentQueryColumn.ProtoReflect.Descriptor instead. func (*DocumentQueryColumn) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{117} + return file_sysml_proto_rawDescGZIP(), []int{118} } func (x *DocumentQueryColumn) GetName() string { @@ -10992,7 +11099,7 @@ type DocumentQueryCell struct { func (x *DocumentQueryCell) Reset() { *x = DocumentQueryCell{} - mi := &file_sysml_proto_msgTypes[118] + mi := &file_sysml_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11004,7 +11111,7 @@ func (x *DocumentQueryCell) String() string { func (*DocumentQueryCell) ProtoMessage() {} func (x *DocumentQueryCell) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[118] + mi := &file_sysml_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11017,7 +11124,7 @@ func (x *DocumentQueryCell) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentQueryCell.ProtoReflect.Descriptor instead. func (*DocumentQueryCell) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{118} + return file_sysml_proto_rawDescGZIP(), []int{119} } func (x *DocumentQueryCell) GetValues() []*DocumentValue { @@ -11043,7 +11150,7 @@ type DocumentQueryRow struct { func (x *DocumentQueryRow) Reset() { *x = DocumentQueryRow{} - mi := &file_sysml_proto_msgTypes[119] + mi := &file_sysml_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11055,7 +11162,7 @@ func (x *DocumentQueryRow) String() string { func (*DocumentQueryRow) ProtoMessage() {} func (x *DocumentQueryRow) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[119] + mi := &file_sysml_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11068,7 +11175,7 @@ func (x *DocumentQueryRow) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentQueryRow.ProtoReflect.Descriptor instead. func (*DocumentQueryRow) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{119} + return file_sysml_proto_rawDescGZIP(), []int{120} } func (x *DocumentQueryRow) GetElement() *DocumentValue { @@ -11099,7 +11206,7 @@ type RunDocumentQueryResponse struct { func (x *RunDocumentQueryResponse) Reset() { *x = RunDocumentQueryResponse{} - mi := &file_sysml_proto_msgTypes[120] + mi := &file_sysml_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11111,7 +11218,7 @@ func (x *RunDocumentQueryResponse) String() string { func (*RunDocumentQueryResponse) ProtoMessage() {} func (x *RunDocumentQueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[120] + mi := &file_sysml_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11124,7 +11231,7 @@ func (x *RunDocumentQueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunDocumentQueryResponse.ProtoReflect.Descriptor instead. func (*RunDocumentQueryResponse) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{120} + return file_sysml_proto_rawDescGZIP(), []int{121} } func (x *RunDocumentQueryResponse) GetColumns() []*DocumentQueryColumn { @@ -11161,7 +11268,7 @@ type RenderDocumentRequest struct { func (x *RenderDocumentRequest) Reset() { *x = RenderDocumentRequest{} - mi := &file_sysml_proto_msgTypes[121] + mi := &file_sysml_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11173,7 +11280,7 @@ func (x *RenderDocumentRequest) String() string { func (*RenderDocumentRequest) ProtoMessage() {} func (x *RenderDocumentRequest) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[121] + mi := &file_sysml_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11186,7 +11293,7 @@ func (x *RenderDocumentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RenderDocumentRequest.ProtoReflect.Descriptor instead. func (*RenderDocumentRequest) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{121} + return file_sysml_proto_rawDescGZIP(), []int{122} } func (x *RenderDocumentRequest) GetModelHash() string { @@ -11223,7 +11330,7 @@ type RenderDocumentResponse struct { func (x *RenderDocumentResponse) Reset() { *x = RenderDocumentResponse{} - mi := &file_sysml_proto_msgTypes[122] + mi := &file_sysml_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11235,7 +11342,7 @@ func (x *RenderDocumentResponse) String() string { func (*RenderDocumentResponse) ProtoMessage() {} func (x *RenderDocumentResponse) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[122] + mi := &file_sysml_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11248,7 +11355,7 @@ func (x *RenderDocumentResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RenderDocumentResponse.ProtoReflect.Descriptor instead. func (*RenderDocumentResponse) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{122} + return file_sysml_proto_rawDescGZIP(), []int{123} } func (x *RenderDocumentResponse) GetMarkdown() string { @@ -11864,7 +11971,7 @@ const file_sysml_proto_rawDesc = "" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04type\x18\x02 \x01(\tR\x04type\x12\"\n" + "\x05value\x18\x03 \x01(\v2\f.sysml.ValueR\x05value\x12\x12\n" + - "\x04unit\x18\x04 \x01(\tR\x04unit\"\xcb\a\n" + + "\x04unit\x18\x04 \x01(\tR\x04unit\"\x85\b\n" + "\x05Value\x12\x1d\n" + "\tint_value\x18\x01 \x01(\x03H\x00R\bintValue\x12\x1f\n" + "\n" + @@ -11893,8 +12000,12 @@ const file_sysml_proto_rawDesc = "" + "metaobject\x18\x14 \x01(\v2\x11.sysml.MetaobjectH\x00R\n" + "metaobject\x129\n" + "\fundetermined\x18\x15 \x01(\v2\x13.sysml.UndeterminedH\x00R\fundetermined\x12$\n" + - "\rbig_int_value\x18\x16 \x01(\tH\x00R\vbigIntValueB\x06\n" + - "\x04kind\"N\n" + + "\rbig_int_value\x18\x16 \x01(\tH\x00R\vbigIntValue\x128\n" + + "\x0erational_value\x18\x17 \x01(\v2\x0f.sysml.RationalH\x00R\rrationalValueB\x06\n" + + "\x04kind\"J\n" + + "\bRational\x12\x1c\n" + + "\tnumerator\x18\x01 \x01(\tR\tnumerator\x12 \n" + + "\vdenominator\x18\x02 \x01(\tR\vdenominator\"N\n" + "\n" + "Metaobject\x12\x1d\n" + "\n" + @@ -11938,11 +12049,12 @@ const file_sysml_proto_rawDesc = "" + "\x04name\x18\x03 \x01(\tR\x04name\x12\"\n" + "\x05value\x18\x04 \x01(\v2\f.sysml.ValueR\x05value\"9\n" + "\rValueSequence\x12(\n" + - "\belements\x18\x01 \x03(\v2\f.sysml.ValueR\belements\"\xd7\x01\n" + + "\belements\x18\x01 \x03(\v2\f.sysml.ValueR\belements\"\x99\x02\n" + "\bQuantity\x12%\n" + "\rint_magnitude\x18\x01 \x01(\x03H\x00R\fintMagnitude\x12'\n" + "\x0ereal_magnitude\x18\x02 \x01(\x01H\x00R\rrealMagnitude\x12,\n" + - "\x11big_int_magnitude\x18\x05 \x01(\tH\x00R\x0fbigIntMagnitude\x12\x12\n" + + "\x11big_int_magnitude\x18\x05 \x01(\tH\x00R\x0fbigIntMagnitude\x12@\n" + + "\x12rational_magnitude\x18\x06 \x01(\v2\x0f.sysml.RationalH\x00R\x11rationalMagnitude\x12\x12\n" + "\x04unit\x18\x03 \x01(\tR\x04unit\x12,\n" + "\tunit_term\x18\x04 \x01(\v2\x0f.sysml.UnitTermR\bunitTermB\v\n" + "\tmagnitude\"k\n" + @@ -12062,7 +12174,7 @@ const file_sysml_proto_rawDesc = "" + "\bbindings\x18\x03 \x03(\v2\x1b.sysml.DocumentQueryBindingR\bbindings\"b\n" + "\x14DocumentQueryBinding\x12\x1c\n" + "\tparameter\x18\x01 \x01(\tR\tparameter\x12,\n" + - "\x06values\x18\x02 \x03(\v2\x14.sysml.DocumentValueR\x06values\"\x95\x04\n" + + "\x06values\x18\x02 \x03(\v2\x14.sysml.DocumentValueR\x06values\"\xcf\x04\n" + "\rDocumentValue\x12\x1f\n" + "\n" + "element_id\x18\x01 \x01(\tH\x00R\telementId\x12#\n" + @@ -12079,7 +12191,8 @@ const file_sysml_proto_rawDesc = "" + " \x01(\v2\x15.sysml.DocumentObjectH\x00R\x06object\x12,\n" + "\x05state\x18\v \x01(\v2\x14.sysml.DocumentStateH\x00R\x05state\x12,\n" + "\x05event\x18\f \x01(\v2\x14.sysml.DocumentEventH\x00R\x05event\x12$\n" + - "\rbig_int_value\x18\r \x01(\tH\x00R\vbigIntValue\x12!\n" + + "\rbig_int_value\x18\r \x01(\tH\x00R\vbigIntValue\x128\n" + + "\x0erational_value\x18\x0e \x01(\v2\x0f.sysml.RationalH\x00R\rrationalValue\x12!\n" + "\felement_type\x18\a \x01(\tR\velementTypeB\x06\n" + "\x04kind\"u\n" + "\x0eDocumentObject\x12\x1f\n" + @@ -12215,7 +12328,7 @@ func file_sysml_proto_rawDescGZIP() []byte { } var file_sysml_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_sysml_proto_msgTypes = make([]protoimpl.MessageInfo, 133) +var file_sysml_proto_msgTypes = make([]protoimpl.MessageInfo, 134) var file_sysml_proto_goTypes = []any{ (FailureReason)(0), // 0: sysml.FailureReason (EditFailure)(0), // 1: sysml.EditFailure @@ -12301,59 +12414,60 @@ var file_sysml_proto_goTypes = []any{ (*MultiplicityInfo)(nil), // 81: sysml.MultiplicityInfo (*AttributeInfo)(nil), // 82: sysml.AttributeInfo (*Value)(nil), // 83: sysml.Value - (*Metaobject)(nil), // 84: sysml.Metaobject - (*Undetermined)(nil), // 85: sysml.Undetermined - (*Function)(nil), // 86: sysml.Function - (*ValueSet)(nil), // 87: sysml.ValueSet - (*TensorQuantity)(nil), // 88: sysml.TensorQuantity - (*Array)(nil), // 89: sysml.Array - (*Vector)(nil), // 90: sysml.Vector - (*VectorQuantity)(nil), // 91: sysml.VectorQuantity - (*Complex)(nil), // 92: sysml.Complex - (*EnumLiteral)(nil), // 93: sysml.EnumLiteral - (*ValueSequence)(nil), // 94: sysml.ValueSequence - (*Quantity)(nil), // 95: sysml.Quantity - (*MeasurementRef)(nil), // 96: sysml.MeasurementRef - (*UnitTerm)(nil), // 97: sysml.UnitTerm - (*UnitFactor)(nil), // 98: sysml.UnitFactor - (*Diagnostic)(nil), // 99: sysml.Diagnostic - (*Span)(nil), // 100: sysml.Span - (*ServerInfoRequest)(nil), // 101: sysml.ServerInfoRequest - (*ServerInfoResponse)(nil), // 102: sysml.ServerInfoResponse - (*QueryRequest)(nil), // 103: sysml.QueryRequest - (*QueryResponse)(nil), // 104: sysml.QueryResponse - (*Query)(nil), // 105: sysml.Query - (*Constraint)(nil), // 106: sysml.Constraint - (*PrimitiveConstraint)(nil), // 107: sysml.PrimitiveConstraint - (*CompositeConstraint)(nil), // 108: sysml.CompositeConstraint - (*QueryResultElement)(nil), // 109: sysml.QueryResultElement - (*SweepRange)(nil), // 110: sysml.SweepRange - (*RunSweepRequest)(nil), // 111: sysml.RunSweepRequest - (*SweepRow)(nil), // 112: sysml.SweepRow - (*RunSweepResponse)(nil), // 113: sysml.RunSweepResponse - (*RunDocumentQueryRequest)(nil), // 114: sysml.RunDocumentQueryRequest - (*DocumentQueryBinding)(nil), // 115: sysml.DocumentQueryBinding - (*DocumentValue)(nil), // 116: sysml.DocumentValue - (*DocumentObject)(nil), // 117: sysml.DocumentObject - (*DocumentVerdict)(nil), // 118: sysml.DocumentVerdict - (*DocumentState)(nil), // 119: sysml.DocumentState - (*DocumentEvent)(nil), // 120: sysml.DocumentEvent - (*DocumentQueryColumn)(nil), // 121: sysml.DocumentQueryColumn - (*DocumentQueryCell)(nil), // 122: sysml.DocumentQueryCell - (*DocumentQueryRow)(nil), // 123: sysml.DocumentQueryRow - (*RunDocumentQueryResponse)(nil), // 124: sysml.RunDocumentQueryResponse - (*RenderDocumentRequest)(nil), // 125: sysml.RenderDocumentRequest - (*RenderDocumentResponse)(nil), // 126: sysml.RenderDocumentResponse - nil, // 127: sysml.RunAnalysisRequest.NamedArgumentsEntry - nil, // 128: sysml.Outcome.OutputsEntry - nil, // 129: sysml.Instance.FeatureValuesEntry - nil, // 130: sysml.ExecuteActionRequest.InputsEntry - nil, // 131: sysml.ExecuteActionResponse.OutputsEntry - nil, // 132: sysml.ExecuteActionResponse.PerformerAttributesEntry - nil, // 133: sysml.ExecuteStateResponse.FinalContextEntry - nil, // 134: sysml.SymbolInfo.MetadataEntry - nil, // 135: sysml.QueryResultElement.PropertiesEntry - nil, // 136: sysml.RunSweepRequest.NamedArgumentsEntry + (*Rational)(nil), // 84: sysml.Rational + (*Metaobject)(nil), // 85: sysml.Metaobject + (*Undetermined)(nil), // 86: sysml.Undetermined + (*Function)(nil), // 87: sysml.Function + (*ValueSet)(nil), // 88: sysml.ValueSet + (*TensorQuantity)(nil), // 89: sysml.TensorQuantity + (*Array)(nil), // 90: sysml.Array + (*Vector)(nil), // 91: sysml.Vector + (*VectorQuantity)(nil), // 92: sysml.VectorQuantity + (*Complex)(nil), // 93: sysml.Complex + (*EnumLiteral)(nil), // 94: sysml.EnumLiteral + (*ValueSequence)(nil), // 95: sysml.ValueSequence + (*Quantity)(nil), // 96: sysml.Quantity + (*MeasurementRef)(nil), // 97: sysml.MeasurementRef + (*UnitTerm)(nil), // 98: sysml.UnitTerm + (*UnitFactor)(nil), // 99: sysml.UnitFactor + (*Diagnostic)(nil), // 100: sysml.Diagnostic + (*Span)(nil), // 101: sysml.Span + (*ServerInfoRequest)(nil), // 102: sysml.ServerInfoRequest + (*ServerInfoResponse)(nil), // 103: sysml.ServerInfoResponse + (*QueryRequest)(nil), // 104: sysml.QueryRequest + (*QueryResponse)(nil), // 105: sysml.QueryResponse + (*Query)(nil), // 106: sysml.Query + (*Constraint)(nil), // 107: sysml.Constraint + (*PrimitiveConstraint)(nil), // 108: sysml.PrimitiveConstraint + (*CompositeConstraint)(nil), // 109: sysml.CompositeConstraint + (*QueryResultElement)(nil), // 110: sysml.QueryResultElement + (*SweepRange)(nil), // 111: sysml.SweepRange + (*RunSweepRequest)(nil), // 112: sysml.RunSweepRequest + (*SweepRow)(nil), // 113: sysml.SweepRow + (*RunSweepResponse)(nil), // 114: sysml.RunSweepResponse + (*RunDocumentQueryRequest)(nil), // 115: sysml.RunDocumentQueryRequest + (*DocumentQueryBinding)(nil), // 116: sysml.DocumentQueryBinding + (*DocumentValue)(nil), // 117: sysml.DocumentValue + (*DocumentObject)(nil), // 118: sysml.DocumentObject + (*DocumentVerdict)(nil), // 119: sysml.DocumentVerdict + (*DocumentState)(nil), // 120: sysml.DocumentState + (*DocumentEvent)(nil), // 121: sysml.DocumentEvent + (*DocumentQueryColumn)(nil), // 122: sysml.DocumentQueryColumn + (*DocumentQueryCell)(nil), // 123: sysml.DocumentQueryCell + (*DocumentQueryRow)(nil), // 124: sysml.DocumentQueryRow + (*RunDocumentQueryResponse)(nil), // 125: sysml.RunDocumentQueryResponse + (*RenderDocumentRequest)(nil), // 126: sysml.RenderDocumentRequest + (*RenderDocumentResponse)(nil), // 127: sysml.RenderDocumentResponse + nil, // 128: sysml.RunAnalysisRequest.NamedArgumentsEntry + nil, // 129: sysml.Outcome.OutputsEntry + nil, // 130: sysml.Instance.FeatureValuesEntry + nil, // 131: sysml.ExecuteActionRequest.InputsEntry + nil, // 132: sysml.ExecuteActionResponse.OutputsEntry + nil, // 133: sysml.ExecuteActionResponse.PerformerAttributesEntry + nil, // 134: sysml.ExecuteStateResponse.FinalContextEntry + nil, // 135: sysml.SymbolInfo.MetadataEntry + nil, // 136: sysml.QueryResultElement.PropertiesEntry + nil, // 137: sysml.RunSweepRequest.NamedArgumentsEntry } var file_sysml_proto_depIdxs = []int32{ 0, // 0: sysml.Verdict.failure_reason:type_name -> sysml.FailureReason @@ -12362,73 +12476,73 @@ var file_sysml_proto_depIdxs = []int32{ 83, // 3: sysml.WitnessAssignment.value:type_name -> sysml.Value 4, // 4: sysml.VerifyConstraintResponse.verdict:type_name -> sysml.Verdict 39, // 5: sysml.VerifyConstraintResponse.instances:type_name -> sysml.Instance - 99, // 6: sysml.VerifyConstraintResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 6: sysml.VerifyConstraintResponse.diagnostics:type_name -> sysml.Diagnostic 4, // 7: sysml.VerifyRequirementResponse.verdict:type_name -> sysml.Verdict 39, // 8: sysml.VerifyRequirementResponse.instances:type_name -> sysml.Instance - 99, // 9: sysml.VerifyRequirementResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 9: sysml.VerifyRequirementResponse.diagnostics:type_name -> sysml.Diagnostic 10, // 10: sysml.VerifyRequirementResponse.verification_verdicts:type_name -> sysml.VerificationVerdict 4, // 11: sysml.VerifySatisfactionResponse.verdicts:type_name -> sysml.Verdict 39, // 12: sysml.VerifySatisfactionResponse.instances:type_name -> sysml.Instance - 99, // 13: sysml.VerifySatisfactionResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 13: sysml.VerifySatisfactionResponse.diagnostics:type_name -> sysml.Diagnostic 0, // 14: sysml.VerifySatisfactionResponse.failure_reason:type_name -> sysml.FailureReason 10, // 15: sysml.VerifySatisfactionResponse.verification_verdicts:type_name -> sysml.VerificationVerdict 4, // 16: sysml.ValidateInstanceResponse.verdicts:type_name -> sysml.Verdict 4, // 17: sysml.ValidateInstanceResponse.summary:type_name -> sysml.Verdict 39, // 18: sysml.ValidateInstanceResponse.instances:type_name -> sysml.Instance - 99, // 19: sysml.ValidateInstanceResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 19: sysml.ValidateInstanceResponse.diagnostics:type_name -> sysml.Diagnostic 0, // 20: sysml.ValidateInstanceResponse.failure_reason:type_name -> sysml.FailureReason 10, // 21: sysml.ValidateInstanceResponse.verification_verdicts:type_name -> sysml.VerificationVerdict 83, // 22: sysml.EvaluateCalcRequest.arguments:type_name -> sysml.Value 83, // 23: sysml.EvaluateCalcResponse.result:type_name -> sysml.Value 18, // 24: sysml.EvaluateCalcResponse.outputs:type_name -> sysml.CalcOutput - 99, // 25: sysml.EvaluateCalcResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 25: sysml.EvaluateCalcResponse.diagnostics:type_name -> sysml.Diagnostic 0, // 26: sysml.EvaluateCalcResponse.failure_reason:type_name -> sysml.FailureReason 6, // 27: sysml.EvaluateCalcResponse.bounds:type_name -> sysml.Bound 83, // 28: sysml.CalcOutput.value:type_name -> sysml.Value 83, // 29: sysml.CaseEvaluation.arguments:type_name -> sysml.Value 83, // 30: sysml.CaseEvaluation.result:type_name -> sysml.Value 83, // 31: sysml.RunAnalysisRequest.arguments:type_name -> sysml.Value - 127, // 32: sysml.RunAnalysisRequest.named_arguments:type_name -> sysml.RunAnalysisRequest.NamedArgumentsEntry + 128, // 32: sysml.RunAnalysisRequest.named_arguments:type_name -> sysml.RunAnalysisRequest.NamedArgumentsEntry 18, // 33: sysml.RunAnalysisResponse.outputs:type_name -> sysml.CalcOutput 4, // 34: sysml.RunAnalysisResponse.verdicts:type_name -> sysml.Verdict 39, // 35: sysml.RunAnalysisResponse.instances:type_name -> sysml.Instance - 99, // 36: sysml.RunAnalysisResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 36: sysml.RunAnalysisResponse.diagnostics:type_name -> sysml.Diagnostic 0, // 37: sysml.RunAnalysisResponse.failure_reason:type_name -> sysml.FailureReason 10, // 38: sysml.RunAnalysisResponse.verification_verdicts:type_name -> sysml.VerificationVerdict 23, // 39: sysml.RunAnalysisResponse.outcomes:type_name -> sysml.Outcome 24, // 40: sysml.RunAnalysisResponse.exploration:type_name -> sysml.ExplorationStatus 19, // 41: sysml.RunAnalysisResponse.evaluations:type_name -> sysml.CaseEvaluation 6, // 42: sysml.RunAnalysisResponse.bounds:type_name -> sysml.Bound - 128, // 43: sysml.Outcome.outputs:type_name -> sysml.Outcome.OutputsEntry - 99, // 44: sysml.Outcome.diagnostics:type_name -> sysml.Diagnostic + 129, // 43: sysml.Outcome.outputs:type_name -> sysml.Outcome.OutputsEntry + 100, // 44: sysml.Outcome.diagnostics:type_name -> sysml.Diagnostic 22, // 45: sysml.Outcome.probability_range:type_name -> sysml.ProbabilityRange 26, // 46: sysml.ListEnginesResponse.engines:type_name -> sysml.EngineInfo 29, // 47: sysml.ParseSourcesRequest.documents:type_name -> sysml.SourceDocument 78, // 48: sysml.ParseSourcesResponse.roots:type_name -> sysml.SymbolInfo - 99, // 49: sysml.ParseSourcesResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 49: sysml.ParseSourcesResponse.diagnostics:type_name -> sysml.Diagnostic 78, // 50: sysml.ParseFileResponse.root:type_name -> sysml.SymbolInfo - 99, // 51: sysml.ParseFileResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 51: sysml.ParseFileResponse.diagnostics:type_name -> sysml.Diagnostic 78, // 52: sysml.SymbolResponse.symbol:type_name -> sysml.SymbolInfo - 99, // 53: sysml.DiagnosticsResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 53: sysml.DiagnosticsResponse.diagnostics:type_name -> sysml.Diagnostic 83, // 54: sysml.EvaluateResponse.result:type_name -> sysml.Value - 99, // 55: sysml.EvaluateResponse.diagnostics:type_name -> sysml.Diagnostic - 129, // 56: sysml.Instance.feature_values:type_name -> sysml.Instance.FeatureValuesEntry + 100, // 55: sysml.EvaluateResponse.diagnostics:type_name -> sysml.Diagnostic + 130, // 56: sysml.Instance.feature_values:type_name -> sysml.Instance.FeatureValuesEntry 83, // 57: sysml.FeatureValue.value:type_name -> sysml.Value 83, // 58: sysml.FeatureValue.values:type_name -> sysml.Value 39, // 59: sysml.InstantiateResponse.instance:type_name -> sysml.Instance - 99, // 60: sysml.InstantiateResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 60: sysml.InstantiateResponse.diagnostics:type_name -> sysml.Diagnostic 39, // 61: sysml.InstantiateResponse.instances:type_name -> sysml.Instance - 130, // 62: sysml.ExecuteActionRequest.inputs:type_name -> sysml.ExecuteActionRequest.InputsEntry - 131, // 63: sysml.ExecuteActionResponse.outputs:type_name -> sysml.ExecuteActionResponse.OutputsEntry - 99, // 64: sysml.ExecuteActionResponse.diagnostics:type_name -> sysml.Diagnostic + 131, // 62: sysml.ExecuteActionRequest.inputs:type_name -> sysml.ExecuteActionRequest.InputsEntry + 132, // 63: sysml.ExecuteActionResponse.outputs:type_name -> sysml.ExecuteActionResponse.OutputsEntry + 100, // 64: sysml.ExecuteActionResponse.diagnostics:type_name -> sysml.Diagnostic 23, // 65: sysml.ExecuteActionResponse.outcomes:type_name -> sysml.Outcome 24, // 66: sysml.ExecuteActionResponse.exploration:type_name -> sysml.ExplorationStatus - 132, // 67: sysml.ExecuteActionResponse.performer_attributes:type_name -> sysml.ExecuteActionResponse.PerformerAttributesEntry - 133, // 68: sysml.ExecuteStateResponse.final_context:type_name -> sysml.ExecuteStateResponse.FinalContextEntry - 99, // 69: sysml.ExecuteStateResponse.diagnostics:type_name -> sysml.Diagnostic + 133, // 67: sysml.ExecuteActionResponse.performer_attributes:type_name -> sysml.ExecuteActionResponse.PerformerAttributesEntry + 134, // 68: sysml.ExecuteStateResponse.final_context:type_name -> sysml.ExecuteStateResponse.FinalContextEntry + 100, // 69: sysml.ExecuteStateResponse.diagnostics:type_name -> sysml.Diagnostic 23, // 70: sysml.ExecuteStateResponse.outcomes:type_name -> sysml.Outcome 24, // 71: sysml.ExecuteStateResponse.exploration:type_name -> sysml.ExplorationStatus - 99, // 72: sysml.ConvertResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 72: sysml.ConvertResponse.diagnostics:type_name -> sysml.Diagnostic 51, // 73: sysml.MigrateResponse.report:type_name -> sysml.MigrationReport 53, // 74: sysml.MigrateResponse.files:type_name -> sysml.MigrationFile 52, // 75: sysml.MigrationReport.entries:type_name -> sysml.MigrationEntry @@ -12455,143 +12569,146 @@ var file_sysml_proto_depIdxs = []int32{ 68, // 96: sysml.AddSequenceEdit.else_body:type_name -> sysml.AddSequenceEdit 77, // 97: sysml.ApplyEditsResponse.applied:type_name -> sysml.AppliedEdit 1, // 98: sysml.ApplyEditsResponse.failure:type_name -> sysml.EditFailure - 99, // 99: sysml.ApplyEditsResponse.diagnostics:type_name -> sysml.Diagnostic + 100, // 99: sysml.ApplyEditsResponse.diagnostics:type_name -> sysml.Diagnostic 75, // 100: sysml.ApplyEditsResponse.documents:type_name -> sysml.EditedDocument 76, // 101: sysml.ApplyEditsResponse.referrers:type_name -> sysml.Referrer - 134, // 102: sysml.SymbolInfo.metadata:type_name -> sysml.SymbolInfo.MetadataEntry + 135, // 102: sysml.SymbolInfo.metadata:type_name -> sysml.SymbolInfo.MetadataEntry 82, // 103: sysml.SymbolInfo.attributes:type_name -> sysml.AttributeInfo 80, // 104: sysml.SymbolInfo.type_info:type_name -> sysml.TypeInfo 81, // 105: sysml.SymbolInfo.multiplicity:type_name -> sysml.MultiplicityInfo 79, // 106: sysml.SymbolInfo.specializations:type_name -> sysml.Specialization 83, // 107: sysml.AttributeInfo.value:type_name -> sysml.Value - 94, // 108: sysml.Value.sequence:type_name -> sysml.ValueSequence - 95, // 109: sysml.Value.quantity:type_name -> sysml.Quantity - 93, // 110: sysml.Value.enum_literal:type_name -> sysml.EnumLiteral - 92, // 111: sysml.Value.complex:type_name -> sysml.Complex - 89, // 112: sysml.Value.array:type_name -> sysml.Array - 90, // 113: sysml.Value.vector:type_name -> sysml.Vector - 91, // 114: sysml.Value.vector_quantity:type_name -> sysml.VectorQuantity - 96, // 115: sysml.Value.measurement_ref:type_name -> sysml.MeasurementRef - 86, // 116: sysml.Value.function:type_name -> sysml.Function - 87, // 117: sysml.Value.set:type_name -> sysml.ValueSet - 88, // 118: sysml.Value.tensor_quantity:type_name -> sysml.TensorQuantity - 84, // 119: sysml.Value.metaobject:type_name -> sysml.Metaobject - 85, // 120: sysml.Value.undetermined:type_name -> sysml.Undetermined - 81, // 121: sysml.Undetermined.count:type_name -> sysml.MultiplicityInfo - 83, // 122: sysml.ValueSet.elements:type_name -> sysml.Value - 95, // 123: sysml.TensorQuantity.components:type_name -> sysml.Quantity - 83, // 124: sysml.Array.elements:type_name -> sysml.Value - 83, // 125: sysml.Vector.components:type_name -> sysml.Value - 95, // 126: sysml.VectorQuantity.components:type_name -> sysml.Quantity - 83, // 127: sysml.EnumLiteral.value:type_name -> sysml.Value - 83, // 128: sysml.ValueSequence.elements:type_name -> sysml.Value - 97, // 129: sysml.Quantity.unit_term:type_name -> sysml.UnitTerm - 97, // 130: sysml.MeasurementRef.unit_term:type_name -> sysml.UnitTerm - 98, // 131: sysml.UnitTerm.factors:type_name -> sysml.UnitFactor - 100, // 132: sysml.Diagnostic.span:type_name -> sysml.Span - 105, // 133: sysml.QueryRequest.query:type_name -> sysml.Query - 109, // 134: sysml.QueryResponse.elements:type_name -> sysml.QueryResultElement - 106, // 135: sysml.Query.where:type_name -> sysml.Constraint - 107, // 136: sysml.Constraint.primitive:type_name -> sysml.PrimitiveConstraint - 108, // 137: sysml.Constraint.composite:type_name -> sysml.CompositeConstraint - 2, // 138: sysml.PrimitiveConstraint.operator:type_name -> sysml.PrimitiveOperator - 3, // 139: sysml.CompositeConstraint.operator:type_name -> sysml.CompositeOperator - 106, // 140: sysml.CompositeConstraint.constraint:type_name -> sysml.Constraint - 135, // 141: sysml.QueryResultElement.properties:type_name -> sysml.QueryResultElement.PropertiesEntry - 83, // 142: sysml.SweepRange.start:type_name -> sysml.Value - 83, // 143: sysml.SweepRange.end:type_name -> sysml.Value - 83, // 144: sysml.SweepRange.step:type_name -> sysml.Value - 83, // 145: sysml.RunSweepRequest.arguments:type_name -> sysml.Value - 136, // 146: sysml.RunSweepRequest.named_arguments:type_name -> sysml.RunSweepRequest.NamedArgumentsEntry - 110, // 147: sysml.RunSweepRequest.ranges:type_name -> sysml.SweepRange - 18, // 148: sysml.SweepRow.inputs:type_name -> sysml.CalcOutput - 18, // 149: sysml.SweepRow.outputs:type_name -> sysml.CalcOutput - 4, // 150: sysml.SweepRow.verdicts:type_name -> sysml.Verdict - 0, // 151: sysml.SweepRow.failure_reason:type_name -> sysml.FailureReason - 19, // 152: sysml.SweepRow.evaluations:type_name -> sysml.CaseEvaluation - 112, // 153: sysml.RunSweepResponse.rows:type_name -> sysml.SweepRow - 99, // 154: sysml.RunSweepResponse.diagnostics:type_name -> sysml.Diagnostic - 0, // 155: sysml.RunSweepResponse.failure_reason:type_name -> sysml.FailureReason - 39, // 156: sysml.RunSweepResponse.instances:type_name -> sysml.Instance - 6, // 157: sysml.RunSweepResponse.bounds:type_name -> sysml.Bound - 115, // 158: sysml.RunDocumentQueryRequest.bindings:type_name -> sysml.DocumentQueryBinding - 116, // 159: sysml.DocumentQueryBinding.values:type_name -> sysml.DocumentValue - 95, // 160: sysml.DocumentValue.quantity:type_name -> sysml.Quantity - 118, // 161: sysml.DocumentValue.verdict:type_name -> sysml.DocumentVerdict - 117, // 162: sysml.DocumentValue.object:type_name -> sysml.DocumentObject - 119, // 163: sysml.DocumentValue.state:type_name -> sysml.DocumentState - 120, // 164: sysml.DocumentValue.event:type_name -> sysml.DocumentEvent - 116, // 165: sysml.DocumentObject.element:type_name -> sysml.DocumentValue - 116, // 166: sysml.DocumentVerdict.assertion:type_name -> sysml.DocumentValue - 117, // 167: sysml.DocumentState.object:type_name -> sysml.DocumentObject - 116, // 168: sysml.DocumentState.state:type_name -> sysml.DocumentValue - 116, // 169: sysml.DocumentEvent.time:type_name -> sysml.DocumentValue - 117, // 170: sysml.DocumentEvent.object:type_name -> sysml.DocumentObject - 117, // 171: sysml.DocumentEvent.target:type_name -> sysml.DocumentObject - 116, // 172: sysml.DocumentQueryCell.values:type_name -> sysml.DocumentValue - 116, // 173: sysml.DocumentQueryRow.element:type_name -> sysml.DocumentValue - 122, // 174: sysml.DocumentQueryRow.cells:type_name -> sysml.DocumentQueryCell - 121, // 175: sysml.RunDocumentQueryResponse.columns:type_name -> sysml.DocumentQueryColumn - 123, // 176: sysml.RunDocumentQueryResponse.rows:type_name -> sysml.DocumentQueryRow - 83, // 177: sysml.RunAnalysisRequest.NamedArgumentsEntry.value:type_name -> sysml.Value - 83, // 178: sysml.Outcome.OutputsEntry.value:type_name -> sysml.Value - 40, // 179: sysml.Instance.FeatureValuesEntry.value:type_name -> sysml.FeatureValue - 83, // 180: sysml.ExecuteActionRequest.InputsEntry.value:type_name -> sysml.Value - 83, // 181: sysml.ExecuteActionResponse.OutputsEntry.value:type_name -> sysml.Value - 83, // 182: sysml.ExecuteActionResponse.PerformerAttributesEntry.value:type_name -> sysml.Value - 83, // 183: sysml.ExecuteStateResponse.FinalContextEntry.value:type_name -> sysml.Value - 83, // 184: sysml.RunSweepRequest.NamedArgumentsEntry.value:type_name -> sysml.Value - 101, // 185: sysml.SysMLService.GetServerInfo:input_type -> sysml.ServerInfoRequest - 28, // 186: sysml.SysMLService.ParseFile:input_type -> sysml.ParseFileRequest - 30, // 187: sysml.SysMLService.ParseSources:input_type -> sysml.ParseSourcesRequest - 33, // 188: sysml.SysMLService.GetSymbol:input_type -> sysml.GetSymbolRequest - 35, // 189: sysml.SysMLService.GetDiagnostics:input_type -> sysml.DiagnosticsRequest - 37, // 190: sysml.SysMLService.Evaluate:input_type -> sysml.EvaluateRequest - 41, // 191: sysml.SysMLService.Instantiate:input_type -> sysml.InstantiateRequest - 43, // 192: sysml.SysMLService.ExecuteAction:input_type -> sysml.ExecuteActionRequest - 45, // 193: sysml.SysMLService.ExecuteState:input_type -> sysml.ExecuteStateRequest - 47, // 194: sysml.SysMLService.Convert:input_type -> sysml.ConvertRequest - 49, // 195: sysml.SysMLService.Migrate:input_type -> sysml.MigrateRequest - 54, // 196: sysml.SysMLService.ApplyEdits:input_type -> sysml.ApplyEditsRequest - 7, // 197: sysml.SysMLService.VerifyConstraint:input_type -> sysml.VerifyConstraintRequest - 9, // 198: sysml.SysMLService.VerifyRequirement:input_type -> sysml.VerifyRequirementRequest - 12, // 199: sysml.SysMLService.VerifySatisfaction:input_type -> sysml.VerifySatisfactionRequest - 14, // 200: sysml.SysMLService.ValidateInstance:input_type -> sysml.ValidateInstanceRequest - 16, // 201: sysml.SysMLService.EvaluateCalc:input_type -> sysml.EvaluateCalcRequest - 20, // 202: sysml.SysMLService.RunAnalysis:input_type -> sysml.RunAnalysisRequest - 111, // 203: sysml.SysMLService.RunSweep:input_type -> sysml.RunSweepRequest - 25, // 204: sysml.SysMLService.ListEngines:input_type -> sysml.ListEnginesRequest - 103, // 205: sysml.SysMLService.Query:input_type -> sysml.QueryRequest - 114, // 206: sysml.SysMLService.RunDocumentQuery:input_type -> sysml.RunDocumentQueryRequest - 125, // 207: sysml.SysMLService.RenderDocument:input_type -> sysml.RenderDocumentRequest - 102, // 208: sysml.SysMLService.GetServerInfo:output_type -> sysml.ServerInfoResponse - 32, // 209: sysml.SysMLService.ParseFile:output_type -> sysml.ParseFileResponse - 31, // 210: sysml.SysMLService.ParseSources:output_type -> sysml.ParseSourcesResponse - 34, // 211: sysml.SysMLService.GetSymbol:output_type -> sysml.SymbolResponse - 36, // 212: sysml.SysMLService.GetDiagnostics:output_type -> sysml.DiagnosticsResponse - 38, // 213: sysml.SysMLService.Evaluate:output_type -> sysml.EvaluateResponse - 42, // 214: sysml.SysMLService.Instantiate:output_type -> sysml.InstantiateResponse - 44, // 215: sysml.SysMLService.ExecuteAction:output_type -> sysml.ExecuteActionResponse - 46, // 216: sysml.SysMLService.ExecuteState:output_type -> sysml.ExecuteStateResponse - 48, // 217: sysml.SysMLService.Convert:output_type -> sysml.ConvertResponse - 50, // 218: sysml.SysMLService.Migrate:output_type -> sysml.MigrateResponse - 74, // 219: sysml.SysMLService.ApplyEdits:output_type -> sysml.ApplyEditsResponse - 8, // 220: sysml.SysMLService.VerifyConstraint:output_type -> sysml.VerifyConstraintResponse - 11, // 221: sysml.SysMLService.VerifyRequirement:output_type -> sysml.VerifyRequirementResponse - 13, // 222: sysml.SysMLService.VerifySatisfaction:output_type -> sysml.VerifySatisfactionResponse - 15, // 223: sysml.SysMLService.ValidateInstance:output_type -> sysml.ValidateInstanceResponse - 17, // 224: sysml.SysMLService.EvaluateCalc:output_type -> sysml.EvaluateCalcResponse - 21, // 225: sysml.SysMLService.RunAnalysis:output_type -> sysml.RunAnalysisResponse - 113, // 226: sysml.SysMLService.RunSweep:output_type -> sysml.RunSweepResponse - 27, // 227: sysml.SysMLService.ListEngines:output_type -> sysml.ListEnginesResponse - 104, // 228: sysml.SysMLService.Query:output_type -> sysml.QueryResponse - 124, // 229: sysml.SysMLService.RunDocumentQuery:output_type -> sysml.RunDocumentQueryResponse - 126, // 230: sysml.SysMLService.RenderDocument:output_type -> sysml.RenderDocumentResponse - 208, // [208:231] is the sub-list for method output_type - 185, // [185:208] is the sub-list for method input_type - 185, // [185:185] is the sub-list for extension type_name - 185, // [185:185] is the sub-list for extension extendee - 0, // [0:185] is the sub-list for field type_name + 95, // 108: sysml.Value.sequence:type_name -> sysml.ValueSequence + 96, // 109: sysml.Value.quantity:type_name -> sysml.Quantity + 94, // 110: sysml.Value.enum_literal:type_name -> sysml.EnumLiteral + 93, // 111: sysml.Value.complex:type_name -> sysml.Complex + 90, // 112: sysml.Value.array:type_name -> sysml.Array + 91, // 113: sysml.Value.vector:type_name -> sysml.Vector + 92, // 114: sysml.Value.vector_quantity:type_name -> sysml.VectorQuantity + 97, // 115: sysml.Value.measurement_ref:type_name -> sysml.MeasurementRef + 87, // 116: sysml.Value.function:type_name -> sysml.Function + 88, // 117: sysml.Value.set:type_name -> sysml.ValueSet + 89, // 118: sysml.Value.tensor_quantity:type_name -> sysml.TensorQuantity + 85, // 119: sysml.Value.metaobject:type_name -> sysml.Metaobject + 86, // 120: sysml.Value.undetermined:type_name -> sysml.Undetermined + 84, // 121: sysml.Value.rational_value:type_name -> sysml.Rational + 81, // 122: sysml.Undetermined.count:type_name -> sysml.MultiplicityInfo + 83, // 123: sysml.ValueSet.elements:type_name -> sysml.Value + 96, // 124: sysml.TensorQuantity.components:type_name -> sysml.Quantity + 83, // 125: sysml.Array.elements:type_name -> sysml.Value + 83, // 126: sysml.Vector.components:type_name -> sysml.Value + 96, // 127: sysml.VectorQuantity.components:type_name -> sysml.Quantity + 83, // 128: sysml.EnumLiteral.value:type_name -> sysml.Value + 83, // 129: sysml.ValueSequence.elements:type_name -> sysml.Value + 84, // 130: sysml.Quantity.rational_magnitude:type_name -> sysml.Rational + 98, // 131: sysml.Quantity.unit_term:type_name -> sysml.UnitTerm + 98, // 132: sysml.MeasurementRef.unit_term:type_name -> sysml.UnitTerm + 99, // 133: sysml.UnitTerm.factors:type_name -> sysml.UnitFactor + 101, // 134: sysml.Diagnostic.span:type_name -> sysml.Span + 106, // 135: sysml.QueryRequest.query:type_name -> sysml.Query + 110, // 136: sysml.QueryResponse.elements:type_name -> sysml.QueryResultElement + 107, // 137: sysml.Query.where:type_name -> sysml.Constraint + 108, // 138: sysml.Constraint.primitive:type_name -> sysml.PrimitiveConstraint + 109, // 139: sysml.Constraint.composite:type_name -> sysml.CompositeConstraint + 2, // 140: sysml.PrimitiveConstraint.operator:type_name -> sysml.PrimitiveOperator + 3, // 141: sysml.CompositeConstraint.operator:type_name -> sysml.CompositeOperator + 107, // 142: sysml.CompositeConstraint.constraint:type_name -> sysml.Constraint + 136, // 143: sysml.QueryResultElement.properties:type_name -> sysml.QueryResultElement.PropertiesEntry + 83, // 144: sysml.SweepRange.start:type_name -> sysml.Value + 83, // 145: sysml.SweepRange.end:type_name -> sysml.Value + 83, // 146: sysml.SweepRange.step:type_name -> sysml.Value + 83, // 147: sysml.RunSweepRequest.arguments:type_name -> sysml.Value + 137, // 148: sysml.RunSweepRequest.named_arguments:type_name -> sysml.RunSweepRequest.NamedArgumentsEntry + 111, // 149: sysml.RunSweepRequest.ranges:type_name -> sysml.SweepRange + 18, // 150: sysml.SweepRow.inputs:type_name -> sysml.CalcOutput + 18, // 151: sysml.SweepRow.outputs:type_name -> sysml.CalcOutput + 4, // 152: sysml.SweepRow.verdicts:type_name -> sysml.Verdict + 0, // 153: sysml.SweepRow.failure_reason:type_name -> sysml.FailureReason + 19, // 154: sysml.SweepRow.evaluations:type_name -> sysml.CaseEvaluation + 113, // 155: sysml.RunSweepResponse.rows:type_name -> sysml.SweepRow + 100, // 156: sysml.RunSweepResponse.diagnostics:type_name -> sysml.Diagnostic + 0, // 157: sysml.RunSweepResponse.failure_reason:type_name -> sysml.FailureReason + 39, // 158: sysml.RunSweepResponse.instances:type_name -> sysml.Instance + 6, // 159: sysml.RunSweepResponse.bounds:type_name -> sysml.Bound + 116, // 160: sysml.RunDocumentQueryRequest.bindings:type_name -> sysml.DocumentQueryBinding + 117, // 161: sysml.DocumentQueryBinding.values:type_name -> sysml.DocumentValue + 96, // 162: sysml.DocumentValue.quantity:type_name -> sysml.Quantity + 119, // 163: sysml.DocumentValue.verdict:type_name -> sysml.DocumentVerdict + 118, // 164: sysml.DocumentValue.object:type_name -> sysml.DocumentObject + 120, // 165: sysml.DocumentValue.state:type_name -> sysml.DocumentState + 121, // 166: sysml.DocumentValue.event:type_name -> sysml.DocumentEvent + 84, // 167: sysml.DocumentValue.rational_value:type_name -> sysml.Rational + 117, // 168: sysml.DocumentObject.element:type_name -> sysml.DocumentValue + 117, // 169: sysml.DocumentVerdict.assertion:type_name -> sysml.DocumentValue + 118, // 170: sysml.DocumentState.object:type_name -> sysml.DocumentObject + 117, // 171: sysml.DocumentState.state:type_name -> sysml.DocumentValue + 117, // 172: sysml.DocumentEvent.time:type_name -> sysml.DocumentValue + 118, // 173: sysml.DocumentEvent.object:type_name -> sysml.DocumentObject + 118, // 174: sysml.DocumentEvent.target:type_name -> sysml.DocumentObject + 117, // 175: sysml.DocumentQueryCell.values:type_name -> sysml.DocumentValue + 117, // 176: sysml.DocumentQueryRow.element:type_name -> sysml.DocumentValue + 123, // 177: sysml.DocumentQueryRow.cells:type_name -> sysml.DocumentQueryCell + 122, // 178: sysml.RunDocumentQueryResponse.columns:type_name -> sysml.DocumentQueryColumn + 124, // 179: sysml.RunDocumentQueryResponse.rows:type_name -> sysml.DocumentQueryRow + 83, // 180: sysml.RunAnalysisRequest.NamedArgumentsEntry.value:type_name -> sysml.Value + 83, // 181: sysml.Outcome.OutputsEntry.value:type_name -> sysml.Value + 40, // 182: sysml.Instance.FeatureValuesEntry.value:type_name -> sysml.FeatureValue + 83, // 183: sysml.ExecuteActionRequest.InputsEntry.value:type_name -> sysml.Value + 83, // 184: sysml.ExecuteActionResponse.OutputsEntry.value:type_name -> sysml.Value + 83, // 185: sysml.ExecuteActionResponse.PerformerAttributesEntry.value:type_name -> sysml.Value + 83, // 186: sysml.ExecuteStateResponse.FinalContextEntry.value:type_name -> sysml.Value + 83, // 187: sysml.RunSweepRequest.NamedArgumentsEntry.value:type_name -> sysml.Value + 102, // 188: sysml.SysMLService.GetServerInfo:input_type -> sysml.ServerInfoRequest + 28, // 189: sysml.SysMLService.ParseFile:input_type -> sysml.ParseFileRequest + 30, // 190: sysml.SysMLService.ParseSources:input_type -> sysml.ParseSourcesRequest + 33, // 191: sysml.SysMLService.GetSymbol:input_type -> sysml.GetSymbolRequest + 35, // 192: sysml.SysMLService.GetDiagnostics:input_type -> sysml.DiagnosticsRequest + 37, // 193: sysml.SysMLService.Evaluate:input_type -> sysml.EvaluateRequest + 41, // 194: sysml.SysMLService.Instantiate:input_type -> sysml.InstantiateRequest + 43, // 195: sysml.SysMLService.ExecuteAction:input_type -> sysml.ExecuteActionRequest + 45, // 196: sysml.SysMLService.ExecuteState:input_type -> sysml.ExecuteStateRequest + 47, // 197: sysml.SysMLService.Convert:input_type -> sysml.ConvertRequest + 49, // 198: sysml.SysMLService.Migrate:input_type -> sysml.MigrateRequest + 54, // 199: sysml.SysMLService.ApplyEdits:input_type -> sysml.ApplyEditsRequest + 7, // 200: sysml.SysMLService.VerifyConstraint:input_type -> sysml.VerifyConstraintRequest + 9, // 201: sysml.SysMLService.VerifyRequirement:input_type -> sysml.VerifyRequirementRequest + 12, // 202: sysml.SysMLService.VerifySatisfaction:input_type -> sysml.VerifySatisfactionRequest + 14, // 203: sysml.SysMLService.ValidateInstance:input_type -> sysml.ValidateInstanceRequest + 16, // 204: sysml.SysMLService.EvaluateCalc:input_type -> sysml.EvaluateCalcRequest + 20, // 205: sysml.SysMLService.RunAnalysis:input_type -> sysml.RunAnalysisRequest + 112, // 206: sysml.SysMLService.RunSweep:input_type -> sysml.RunSweepRequest + 25, // 207: sysml.SysMLService.ListEngines:input_type -> sysml.ListEnginesRequest + 104, // 208: sysml.SysMLService.Query:input_type -> sysml.QueryRequest + 115, // 209: sysml.SysMLService.RunDocumentQuery:input_type -> sysml.RunDocumentQueryRequest + 126, // 210: sysml.SysMLService.RenderDocument:input_type -> sysml.RenderDocumentRequest + 103, // 211: sysml.SysMLService.GetServerInfo:output_type -> sysml.ServerInfoResponse + 32, // 212: sysml.SysMLService.ParseFile:output_type -> sysml.ParseFileResponse + 31, // 213: sysml.SysMLService.ParseSources:output_type -> sysml.ParseSourcesResponse + 34, // 214: sysml.SysMLService.GetSymbol:output_type -> sysml.SymbolResponse + 36, // 215: sysml.SysMLService.GetDiagnostics:output_type -> sysml.DiagnosticsResponse + 38, // 216: sysml.SysMLService.Evaluate:output_type -> sysml.EvaluateResponse + 42, // 217: sysml.SysMLService.Instantiate:output_type -> sysml.InstantiateResponse + 44, // 218: sysml.SysMLService.ExecuteAction:output_type -> sysml.ExecuteActionResponse + 46, // 219: sysml.SysMLService.ExecuteState:output_type -> sysml.ExecuteStateResponse + 48, // 220: sysml.SysMLService.Convert:output_type -> sysml.ConvertResponse + 50, // 221: sysml.SysMLService.Migrate:output_type -> sysml.MigrateResponse + 74, // 222: sysml.SysMLService.ApplyEdits:output_type -> sysml.ApplyEditsResponse + 8, // 223: sysml.SysMLService.VerifyConstraint:output_type -> sysml.VerifyConstraintResponse + 11, // 224: sysml.SysMLService.VerifyRequirement:output_type -> sysml.VerifyRequirementResponse + 13, // 225: sysml.SysMLService.VerifySatisfaction:output_type -> sysml.VerifySatisfactionResponse + 15, // 226: sysml.SysMLService.ValidateInstance:output_type -> sysml.ValidateInstanceResponse + 17, // 227: sysml.SysMLService.EvaluateCalc:output_type -> sysml.EvaluateCalcResponse + 21, // 228: sysml.SysMLService.RunAnalysis:output_type -> sysml.RunAnalysisResponse + 114, // 229: sysml.SysMLService.RunSweep:output_type -> sysml.RunSweepResponse + 27, // 230: sysml.SysMLService.ListEngines:output_type -> sysml.ListEnginesResponse + 105, // 231: sysml.SysMLService.Query:output_type -> sysml.QueryResponse + 125, // 232: sysml.SysMLService.RunDocumentQuery:output_type -> sysml.RunDocumentQueryResponse + 127, // 233: sysml.SysMLService.RenderDocument:output_type -> sysml.RenderDocumentResponse + 211, // [211:234] is the sub-list for method output_type + 188, // [188:211] is the sub-list for method input_type + 188, // [188:188] is the sub-list for extension type_name + 188, // [188:188] is the sub-list for extension extendee + 0, // [0:188] is the sub-list for field type_name } func init() { file_sysml_proto_init() } @@ -12660,17 +12777,19 @@ func file_sysml_proto_init() { (*Value_Metaobject)(nil), (*Value_Undetermined)(nil), (*Value_BigIntValue)(nil), + (*Value_RationalValue)(nil), } - file_sysml_proto_msgTypes[91].OneofWrappers = []any{ + file_sysml_proto_msgTypes[92].OneofWrappers = []any{ (*Quantity_IntMagnitude)(nil), (*Quantity_RealMagnitude)(nil), (*Quantity_BigIntMagnitude)(nil), + (*Quantity_RationalMagnitude)(nil), } - file_sysml_proto_msgTypes[102].OneofWrappers = []any{ + file_sysml_proto_msgTypes[103].OneofWrappers = []any{ (*Constraint_Primitive)(nil), (*Constraint_Composite)(nil), } - file_sysml_proto_msgTypes[112].OneofWrappers = []any{ + file_sysml_proto_msgTypes[113].OneofWrappers = []any{ (*DocumentValue_ElementId)(nil), (*DocumentValue_StringValue)(nil), (*DocumentValue_IntValue)(nil), @@ -12683,6 +12802,7 @@ func file_sysml_proto_init() { (*DocumentValue_State)(nil), (*DocumentValue_Event)(nil), (*DocumentValue_BigIntValue)(nil), + (*DocumentValue_RationalValue)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -12690,7 +12810,7 @@ func file_sysml_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_sysml_proto_rawDesc), len(file_sysml_proto_rawDesc)), NumEnums: 4, - NumMessages: 133, + NumMessages: 134, NumExtensions: 0, NumServices: 1, }, diff --git a/api/proto/sysml.proto b/api/proto/sysml.proto index e3e5fc690b..6c1be48d75 100644 --- a/api/proto/sysml.proto +++ b/api/proto/sysml.proto @@ -1525,9 +1525,20 @@ message Value { // An Integer beyond int64, in decimal (`-` signed, no `+`, no leading // zeros). An Integer that fits int64 is always int_value, never this. string big_int_value = 22; + // An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1` + // or `1 / 3` evaluates; one a double holds exactly crosses as real_value. + Rational rational_value = 23; } } +// Rational is an exact rational number in lowest terms: numerator over a +// positive denominator, each written as big_int_value is. A decoder rejects one +// not in lowest terms or one a double holds exactly, so no number has two spellings. +message Rational { + string numerator = 1; + string denominator = 2; +} + // Metaobject is an element of the model held as an instance of its reflective // metaclass: what `x meta KerML::Feature`, or the last element of // `x.metadata`, evaluates to (KerML 7.4.9.2, 8.3.4.8.15). It crosses as the @@ -1662,6 +1673,8 @@ message Quantity { double real_magnitude = 2; // An Integer magnitude beyond int64, in decimal, as Value.big_int_value. string big_int_magnitude = 5; + // An exact Rational magnitude no double holds exactly, as Value.rational_value. + Rational rational_magnitude = 6; } // Unit as written ("km/h") or as an operation composed it ("m/s"); empty for // one never written down, described by unit_term alone. @@ -2081,6 +2094,8 @@ message DocumentValue { DocumentEvent event = 12; // a row Events answered; answered, never bound // An Integer beyond int64, in decimal, as Value.big_int_value. string big_int_value = 13; + // An exact Rational no double holds exactly, as Value.rational_value. + Rational rational_value = 14; } // Metamodel type of element_id ("PartUsage", ...); answered, ignored when bound. string element_type = 7; diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValue.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValue.java index b32bf922ec..3fc7e5060b 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValue.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValue.java @@ -69,6 +69,7 @@ public enum KindCase STATE(11), EVENT(12), BIG_INT_VALUE(13), + RATIONAL_VALUE(14), KIND_NOT_SET(0); private final int value; private KindCase(int value) { @@ -98,6 +99,7 @@ public static KindCase forNumber(int value) { case 11: return STATE; case 12: return EVENT; case 13: return BIG_INT_VALUE; + case 14: return RATIONAL_VALUE; case 0: return KIND_NOT_SET; default: return null; } @@ -592,6 +594,49 @@ public java.lang.String getBigIntValue() { } } + public static final int RATIONAL_VALUE_FIELD_NUMBER = 14; + /** + *
+   * An exact Rational no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + * @return Whether the rationalValue field is set. + */ + @java.lang.Override + public boolean hasRationalValue() { + return kindCase_ == 14; + } + /** + *
+   * An exact Rational no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + * @return The rationalValue. + */ + @java.lang.Override + public org.openmbee.opensysml.proto.Rational getRationalValue() { + if (kindCase_ == 14) { + return (org.openmbee.opensysml.proto.Rational) kind_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + /** + *
+   * An exact Rational no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.RationalOrBuilder getRationalValueOrBuilder() { + if (kindCase_ == 14) { + return (org.openmbee.opensysml.proto.Rational) kind_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + public static final int ELEMENT_TYPE_FIELD_NUMBER = 7; @SuppressWarnings("serial") private volatile java.lang.Object elementType_ = ""; @@ -696,6 +741,9 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (kindCase_ == 13) { com.google.protobuf.GeneratedMessage.writeString(output, 13, kind_); } + if (kindCase_ == 14) { + output.writeMessage(14, (org.openmbee.opensysml.proto.Rational) kind_); + } getUnknownFields().writeTo(output); } @@ -757,6 +805,10 @@ public int getSerializedSize() { if (kindCase_ == 13) { size += com.google.protobuf.GeneratedMessage.computeStringSize(13, kind_); } + if (kindCase_ == 14) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(14, (org.openmbee.opensysml.proto.Rational) kind_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -825,6 +877,10 @@ public boolean equals(final java.lang.Object obj) { if (!getBigIntValue() .equals(other.getBigIntValue())) return false; break; + case 14: + if (!getRationalValue() + .equals(other.getRationalValue())) return false; + break; case 0: default: } @@ -894,6 +950,10 @@ public int hashCode() { hash = (37 * hash) + BIG_INT_VALUE_FIELD_NUMBER; hash = (53 * hash) + getBigIntValue().hashCode(); break; + case 14: + hash = (37 * hash) + RATIONAL_VALUE_FIELD_NUMBER; + hash = (53 * hash) + getRationalValue().hashCode(); + break; case 0: default: } @@ -1050,6 +1110,9 @@ public Builder clear() { if (eventBuilder_ != null) { eventBuilder_.clear(); } + if (rationalValueBuilder_ != null) { + rationalValueBuilder_.clear(); + } elementType_ = ""; kindCase_ = 0; kind_ = null; @@ -1087,7 +1150,7 @@ public org.openmbee.opensysml.proto.DocumentValue buildPartial() { private void buildPartial0(org.openmbee.opensysml.proto.DocumentValue result) { int from_bitField0_ = bitField0_; - if (((from_bitField0_ & 0x00001000) != 0)) { + if (((from_bitField0_ & 0x00002000) != 0)) { result.elementType_ = elementType_; } } @@ -1115,6 +1178,10 @@ private void buildPartialOneofs(org.openmbee.opensysml.proto.DocumentValue resul eventBuilder_ != null) { result.kind_ = eventBuilder_.build(); } + if (kindCase_ == 14 && + rationalValueBuilder_ != null) { + result.kind_ = rationalValueBuilder_.build(); + } } @java.lang.Override @@ -1131,7 +1198,7 @@ public Builder mergeFrom(org.openmbee.opensysml.proto.DocumentValue other) { if (other == org.openmbee.opensysml.proto.DocumentValue.getDefaultInstance()) return this; if (!other.getElementType().isEmpty()) { elementType_ = other.elementType_; - bitField0_ |= 0x00001000; + bitField0_ |= 0x00002000; onChanged(); } switch (other.getKindCase()) { @@ -1189,6 +1256,10 @@ public Builder mergeFrom(org.openmbee.opensysml.proto.DocumentValue other) { onChanged(); break; } + case RATIONAL_VALUE: { + mergeRationalValue(other.getRationalValue()); + break; + } case KIND_NOT_SET: { break; } @@ -1253,7 +1324,7 @@ public Builder mergeFrom( } // case 48 case 58: { elementType_ = input.readStringRequireUtf8(); - bitField0_ |= 0x00001000; + bitField0_ |= 0x00002000; break; } // case 58 case 66: { @@ -1297,6 +1368,13 @@ public Builder mergeFrom( kind_ = s; break; } // case 106 + case 114: { + input.readMessage( + internalGetRationalValueFieldBuilder().getBuilder(), + extensionRegistry); + kindCase_ = 14; + break; + } // case 114 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -2714,6 +2792,184 @@ public Builder setBigIntValueBytes( return this; } + private com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder> rationalValueBuilder_; + /** + *
+     * An exact Rational no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + * @return Whether the rationalValue field is set. + */ + @java.lang.Override + public boolean hasRationalValue() { + return kindCase_ == 14; + } + /** + *
+     * An exact Rational no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + * @return The rationalValue. + */ + @java.lang.Override + public org.openmbee.opensysml.proto.Rational getRationalValue() { + if (rationalValueBuilder_ == null) { + if (kindCase_ == 14) { + return (org.openmbee.opensysml.proto.Rational) kind_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } else { + if (kindCase_ == 14) { + return rationalValueBuilder_.getMessage(); + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + } + /** + *
+     * An exact Rational no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + */ + public Builder setRationalValue(org.openmbee.opensysml.proto.Rational value) { + if (rationalValueBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + kind_ = value; + onChanged(); + } else { + rationalValueBuilder_.setMessage(value); + } + kindCase_ = 14; + return this; + } + /** + *
+     * An exact Rational no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + */ + public Builder setRationalValue( + org.openmbee.opensysml.proto.Rational.Builder builderForValue) { + if (rationalValueBuilder_ == null) { + kind_ = builderForValue.build(); + onChanged(); + } else { + rationalValueBuilder_.setMessage(builderForValue.build()); + } + kindCase_ = 14; + return this; + } + /** + *
+     * An exact Rational no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + */ + public Builder mergeRationalValue(org.openmbee.opensysml.proto.Rational value) { + if (rationalValueBuilder_ == null) { + if (kindCase_ == 14 && + kind_ != org.openmbee.opensysml.proto.Rational.getDefaultInstance()) { + kind_ = org.openmbee.opensysml.proto.Rational.newBuilder((org.openmbee.opensysml.proto.Rational) kind_) + .mergeFrom(value).buildPartial(); + } else { + kind_ = value; + } + onChanged(); + } else { + if (kindCase_ == 14) { + rationalValueBuilder_.mergeFrom(value); + } else { + rationalValueBuilder_.setMessage(value); + } + } + kindCase_ = 14; + return this; + } + /** + *
+     * An exact Rational no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + */ + public Builder clearRationalValue() { + if (rationalValueBuilder_ == null) { + if (kindCase_ == 14) { + kindCase_ = 0; + kind_ = null; + onChanged(); + } + } else { + if (kindCase_ == 14) { + kindCase_ = 0; + kind_ = null; + } + rationalValueBuilder_.clear(); + } + return this; + } + /** + *
+     * An exact Rational no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + */ + public org.openmbee.opensysml.proto.Rational.Builder getRationalValueBuilder() { + return internalGetRationalValueFieldBuilder().getBuilder(); + } + /** + *
+     * An exact Rational no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.RationalOrBuilder getRationalValueOrBuilder() { + if ((kindCase_ == 14) && (rationalValueBuilder_ != null)) { + return rationalValueBuilder_.getMessageOrBuilder(); + } else { + if (kindCase_ == 14) { + return (org.openmbee.opensysml.proto.Rational) kind_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + } + /** + *
+     * An exact Rational no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + */ + private com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder> + internalGetRationalValueFieldBuilder() { + if (rationalValueBuilder_ == null) { + if (!(kindCase_ == 14)) { + kind_ = org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + rationalValueBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder>( + (org.openmbee.opensysml.proto.Rational) kind_, + getParentForChildren(), + isClean()); + kind_ = null; + } + kindCase_ = 14; + onChanged(); + return rationalValueBuilder_; + } + private java.lang.Object elementType_ = ""; /** *
@@ -2769,7 +3025,7 @@ public Builder setElementType(
         java.lang.String value) {
       if (value == null) { throw new NullPointerException(); }
       elementType_ = value;
-      bitField0_ |= 0x00001000;
+      bitField0_ |= 0x00002000;
       onChanged();
       return this;
     }
@@ -2783,7 +3039,7 @@ public Builder setElementType(
      */
     public Builder clearElementType() {
       elementType_ = getDefaultInstance().getElementType();
-      bitField0_ = (bitField0_ & ~0x00001000);
+      bitField0_ = (bitField0_ & ~0x00002000);
       onChanged();
       return this;
     }
@@ -2801,7 +3057,7 @@ public Builder setElementTypeBytes(
       if (value == null) { throw new NullPointerException(); }
       checkByteStringIsUtf8(value);
       elementType_ = value;
-      bitField0_ |= 0x00001000;
+      bitField0_ |= 0x00002000;
       onChanged();
       return this;
     }
diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValueOrBuilder.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValueOrBuilder.java
index f70bc5c734..0e087e6276 100644
--- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValueOrBuilder.java
+++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValueOrBuilder.java
@@ -264,6 +264,33 @@ public interface DocumentValueOrBuilder extends
   com.google.protobuf.ByteString
       getBigIntValueBytes();
 
+  /**
+   * 
+   * An exact Rational no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + * @return Whether the rationalValue field is set. + */ + boolean hasRationalValue(); + /** + *
+   * An exact Rational no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + * @return The rationalValue. + */ + org.openmbee.opensysml.proto.Rational getRationalValue(); + /** + *
+   * An exact Rational no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; + */ + org.openmbee.opensysml.proto.RationalOrBuilder getRationalValueOrBuilder(); + /** *
    * Metamodel type of element_id ("PartUsage", ...); answered, ignored when bound.
diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Quantity.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Quantity.java
index 4eb40e861c..2bc931c279 100644
--- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Quantity.java
+++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Quantity.java
@@ -59,6 +59,7 @@ public enum MagnitudeCase
     INT_MAGNITUDE(1),
     REAL_MAGNITUDE(2),
     BIG_INT_MAGNITUDE(5),
+    RATIONAL_MAGNITUDE(6),
     MAGNITUDE_NOT_SET(0);
     private final int value;
     private MagnitudeCase(int value) {
@@ -79,6 +80,7 @@ public static MagnitudeCase forNumber(int value) {
         case 1: return INT_MAGNITUDE;
         case 2: return REAL_MAGNITUDE;
         case 5: return BIG_INT_MAGNITUDE;
+        case 6: return RATIONAL_MAGNITUDE;
         case 0: return MAGNITUDE_NOT_SET;
         default: return null;
       }
@@ -200,6 +202,49 @@ public java.lang.String getBigIntMagnitude() {
     }
   }
 
+  public static final int RATIONAL_MAGNITUDE_FIELD_NUMBER = 6;
+  /**
+   * 
+   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + * @return Whether the rationalMagnitude field is set. + */ + @java.lang.Override + public boolean hasRationalMagnitude() { + return magnitudeCase_ == 6; + } + /** + *
+   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + * @return The rationalMagnitude. + */ + @java.lang.Override + public org.openmbee.opensysml.proto.Rational getRationalMagnitude() { + if (magnitudeCase_ == 6) { + return (org.openmbee.opensysml.proto.Rational) magnitude_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + /** + *
+   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.RationalOrBuilder getRationalMagnitudeOrBuilder() { + if (magnitudeCase_ == 6) { + return (org.openmbee.opensysml.proto.Rational) magnitude_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + public static final int UNIT_FIELD_NUMBER = 3; @SuppressWarnings("serial") private volatile java.lang.Object unit_ = ""; @@ -324,6 +369,9 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (magnitudeCase_ == 5) { com.google.protobuf.GeneratedMessage.writeString(output, 5, magnitude_); } + if (magnitudeCase_ == 6) { + output.writeMessage(6, (org.openmbee.opensysml.proto.Rational) magnitude_); + } getUnknownFields().writeTo(output); } @@ -353,6 +401,10 @@ public int getSerializedSize() { if (magnitudeCase_ == 5) { size += com.google.protobuf.GeneratedMessage.computeStringSize(5, magnitude_); } + if (magnitudeCase_ == 6) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(6, (org.openmbee.opensysml.proto.Rational) magnitude_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -390,6 +442,10 @@ public boolean equals(final java.lang.Object obj) { if (!getBigIntMagnitude() .equals(other.getBigIntMagnitude())) return false; break; + case 6: + if (!getRationalMagnitude() + .equals(other.getRationalMagnitude())) return false; + break; case 0: default: } @@ -425,6 +481,10 @@ public int hashCode() { hash = (37 * hash) + BIG_INT_MAGNITUDE_FIELD_NUMBER; hash = (53 * hash) + getBigIntMagnitude().hashCode(); break; + case 6: + hash = (37 * hash) + RATIONAL_MAGNITUDE_FIELD_NUMBER; + hash = (53 * hash) + getRationalMagnitude().hashCode(); + break; case 0: default: } @@ -570,6 +630,9 @@ private void maybeForceBuilderInitialization() { public Builder clear() { super.clear(); bitField0_ = 0; + if (rationalMagnitudeBuilder_ != null) { + rationalMagnitudeBuilder_.clear(); + } unit_ = ""; unitTerm_ = null; if (unitTermBuilder_ != null) { @@ -612,11 +675,11 @@ public org.openmbee.opensysml.proto.Quantity buildPartial() { private void buildPartial0(org.openmbee.opensysml.proto.Quantity result) { int from_bitField0_ = bitField0_; - if (((from_bitField0_ & 0x00000008) != 0)) { + if (((from_bitField0_ & 0x00000010) != 0)) { result.unit_ = unit_; } int to_bitField0_ = 0; - if (((from_bitField0_ & 0x00000010) != 0)) { + if (((from_bitField0_ & 0x00000020) != 0)) { result.unitTerm_ = unitTermBuilder_ == null ? unitTerm_ : unitTermBuilder_.build(); @@ -628,6 +691,10 @@ private void buildPartial0(org.openmbee.opensysml.proto.Quantity result) { private void buildPartialOneofs(org.openmbee.opensysml.proto.Quantity result) { result.magnitudeCase_ = magnitudeCase_; result.magnitude_ = this.magnitude_; + if (magnitudeCase_ == 6 && + rationalMagnitudeBuilder_ != null) { + result.magnitude_ = rationalMagnitudeBuilder_.build(); + } } @java.lang.Override @@ -644,7 +711,7 @@ public Builder mergeFrom(org.openmbee.opensysml.proto.Quantity other) { if (other == org.openmbee.opensysml.proto.Quantity.getDefaultInstance()) return this; if (!other.getUnit().isEmpty()) { unit_ = other.unit_; - bitField0_ |= 0x00000008; + bitField0_ |= 0x00000010; onChanged(); } if (other.hasUnitTerm()) { @@ -665,6 +732,10 @@ public Builder mergeFrom(org.openmbee.opensysml.proto.Quantity other) { onChanged(); break; } + case RATIONAL_MAGNITUDE: { + mergeRationalMagnitude(other.getRationalMagnitude()); + break; + } case MAGNITUDE_NOT_SET: { break; } @@ -707,14 +778,14 @@ public Builder mergeFrom( } // case 17 case 26: { unit_ = input.readStringRequireUtf8(); - bitField0_ |= 0x00000008; + bitField0_ |= 0x00000010; break; } // case 26 case 34: { input.readMessage( internalGetUnitTermFieldBuilder().getBuilder(), extensionRegistry); - bitField0_ |= 0x00000010; + bitField0_ |= 0x00000020; break; } // case 34 case 42: { @@ -723,6 +794,13 @@ public Builder mergeFrom( magnitude_ = s; break; } // case 42 + case 50: { + input.readMessage( + internalGetRationalMagnitudeFieldBuilder().getBuilder(), + extensionRegistry); + magnitudeCase_ = 6; + break; + } // case 50 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -956,6 +1034,184 @@ public Builder setBigIntMagnitudeBytes( return this; } + private com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder> rationalMagnitudeBuilder_; + /** + *
+     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + * @return Whether the rationalMagnitude field is set. + */ + @java.lang.Override + public boolean hasRationalMagnitude() { + return magnitudeCase_ == 6; + } + /** + *
+     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + * @return The rationalMagnitude. + */ + @java.lang.Override + public org.openmbee.opensysml.proto.Rational getRationalMagnitude() { + if (rationalMagnitudeBuilder_ == null) { + if (magnitudeCase_ == 6) { + return (org.openmbee.opensysml.proto.Rational) magnitude_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } else { + if (magnitudeCase_ == 6) { + return rationalMagnitudeBuilder_.getMessage(); + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + } + /** + *
+     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + */ + public Builder setRationalMagnitude(org.openmbee.opensysml.proto.Rational value) { + if (rationalMagnitudeBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + magnitude_ = value; + onChanged(); + } else { + rationalMagnitudeBuilder_.setMessage(value); + } + magnitudeCase_ = 6; + return this; + } + /** + *
+     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + */ + public Builder setRationalMagnitude( + org.openmbee.opensysml.proto.Rational.Builder builderForValue) { + if (rationalMagnitudeBuilder_ == null) { + magnitude_ = builderForValue.build(); + onChanged(); + } else { + rationalMagnitudeBuilder_.setMessage(builderForValue.build()); + } + magnitudeCase_ = 6; + return this; + } + /** + *
+     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + */ + public Builder mergeRationalMagnitude(org.openmbee.opensysml.proto.Rational value) { + if (rationalMagnitudeBuilder_ == null) { + if (magnitudeCase_ == 6 && + magnitude_ != org.openmbee.opensysml.proto.Rational.getDefaultInstance()) { + magnitude_ = org.openmbee.opensysml.proto.Rational.newBuilder((org.openmbee.opensysml.proto.Rational) magnitude_) + .mergeFrom(value).buildPartial(); + } else { + magnitude_ = value; + } + onChanged(); + } else { + if (magnitudeCase_ == 6) { + rationalMagnitudeBuilder_.mergeFrom(value); + } else { + rationalMagnitudeBuilder_.setMessage(value); + } + } + magnitudeCase_ = 6; + return this; + } + /** + *
+     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + */ + public Builder clearRationalMagnitude() { + if (rationalMagnitudeBuilder_ == null) { + if (magnitudeCase_ == 6) { + magnitudeCase_ = 0; + magnitude_ = null; + onChanged(); + } + } else { + if (magnitudeCase_ == 6) { + magnitudeCase_ = 0; + magnitude_ = null; + } + rationalMagnitudeBuilder_.clear(); + } + return this; + } + /** + *
+     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + */ + public org.openmbee.opensysml.proto.Rational.Builder getRationalMagnitudeBuilder() { + return internalGetRationalMagnitudeFieldBuilder().getBuilder(); + } + /** + *
+     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.RationalOrBuilder getRationalMagnitudeOrBuilder() { + if ((magnitudeCase_ == 6) && (rationalMagnitudeBuilder_ != null)) { + return rationalMagnitudeBuilder_.getMessageOrBuilder(); + } else { + if (magnitudeCase_ == 6) { + return (org.openmbee.opensysml.proto.Rational) magnitude_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + } + /** + *
+     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + */ + private com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder> + internalGetRationalMagnitudeFieldBuilder() { + if (rationalMagnitudeBuilder_ == null) { + if (!(magnitudeCase_ == 6)) { + magnitude_ = org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + rationalMagnitudeBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder>( + (org.openmbee.opensysml.proto.Rational) magnitude_, + getParentForChildren(), + isClean()); + magnitude_ = null; + } + magnitudeCase_ = 6; + onChanged(); + return rationalMagnitudeBuilder_; + } + private java.lang.Object unit_ = ""; /** *
@@ -1014,7 +1270,7 @@ public Builder setUnit(
         java.lang.String value) {
       if (value == null) { throw new NullPointerException(); }
       unit_ = value;
-      bitField0_ |= 0x00000008;
+      bitField0_ |= 0x00000010;
       onChanged();
       return this;
     }
@@ -1029,7 +1285,7 @@ public Builder setUnit(
      */
     public Builder clearUnit() {
       unit_ = getDefaultInstance().getUnit();
-      bitField0_ = (bitField0_ & ~0x00000008);
+      bitField0_ = (bitField0_ & ~0x00000010);
       onChanged();
       return this;
     }
@@ -1048,7 +1304,7 @@ public Builder setUnitBytes(
       if (value == null) { throw new NullPointerException(); }
       checkByteStringIsUtf8(value);
       unit_ = value;
-      bitField0_ |= 0x00000008;
+      bitField0_ |= 0x00000010;
       onChanged();
       return this;
     }
@@ -1067,7 +1323,7 @@ public Builder setUnitBytes(
      * @return Whether the unitTerm field is set.
      */
     public boolean hasUnitTerm() {
-      return ((bitField0_ & 0x00000010) != 0);
+      return ((bitField0_ & 0x00000020) != 0);
     }
     /**
      * 
@@ -1104,7 +1360,7 @@ public Builder setUnitTerm(org.openmbee.opensysml.proto.UnitTerm value) {
       } else {
         unitTermBuilder_.setMessage(value);
       }
-      bitField0_ |= 0x00000010;
+      bitField0_ |= 0x00000020;
       onChanged();
       return this;
     }
@@ -1124,7 +1380,7 @@ public Builder setUnitTerm(
       } else {
         unitTermBuilder_.setMessage(builderForValue.build());
       }
-      bitField0_ |= 0x00000010;
+      bitField0_ |= 0x00000020;
       onChanged();
       return this;
     }
@@ -1139,7 +1395,7 @@ public Builder setUnitTerm(
      */
     public Builder mergeUnitTerm(org.openmbee.opensysml.proto.UnitTerm value) {
       if (unitTermBuilder_ == null) {
-        if (((bitField0_ & 0x00000010) != 0) &&
+        if (((bitField0_ & 0x00000020) != 0) &&
           unitTerm_ != null &&
           unitTerm_ != org.openmbee.opensysml.proto.UnitTerm.getDefaultInstance()) {
           getUnitTermBuilder().mergeFrom(value);
@@ -1150,7 +1406,7 @@ public Builder mergeUnitTerm(org.openmbee.opensysml.proto.UnitTerm value) {
         unitTermBuilder_.mergeFrom(value);
       }
       if (unitTerm_ != null) {
-        bitField0_ |= 0x00000010;
+        bitField0_ |= 0x00000020;
         onChanged();
       }
       return this;
@@ -1165,7 +1421,7 @@ public Builder mergeUnitTerm(org.openmbee.opensysml.proto.UnitTerm value) {
      * .sysml.UnitTerm unit_term = 4 [json_name = "unitTerm"];
      */
     public Builder clearUnitTerm() {
-      bitField0_ = (bitField0_ & ~0x00000010);
+      bitField0_ = (bitField0_ & ~0x00000020);
       unitTerm_ = null;
       if (unitTermBuilder_ != null) {
         unitTermBuilder_.dispose();
@@ -1184,7 +1440,7 @@ public Builder clearUnitTerm() {
      * .sysml.UnitTerm unit_term = 4 [json_name = "unitTerm"];
      */
     public org.openmbee.opensysml.proto.UnitTerm.Builder getUnitTermBuilder() {
-      bitField0_ |= 0x00000010;
+      bitField0_ |= 0x00000020;
       onChanged();
       return internalGetUnitTermFieldBuilder().getBuilder();
     }
diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/QuantityOrBuilder.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/QuantityOrBuilder.java
index 7253edf15a..6e2bfcb306 100644
--- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/QuantityOrBuilder.java
+++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/QuantityOrBuilder.java
@@ -61,6 +61,33 @@ public interface QuantityOrBuilder extends
   com.google.protobuf.ByteString
       getBigIntMagnitudeBytes();
 
+  /**
+   * 
+   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + * @return Whether the rationalMagnitude field is set. + */ + boolean hasRationalMagnitude(); + /** + *
+   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + * @return The rationalMagnitude. + */ + org.openmbee.opensysml.proto.Rational getRationalMagnitude(); + /** + *
+   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * 
+ * + * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; + */ + org.openmbee.opensysml.proto.RationalOrBuilder getRationalMagnitudeOrBuilder(); + /** *
    * Unit as written ("km/h") or as an operation composed it ("m/s"); empty for
diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Rational.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Rational.java
new file mode 100644
index 0000000000..9db9b0505a
--- /dev/null
+++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Rational.java
@@ -0,0 +1,650 @@
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+// NO CHECKED-IN PROTOBUF GENCODE
+// source: sysml.proto
+// Protobuf Java Version: 4.33.1
+
+package org.openmbee.opensysml.proto;
+
+/**
+ * 
+ * Rational is an exact rational number in lowest terms: numerator over a
+ * positive denominator, each written as big_int_value is. A decoder rejects one
+ * not in lowest terms or one a double holds exactly, so no number has two spellings.
+ * 
+ * + * Protobuf type {@code sysml.Rational} + */ +@com.google.protobuf.Generated +public final class Rational extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:sysml.Rational) + RationalOrBuilder { +private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "Rational"); + } + // Use Rational.newBuilder() to construct. + private Rational(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private Rational() { + numerator_ = ""; + denominator_ = ""; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_Rational_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_Rational_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.openmbee.opensysml.proto.Rational.class, org.openmbee.opensysml.proto.Rational.Builder.class); + } + + public static final int NUMERATOR_FIELD_NUMBER = 1; + @SuppressWarnings("serial") + private volatile java.lang.Object numerator_ = ""; + /** + * string numerator = 1 [json_name = "numerator"]; + * @return The numerator. + */ + @java.lang.Override + public java.lang.String getNumerator() { + java.lang.Object ref = numerator_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + numerator_ = s; + return s; + } + } + /** + * string numerator = 1 [json_name = "numerator"]; + * @return The bytes for numerator. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getNumeratorBytes() { + java.lang.Object ref = numerator_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + numerator_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int DENOMINATOR_FIELD_NUMBER = 2; + @SuppressWarnings("serial") + private volatile java.lang.Object denominator_ = ""; + /** + * string denominator = 2 [json_name = "denominator"]; + * @return The denominator. + */ + @java.lang.Override + public java.lang.String getDenominator() { + java.lang.Object ref = denominator_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + denominator_ = s; + return s; + } + } + /** + * string denominator = 2 [json_name = "denominator"]; + * @return The bytes for denominator. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getDenominatorBytes() { + java.lang.Object ref = denominator_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + denominator_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(numerator_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 1, numerator_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(denominator_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 2, denominator_); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(numerator_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(1, numerator_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(denominator_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(2, denominator_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof org.openmbee.opensysml.proto.Rational)) { + return super.equals(obj); + } + org.openmbee.opensysml.proto.Rational other = (org.openmbee.opensysml.proto.Rational) obj; + + if (!getNumerator() + .equals(other.getNumerator())) return false; + if (!getDenominator() + .equals(other.getDenominator())) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + NUMERATOR_FIELD_NUMBER; + hash = (53 * hash) + getNumerator().hashCode(); + hash = (37 * hash) + DENOMINATOR_FIELD_NUMBER; + hash = (53 * hash) + getDenominator().hashCode(); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static org.openmbee.opensysml.proto.Rational parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.openmbee.opensysml.proto.Rational parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.openmbee.opensysml.proto.Rational parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.openmbee.opensysml.proto.Rational parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.openmbee.opensysml.proto.Rational parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.openmbee.opensysml.proto.Rational parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.openmbee.opensysml.proto.Rational parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static org.openmbee.opensysml.proto.Rational parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static org.openmbee.opensysml.proto.Rational parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static org.openmbee.opensysml.proto.Rational parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static org.openmbee.opensysml.proto.Rational parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static org.openmbee.opensysml.proto.Rational parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(org.openmbee.opensysml.proto.Rational prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + *
+   * Rational is an exact rational number in lowest terms: numerator over a
+   * positive denominator, each written as big_int_value is. A decoder rejects one
+   * not in lowest terms or one a double holds exactly, so no number has two spellings.
+   * 
+ * + * Protobuf type {@code sysml.Rational} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:sysml.Rational) + org.openmbee.opensysml.proto.RationalOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_Rational_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_Rational_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.openmbee.opensysml.proto.Rational.class, org.openmbee.opensysml.proto.Rational.Builder.class); + } + + // Construct using org.openmbee.opensysml.proto.Rational.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + numerator_ = ""; + denominator_ = ""; + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_Rational_descriptor; + } + + @java.lang.Override + public org.openmbee.opensysml.proto.Rational getDefaultInstanceForType() { + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + + @java.lang.Override + public org.openmbee.opensysml.proto.Rational build() { + org.openmbee.opensysml.proto.Rational result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public org.openmbee.opensysml.proto.Rational buildPartial() { + org.openmbee.opensysml.proto.Rational result = new org.openmbee.opensysml.proto.Rational(this); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartial0(org.openmbee.opensysml.proto.Rational result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.numerator_ = numerator_; + } + if (((from_bitField0_ & 0x00000002) != 0)) { + result.denominator_ = denominator_; + } + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.openmbee.opensysml.proto.Rational) { + return mergeFrom((org.openmbee.opensysml.proto.Rational)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.openmbee.opensysml.proto.Rational other) { + if (other == org.openmbee.opensysml.proto.Rational.getDefaultInstance()) return this; + if (!other.getNumerator().isEmpty()) { + numerator_ = other.numerator_; + bitField0_ |= 0x00000001; + onChanged(); + } + if (!other.getDenominator().isEmpty()) { + denominator_ = other.denominator_; + bitField0_ |= 0x00000002; + onChanged(); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + numerator_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000001; + break; + } // case 10 + case 18: { + denominator_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000002; + break; + } // case 18 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private java.lang.Object numerator_ = ""; + /** + * string numerator = 1 [json_name = "numerator"]; + * @return The numerator. + */ + public java.lang.String getNumerator() { + java.lang.Object ref = numerator_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + numerator_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string numerator = 1 [json_name = "numerator"]; + * @return The bytes for numerator. + */ + public com.google.protobuf.ByteString + getNumeratorBytes() { + java.lang.Object ref = numerator_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + numerator_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string numerator = 1 [json_name = "numerator"]; + * @param value The numerator to set. + * @return This builder for chaining. + */ + public Builder setNumerator( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + numerator_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * string numerator = 1 [json_name = "numerator"]; + * @return This builder for chaining. + */ + public Builder clearNumerator() { + numerator_ = getDefaultInstance().getNumerator(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + return this; + } + /** + * string numerator = 1 [json_name = "numerator"]; + * @param value The bytes for numerator to set. + * @return This builder for chaining. + */ + public Builder setNumeratorBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + numerator_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + + private java.lang.Object denominator_ = ""; + /** + * string denominator = 2 [json_name = "denominator"]; + * @return The denominator. + */ + public java.lang.String getDenominator() { + java.lang.Object ref = denominator_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + denominator_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string denominator = 2 [json_name = "denominator"]; + * @return The bytes for denominator. + */ + public com.google.protobuf.ByteString + getDenominatorBytes() { + java.lang.Object ref = denominator_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + denominator_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string denominator = 2 [json_name = "denominator"]; + * @param value The denominator to set. + * @return This builder for chaining. + */ + public Builder setDenominator( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + denominator_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * string denominator = 2 [json_name = "denominator"]; + * @return This builder for chaining. + */ + public Builder clearDenominator() { + denominator_ = getDefaultInstance().getDenominator(); + bitField0_ = (bitField0_ & ~0x00000002); + onChanged(); + return this; + } + /** + * string denominator = 2 [json_name = "denominator"]; + * @param value The bytes for denominator to set. + * @return This builder for chaining. + */ + public Builder setDenominatorBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + denominator_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:sysml.Rational) + } + + // @@protoc_insertion_point(class_scope:sysml.Rational) + private static final org.openmbee.opensysml.proto.Rational DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new org.openmbee.opensysml.proto.Rational(); + } + + public static org.openmbee.opensysml.proto.Rational getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public Rational parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public org.openmbee.opensysml.proto.Rational getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + +} + diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/RationalOrBuilder.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/RationalOrBuilder.java new file mode 100644 index 0000000000..9be6003de9 --- /dev/null +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/RationalOrBuilder.java @@ -0,0 +1,36 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: sysml.proto +// Protobuf Java Version: 4.33.1 + +package org.openmbee.opensysml.proto; + +@com.google.protobuf.Generated +public interface RationalOrBuilder extends + // @@protoc_insertion_point(interface_extends:sysml.Rational) + com.google.protobuf.MessageOrBuilder { + + /** + * string numerator = 1 [json_name = "numerator"]; + * @return The numerator. + */ + java.lang.String getNumerator(); + /** + * string numerator = 1 [json_name = "numerator"]; + * @return The bytes for numerator. + */ + com.google.protobuf.ByteString + getNumeratorBytes(); + + /** + * string denominator = 2 [json_name = "denominator"]; + * @return The denominator. + */ + java.lang.String getDenominator(); + /** + * string denominator = 2 [json_name = "denominator"]; + * @return The bytes for denominator. + */ + com.google.protobuf.ByteString + getDenominatorBytes(); +} diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Sysml.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Sysml.java index a53588f595..15637c2abf 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Sysml.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Sysml.java @@ -466,6 +466,11 @@ public static void registerAllExtensions( static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_sysml_Value_fieldAccessorTable; + static final com.google.protobuf.Descriptors.Descriptor + internal_static_sysml_Rational_descriptor; + static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_sysml_Rational_fieldAccessorTable; static final com.google.protobuf.Descriptors.Descriptor internal_static_sysml_Metaobject_descriptor; static final @@ -1123,7 +1128,7 @@ public static void registerAllExtensions( "\n\005upper\030\002 \001(\tR\005upper\"o\n\rAttributeInfo\022\022\n" + "\004name\030\001 \001(\tR\004name\022\022\n\004type\030\002 \001(\tR\004type\022\"\n" + "\005value\030\003 \001(\0132\014.sysml.ValueR\005value\022\022\n\004uni" + - "t\030\004 \001(\tR\004unit\"\313\007\n\005Value\022\035\n\tint_value\030\001 \001" + + "t\030\004 \001(\tR\004unit\"\205\010\n\005Value\022\035\n\tint_value\030\001 \001" + "(\003H\000R\010intValue\022\037\n\nreal_value\030\002 \001(\001H\000R\tre" + "alValue\022\037\n\nbool_value\030\003 \001(\010H\000R\tboolValue" + "\022#\n\014string_value\030\004 \001(\tH\000R\013stringValue\022!\n" + @@ -1147,32 +1152,37 @@ public static void registerAllExtensions( "ect\030\024 \001(\0132\021.sysml.MetaobjectH\000R\nmetaobje" + "ct\0229\n\014undetermined\030\025 \001(\0132\023.sysml.Undeter" + "minedH\000R\014undetermined\022$\n\rbig_int_value\030\026" + - " \001(\tH\000R\013bigIntValueB\006\n\004kind\"N\n\nMetaobjec" + - "t\022\035\n\nelement_id\030\001 \001(\tR\telementId\022!\n\014meta" + - "class_id\030\002 \001(\tR\013metaclassId\"U\n\014Undetermi" + - "ned\022\026\n\006reason\030\001 \001(\tR\006reason\022-\n\005count\030\002 \001" + - "(\0132\027.sysml.MultiplicityInfoR\005count\"<\n\010Fu" + - "nction\022\027\n\007calc_id\030\001 \001(\tR\006calcId\022\027\n\007self_" + - "id\030\002 \001(\003R\006selfId\"4\n\010ValueSet\022(\n\010elements" + - "\030\001 \003(\0132\014.sysml.ValueR\010elements\"a\n\016Tensor" + - "Quantity\022\036\n\ndimensions\030\001 \003(\003R\ndimensions" + - "\022/\n\ncomponents\030\002 \003(\0132\017.sysml.QuantityR\nc" + - "omponents\"Q\n\005Array\022\036\n\ndimensions\030\001 \003(\003R\n" + - "dimensions\022(\n\010elements\030\002 \003(\0132\014.sysml.Val" + - "ueR\010elements\"6\n\006Vector\022,\n\ncomponents\030\001 \003" + - "(\0132\014.sysml.ValueR\ncomponents\"A\n\016VectorQu" + - "antity\022/\n\ncomponents\030\001 \003(\0132\017.sysml.Quant" + - "ityR\ncomponents\";\n\007Complex\022\022\n\004real\030\001 \001(\001" + - "R\004real\022\034\n\timaginary\030\002 \001(\001R\timaginary\"\213\001\n" + - "\013EnumLiteral\022\035\n\nliteral_id\030\001 \001(\tR\tlitera" + - "lId\022%\n\016enumeration_id\030\002 \001(\tR\renumeration" + - "Id\022\022\n\004name\030\003 \001(\tR\004name\022\"\n\005value\030\004 \001(\0132\014." + - "sysml.ValueR\005value\"9\n\rValueSequence\022(\n\010e" + - "lements\030\001 \003(\0132\014.sysml.ValueR\010elements\"\327\001" + - "\n\010Quantity\022%\n\rint_magnitude\030\001 \001(\003H\000R\014int" + - "Magnitude\022\'\n\016real_magnitude\030\002 \001(\001H\000R\rrea" + - "lMagnitude\022,\n\021big_int_magnitude\030\005 \001(\tH\000R" + - "\017bigIntMagnitude\022\022\n\004unit\030\003 \001(\tR\004unit\022,\n\t" + + " \001(\tH\000R\013bigIntValue\0228\n\016rational_value\030\027 " + + "\001(\0132\017.sysml.RationalH\000R\rrationalValueB\006\n" + + "\004kind\"J\n\010Rational\022\034\n\tnumerator\030\001 \001(\tR\tnu" + + "merator\022 \n\013denominator\030\002 \001(\tR\013denominato" + + "r\"N\n\nMetaobject\022\035\n\nelement_id\030\001 \001(\tR\tele" + + "mentId\022!\n\014metaclass_id\030\002 \001(\tR\013metaclassI" + + "d\"U\n\014Undetermined\022\026\n\006reason\030\001 \001(\tR\006reaso" + + "n\022-\n\005count\030\002 \001(\0132\027.sysml.MultiplicityInf" + + "oR\005count\"<\n\010Function\022\027\n\007calc_id\030\001 \001(\tR\006c" + + "alcId\022\027\n\007self_id\030\002 \001(\003R\006selfId\"4\n\010ValueS" + + "et\022(\n\010elements\030\001 \003(\0132\014.sysml.ValueR\010elem" + + "ents\"a\n\016TensorQuantity\022\036\n\ndimensions\030\001 \003" + + "(\003R\ndimensions\022/\n\ncomponents\030\002 \003(\0132\017.sys" + + "ml.QuantityR\ncomponents\"Q\n\005Array\022\036\n\ndime" + + "nsions\030\001 \003(\003R\ndimensions\022(\n\010elements\030\002 \003" + + "(\0132\014.sysml.ValueR\010elements\"6\n\006Vector\022,\n\n" + + "components\030\001 \003(\0132\014.sysml.ValueR\ncomponen" + + "ts\"A\n\016VectorQuantity\022/\n\ncomponents\030\001 \003(\013" + + "2\017.sysml.QuantityR\ncomponents\";\n\007Complex" + + "\022\022\n\004real\030\001 \001(\001R\004real\022\034\n\timaginary\030\002 \001(\001R" + + "\timaginary\"\213\001\n\013EnumLiteral\022\035\n\nliteral_id" + + "\030\001 \001(\tR\tliteralId\022%\n\016enumeration_id\030\002 \001(" + + "\tR\renumerationId\022\022\n\004name\030\003 \001(\tR\004name\022\"\n\005" + + "value\030\004 \001(\0132\014.sysml.ValueR\005value\"9\n\rValu" + + "eSequence\022(\n\010elements\030\001 \003(\0132\014.sysml.Valu" + + "eR\010elements\"\231\002\n\010Quantity\022%\n\rint_magnitud" + + "e\030\001 \001(\003H\000R\014intMagnitude\022\'\n\016real_magnitud" + + "e\030\002 \001(\001H\000R\rrealMagnitude\022,\n\021big_int_magn" + + "itude\030\005 \001(\tH\000R\017bigIntMagnitude\022@\n\022ration" + + "al_magnitude\030\006 \001(\0132\017.sysml.RationalH\000R\021r" + + "ationalMagnitude\022\022\n\004unit\030\003 \001(\tR\004unit\022,\n\t" + "unit_term\030\004 \001(\0132\017.sysml.UnitTermR\010unitTe" + "rmB\013\n\tmagnitude\"k\n\016MeasurementRef\022\022\n\004uni" + "t\030\001 \001(\tR\004unit\022,\n\tunit_term\030\002 \001(\0132\017.sysml" + @@ -1252,7 +1262,7 @@ public static void registerAllExtensions( "bindings\030\003 \003(\0132\033.sysml.DocumentQueryBind" + "ingR\010bindings\"b\n\024DocumentQueryBinding\022\034\n" + "\tparameter\030\001 \001(\tR\tparameter\022,\n\006values\030\002 " + - "\003(\0132\024.sysml.DocumentValueR\006values\"\225\004\n\rDo" + + "\003(\0132\024.sysml.DocumentValueR\006values\"\317\004\n\rDo" + "cumentValue\022\037\n\nelement_id\030\001 \001(\tH\000R\teleme" + "ntId\022#\n\014string_value\030\002 \001(\tH\000R\013stringValu" + "e\022\035\n\tint_value\030\003 \001(\003H\000R\010intValue\022\037\n\nreal" + @@ -1265,117 +1275,118 @@ public static void registerAllExtensions( "state\030\013 \001(\0132\024.sysml.DocumentStateH\000R\005sta" + "te\022,\n\005event\030\014 \001(\0132\024.sysml.DocumentEventH" + "\000R\005event\022$\n\rbig_int_value\030\r \001(\tH\000R\013bigIn" + - "tValue\022!\n\014element_type\030\007 \001(\tR\013elementTyp" + - "eB\006\n\004kind\"u\n\016DocumentObject\022\037\n\013instance_" + - "id\030\001 \001(\003R\ninstanceId\022\022\n\004path\030\002 \001(\tR\004path" + - "\022.\n\007element\030\003 \001(\0132\024.sysml.DocumentValueR" + - "\007element\"\365\001\n\017DocumentVerdict\0222\n\tassertio" + - "n\030\001 \001(\0132\024.sysml.DocumentValueR\tassertion" + - "\022\022\n\004kind\030\002 \001(\tR\004kind\022\022\n\004text\030\003 \001(\tR\004text" + - "\022\022\n\004path\030\004 \001(\tR\004path\022\030\n\007verdict\030\005 \001(\tR\007v" + - "erdict\022\034\n\tcondition\030\006 \001(\tR\tcondition\022\026\n\006" + - "reason\030\007 \001(\tR\006reason\022\"\n\014verification\030\010 \003" + - "(\tR\014verification\"\355\001\n\rDocumentState\022-\n\006ob" + - "ject\030\001 \001(\0132\025.sysml.DocumentObjectR\006objec" + - "t\022\030\n\007machine\030\002 \001(\tR\007machine\022\022\n\004name\030\003 \001(" + - "\tR\004name\022\035\n\nstate_path\030\004 \001(\tR\tstatePath\022*" + - "\n\005state\030\005 \001(\0132\024.sysml.DocumentValueR\005sta" + - "te\022\026\n\006region\030\006 \001(\tR\006region\022\034\n\tenclosing\030" + - "\007 \003(\tR\tenclosing\"\375\002\n\rDocumentEvent\022\022\n\004ki" + - "nd\030\001 \001(\tR\004kind\022(\n\004time\030\002 \001(\0132\024.sysml.Doc" + - "umentValueR\004time\022-\n\006object\030\003 \001(\0132\025.sysml" + - ".DocumentObjectR\006object\022\030\n\007machine\030\004 \001(\t" + - "R\007machine\022\024\n\005state\030\005 \001(\tR\005state\022\022\n\004from\030" + - "\006 \001(\tR\004from\022\016\n\002to\030\007 \001(\tR\002to\022-\n\006target\030\010 " + - "\001(\0132\025.sysml.DocumentObjectR\006target\022\024\n\005ev" + - "ent\030\t \001(\tR\005event\022\030\n\007payload\030\n \003(\tR\007paylo" + - "ad\022\"\n\014alternatives\030\013 \003(\tR\014alternatives\022\024" + - "\n\005taken\030\014 \001(\tR\005taken\022\022\n\004text\030\r \001(\tR\004text" + - "\")\n\023DocumentQueryColumn\022\022\n\004name\030\001 \001(\tR\004n" + - "ame\"A\n\021DocumentQueryCell\022,\n\006values\030\001 \003(\013" + - "2\024.sysml.DocumentValueR\006values\"r\n\020Docume" + - "ntQueryRow\022.\n\007element\030\001 \001(\0132\024.sysml.Docu" + - "mentValueR\007element\022.\n\005cells\030\002 \003(\0132\030.sysm" + - "l.DocumentQueryCellR\005cells\"}\n\030RunDocumen" + - "tQueryResponse\0224\n\007columns\030\001 \003(\0132\032.sysml." + - "DocumentQueryColumnR\007columns\022+\n\004rows\030\002 \003" + - "(\0132\027.sysml.DocumentQueryRowR\004rows\"k\n\025Ren" + - "derDocumentRequest\022\035\n\nmodel_hash\030\001 \001(\tR\t" + - "modelHash\022\037\n\013document_id\030\002 \001(\tR\ndocument" + - "Id\022\022\n\004form\030\003 \001(\tR\004form\"H\n\026RenderDocument" + - "Response\022\032\n\010markdown\030\001 \001(\tR\010markdown\022\022\n\004" + - "html\030\002 \001(\tR\004html*\261\001\n\rFailureReason\022\036\n\032FA" + - "ILURE_REASON_UNSPECIFIED\020\000\022\035\n\031FAILURE_RE" + - "ASON_EVALUATION\020\001\022\035\n\031FAILURE_REASON_WRON" + - "G_KIND\020\002\022$\n FAILURE_REASON_AMBIGUOUS_SUB" + - "JECT\020\003\022\034\n\030FAILURE_REASON_UNDECIDED\020\004*\214\005\n" + - "\013EditFailure\022\034\n\030EDIT_FAILURE_UNSPECIFIED" + - "\020\000\022\036\n\032EDIT_FAILURE_NO_OPERATIONS\020\001\022\037\n\033ED" + - "IT_FAILURE_UNKNOWN_TARGET\020\002\022!\n\035EDIT_FAIL" + - "URE_AMBIGUOUS_TARGET\020\003\022\033\n\027EDIT_FAILURE_N" + - "OT_VALUED\020\004\022\036\n\032EDIT_FAILURE_INVALID_VALU" + - "E\020\005\022\035\n\031EDIT_FAILURE_INVALID_NAME\020\006\022\032\n\026ED" + - "IT_FAILURE_NOT_NAMED\020\007\022\"\n\036EDIT_FAILURE_R" + - "ENAME_REFERENCED\020\010\022\"\n\036EDIT_FAILURE_OVERL" + - "APPING_EDITS\020\t\022\037\n\033EDIT_FAILURE_RESULT_IN" + - "VALID\020\n\022\036\n\032EDIT_FAILURE_OWNER_UNKNOWN\020\013\022" + - "$\n EDIT_FAILURE_OWNER_NOT_NAMESPACE\020\014\022\035\n" + - "\031EDIT_FAILURE_ILLEGAL_KIND\020\r\022\"\n\036EDIT_FAI" + - "LURE_MEMBER_NAME_TAKEN\020\016\022\"\n\036EDIT_FAILURE" + - "_DELETE_REFERENCED\020\017\022$\n EDIT_FAILURE_OWN" + - "ER_INSIDE_TARGET\020\020\022 \n\034EDIT_FAILURE_MOVE_" + - "REFERENCED\020\021\022%\n!EDIT_FAILURE_REFERENCED_" + - "ELSEWHERE\020\022*\222\001\n\021PrimitiveOperator\022\"\n\036PRI" + - "MITIVE_OPERATOR_UNSPECIFIED\020\000\022\034\n\030PRIMITI" + - "VE_OPERATOR_EQUAL\020\001\022\036\n\032PRIMITIVE_OPERATO" + - "R_GREATER\020\002\022\033\n\027PRIMITIVE_OPERATOR_LESS\020\003" + - "*n\n\021CompositeOperator\022\"\n\036COMPOSITE_OPERA" + - "TOR_UNSPECIFIED\020\000\022\032\n\026COMPOSITE_OPERATOR_" + - "AND\020\001\022\031\n\025COMPOSITE_OPERATOR_OR\020\0022\371\014\n\014Sys" + - "MLService\022D\n\rGetServerInfo\022\030.sysml.Serve" + - "rInfoRequest\032\031.sysml.ServerInfoResponse\022" + - ">\n\tParseFile\022\027.sysml.ParseFileRequest\032\030." + - "sysml.ParseFileResponse\022G\n\014ParseSources\022" + - "\032.sysml.ParseSourcesRequest\032\033.sysml.Pars" + - "eSourcesResponse\022;\n\tGetSymbol\022\027.sysml.Ge" + - "tSymbolRequest\032\025.sysml.SymbolResponse\022G\n" + - "\016GetDiagnostics\022\031.sysml.DiagnosticsReque" + - "st\032\032.sysml.DiagnosticsResponse\022;\n\010Evalua" + - "te\022\026.sysml.EvaluateRequest\032\027.sysml.Evalu" + - "ateResponse\022D\n\013Instantiate\022\031.sysml.Insta" + - "ntiateRequest\032\032.sysml.InstantiateRespons" + - "e\022J\n\rExecuteAction\022\033.sysml.ExecuteAction" + - "Request\032\034.sysml.ExecuteActionResponse\022G\n" + - "\014ExecuteState\022\032.sysml.ExecuteStateReques" + - "t\032\033.sysml.ExecuteStateResponse\0228\n\007Conver" + - "t\022\025.sysml.ConvertRequest\032\026.sysml.Convert" + - "Response\0228\n\007Migrate\022\025.sysml.MigrateReque" + - "st\032\026.sysml.MigrateResponse\022A\n\nApplyEdits" + - "\022\030.sysml.ApplyEditsRequest\032\031.sysml.Apply" + - "EditsResponse\022S\n\020VerifyConstraint\022\036.sysm" + - "l.VerifyConstraintRequest\032\037.sysml.Verify" + - "ConstraintResponse\022V\n\021VerifyRequirement\022" + - "\037.sysml.VerifyRequirementRequest\032 .sysml" + - ".VerifyRequirementResponse\022Y\n\022VerifySati" + - "sfaction\022 .sysml.VerifySatisfactionReque" + - "st\032!.sysml.VerifySatisfactionResponse\022S\n" + - "\020ValidateInstance\022\036.sysml.ValidateInstan" + - "ceRequest\032\037.sysml.ValidateInstanceRespon" + - "se\022G\n\014EvaluateCalc\022\032.sysml.EvaluateCalcR" + - "equest\032\033.sysml.EvaluateCalcResponse\022D\n\013R" + - "unAnalysis\022\031.sysml.RunAnalysisRequest\032\032." + - "sysml.RunAnalysisResponse\022;\n\010RunSweep\022\026." + - "sysml.RunSweepRequest\032\027.sysml.RunSweepRe" + - "sponse\022D\n\013ListEngines\022\031.sysml.ListEngine" + - "sRequest\032\032.sysml.ListEnginesResponse\0222\n\005" + - "Query\022\023.sysml.QueryRequest\032\024.sysml.Query" + - "Response\022S\n\020RunDocumentQuery\022\036.sysml.Run" + - "DocumentQueryRequest\032\037.sysml.RunDocument" + - "QueryResponse\022M\n\016RenderDocument\022\034.sysml." + - "RenderDocumentRequest\032\035.sysml.RenderDocu" + - "mentResponseBJ\n\034org.openmbee.opensysml.p" + - "rotoP\001Z(github.com/Open-MBEE/OpenSysML/a" + - "pi/protob\006proto3" + "tValue\0228\n\016rational_value\030\016 \001(\0132\017.sysml.R" + + "ationalH\000R\rrationalValue\022!\n\014element_type" + + "\030\007 \001(\tR\013elementTypeB\006\n\004kind\"u\n\016DocumentO" + + "bject\022\037\n\013instance_id\030\001 \001(\003R\ninstanceId\022\022" + + "\n\004path\030\002 \001(\tR\004path\022.\n\007element\030\003 \001(\0132\024.sy" + + "sml.DocumentValueR\007element\"\365\001\n\017DocumentV" + + "erdict\0222\n\tassertion\030\001 \001(\0132\024.sysml.Docume" + + "ntValueR\tassertion\022\022\n\004kind\030\002 \001(\tR\004kind\022\022" + + "\n\004text\030\003 \001(\tR\004text\022\022\n\004path\030\004 \001(\tR\004path\022\030" + + "\n\007verdict\030\005 \001(\tR\007verdict\022\034\n\tcondition\030\006 " + + "\001(\tR\tcondition\022\026\n\006reason\030\007 \001(\tR\006reason\022\"" + + "\n\014verification\030\010 \003(\tR\014verification\"\355\001\n\rD" + + "ocumentState\022-\n\006object\030\001 \001(\0132\025.sysml.Doc" + + "umentObjectR\006object\022\030\n\007machine\030\002 \001(\tR\007ma" + + "chine\022\022\n\004name\030\003 \001(\tR\004name\022\035\n\nstate_path\030" + + "\004 \001(\tR\tstatePath\022*\n\005state\030\005 \001(\0132\024.sysml." + + "DocumentValueR\005state\022\026\n\006region\030\006 \001(\tR\006re" + + "gion\022\034\n\tenclosing\030\007 \003(\tR\tenclosing\"\375\002\n\rD" + + "ocumentEvent\022\022\n\004kind\030\001 \001(\tR\004kind\022(\n\004time" + + "\030\002 \001(\0132\024.sysml.DocumentValueR\004time\022-\n\006ob" + + "ject\030\003 \001(\0132\025.sysml.DocumentObjectR\006objec" + + "t\022\030\n\007machine\030\004 \001(\tR\007machine\022\024\n\005state\030\005 \001" + + "(\tR\005state\022\022\n\004from\030\006 \001(\tR\004from\022\016\n\002to\030\007 \001(" + + "\tR\002to\022-\n\006target\030\010 \001(\0132\025.sysml.DocumentOb" + + "jectR\006target\022\024\n\005event\030\t \001(\tR\005event\022\030\n\007pa" + + "yload\030\n \003(\tR\007payload\022\"\n\014alternatives\030\013 \003" + + "(\tR\014alternatives\022\024\n\005taken\030\014 \001(\tR\005taken\022\022" + + "\n\004text\030\r \001(\tR\004text\")\n\023DocumentQueryColum" + + "n\022\022\n\004name\030\001 \001(\tR\004name\"A\n\021DocumentQueryCe" + + "ll\022,\n\006values\030\001 \003(\0132\024.sysml.DocumentValue" + + "R\006values\"r\n\020DocumentQueryRow\022.\n\007element\030" + + "\001 \001(\0132\024.sysml.DocumentValueR\007element\022.\n\005" + + "cells\030\002 \003(\0132\030.sysml.DocumentQueryCellR\005c" + + "ells\"}\n\030RunDocumentQueryResponse\0224\n\007colu" + + "mns\030\001 \003(\0132\032.sysml.DocumentQueryColumnR\007c" + + "olumns\022+\n\004rows\030\002 \003(\0132\027.sysml.DocumentQue" + + "ryRowR\004rows\"k\n\025RenderDocumentRequest\022\035\n\n" + + "model_hash\030\001 \001(\tR\tmodelHash\022\037\n\013document_" + + "id\030\002 \001(\tR\ndocumentId\022\022\n\004form\030\003 \001(\tR\004form" + + "\"H\n\026RenderDocumentResponse\022\032\n\010markdown\030\001" + + " \001(\tR\010markdown\022\022\n\004html\030\002 \001(\tR\004html*\261\001\n\rF" + + "ailureReason\022\036\n\032FAILURE_REASON_UNSPECIFI" + + "ED\020\000\022\035\n\031FAILURE_REASON_EVALUATION\020\001\022\035\n\031F" + + "AILURE_REASON_WRONG_KIND\020\002\022$\n FAILURE_RE" + + "ASON_AMBIGUOUS_SUBJECT\020\003\022\034\n\030FAILURE_REAS" + + "ON_UNDECIDED\020\004*\214\005\n\013EditFailure\022\034\n\030EDIT_F" + + "AILURE_UNSPECIFIED\020\000\022\036\n\032EDIT_FAILURE_NO_" + + "OPERATIONS\020\001\022\037\n\033EDIT_FAILURE_UNKNOWN_TAR" + + "GET\020\002\022!\n\035EDIT_FAILURE_AMBIGUOUS_TARGET\020\003" + + "\022\033\n\027EDIT_FAILURE_NOT_VALUED\020\004\022\036\n\032EDIT_FA" + + "ILURE_INVALID_VALUE\020\005\022\035\n\031EDIT_FAILURE_IN" + + "VALID_NAME\020\006\022\032\n\026EDIT_FAILURE_NOT_NAMED\020\007" + + "\022\"\n\036EDIT_FAILURE_RENAME_REFERENCED\020\010\022\"\n\036" + + "EDIT_FAILURE_OVERLAPPING_EDITS\020\t\022\037\n\033EDIT" + + "_FAILURE_RESULT_INVALID\020\n\022\036\n\032EDIT_FAILUR" + + "E_OWNER_UNKNOWN\020\013\022$\n EDIT_FAILURE_OWNER_" + + "NOT_NAMESPACE\020\014\022\035\n\031EDIT_FAILURE_ILLEGAL_" + + "KIND\020\r\022\"\n\036EDIT_FAILURE_MEMBER_NAME_TAKEN" + + "\020\016\022\"\n\036EDIT_FAILURE_DELETE_REFERENCED\020\017\022$" + + "\n EDIT_FAILURE_OWNER_INSIDE_TARGET\020\020\022 \n\034" + + "EDIT_FAILURE_MOVE_REFERENCED\020\021\022%\n!EDIT_F" + + "AILURE_REFERENCED_ELSEWHERE\020\022*\222\001\n\021Primit" + + "iveOperator\022\"\n\036PRIMITIVE_OPERATOR_UNSPEC" + + "IFIED\020\000\022\034\n\030PRIMITIVE_OPERATOR_EQUAL\020\001\022\036\n" + + "\032PRIMITIVE_OPERATOR_GREATER\020\002\022\033\n\027PRIMITI" + + "VE_OPERATOR_LESS\020\003*n\n\021CompositeOperator\022" + + "\"\n\036COMPOSITE_OPERATOR_UNSPECIFIED\020\000\022\032\n\026C" + + "OMPOSITE_OPERATOR_AND\020\001\022\031\n\025COMPOSITE_OPE" + + "RATOR_OR\020\0022\371\014\n\014SysMLService\022D\n\rGetServer" + + "Info\022\030.sysml.ServerInfoRequest\032\031.sysml.S" + + "erverInfoResponse\022>\n\tParseFile\022\027.sysml.P" + + "arseFileRequest\032\030.sysml.ParseFileRespons" + + "e\022G\n\014ParseSources\022\032.sysml.ParseSourcesRe" + + "quest\032\033.sysml.ParseSourcesResponse\022;\n\tGe" + + "tSymbol\022\027.sysml.GetSymbolRequest\032\025.sysml" + + ".SymbolResponse\022G\n\016GetDiagnostics\022\031.sysm" + + "l.DiagnosticsRequest\032\032.sysml.Diagnostics" + + "Response\022;\n\010Evaluate\022\026.sysml.EvaluateReq" + + "uest\032\027.sysml.EvaluateResponse\022D\n\013Instant" + + "iate\022\031.sysml.InstantiateRequest\032\032.sysml." + + "InstantiateResponse\022J\n\rExecuteAction\022\033.s" + + "ysml.ExecuteActionRequest\032\034.sysml.Execut" + + "eActionResponse\022G\n\014ExecuteState\022\032.sysml." + + "ExecuteStateRequest\032\033.sysml.ExecuteState" + + "Response\0228\n\007Convert\022\025.sysml.ConvertReque" + + "st\032\026.sysml.ConvertResponse\0228\n\007Migrate\022\025." + + "sysml.MigrateRequest\032\026.sysml.MigrateResp" + + "onse\022A\n\nApplyEdits\022\030.sysml.ApplyEditsReq" + + "uest\032\031.sysml.ApplyEditsResponse\022S\n\020Verif" + + "yConstraint\022\036.sysml.VerifyConstraintRequ" + + "est\032\037.sysml.VerifyConstraintResponse\022V\n\021" + + "VerifyRequirement\022\037.sysml.VerifyRequirem" + + "entRequest\032 .sysml.VerifyRequirementResp" + + "onse\022Y\n\022VerifySatisfaction\022 .sysml.Verif" + + "ySatisfactionRequest\032!.sysml.VerifySatis" + + "factionResponse\022S\n\020ValidateInstance\022\036.sy" + + "sml.ValidateInstanceRequest\032\037.sysml.Vali" + + "dateInstanceResponse\022G\n\014EvaluateCalc\022\032.s" + + "ysml.EvaluateCalcRequest\032\033.sysml.Evaluat" + + "eCalcResponse\022D\n\013RunAnalysis\022\031.sysml.Run" + + "AnalysisRequest\032\032.sysml.RunAnalysisRespo" + + "nse\022;\n\010RunSweep\022\026.sysml.RunSweepRequest\032" + + "\027.sysml.RunSweepResponse\022D\n\013ListEngines\022" + + "\031.sysml.ListEnginesRequest\032\032.sysml.ListE" + + "nginesResponse\0222\n\005Query\022\023.sysml.QueryReq" + + "uest\032\024.sysml.QueryResponse\022S\n\020RunDocumen" + + "tQuery\022\036.sysml.RunDocumentQueryRequest\032\037" + + ".sysml.RunDocumentQueryResponse\022M\n\016Rende" + + "rDocument\022\034.sysml.RenderDocumentRequest\032" + + "\035.sysml.RenderDocumentResponseBJ\n\034org.op" + + "enmbee.opensysml.protoP\001Z(github.com/Ope" + + "n-MBEE/OpenSysML/api/protob\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, @@ -1908,159 +1919,165 @@ public static void registerAllExtensions( internal_static_sysml_Value_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Value_descriptor, - new java.lang.String[] { "IntValue", "RealValue", "BoolValue", "StringValue", "InstanceId", "Sequence", "Null", "Quantity", "EnumLiteral", "Unset", "Complex", "Array", "Vector", "VectorQuantity", "MeasurementRef", "Infinity", "Function", "Set", "TensorQuantity", "Metaobject", "Undetermined", "BigIntValue", "Kind", }); - internal_static_sysml_Metaobject_descriptor = + new java.lang.String[] { "IntValue", "RealValue", "BoolValue", "StringValue", "InstanceId", "Sequence", "Null", "Quantity", "EnumLiteral", "Unset", "Complex", "Array", "Vector", "VectorQuantity", "MeasurementRef", "Infinity", "Function", "Set", "TensorQuantity", "Metaobject", "Undetermined", "BigIntValue", "RationalValue", "Kind", }); + internal_static_sysml_Rational_descriptor = getDescriptor().getMessageType(80); + internal_static_sysml_Rational_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_sysml_Rational_descriptor, + new java.lang.String[] { "Numerator", "Denominator", }); + internal_static_sysml_Metaobject_descriptor = + getDescriptor().getMessageType(81); internal_static_sysml_Metaobject_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Metaobject_descriptor, new java.lang.String[] { "ElementId", "MetaclassId", }); internal_static_sysml_Undetermined_descriptor = - getDescriptor().getMessageType(81); + getDescriptor().getMessageType(82); internal_static_sysml_Undetermined_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Undetermined_descriptor, new java.lang.String[] { "Reason", "Count", }); internal_static_sysml_Function_descriptor = - getDescriptor().getMessageType(82); + getDescriptor().getMessageType(83); internal_static_sysml_Function_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Function_descriptor, new java.lang.String[] { "CalcId", "SelfId", }); internal_static_sysml_ValueSet_descriptor = - getDescriptor().getMessageType(83); + getDescriptor().getMessageType(84); internal_static_sysml_ValueSet_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_ValueSet_descriptor, new java.lang.String[] { "Elements", }); internal_static_sysml_TensorQuantity_descriptor = - getDescriptor().getMessageType(84); + getDescriptor().getMessageType(85); internal_static_sysml_TensorQuantity_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_TensorQuantity_descriptor, new java.lang.String[] { "Dimensions", "Components", }); internal_static_sysml_Array_descriptor = - getDescriptor().getMessageType(85); + getDescriptor().getMessageType(86); internal_static_sysml_Array_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Array_descriptor, new java.lang.String[] { "Dimensions", "Elements", }); internal_static_sysml_Vector_descriptor = - getDescriptor().getMessageType(86); + getDescriptor().getMessageType(87); internal_static_sysml_Vector_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Vector_descriptor, new java.lang.String[] { "Components", }); internal_static_sysml_VectorQuantity_descriptor = - getDescriptor().getMessageType(87); + getDescriptor().getMessageType(88); internal_static_sysml_VectorQuantity_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_VectorQuantity_descriptor, new java.lang.String[] { "Components", }); internal_static_sysml_Complex_descriptor = - getDescriptor().getMessageType(88); + getDescriptor().getMessageType(89); internal_static_sysml_Complex_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Complex_descriptor, new java.lang.String[] { "Real", "Imaginary", }); internal_static_sysml_EnumLiteral_descriptor = - getDescriptor().getMessageType(89); + getDescriptor().getMessageType(90); internal_static_sysml_EnumLiteral_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_EnumLiteral_descriptor, new java.lang.String[] { "LiteralId", "EnumerationId", "Name", "Value", }); internal_static_sysml_ValueSequence_descriptor = - getDescriptor().getMessageType(90); + getDescriptor().getMessageType(91); internal_static_sysml_ValueSequence_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_ValueSequence_descriptor, new java.lang.String[] { "Elements", }); internal_static_sysml_Quantity_descriptor = - getDescriptor().getMessageType(91); + getDescriptor().getMessageType(92); internal_static_sysml_Quantity_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Quantity_descriptor, - new java.lang.String[] { "IntMagnitude", "RealMagnitude", "BigIntMagnitude", "Unit", "UnitTerm", "Magnitude", }); + new java.lang.String[] { "IntMagnitude", "RealMagnitude", "BigIntMagnitude", "RationalMagnitude", "Unit", "UnitTerm", "Magnitude", }); internal_static_sysml_MeasurementRef_descriptor = - getDescriptor().getMessageType(92); + getDescriptor().getMessageType(93); internal_static_sysml_MeasurementRef_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_MeasurementRef_descriptor, new java.lang.String[] { "Unit", "UnitTerm", "UnitId", }); internal_static_sysml_UnitTerm_descriptor = - getDescriptor().getMessageType(93); + getDescriptor().getMessageType(94); internal_static_sysml_UnitTerm_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_UnitTerm_descriptor, new java.lang.String[] { "ScaleNum", "ScaleDen", "Factors", }); internal_static_sysml_UnitFactor_descriptor = - getDescriptor().getMessageType(94); + getDescriptor().getMessageType(95); internal_static_sysml_UnitFactor_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_UnitFactor_descriptor, new java.lang.String[] { "UnitId", "Exponent", }); internal_static_sysml_Diagnostic_descriptor = - getDescriptor().getMessageType(95); + getDescriptor().getMessageType(96); internal_static_sysml_Diagnostic_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Diagnostic_descriptor, new java.lang.String[] { "Severity", "Message", "Span", "Code", }); internal_static_sysml_Span_descriptor = - getDescriptor().getMessageType(96); + getDescriptor().getMessageType(97); internal_static_sysml_Span_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Span_descriptor, new java.lang.String[] { "File", "StartLine", "StartCol", "EndLine", "EndCol", }); internal_static_sysml_ServerInfoRequest_descriptor = - getDescriptor().getMessageType(97); + getDescriptor().getMessageType(98); internal_static_sysml_ServerInfoRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_ServerInfoRequest_descriptor, new java.lang.String[] { }); internal_static_sysml_ServerInfoResponse_descriptor = - getDescriptor().getMessageType(98); + getDescriptor().getMessageType(99); internal_static_sysml_ServerInfoResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_ServerInfoResponse_descriptor, new java.lang.String[] { "Version", "Capabilities", }); internal_static_sysml_QueryRequest_descriptor = - getDescriptor().getMessageType(99); + getDescriptor().getMessageType(100); internal_static_sysml_QueryRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_QueryRequest_descriptor, new java.lang.String[] { "ModelHash", "Query", "OslcQuery", }); internal_static_sysml_QueryResponse_descriptor = - getDescriptor().getMessageType(100); + getDescriptor().getMessageType(101); internal_static_sysml_QueryResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_QueryResponse_descriptor, new java.lang.String[] { "Elements", }); internal_static_sysml_Query_descriptor = - getDescriptor().getMessageType(101); + getDescriptor().getMessageType(102); internal_static_sysml_Query_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Query_descriptor, new java.lang.String[] { "Scope", "Select", "Where", }); internal_static_sysml_Constraint_descriptor = - getDescriptor().getMessageType(102); + getDescriptor().getMessageType(103); internal_static_sysml_Constraint_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Constraint_descriptor, new java.lang.String[] { "Primitive", "Composite", "Constraint", }); internal_static_sysml_PrimitiveConstraint_descriptor = - getDescriptor().getMessageType(103); + getDescriptor().getMessageType(104); internal_static_sysml_PrimitiveConstraint_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_PrimitiveConstraint_descriptor, new java.lang.String[] { "Inverse", "Property", "Operator", "Value", }); internal_static_sysml_CompositeConstraint_descriptor = - getDescriptor().getMessageType(104); + getDescriptor().getMessageType(105); internal_static_sysml_CompositeConstraint_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_CompositeConstraint_descriptor, new java.lang.String[] { "Operator", "Constraint", }); internal_static_sysml_QueryResultElement_descriptor = - getDescriptor().getMessageType(105); + getDescriptor().getMessageType(106); internal_static_sysml_QueryResultElement_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_QueryResultElement_descriptor, @@ -2072,13 +2089,13 @@ public static void registerAllExtensions( internal_static_sysml_QueryResultElement_PropertiesEntry_descriptor, new java.lang.String[] { "Key", "Value", }); internal_static_sysml_SweepRange_descriptor = - getDescriptor().getMessageType(106); + getDescriptor().getMessageType(107); internal_static_sysml_SweepRange_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_SweepRange_descriptor, new java.lang.String[] { "Parameter", "Start", "End", "Step", }); internal_static_sysml_RunSweepRequest_descriptor = - getDescriptor().getMessageType(107); + getDescriptor().getMessageType(108); internal_static_sysml_RunSweepRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RunSweepRequest_descriptor, @@ -2090,91 +2107,91 @@ public static void registerAllExtensions( internal_static_sysml_RunSweepRequest_NamedArgumentsEntry_descriptor, new java.lang.String[] { "Key", "Value", }); internal_static_sysml_SweepRow_descriptor = - getDescriptor().getMessageType(108); + getDescriptor().getMessageType(109); internal_static_sysml_SweepRow_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_SweepRow_descriptor, new java.lang.String[] { "Inputs", "Outputs", "Verdicts", "ElapsedMicros", "Error", "FailureReason", "Evaluations", }); internal_static_sysml_RunSweepResponse_descriptor = - getDescriptor().getMessageType(109); + getDescriptor().getMessageType(110); internal_static_sysml_RunSweepResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RunSweepResponse_descriptor, new java.lang.String[] { "Rows", "Parameters", "Sampled", "Seed", "Error", "Diagnostics", "FailureReason", "Instances", "Engine", "Strength", "Bounds", }); internal_static_sysml_RunDocumentQueryRequest_descriptor = - getDescriptor().getMessageType(110); + getDescriptor().getMessageType(111); internal_static_sysml_RunDocumentQueryRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RunDocumentQueryRequest_descriptor, new java.lang.String[] { "ModelHash", "QueryId", "Bindings", }); internal_static_sysml_DocumentQueryBinding_descriptor = - getDescriptor().getMessageType(111); + getDescriptor().getMessageType(112); internal_static_sysml_DocumentQueryBinding_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentQueryBinding_descriptor, new java.lang.String[] { "Parameter", "Values", }); internal_static_sysml_DocumentValue_descriptor = - getDescriptor().getMessageType(112); + getDescriptor().getMessageType(113); internal_static_sysml_DocumentValue_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentValue_descriptor, - new java.lang.String[] { "ElementId", "StringValue", "IntValue", "RealValue", "BoolValue", "Infinity", "Quantity", "Verdict", "Object", "State", "Event", "BigIntValue", "ElementType", "Kind", }); + new java.lang.String[] { "ElementId", "StringValue", "IntValue", "RealValue", "BoolValue", "Infinity", "Quantity", "Verdict", "Object", "State", "Event", "BigIntValue", "RationalValue", "ElementType", "Kind", }); internal_static_sysml_DocumentObject_descriptor = - getDescriptor().getMessageType(113); + getDescriptor().getMessageType(114); internal_static_sysml_DocumentObject_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentObject_descriptor, new java.lang.String[] { "InstanceId", "Path", "Element", }); internal_static_sysml_DocumentVerdict_descriptor = - getDescriptor().getMessageType(114); + getDescriptor().getMessageType(115); internal_static_sysml_DocumentVerdict_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentVerdict_descriptor, new java.lang.String[] { "Assertion", "Kind", "Text", "Path", "Verdict", "Condition", "Reason", "Verification", }); internal_static_sysml_DocumentState_descriptor = - getDescriptor().getMessageType(115); + getDescriptor().getMessageType(116); internal_static_sysml_DocumentState_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentState_descriptor, new java.lang.String[] { "Object", "Machine", "Name", "StatePath", "State", "Region", "Enclosing", }); internal_static_sysml_DocumentEvent_descriptor = - getDescriptor().getMessageType(116); + getDescriptor().getMessageType(117); internal_static_sysml_DocumentEvent_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentEvent_descriptor, new java.lang.String[] { "Kind", "Time", "Object", "Machine", "State", "From", "To", "Target", "Event", "Payload", "Alternatives", "Taken", "Text", }); internal_static_sysml_DocumentQueryColumn_descriptor = - getDescriptor().getMessageType(117); + getDescriptor().getMessageType(118); internal_static_sysml_DocumentQueryColumn_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentQueryColumn_descriptor, new java.lang.String[] { "Name", }); internal_static_sysml_DocumentQueryCell_descriptor = - getDescriptor().getMessageType(118); + getDescriptor().getMessageType(119); internal_static_sysml_DocumentQueryCell_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentQueryCell_descriptor, new java.lang.String[] { "Values", }); internal_static_sysml_DocumentQueryRow_descriptor = - getDescriptor().getMessageType(119); + getDescriptor().getMessageType(120); internal_static_sysml_DocumentQueryRow_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentQueryRow_descriptor, new java.lang.String[] { "Element", "Cells", }); internal_static_sysml_RunDocumentQueryResponse_descriptor = - getDescriptor().getMessageType(120); + getDescriptor().getMessageType(121); internal_static_sysml_RunDocumentQueryResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RunDocumentQueryResponse_descriptor, new java.lang.String[] { "Columns", "Rows", }); internal_static_sysml_RenderDocumentRequest_descriptor = - getDescriptor().getMessageType(121); + getDescriptor().getMessageType(122); internal_static_sysml_RenderDocumentRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RenderDocumentRequest_descriptor, new java.lang.String[] { "ModelHash", "DocumentId", "Form", }); internal_static_sysml_RenderDocumentResponse_descriptor = - getDescriptor().getMessageType(122); + getDescriptor().getMessageType(123); internal_static_sysml_RenderDocumentResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RenderDocumentResponse_descriptor, diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java index a226833059..e7e88f3f0c 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java @@ -75,6 +75,7 @@ public enum KindCase METAOBJECT(20), UNDETERMINED(21), BIG_INT_VALUE(22), + RATIONAL_VALUE(23), KIND_NOT_SET(0); private final int value; private KindCase(int value) { @@ -114,6 +115,7 @@ public static KindCase forNumber(int value) { case 20: return METAOBJECT; case 21: return UNDETERMINED; case 22: return BIG_INT_VALUE; + case 23: return RATIONAL_VALUE; case 0: return KIND_NOT_SET; default: return null; } @@ -1006,6 +1008,52 @@ public java.lang.String getBigIntValue() { } } + public static final int RATIONAL_VALUE_FIELD_NUMBER = 23; + /** + *
+   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + * @return Whether the rationalValue field is set. + */ + @java.lang.Override + public boolean hasRationalValue() { + return kindCase_ == 23; + } + /** + *
+   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + * @return The rationalValue. + */ + @java.lang.Override + public org.openmbee.opensysml.proto.Rational getRationalValue() { + if (kindCase_ == 23) { + return (org.openmbee.opensysml.proto.Rational) kind_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + /** + *
+   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.RationalOrBuilder getRationalValueOrBuilder() { + if (kindCase_ == 23) { + return (org.openmbee.opensysml.proto.Rational) kind_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + private byte memoizedIsInitialized = -1; @java.lang.Override public final boolean isInitialized() { @@ -1092,6 +1140,9 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (kindCase_ == 22) { com.google.protobuf.GeneratedMessage.writeString(output, 22, kind_); } + if (kindCase_ == 23) { + output.writeMessage(23, (org.openmbee.opensysml.proto.Rational) kind_); + } getUnknownFields().writeTo(output); } @@ -1192,6 +1243,10 @@ public int getSerializedSize() { if (kindCase_ == 22) { size += com.google.protobuf.GeneratedMessage.computeStringSize(22, kind_); } + if (kindCase_ == 23) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(23, (org.openmbee.opensysml.proto.Rational) kind_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -1298,6 +1353,10 @@ public boolean equals(final java.lang.Object obj) { if (!getBigIntValue() .equals(other.getBigIntValue())) return false; break; + case 23: + if (!getRationalValue() + .equals(other.getRationalValue())) return false; + break; case 0: default: } @@ -1407,6 +1466,10 @@ public int hashCode() { hash = (37 * hash) + BIG_INT_VALUE_FIELD_NUMBER; hash = (53 * hash) + getBigIntValue().hashCode(); break; + case 23: + hash = (37 * hash) + RATIONAL_VALUE_FIELD_NUMBER; + hash = (53 * hash) + getRationalValue().hashCode(); + break; case 0: default: } @@ -1584,6 +1647,9 @@ public Builder clear() { if (undeterminedBuilder_ != null) { undeterminedBuilder_.clear(); } + if (rationalValueBuilder_ != null) { + rationalValueBuilder_.clear(); + } kindCase_ = 0; kind_ = null; return this; @@ -1677,6 +1743,10 @@ private void buildPartialOneofs(org.openmbee.opensysml.proto.Value result) { undeterminedBuilder_ != null) { result.kind_ = undeterminedBuilder_.build(); } + if (kindCase_ == 23 && + rationalValueBuilder_ != null) { + result.kind_ = rationalValueBuilder_.build(); + } } @java.lang.Override @@ -1786,6 +1856,10 @@ public Builder mergeFrom(org.openmbee.opensysml.proto.Value other) { onChanged(); break; } + case RATIONAL_VALUE: { + mergeRationalValue(other.getRationalValue()); + break; + } case KIND_NOT_SET: { break; } @@ -1955,6 +2029,13 @@ public Builder mergeFrom( kind_ = s; break; } // case 178 + case 186: { + input.readMessage( + internalGetRationalValueFieldBuilder().getBuilder(), + extensionRegistry); + kindCase_ = 23; + break; + } // case 186 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -4883,6 +4964,193 @@ public Builder setBigIntValueBytes( return this; } + private com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder> rationalValueBuilder_; + /** + *
+     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + * @return Whether the rationalValue field is set. + */ + @java.lang.Override + public boolean hasRationalValue() { + return kindCase_ == 23; + } + /** + *
+     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + * @return The rationalValue. + */ + @java.lang.Override + public org.openmbee.opensysml.proto.Rational getRationalValue() { + if (rationalValueBuilder_ == null) { + if (kindCase_ == 23) { + return (org.openmbee.opensysml.proto.Rational) kind_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } else { + if (kindCase_ == 23) { + return rationalValueBuilder_.getMessage(); + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + } + /** + *
+     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + */ + public Builder setRationalValue(org.openmbee.opensysml.proto.Rational value) { + if (rationalValueBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + kind_ = value; + onChanged(); + } else { + rationalValueBuilder_.setMessage(value); + } + kindCase_ = 23; + return this; + } + /** + *
+     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + */ + public Builder setRationalValue( + org.openmbee.opensysml.proto.Rational.Builder builderForValue) { + if (rationalValueBuilder_ == null) { + kind_ = builderForValue.build(); + onChanged(); + } else { + rationalValueBuilder_.setMessage(builderForValue.build()); + } + kindCase_ = 23; + return this; + } + /** + *
+     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + */ + public Builder mergeRationalValue(org.openmbee.opensysml.proto.Rational value) { + if (rationalValueBuilder_ == null) { + if (kindCase_ == 23 && + kind_ != org.openmbee.opensysml.proto.Rational.getDefaultInstance()) { + kind_ = org.openmbee.opensysml.proto.Rational.newBuilder((org.openmbee.opensysml.proto.Rational) kind_) + .mergeFrom(value).buildPartial(); + } else { + kind_ = value; + } + onChanged(); + } else { + if (kindCase_ == 23) { + rationalValueBuilder_.mergeFrom(value); + } else { + rationalValueBuilder_.setMessage(value); + } + } + kindCase_ = 23; + return this; + } + /** + *
+     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + */ + public Builder clearRationalValue() { + if (rationalValueBuilder_ == null) { + if (kindCase_ == 23) { + kindCase_ = 0; + kind_ = null; + onChanged(); + } + } else { + if (kindCase_ == 23) { + kindCase_ = 0; + kind_ = null; + } + rationalValueBuilder_.clear(); + } + return this; + } + /** + *
+     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + */ + public org.openmbee.opensysml.proto.Rational.Builder getRationalValueBuilder() { + return internalGetRationalValueFieldBuilder().getBuilder(); + } + /** + *
+     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.RationalOrBuilder getRationalValueOrBuilder() { + if ((kindCase_ == 23) && (rationalValueBuilder_ != null)) { + return rationalValueBuilder_.getMessageOrBuilder(); + } else { + if (kindCase_ == 23) { + return (org.openmbee.opensysml.proto.Rational) kind_; + } + return org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + } + /** + *
+     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + */ + private com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder> + internalGetRationalValueFieldBuilder() { + if (rationalValueBuilder_ == null) { + if (!(kindCase_ == 23)) { + kind_ = org.openmbee.opensysml.proto.Rational.getDefaultInstance(); + } + rationalValueBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder>( + (org.openmbee.opensysml.proto.Rational) kind_, + getParentForChildren(), + isClean()); + kind_ = null; + } + kindCase_ = 23; + onChanged(); + return rationalValueBuilder_; + } + // @@protoc_insertion_point(builder_scope:sysml.Value) } diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java index f3f4d5186c..c1a703f2be 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java @@ -514,5 +514,35 @@ public interface ValueOrBuilder extends com.google.protobuf.ByteString getBigIntValueBytes(); + /** + *
+   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + * @return Whether the rationalValue field is set. + */ + boolean hasRationalValue(); + /** + *
+   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + * @return The rationalValue. + */ + org.openmbee.opensysml.proto.Rational getRationalValue(); + /** + *
+   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
+   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * 
+ * + * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; + */ + org.openmbee.opensysml.proto.RationalOrBuilder getRationalValueOrBuilder(); + org.openmbee.opensysml.proto.Value.KindCase getKindCase(); } diff --git a/client/node/src/generated/sysml_pb.ts b/client/node/src/generated/sysml_pb.ts index b2959024ab..a802b46a5a 100644 --- a/client/node/src/generated/sysml_pb.ts +++ b/client/node/src/generated/sysml_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file sysml.proto. */ export const file_sysml: GenFile = /*@__PURE__*/ - fileDesc("CgtzeXNtbC5wcm90bxIFc3lzbWwihgMKB1ZlcmRpY3QSDAoEa2luZBgBIAEoCRISCgplbGVtZW50X2lkGAIgASgJEg8KB2VsZW1lbnQYAyABKAkSDQoFaG9sZHMYBCABKAgSEQoJY29uZGl0aW9uGAUgASgJEhMKC2luc3RhbmNlX2lkGAYgASgDEhgKEGluc3RhbmNlX3R5cGVfaWQYByABKAkSDQoFZXJyb3IYCCABKAkSLAoOZmFpbHVyZV9yZWFzb24YCSABKA4yFC5zeXNtbC5GYWlsdXJlUmVhc29uEhYKDnJlcXVpcmVtZW50X2lkGAogASgJEg4KBmVuZ2luZRgLIAEoCRIQCghzdHJlbmd0aBgMIAEoCRIcCgZib3VuZHMYDSADKAsyDC5zeXNtbC5Cb3VuZBIVCg1pbnN0YW5jZV9wYXRoGA4gASgJEhAKCHF1ZXN0aW9uGA8gASgJEg4KBnN0YXR1cxgQIAEoCRIpCgd3aXRuZXNzGBEgAygLMhguc3lzbWwuV2l0bmVzc0Fzc2lnbm1lbnQiXgoRV2l0bmVzc0Fzc2lnbm1lbnQSDwoHZmVhdHVyZRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlEgwKBHVuaXQYAyABKAkSDQoFZXhhY3QYBCABKAkiNQoFQm91bmQSDAoEbmFtZRgBIAEoCRINCgVsaW1pdBgCIAEoAxIPCgdyZWFjaGVkGAMgASgIIn0KF1ZlcmlmeUNvbnN0cmFpbnRSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJEhkKEXN1YmplY3Rfc3ltYm9sX2lkGAMgASgJEg4KBmVuZ2luZRgEIAEoCRIQCghxdWVzdGlvbhgFIAEoCSKWAQoYVmVyaWZ5Q29uc3RyYWludFJlc3BvbnNlEh8KB3ZlcmRpY3QYASABKAsyDi5zeXNtbC5WZXJkaWN0EiIKCWluc3RhbmNlcxgCIAMoCzIPLnN5c21sLkluc3RhbmNlEg0KBWVycm9yGAMgASgJEiYKC2RpYWdub3N0aWNzGAQgAygLMhEuc3lzbWwuRGlhZ25vc3RpYyJ+ChhWZXJpZnlSZXF1aXJlbWVudFJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkSGQoRc3ViamVjdF9zeW1ib2xfaWQYAyABKAkSDgoGZW5naW5lGAQgASgJEhAKCHF1ZXN0aW9uGAUgASgJIm0KE1ZlcmlmaWNhdGlvblZlcmRpY3QSDwoHY2FzZV9pZBgBIAEoCRIMCgRraW5kGAIgASgJEg4KBmRldGFpbBgDIAEoCRIPCgdzdWJjYXNlGAQgASgIEhYKDnJlcXVpcmVtZW50X2lkGAUgASgJItIBChlWZXJpZnlSZXF1aXJlbWVudFJlc3BvbnNlEh8KB3ZlcmRpY3QYASABKAsyDi5zeXNtbC5WZXJkaWN0EiIKCWluc3RhbmNlcxgCIAMoCzIPLnN5c21sLkluc3RhbmNlEg0KBWVycm9yGAMgASgJEiYKC2RpYWdub3N0aWNzGAQgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxI5ChV2ZXJpZmljYXRpb25fdmVyZGljdHMYBSADKAsyGi5zeXNtbC5WZXJpZmljYXRpb25WZXJkaWN0ImQKGVZlcmlmeVNhdGlzZmFjdGlvblJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkSDgoGZW5naW5lGAMgASgJEhAKCHF1ZXN0aW9uGAQgASgJIoICChpWZXJpZnlTYXRpc2ZhY3Rpb25SZXNwb25zZRIgCgh2ZXJkaWN0cxgBIAMoCzIOLnN5c21sLlZlcmRpY3QSIgoJaW5zdGFuY2VzGAIgAygLMg8uc3lzbWwuSW5zdGFuY2USDQoFZXJyb3IYAyABKAkSJgoLZGlhZ25vc3RpY3MYBCADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAUgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhI5ChV2ZXJpZmljYXRpb25fdmVyZGljdHMYBiADKAsyGi5zeXNtbC5WZXJpZmljYXRpb25WZXJkaWN0IlAKF1ZhbGlkYXRlSW5zdGFuY2VSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJEg4KBmVuZ2luZRgDIAEoCSKyAgoYVmFsaWRhdGVJbnN0YW5jZVJlc3BvbnNlEiAKCHZlcmRpY3RzGAEgAygLMg4uc3lzbWwuVmVyZGljdBIfCgdzdW1tYXJ5GAIgASgLMg4uc3lzbWwuVmVyZGljdBIiCglpbnN0YW5jZXMYAyADKAsyDy5zeXNtbC5JbnN0YW5jZRINCgVlcnJvchgEIAEoCRImCgtkaWFnbm9zdGljcxgFIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSLAoOZmFpbHVyZV9yZWFzb24YBiABKA4yFC5zeXNtbC5GYWlsdXJlUmVhc29uEjkKFXZlcmlmaWNhdGlvbl92ZXJkaWN0cxgHIAMoCzIaLnN5c21sLlZlcmlmaWNhdGlvblZlcmRpY3QSDwoHYm91bmRlZBgIIAEoCCJtChNFdmFsdWF0ZUNhbGNSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJEh8KCWFyZ3VtZW50cxgDIAMoCzIMLnN5c21sLlZhbHVlEg4KBmVuZ2luZRgEIAEoCSL9AQoURXZhbHVhdGVDYWxjUmVzcG9uc2USHAoGcmVzdWx0GAEgASgLMgwuc3lzbWwuVmFsdWUSIgoHb3V0cHV0cxgCIAMoCzIRLnN5c21sLkNhbGNPdXRwdXQSDQoFZXJyb3IYAyABKAkSJgoLZGlhZ25vc3RpY3MYBCADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAUgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhIOCgZlbmdpbmUYBiABKAkSEAoIc3RyZW5ndGgYByABKAkSHAoGYm91bmRzGAggAygLMgwuc3lzbWwuQm91bmQiNwoKQ2FsY091dHB1dBIMCgRuYW1lGAEgASgJEhsKBXZhbHVlGAIgASgLMgwuc3lzbWwuVmFsdWUikwEKDkNhc2VFdmFsdWF0aW9uEhMKC2Z1bmN0aW9uX2lkGAEgASgJEh8KCWFyZ3VtZW50cxgCIAMoCzIMLnN5c21sLlZhbHVlEhwKBnJlc3VsdBgDIAEoCzIMLnN5c21sLlZhbHVlEg0KBWVycm9yGAQgASgJEhAKCHNlbGVjdGVkGAUgASgIEgwKBHRpZWQYBiABKAgipgIKElJ1bkFuYWx5c2lzUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhEKCXN5bWJvbF9pZBgCIAEoCRIZChFzdWJqZWN0X3N5bWJvbF9pZBgDIAEoCRIfCglhcmd1bWVudHMYBCADKAsyDC5zeXNtbC5WYWx1ZRJGCg9uYW1lZF9hcmd1bWVudHMYBSADKAsyLS5zeXNtbC5SdW5BbmFseXNpc1JlcXVlc3QuTmFtZWRBcmd1bWVudHNFbnRyeRIQCghzY2hlZHVsZRgGIAEoCRIOCgZlbmdpbmUYByABKAkaQwoTTmFtZWRBcmd1bWVudHNFbnRyeRILCgNrZXkYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZToCOAEi3AMKE1J1bkFuYWx5c2lzUmVzcG9uc2USIgoHb3V0cHV0cxgBIAMoCzIRLnN5c21sLkNhbGNPdXRwdXQSIAoIdmVyZGljdHMYAiADKAsyDi5zeXNtbC5WZXJkaWN0EiIKCWluc3RhbmNlcxgDIAMoCzIPLnN5c21sLkluc3RhbmNlEg0KBWVycm9yGAQgASgJEiYKC2RpYWdub3N0aWNzGAUgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxIsCg5mYWlsdXJlX3JlYXNvbhgGIAEoDjIULnN5c21sLkZhaWx1cmVSZWFzb24SOQoVdmVyaWZpY2F0aW9uX3ZlcmRpY3RzGAcgAygLMhouc3lzbWwuVmVyaWZpY2F0aW9uVmVyZGljdBIgCghvdXRjb21lcxgIIAMoCzIOLnN5c21sLk91dGNvbWUSLQoLZXhwbG9yYXRpb24YCSABKAsyGC5zeXNtbC5FeHBsb3JhdGlvblN0YXR1cxIqCgtldmFsdWF0aW9ucxgKIAMoCzIVLnN5c21sLkNhc2VFdmFsdWF0aW9uEg4KBmVuZ2luZRgLIAEoCRIQCghzdHJlbmd0aBgMIAEoCRIcCgZib3VuZHMYDSADKAsyDC5zeXNtbC5Cb3VuZCIsChBQcm9iYWJpbGl0eVJhbmdlEgsKA21pbhgBIAEoARILCgNtYXgYAiABKAEiywIKB091dGNvbWUSLAoHb3V0cHV0cxgBIAMoCzIbLnN5c21sLk91dGNvbWUuT3V0cHV0c0VudHJ5EhMKC2ZpbmFsX3N0YXRlGAIgASgJEhYKDnN0YXRlc192aXNpdGVkGAMgAygJEg0KBWVycm9yGAQgASgJEhYKDmxpbmVhcml6YXRpb25zGAUgASgFEg8KB3dpdG5lc3MYBiADKAkSJgoLZGlhZ25vc3RpY3MYByADKAsyES5zeXNtbC5EaWFnbm9zdGljEhMKC3Byb2JhYmlsaXR5GAggASgBEjIKEXByb2JhYmlsaXR5X3JhbmdlGAkgASgLMhcuc3lzbWwuUHJvYmFiaWxpdHlSYW5nZRo8CgxPdXRwdXRzRW50cnkSCwoDa2V5GAEgASgJEhsKBXZhbHVlGAIgASgLMgwuc3lzbWwuVmFsdWU6AjgBIrUBChFFeHBsb3JhdGlvblN0YXR1cxIQCghjb21wbGV0ZRgBIAEoCBIMCgRydW5zGAIgASgFEhMKC2J1ZGdldHNfaGl0GAMgAygJEhMKC3J1bnNfYnVkZ2V0GAQgASgFEhQKDGRlcHRoX2J1ZGdldBgFIAEoBRIhChlwcm9iYWJpbGl0aWVzX2xvd2VyX2JvdW5kGAYgASgIEh0KFWZhaWxlZF9saW5lYXJpemF0aW9ucxgHIAEoBSIUChJMaXN0RW5naW5lc1JlcXVlc3Qi/AEKCkVuZ2luZUluZm8SDAoEbmFtZRgBIAEoCRIRCglhdXRob3JpdHkYAiABKAkSDwoHYW5zd2VycxgDIAMoCRIOCgZib3VuZHMYBCADKAkSDwoHcHJvY2VzcxgFIAEoCRIVCg1wcm9jZXNzX2ZvdW5kGAYgASgJEg0KBXJlYWR5GAcgASgIEhMKC3VuYXZhaWxhYmxlGAggASgJEgwKBGtpbmQYCSABKAkSEAoIcHJvdG9jb2wYCiABKAkSDgoGc291cmNlGAsgASgJEg8KB2NvbW1hbmQYDCABKAkSDwoHdmVyc2lvbhgNIAEoCRIOCgZzZXJ2ZWQYDiABKAgiOQoTTGlzdEVuZ2luZXNSZXNwb25zZRIiCgdlbmdpbmVzGAEgAygLMhEuc3lzbWwuRW5naW5lSW5mbyKMAQoQUGFyc2VGaWxlUmVxdWVzdBITCglmaWxlX3BhdGgYASABKAlIABIRCgdjb250ZW50GAIgASgJSAASGAoMY29udGVudF9oYXNoGAMgASgJQgIYARIQCghsYW5ndWFnZRgEIAEoCRIaChJzdHJpY3RfY29uZm9ybWFuY2UYBSABKAhCCAoGc291cmNlImIKDlNvdXJjZURvY3VtZW50EhMKCWZpbGVfcGF0aBgBIAEoCUgAEhEKB2NvbnRlbnQYAiABKAlIABIQCghsYW5ndWFnZRgDIAEoCRIMCgRuYW1lGAQgASgJQggKBnNvdXJjZSJbChNQYXJzZVNvdXJjZXNSZXF1ZXN0EigKCWRvY3VtZW50cxgBIAMoCzIVLnN5c21sLlNvdXJjZURvY3VtZW50EhoKEnN0cmljdF9jb25mb3JtYW5jZRgCIAEoCCKDAQoUUGFyc2VTb3VyY2VzUmVzcG9uc2USEgoKbW9kZWxfaGFzaBgBIAEoCRIgCgVyb290cxgCIAMoCzIRLnN5c21sLlN5bWJvbEluZm8SJgoLZGlhZ25vc3RpY3MYAyADKAsyES5zeXNtbC5EaWFnbm9zdGljEg0KBWVycm9yGAQgASgJIn8KEVBhcnNlRmlsZVJlc3BvbnNlEhIKCm1vZGVsX2hhc2gYASABKAkSHwoEcm9vdBgCIAEoCzIRLnN5c21sLlN5bWJvbEluZm8SJgoLZGlhZ25vc3RpY3MYAyADKAsyES5zeXNtbC5EaWFnbm9zdGljEg0KBWVycm9yGAQgASgJIjkKEEdldFN5bWJvbFJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkiQgoOU3ltYm9sUmVzcG9uc2USIQoGc3ltYm9sGAEgASgLMhEuc3lzbWwuU3ltYm9sSW5mbxINCgVlcnJvchgCIAEoCSIoChJEaWFnbm9zdGljc1JlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCSJMChNEaWFnbm9zdGljc1Jlc3BvbnNlEiYKC2RpYWdub3N0aWNzGAEgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxINCgVlcnJvchgCIAEoCSJvCg9FdmFsdWF0ZVJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRISCgpleHByZXNzaW9uGAIgASgJEhkKEWNvbnRleHRfc3ltYm9sX2lkGAMgASgJEhkKEXN1YmplY3Rfc3ltYm9sX2lkGAQgASgJImcKEEV2YWx1YXRlUmVzcG9uc2USHAoGcmVzdWx0GAEgASgLMgwuc3lzbWwuVmFsdWUSDQoFZXJyb3IYAiABKAkSJgoLZGlhZ25vc3RpY3MYAyADKAsyES5zeXNtbC5EaWFnbm9zdGljIsIBCghJbnN0YW5jZRIKCgJpZBgBIAEoAxIWCg50eXBlX3N5bWJvbF9pZBgCIAEoCRI6Cg5mZWF0dXJlX3ZhbHVlcxgEIAMoCzIiLnN5c21sLkluc3RhbmNlLkZlYXR1cmVWYWx1ZXNFbnRyeRpJChJGZWF0dXJlVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEiIKBXZhbHVlGAIgASgLMhMuc3lzbWwuRmVhdHVyZVZhbHVlOgI4AUoECAMQBFIFc2xvdHMihAEKDEZlYXR1cmVWYWx1ZRIUCgxmZWF0dXJlX25hbWUYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZRIcCgZ2YWx1ZXMYAyADKAsyDC5zeXNtbC5WYWx1ZRIUCgxtYXRlcmlhbGl6ZWQYBCABKAgSDQoFZXJyb3IYBSABKAkiOwoSSW5zdGFudGlhdGVSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJIpMBChNJbnN0YW50aWF0ZVJlc3BvbnNlEiEKCGluc3RhbmNlGAEgASgLMg8uc3lzbWwuSW5zdGFuY2USDQoFZXJyb3IYAiABKAkSJgoLZGlhZ25vc3RpY3MYAyADKAsyES5zeXNtbC5EaWFnbm9zdGljEiIKCWluc3RhbmNlcxgEIAMoCzIPLnN5c21sLkluc3RhbmNlIukBChRFeGVjdXRlQWN0aW9uUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhgKEGFjdGlvbl9zeW1ib2xfaWQYAiABKAkSNwoGaW5wdXRzGAMgAygLMicuc3lzbWwuRXhlY3V0ZUFjdGlvblJlcXVlc3QuSW5wdXRzRW50cnkSEAoIc2NoZWR1bGUYBCABKAkSGwoTcGVyZm9ybWVyX3N5bWJvbF9pZBgFIAEoCRo7CgtJbnB1dHNFbnRyeRILCgNrZXkYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZToCOAEizAMKFUV4ZWN1dGVBY3Rpb25SZXNwb25zZRI6CgdvdXRwdXRzGAEgAygLMikuc3lzbWwuRXhlY3V0ZUFjdGlvblJlc3BvbnNlLk91dHB1dHNFbnRyeRINCgVlcnJvchgCIAEoCRImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSIAoIb3V0Y29tZXMYBCADKAsyDi5zeXNtbC5PdXRjb21lEi0KC2V4cGxvcmF0aW9uGAUgASgLMhguc3lzbWwuRXhwbG9yYXRpb25TdGF0dXMSEgoKZmluYWxfdGltZRgGIAEoARJTChRwZXJmb3JtZXJfYXR0cmlidXRlcxgHIAMoCzI1LnN5c21sLkV4ZWN1dGVBY3Rpb25SZXNwb25zZS5QZXJmb3JtZXJBdHRyaWJ1dGVzRW50cnkaPAoMT3V0cHV0c0VudHJ5EgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlOgI4ARpIChhQZXJmb3JtZXJBdHRyaWJ1dGVzRW50cnkSCwoDa2V5GAEgASgJEhsKBXZhbHVlGAIgASgLMgwuc3lzbWwuVmFsdWU6AjgBIokBChNFeGVjdXRlU3RhdGVSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSHwoXc3RhdGVfbWFjaGluZV9zeW1ib2xfaWQYAiABKAkSDgoGZXZlbnRzGAMgAygJEhAKCHNjaGVkdWxlGAQgASgJEhsKE3BlcmZvcm1lcl9zeW1ib2xfaWQYBSABKAki0wIKFEV4ZWN1dGVTdGF0ZVJlc3BvbnNlEhYKDnN0YXRlc192aXNpdGVkGAEgAygJEkQKDWZpbmFsX2NvbnRleHQYAiADKAsyLS5zeXNtbC5FeGVjdXRlU3RhdGVSZXNwb25zZS5GaW5hbENvbnRleHRFbnRyeRINCgVlcnJvchgDIAEoCRImCgtkaWFnbm9zdGljcxgEIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSIAoIb3V0Y29tZXMYBSADKAsyDi5zeXNtbC5PdXRjb21lEi0KC2V4cGxvcmF0aW9uGAYgASgLMhguc3lzbWwuRXhwbG9yYXRpb25TdGF0dXMSEgoKZmluYWxfdGltZRgHIAEoARpBChFGaW5hbENvbnRleHRFbnRyeRILCgNrZXkYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZToCOAEisQEKDkNvbnZlcnRSZXF1ZXN0EhMKCWZpbGVfcGF0aBgBIAEoCUgAEhEKB2NvbnRlbnQYAiABKAlIABIUCgptb2RlbF9oYXNoGAYgASgJSAASEwoLZnJvbV9mb3JtYXQYAyABKAkSEQoJdG9fZm9ybWF0GAQgASgJEh4KFnRvbGVyYXRlX3N5bnRheF9lcnJvcnMYBSABKAgSDwoHaWRfZm9ybRgHIAEoCUIICgZzb3VyY2UitAEKD0NvbnZlcnRSZXNwb25zZRIPCgdjb250ZW50GAEgASgJEhMKC2Zyb21fZm9ybWF0GAIgASgJEhEKCXRvX2Zvcm1hdBgDIAEoCRINCgVlcnJvchgEIAEoCRImCgtkaWFnbm9zdGljcxgFIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSFAoMZXhwZXJpbWVudGFsGAYgASgIEhsKE2V4cGVyaW1lbnRhbF9ub3RpY2UYByABKAki7gEKDk1pZ3JhdGVSZXF1ZXN0EhMKCWZpbGVfcGF0aBgBIAEoCUgAEhEKB2NvbnRlbnQYAiABKAxIABITCgtmcm9tX2Zvcm1hdBgDIAEoCRIRCgl0b19mb3JtYXQYBCABKAkSDgoGcmVwb3J0GAUgASgIEg8KB3Jlc3VsdHMYBiABKAgSFQoLbGF5b3V0X3BhdGgYByABKAlIARIYCg5sYXlvdXRfY29udGVudBgIIAEoCUgBEhYKDmltYWdlX2Jhc2VfdXJsGAkgASgJEg4KBnN0cmljdBgKIAEoCEIICgZzb3VyY2VCCAoGbGF5b3V0IuoBCg9NaWdyYXRlUmVzcG9uc2USDwoHY29udGVudBgBIAEoCRITCgtmcm9tX2Zvcm1hdBgCIAEoCRIRCgl0b19mb3JtYXQYAyABKAkSDQoFZXJyb3IYBCABKAkSFAoMZXhwZXJpbWVudGFsGAUgASgIEhsKE2V4cGVyaW1lbnRhbF9ub3RpY2UYBiABKAkSJgoGcmVwb3J0GAcgASgLMhYuc3lzbWwuTWlncmF0aW9uUmVwb3J0Eg8KB3Jlc3VsdHMYCCABKAkSIwoFZmlsZXMYCSADKAsyFC5zeXNtbC5NaWdyYXRpb25GaWxlIsMBCg9NaWdyYXRpb25SZXBvcnQSDgoGc291cmNlGAEgASgJEhAKCGV4cG9ydGVyGAIgASgJEg8KB3N1bW1hcnkYAyABKAkSDgoGbWFwcGVkGAQgASgFEhQKDGFwcHJveGltYXRlZBgFIAEoBRIQCgh1bm1hcHBlZBgGIAEoBRIPCgdza2lwcGVkGAcgASgFEiYKB2VudHJpZXMYCCADKAsyFS5zeXNtbC5NaWdyYXRpb25FbnRyeRIMCgR0ZXh0GAkgASgJImcKDk1pZ3JhdGlvbkVudHJ5EgoKAmlkGAEgASgJEgwKBGtpbmQYAiABKAkSDAoEbmFtZRgDIAEoCRIOCgZ0YXJnZXQYBCABKAkSDwoHdmVyZGljdBgFIAEoCRIMCgRub3RlGAYgASgJIi4KDU1pZ3JhdGlvbkZpbGUSDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgMIn0KEUFwcGx5RWRpdHNSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSKAoKb3BlcmF0aW9ucxgCIAMoCzIULnN5c21sLkVkaXRPcGVyYXRpb24SEAoIZG9jdW1lbnQYAyABKAkSGAoQYWNjZXB0X2RvY3VtZW50cxgEIAEoCCLDBgoNRWRpdE9wZXJhdGlvbhIoCglzZXRfdmFsdWUYASABKAsyEy5zeXNtbC5TZXRWYWx1ZUVkaXRIABIjCgZyZW5hbWUYAiABKAsyES5zeXNtbC5SZW5hbWVFZGl0SAASKgoKYWRkX21lbWJlchgDIAEoCzIULnN5c21sLkFkZE1lbWJlckVkaXRIABIjCgZkZWxldGUYBCABKAsyES5zeXNtbC5EZWxldGVFZGl0SAASHwoEbW92ZRgFIAEoCzIPLnN5c21sLk1vdmVFZGl0SAASMgoOYWRkX2Nvbm5lY3Rpb24YBiABKAsyGC5zeXNtbC5BZGRDb25uZWN0aW9uRWRpdEgAEiwKC2FkZF9zYXRpc2Z5GAcgASgLMhUuc3lzbWwuQWRkU2F0aXNmeUVkaXRIABJJChphZGRfcmVxdWlyZW1lbnRfY29uc3RyYWludBgIIAEoCzIjLnN5c21sLkFkZFJlcXVpcmVtZW50Q29uc3RyYWludEVkaXRIABIyCg5hZGRfdHJhbnNpdGlvbhgJIAEoCzIYLnN5c21sLkFkZFRyYW5zaXRpb25FZGl0SAASKgoKYWRkX2ltcG9ydBgKIAEoCzIULnN5c21sLkFkZEltcG9ydEVkaXRIABI4ChFhZGRfZG9jdW1lbnRhdGlvbhgLIAEoCzIbLnN5c21sLkFkZERvY3VtZW50YXRpb25FZGl0SAASKgoKYWRkX3ZlcmlmeRgMIAEoCzIULnN5c21sLkFkZFZlcmlmeUVkaXRIABIuCgxhZGRfbWV0YWRhdGEYDSABKAsyFi5zeXNtbC5BZGRNZXRhZGF0YUVkaXRIABIuCgxhZGRfc2VxdWVuY2UYDiABKAsyFi5zeXNtbC5BZGRTZXF1ZW5jZUVkaXRIABI7ChNhZGRfbWV0YWRhdGFfcHJlZml4GA8gASgLMhwuc3lzbWwuQWRkTWV0YWRhdGFQcmVmaXhFZGl0SAASLAoLYWRkX2NvbW1lbnQYECABKAsyFS5zeXNtbC5BZGRDb21tZW50RWRpdEgAEiYKCGFkZF9ub3RlGBIgASgLMhIuc3lzbWwuQWRkTm90ZUVkaXRIAEILCglvcGVyYXRpb24ikgIKDUFkZE1lbWJlckVkaXQSDQoFb3duZXIYASABKAkSDAoEa2luZBgCIAEoCRIMCgRuYW1lGAMgASgJEgwKBHR5cGUYBCABKAkSFAoMbXVsdGlwbGljaXR5GAUgASgJEg0KBXZhbHVlGAYgASgJEhMKC3NwZWNpYWxpemVzGAcgAygJEhMKC2lzX2Fic3RyYWN0GAggASgIEhEKCXJlZGVmaW5lcxgJIAMoCRISCgppc19kZWZhdWx0GAogASgIEhEKCWRpcmVjdGlvbhgLIAEoCRIZChFtZXRhZGF0YV9wcmVmaXhlcxgOIAMoCRILCgNkb2MYDCABKAkSFwoPYm9keV9leHByZXNzaW9uGA0gASgJInkKDkFkZFNhdGlzZnlFZGl0Eg0KBW93bmVyGAEgASgJEhMKC3JlcXVpcmVtZW50GAIgASgJEhoKEnNhdGlzZnlpbmdfZmVhdHVyZRgDIAEoCRITCgtpc19hc3NlcnRlZBgEIAEoCBISCgppc19uZWdhdGVkGAUgASgIIl0KHEFkZFJlcXVpcmVtZW50Q29uc3RyYWludEVkaXQSDQoFb3duZXIYASABKAkSDAoEa2luZBgCIAEoCRISCgpleHByZXNzaW9uGAMgASgJEgwKBG5hbWUYBCABKAkikQEKEUFkZFRyYW5zaXRpb25FZGl0Eg0KBW93bmVyGAEgASgJEgwKBG5hbWUYAiABKAkSDgoGc291cmNlGAMgASgJEg4KBnRhcmdldBgEIAEoCRIPCgd0cmlnZ2VyGAUgASgJEg0KBWd1YXJkGAYgASgJEg4KBmVmZmVjdBgHIAEoCRIPCgdpbml0aWFsGAggASgIIjMKDUFkZFZlcmlmeUVkaXQSDQoFb3duZXIYASABKAkSEwoLcmVxdWlyZW1lbnQYAiABKAkiNgoUTWV0YWRhdGFGZWF0dXJlVmFsdWUSDwoHZmVhdHVyZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSKUAQoPQWRkTWV0YWRhdGFFZGl0Eg0KBW93bmVyGAEgASgJEhUKDW1ldGFkYXRhX3R5cGUYAiABKAkSDAoEbmFtZRgDIAEoCRINCgVhYm91dBgEIAMoCRIrCgZ2YWx1ZXMYBSADKAsyGy5zeXNtbC5NZXRhZGF0YUZlYXR1cmVWYWx1ZRIRCglzaG9ydGhhbmQYBiABKAgiPgoVQWRkTWV0YWRhdGFQcmVmaXhFZGl0Eg4KBnRhcmdldBgBIAEoCRIVCg1tZXRhZGF0YV90eXBlGAIgASgJIoABCg1BZGRJbXBvcnRFZGl0Eg0KBW93bmVyGAEgASgJEhIKCnZpc2liaWxpdHkYAiABKAkSDgoGdGFyZ2V0GAMgASgJEhQKDGlzX3JlY3Vyc2l2ZRgEIAEoCBIVCg1pc19pbXBvcnRfYWxsGAUgASgIEg8KB2ZpbHRlcnMYBiADKAkiYwoUQWRkRG9jdW1lbnRhdGlvbkVkaXQSDgoGdGFyZ2V0GAEgASgJEgwKBGJvZHkYAiABKAkSDAoEbmFtZRgDIAEoCRIOCgZsb2NhbGUYBCABKAkSDwoHcmVwbGFjZRgFIAEoCCJaCg5BZGRDb21tZW50RWRpdBINCgVvd25lchgBIAEoCRIMCgRib2R5GAIgASgJEgwKBG5hbWUYAyABKAkSDQoFYWJvdXQYBCADKAkSDgoGbG9jYWxlGAUgASgJIisKC0FkZE5vdGVFZGl0Eg4KBnRhcmdldBgBIAEoCRIMCgR0ZXh0GAIgASgJIs0CCg9BZGRTZXF1ZW5jZUVkaXQSDQoFb3duZXIYASABKAkSDwoHa2V5d29yZBgCIAEoCRILCgNyZWYYAyABKAkSEwoLbWVtYmVyX2tpbmQYBCABKAkSEwoLbWVtYmVyX25hbWUYBSABKAkSDAoEdHlwZRgGIAEoCRINCgVhZnRlchgHIAEoCRIRCgljb25kaXRpb24YCCABKAkSDQoFdmFsdWUYCSABKAkSDgoGdGFyZ2V0GAogASgJEgsKA3ZpYRgLIAEoCRINCgV1bnRpbBgMIAEoCRIkCgRib2R5GA0gAygLMhYuc3lzbWwuQWRkU2VxdWVuY2VFZGl0EikKCWVsc2VfYm9keRgOIAMoCzIWLnN5c21sLkFkZFNlcXVlbmNlRWRpdBIUCgxtdWx0aXBsaWNpdHkYDyABKAkSEQoJcGFyYW1ldGVyGBAgASgJIm4KEUFkZENvbm5lY3Rpb25FZGl0Eg0KBW93bmVyGAEgASgJEgwKBGtpbmQYAiABKAkSEAoIZnJvbV9lbmQYAyABKAkSDgoGdG9fZW5kGAQgASgJEgwKBG5hbWUYBSABKAkSDAoEdHlwZRgGIAEoCSItCgpEZWxldGVFZGl0Eg4KBnRhcmdldBgBIAEoCRIPCgdjYXNjYWRlGAIgASgIIikKCE1vdmVFZGl0Eg4KBnRhcmdldBgBIAEoCRINCgVvd25lchgCIAEoCSItCgxTZXRWYWx1ZUVkaXQSDgoGdGFyZ2V0GAEgASgJEg0KBXZhbHVlGAIgASgJIi4KClJlbmFtZUVkaXQSDgoGdGFyZ2V0GAEgASgJEhAKCG5ld19uYW1lGAIgASgJIpACChJBcHBseUVkaXRzUmVzcG9uc2USDwoHY29udGVudBgBIAEoCRIjCgdhcHBsaWVkGAIgAygLMhIuc3lzbWwuQXBwbGllZEVkaXQSDQoFZXJyb3IYAyABKAkSIwoHZmFpbHVyZRgEIAEoDjISLnN5c21sLkVkaXRGYWlsdXJlEiYKC2RpYWdub3N0aWNzGAUgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxIaChJyZWZlcnJpbmdfZWxlbWVudHMYBiADKAkSKAoJZG9jdW1lbnRzGAcgAygLMhUuc3lzbWwuRWRpdGVkRG9jdW1lbnQSIgoJcmVmZXJyZXJzGAggAygLMg8uc3lzbWwuUmVmZXJyZXIiLwoORWRpdGVkRG9jdW1lbnQSDAoEbmFtZRgBIAEoCRIPCgdjb250ZW50GAIgASgJIioKCFJlZmVycmVyEgwKBG5hbWUYASABKAkSEAoIZG9jdW1lbnQYAiABKAkijAEKC0FwcGxpZWRFZGl0EhcKD29wZXJhdGlvbl9pbmRleBgBIAEoBRIOCgZ0YXJnZXQYAiABKAkSDgoGb2Zmc2V0GAMgASgFEg4KBmxlbmd0aBgEIAEoBRIQCghvbGRfdGV4dBgFIAEoCRIQCghuZXdfdGV4dBgGIAEoCRIQCghkb2N1bWVudBgHIAEoCSL9AgoKU3ltYm9sSW5mbxIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGtpbmQYAyABKAkSMQoIbWV0YWRhdGEYBCADKAsyHy5zeXNtbC5TeW1ib2xJbmZvLk1ldGFkYXRhRW50cnkSEQoJY2hpbGRfaWRzGAUgAygJEigKCmF0dHJpYnV0ZXMYBiADKAsyFC5zeXNtbC5BdHRyaWJ1dGVJbmZvEiIKCXR5cGVfaW5mbxgHIAEoCzIPLnN5c21sLlR5cGVJbmZvEi0KDG11bHRpcGxpY2l0eRgIIAEoCzIXLnN5c21sLk11bHRpcGxpY2l0eUluZm8SLgoPc3BlY2lhbGl6YXRpb25zGAkgAygLMhUuc3lzbWwuU3BlY2lhbGl6YXRpb24SIwobd2l0aGhlbGRfbGlicmFyeV9hdHRyaWJ1dGVzGAogASgFGi8KDU1ldGFkYXRhRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJYCg5TcGVjaWFsaXphdGlvbhIMCgRraW5kGAEgASgJEhAKCGRlY2xhcmVkGAIgASgJEhEKCXRhcmdldF9pZBgDIAEoCRITCgt0YXJnZXRfa2luZBgEIAEoCSKVAQoIVHlwZUluZm8SEAoIZGVjbGFyZWQYASABKAkSEwoLcmVzb2x2ZWRfaWQYAiABKAkSFQoNcmVzb2x2ZWRfa2luZBgDIAEoCRIRCglwcmltaXRpdmUYBCABKAkSGAoQcHJpbWl0aXZlX3NvdXJjZRgFIAEoCRIQCghxdWFudGl0eRgGIAEoCBIMCgR1bml0GAcgASgJIjAKEE11bHRpcGxpY2l0eUluZm8SDQoFbG93ZXIYASABKAkSDQoFdXBwZXIYAiABKAkiVgoNQXR0cmlidXRlSW5mbxIMCgRuYW1lGAEgASgJEgwKBHR5cGUYAiABKAkSGwoFdmFsdWUYAyABKAsyDC5zeXNtbC5WYWx1ZRIMCgR1bml0GAQgASgJItwFCgVWYWx1ZRITCglpbnRfdmFsdWUYASABKANIABIUCgpyZWFsX3ZhbHVlGAIgASgBSAASFAoKYm9vbF92YWx1ZRgDIAEoCEgAEhYKDHN0cmluZ192YWx1ZRgEIAEoCUgAEhUKC2luc3RhbmNlX2lkGAUgASgDSAASKAoIc2VxdWVuY2UYBiABKAsyFC5zeXNtbC5WYWx1ZVNlcXVlbmNlSAASDgoEbnVsbBgHIAEoCUgAEiMKCHF1YW50aXR5GAggASgLMg8uc3lzbWwuUXVhbnRpdHlIABIqCgxlbnVtX2xpdGVyYWwYCSABKAsyEi5zeXNtbC5FbnVtTGl0ZXJhbEgAEg8KBXVuc2V0GAogASgISAASIQoHY29tcGxleBgLIAEoCzIOLnN5c21sLkNvbXBsZXhIABIdCgVhcnJheRgMIAEoCzIMLnN5c21sLkFycmF5SAASHwoGdmVjdG9yGA0gASgLMg0uc3lzbWwuVmVjdG9ySAASMAoPdmVjdG9yX3F1YW50aXR5GA4gASgLMhUuc3lzbWwuVmVjdG9yUXVhbnRpdHlIABIwCg9tZWFzdXJlbWVudF9yZWYYDyABKAsyFS5zeXNtbC5NZWFzdXJlbWVudFJlZkgAEhIKCGluZmluaXR5GBAgASgISAASIwoIZnVuY3Rpb24YESABKAsyDy5zeXNtbC5GdW5jdGlvbkgAEh4KA3NldBgSIAEoCzIPLnN5c21sLlZhbHVlU2V0SAASMAoPdGVuc29yX3F1YW50aXR5GBMgASgLMhUuc3lzbWwuVGVuc29yUXVhbnRpdHlIABInCgptZXRhb2JqZWN0GBQgASgLMhEuc3lzbWwuTWV0YW9iamVjdEgAEisKDHVuZGV0ZXJtaW5lZBgVIAEoCzITLnN5c21sLlVuZGV0ZXJtaW5lZEgAEhcKDWJpZ19pbnRfdmFsdWUYFiABKAlIAEIGCgRraW5kIjYKCk1ldGFvYmplY3QSEgoKZWxlbWVudF9pZBgBIAEoCRIUCgxtZXRhY2xhc3NfaWQYAiABKAkiRgoMVW5kZXRlcm1pbmVkEg4KBnJlYXNvbhgBIAEoCRImCgVjb3VudBgCIAEoCzIXLnN5c21sLk11bHRpcGxpY2l0eUluZm8iLAoIRnVuY3Rpb24SDwoHY2FsY19pZBgBIAEoCRIPCgdzZWxmX2lkGAIgASgDIioKCFZhbHVlU2V0Eh4KCGVsZW1lbnRzGAEgAygLMgwuc3lzbWwuVmFsdWUiSQoOVGVuc29yUXVhbnRpdHkSEgoKZGltZW5zaW9ucxgBIAMoAxIjCgpjb21wb25lbnRzGAIgAygLMg8uc3lzbWwuUXVhbnRpdHkiOwoFQXJyYXkSEgoKZGltZW5zaW9ucxgBIAMoAxIeCghlbGVtZW50cxgCIAMoCzIMLnN5c21sLlZhbHVlIioKBlZlY3RvchIgCgpjb21wb25lbnRzGAEgAygLMgwuc3lzbWwuVmFsdWUiNQoOVmVjdG9yUXVhbnRpdHkSIwoKY29tcG9uZW50cxgBIAMoCzIPLnN5c21sLlF1YW50aXR5IioKB0NvbXBsZXgSDAoEcmVhbBgBIAEoARIRCglpbWFnaW5hcnkYAiABKAEiZAoLRW51bUxpdGVyYWwSEgoKbGl0ZXJhbF9pZBgBIAEoCRIWCg5lbnVtZXJhdGlvbl9pZBgCIAEoCRIMCgRuYW1lGAMgASgJEhsKBXZhbHVlGAQgASgLMgwuc3lzbWwuVmFsdWUiLwoNVmFsdWVTZXF1ZW5jZRIeCghlbGVtZW50cxgBIAMoCzIMLnN5c21sLlZhbHVlIpkBCghRdWFudGl0eRIXCg1pbnRfbWFnbml0dWRlGAEgASgDSAASGAoOcmVhbF9tYWduaXR1ZGUYAiABKAFIABIbChFiaWdfaW50X21hZ25pdHVkZRgFIAEoCUgAEgwKBHVuaXQYAyABKAkSIgoJdW5pdF90ZXJtGAQgASgLMg8uc3lzbWwuVW5pdFRlcm1CCwoJbWFnbml0dWRlIlMKDk1lYXN1cmVtZW50UmVmEgwKBHVuaXQYASABKAkSIgoJdW5pdF90ZXJtGAIgASgLMg8uc3lzbWwuVW5pdFRlcm0SDwoHdW5pdF9pZBgDIAEoCSJUCghVbml0VGVybRIRCglzY2FsZV9udW0YASABKAESEQoJc2NhbGVfZGVuGAIgASgBEiIKB2ZhY3RvcnMYAyADKAsyES5zeXNtbC5Vbml0RmFjdG9yIi8KClVuaXRGYWN0b3ISDwoHdW5pdF9pZBgBIAEoCRIQCghleHBvbmVudBgCIAEoASJYCgpEaWFnbm9zdGljEhAKCHNldmVyaXR5GAEgASgJEg8KB21lc3NhZ2UYAiABKAkSGQoEc3BhbhgDIAEoCzILLnN5c21sLlNwYW4SDAoEY29kZRgEIAEoCSJeCgRTcGFuEgwKBGZpbGUYASABKAkSEgoKc3RhcnRfbGluZRgCIAEoBRIRCglzdGFydF9jb2wYAyABKAUSEAoIZW5kX2xpbmUYBCABKAUSDwoHZW5kX2NvbBgFIAEoBSITChFTZXJ2ZXJJbmZvUmVxdWVzdCI7ChJTZXJ2ZXJJbmZvUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCRIUCgxjYXBhYmlsaXRpZXMYAiADKAkiUwoMUXVlcnlSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSGwoFcXVlcnkYAiABKAsyDC5zeXNtbC5RdWVyeRISCgpvc2xjX3F1ZXJ5GAMgASgJIjwKDVF1ZXJ5UmVzcG9uc2USKwoIZWxlbWVudHMYASADKAsyGS5zeXNtbC5RdWVyeVJlc3VsdEVsZW1lbnQiSAoFUXVlcnkSDQoFc2NvcGUYASADKAkSDgoGc2VsZWN0GAIgAygJEiAKBXdoZXJlGAMgASgLMhEuc3lzbWwuQ29uc3RyYWludCJ8CgpDb25zdHJhaW50Ei8KCXByaW1pdGl2ZRgBIAEoCzIaLnN5c21sLlByaW1pdGl2ZUNvbnN0cmFpbnRIABIvCgljb21wb3NpdGUYAiABKAsyGi5zeXNtbC5Db21wb3NpdGVDb25zdHJhaW50SABCDAoKY29uc3RyYWludCJzChNQcmltaXRpdmVDb25zdHJhaW50Eg8KB2ludmVyc2UYASABKAgSEAoIcHJvcGVydHkYAiABKAkSKgoIb3BlcmF0b3IYAyABKA4yGC5zeXNtbC5QcmltaXRpdmVPcGVyYXRvchINCgV2YWx1ZRgEIAMoCSJoChNDb21wb3NpdGVDb25zdHJhaW50EioKCG9wZXJhdG9yGAEgASgOMhguc3lzbWwuQ29tcG9zaXRlT3BlcmF0b3ISJQoKY29uc3RyYWludBgCIAMoCzIRLnN5c21sLkNvbnN0cmFpbnQioAEKElF1ZXJ5UmVzdWx0RWxlbWVudBIKCgJpZBgBIAEoCRIMCgR0eXBlGAIgASgJEj0KCnByb3BlcnRpZXMYAyADKAsyKS5zeXNtbC5RdWVyeVJlc3VsdEVsZW1lbnQuUHJvcGVydGllc0VudHJ5GjEKD1Byb3BlcnRpZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBInMKClN3ZWVwUmFuZ2USEQoJcGFyYW1ldGVyGAEgASgJEhsKBXN0YXJ0GAIgASgLMgwuc3lzbWwuVmFsdWUSGQoDZW5kGAMgASgLMgwuc3lzbWwuVmFsdWUSGgoEc3RlcBgEIAEoCzIMLnN5c21sLlZhbHVlItACCg9SdW5Td2VlcFJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkSGQoRc3ViamVjdF9zeW1ib2xfaWQYAyABKAkSHwoJYXJndW1lbnRzGAQgAygLMgwuc3lzbWwuVmFsdWUSQwoPbmFtZWRfYXJndW1lbnRzGAUgAygLMiouc3lzbWwuUnVuU3dlZXBSZXF1ZXN0Lk5hbWVkQXJndW1lbnRzRW50cnkSIQoGcmFuZ2VzGAYgAygLMhEuc3lzbWwuU3dlZXBSYW5nZRIPCgdzYW1wbGVzGAcgASgDEgwKBHNlZWQYCCABKAQSDgoGZW5naW5lGAkgASgJGkMKE05hbWVkQXJndW1lbnRzRW50cnkSCwoDa2V5GAEgASgJEhsKBXZhbHVlGAIgASgLMgwuc3lzbWwuVmFsdWU6AjgBIvQBCghTd2VlcFJvdxIhCgZpbnB1dHMYASADKAsyES5zeXNtbC5DYWxjT3V0cHV0EiIKB291dHB1dHMYAiADKAsyES5zeXNtbC5DYWxjT3V0cHV0EiAKCHZlcmRpY3RzGAMgAygLMg4uc3lzbWwuVmVyZGljdBIWCg5lbGFwc2VkX21pY3JvcxgEIAEoAxINCgVlcnJvchgFIAEoCRIsCg5mYWlsdXJlX3JlYXNvbhgGIAEoDjIULnN5c21sLkZhaWx1cmVSZWFzb24SKgoLZXZhbHVhdGlvbnMYByADKAsyFS5zeXNtbC5DYXNlRXZhbHVhdGlvbiKtAgoQUnVuU3dlZXBSZXNwb25zZRIdCgRyb3dzGAEgAygLMg8uc3lzbWwuU3dlZXBSb3cSEgoKcGFyYW1ldGVycxgCIAMoCRIPCgdzYW1wbGVkGAMgASgIEgwKBHNlZWQYBCABKAQSDQoFZXJyb3IYBSABKAkSJgoLZGlhZ25vc3RpY3MYBiADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAcgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhIiCglpbnN0YW5jZXMYCCADKAsyDy5zeXNtbC5JbnN0YW5jZRIOCgZlbmdpbmUYCSABKAkSEAoIc3RyZW5ndGgYCiABKAkSHAoGYm91bmRzGAsgAygLMgwuc3lzbWwuQm91bmQibgoXUnVuRG9jdW1lbnRRdWVyeVJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIQCghxdWVyeV9pZBgCIAEoCRItCghiaW5kaW5ncxgDIAMoCzIbLnN5c21sLkRvY3VtZW50UXVlcnlCaW5kaW5nIk8KFERvY3VtZW50UXVlcnlCaW5kaW5nEhEKCXBhcmFtZXRlchgBIAEoCRIkCgZ2YWx1ZXMYAiADKAsyFC5zeXNtbC5Eb2N1bWVudFZhbHVlIpADCg1Eb2N1bWVudFZhbHVlEhQKCmVsZW1lbnRfaWQYASABKAlIABIWCgxzdHJpbmdfdmFsdWUYAiABKAlIABITCglpbnRfdmFsdWUYAyABKANIABIUCgpyZWFsX3ZhbHVlGAQgASgBSAASFAoKYm9vbF92YWx1ZRgFIAEoCEgAEhIKCGluZmluaXR5GAYgASgISAASIwoIcXVhbnRpdHkYCCABKAsyDy5zeXNtbC5RdWFudGl0eUgAEikKB3ZlcmRpY3QYCSABKAsyFi5zeXNtbC5Eb2N1bWVudFZlcmRpY3RIABInCgZvYmplY3QYCiABKAsyFS5zeXNtbC5Eb2N1bWVudE9iamVjdEgAEiUKBXN0YXRlGAsgASgLMhQuc3lzbWwuRG9jdW1lbnRTdGF0ZUgAEiUKBWV2ZW50GAwgASgLMhQuc3lzbWwuRG9jdW1lbnRFdmVudEgAEhcKDWJpZ19pbnRfdmFsdWUYDSABKAlIABIUCgxlbGVtZW50X3R5cGUYByABKAlCBgoEa2luZCJaCg5Eb2N1bWVudE9iamVjdBITCgtpbnN0YW5jZV9pZBgBIAEoAxIMCgRwYXRoGAIgASgJEiUKB2VsZW1lbnQYAyABKAsyFC5zeXNtbC5Eb2N1bWVudFZhbHVlIq4BCg9Eb2N1bWVudFZlcmRpY3QSJwoJYXNzZXJ0aW9uGAEgASgLMhQuc3lzbWwuRG9jdW1lbnRWYWx1ZRIMCgRraW5kGAIgASgJEgwKBHRleHQYAyABKAkSDAoEcGF0aBgEIAEoCRIPCgd2ZXJkaWN0GAUgASgJEhEKCWNvbmRpdGlvbhgGIAEoCRIOCgZyZWFzb24YByABKAkSFAoMdmVyaWZpY2F0aW9uGAggAygJIrEBCg1Eb2N1bWVudFN0YXRlEiUKBm9iamVjdBgBIAEoCzIVLnN5c21sLkRvY3VtZW50T2JqZWN0Eg8KB21hY2hpbmUYAiABKAkSDAoEbmFtZRgDIAEoCRISCgpzdGF0ZV9wYXRoGAQgASgJEiMKBXN0YXRlGAUgASgLMhQuc3lzbWwuRG9jdW1lbnRWYWx1ZRIOCgZyZWdpb24YBiABKAkSEQoJZW5jbG9zaW5nGAcgAygJIpwCCg1Eb2N1bWVudEV2ZW50EgwKBGtpbmQYASABKAkSIgoEdGltZRgCIAEoCzIULnN5c21sLkRvY3VtZW50VmFsdWUSJQoGb2JqZWN0GAMgASgLMhUuc3lzbWwuRG9jdW1lbnRPYmplY3QSDwoHbWFjaGluZRgEIAEoCRINCgVzdGF0ZRgFIAEoCRIMCgRmcm9tGAYgASgJEgoKAnRvGAcgASgJEiUKBnRhcmdldBgIIAEoCzIVLnN5c21sLkRvY3VtZW50T2JqZWN0Eg0KBWV2ZW50GAkgASgJEg8KB3BheWxvYWQYCiADKAkSFAoMYWx0ZXJuYXRpdmVzGAsgAygJEg0KBXRha2VuGAwgASgJEgwKBHRleHQYDSABKAkiIwoTRG9jdW1lbnRRdWVyeUNvbHVtbhIMCgRuYW1lGAEgASgJIjkKEURvY3VtZW50UXVlcnlDZWxsEiQKBnZhbHVlcxgBIAMoCzIULnN5c21sLkRvY3VtZW50VmFsdWUiYgoQRG9jdW1lbnRRdWVyeVJvdxIlCgdlbGVtZW50GAEgASgLMhQuc3lzbWwuRG9jdW1lbnRWYWx1ZRInCgVjZWxscxgCIAMoCzIYLnN5c21sLkRvY3VtZW50UXVlcnlDZWxsIm4KGFJ1bkRvY3VtZW50UXVlcnlSZXNwb25zZRIrCgdjb2x1bW5zGAEgAygLMhouc3lzbWwuRG9jdW1lbnRRdWVyeUNvbHVtbhIlCgRyb3dzGAIgAygLMhcuc3lzbWwuRG9jdW1lbnRRdWVyeVJvdyJOChVSZW5kZXJEb2N1bWVudFJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRITCgtkb2N1bWVudF9pZBgCIAEoCRIMCgRmb3JtGAMgASgJIjgKFlJlbmRlckRvY3VtZW50UmVzcG9uc2USEAoIbWFya2Rvd24YASABKAkSDAoEaHRtbBgCIAEoCSqxAQoNRmFpbHVyZVJlYXNvbhIeChpGQUlMVVJFX1JFQVNPTl9VTlNQRUNJRklFRBAAEh0KGUZBSUxVUkVfUkVBU09OX0VWQUxVQVRJT04QARIdChlGQUlMVVJFX1JFQVNPTl9XUk9OR19LSU5EEAISJAogRkFJTFVSRV9SRUFTT05fQU1CSUdVT1VTX1NVQkpFQ1QQAxIcChhGQUlMVVJFX1JFQVNPTl9VTkRFQ0lERUQQBCqMBQoLRWRpdEZhaWx1cmUSHAoYRURJVF9GQUlMVVJFX1VOU1BFQ0lGSUVEEAASHgoaRURJVF9GQUlMVVJFX05PX09QRVJBVElPTlMQARIfChtFRElUX0ZBSUxVUkVfVU5LTk9XTl9UQVJHRVQQAhIhCh1FRElUX0ZBSUxVUkVfQU1CSUdVT1VTX1RBUkdFVBADEhsKF0VESVRfRkFJTFVSRV9OT1RfVkFMVUVEEAQSHgoaRURJVF9GQUlMVVJFX0lOVkFMSURfVkFMVUUQBRIdChlFRElUX0ZBSUxVUkVfSU5WQUxJRF9OQU1FEAYSGgoWRURJVF9GQUlMVVJFX05PVF9OQU1FRBAHEiIKHkVESVRfRkFJTFVSRV9SRU5BTUVfUkVGRVJFTkNFRBAIEiIKHkVESVRfRkFJTFVSRV9PVkVSTEFQUElOR19FRElUUxAJEh8KG0VESVRfRkFJTFVSRV9SRVNVTFRfSU5WQUxJRBAKEh4KGkVESVRfRkFJTFVSRV9PV05FUl9VTktOT1dOEAsSJAogRURJVF9GQUlMVVJFX09XTkVSX05PVF9OQU1FU1BBQ0UQDBIdChlFRElUX0ZBSUxVUkVfSUxMRUdBTF9LSU5EEA0SIgoeRURJVF9GQUlMVVJFX01FTUJFUl9OQU1FX1RBS0VOEA4SIgoeRURJVF9GQUlMVVJFX0RFTEVURV9SRUZFUkVOQ0VEEA8SJAogRURJVF9GQUlMVVJFX09XTkVSX0lOU0lERV9UQVJHRVQQEBIgChxFRElUX0ZBSUxVUkVfTU9WRV9SRUZFUkVOQ0VEEBESJQohRURJVF9GQUlMVVJFX1JFRkVSRU5DRURfRUxTRVdIRVJFEBIqkgEKEVByaW1pdGl2ZU9wZXJhdG9yEiIKHlBSSU1JVElWRV9PUEVSQVRPUl9VTlNQRUNJRklFRBAAEhwKGFBSSU1JVElWRV9PUEVSQVRPUl9FUVVBTBABEh4KGlBSSU1JVElWRV9PUEVSQVRPUl9HUkVBVEVSEAISGwoXUFJJTUlUSVZFX09QRVJBVE9SX0xFU1MQAypuChFDb21wb3NpdGVPcGVyYXRvchIiCh5DT01QT1NJVEVfT1BFUkFUT1JfVU5TUEVDSUZJRUQQABIaChZDT01QT1NJVEVfT1BFUkFUT1JfQU5EEAESGQoVQ09NUE9TSVRFX09QRVJBVE9SX09SEAIy+QwKDFN5c01MU2VydmljZRJECg1HZXRTZXJ2ZXJJbmZvEhguc3lzbWwuU2VydmVySW5mb1JlcXVlc3QaGS5zeXNtbC5TZXJ2ZXJJbmZvUmVzcG9uc2USPgoJUGFyc2VGaWxlEhcuc3lzbWwuUGFyc2VGaWxlUmVxdWVzdBoYLnN5c21sLlBhcnNlRmlsZVJlc3BvbnNlEkcKDFBhcnNlU291cmNlcxIaLnN5c21sLlBhcnNlU291cmNlc1JlcXVlc3QaGy5zeXNtbC5QYXJzZVNvdXJjZXNSZXNwb25zZRI7CglHZXRTeW1ib2wSFy5zeXNtbC5HZXRTeW1ib2xSZXF1ZXN0GhUuc3lzbWwuU3ltYm9sUmVzcG9uc2USRwoOR2V0RGlhZ25vc3RpY3MSGS5zeXNtbC5EaWFnbm9zdGljc1JlcXVlc3QaGi5zeXNtbC5EaWFnbm9zdGljc1Jlc3BvbnNlEjsKCEV2YWx1YXRlEhYuc3lzbWwuRXZhbHVhdGVSZXF1ZXN0Ghcuc3lzbWwuRXZhbHVhdGVSZXNwb25zZRJECgtJbnN0YW50aWF0ZRIZLnN5c21sLkluc3RhbnRpYXRlUmVxdWVzdBoaLnN5c21sLkluc3RhbnRpYXRlUmVzcG9uc2USSgoNRXhlY3V0ZUFjdGlvbhIbLnN5c21sLkV4ZWN1dGVBY3Rpb25SZXF1ZXN0Ghwuc3lzbWwuRXhlY3V0ZUFjdGlvblJlc3BvbnNlEkcKDEV4ZWN1dGVTdGF0ZRIaLnN5c21sLkV4ZWN1dGVTdGF0ZVJlcXVlc3QaGy5zeXNtbC5FeGVjdXRlU3RhdGVSZXNwb25zZRI4CgdDb252ZXJ0EhUuc3lzbWwuQ29udmVydFJlcXVlc3QaFi5zeXNtbC5Db252ZXJ0UmVzcG9uc2USOAoHTWlncmF0ZRIVLnN5c21sLk1pZ3JhdGVSZXF1ZXN0GhYuc3lzbWwuTWlncmF0ZVJlc3BvbnNlEkEKCkFwcGx5RWRpdHMSGC5zeXNtbC5BcHBseUVkaXRzUmVxdWVzdBoZLnN5c21sLkFwcGx5RWRpdHNSZXNwb25zZRJTChBWZXJpZnlDb25zdHJhaW50Eh4uc3lzbWwuVmVyaWZ5Q29uc3RyYWludFJlcXVlc3QaHy5zeXNtbC5WZXJpZnlDb25zdHJhaW50UmVzcG9uc2USVgoRVmVyaWZ5UmVxdWlyZW1lbnQSHy5zeXNtbC5WZXJpZnlSZXF1aXJlbWVudFJlcXVlc3QaIC5zeXNtbC5WZXJpZnlSZXF1aXJlbWVudFJlc3BvbnNlElkKElZlcmlmeVNhdGlzZmFjdGlvbhIgLnN5c21sLlZlcmlmeVNhdGlzZmFjdGlvblJlcXVlc3QaIS5zeXNtbC5WZXJpZnlTYXRpc2ZhY3Rpb25SZXNwb25zZRJTChBWYWxpZGF0ZUluc3RhbmNlEh4uc3lzbWwuVmFsaWRhdGVJbnN0YW5jZVJlcXVlc3QaHy5zeXNtbC5WYWxpZGF0ZUluc3RhbmNlUmVzcG9uc2USRwoMRXZhbHVhdGVDYWxjEhouc3lzbWwuRXZhbHVhdGVDYWxjUmVxdWVzdBobLnN5c21sLkV2YWx1YXRlQ2FsY1Jlc3BvbnNlEkQKC1J1bkFuYWx5c2lzEhkuc3lzbWwuUnVuQW5hbHlzaXNSZXF1ZXN0Ghouc3lzbWwuUnVuQW5hbHlzaXNSZXNwb25zZRI7CghSdW5Td2VlcBIWLnN5c21sLlJ1blN3ZWVwUmVxdWVzdBoXLnN5c21sLlJ1blN3ZWVwUmVzcG9uc2USRAoLTGlzdEVuZ2luZXMSGS5zeXNtbC5MaXN0RW5naW5lc1JlcXVlc3QaGi5zeXNtbC5MaXN0RW5naW5lc1Jlc3BvbnNlEjIKBVF1ZXJ5EhMuc3lzbWwuUXVlcnlSZXF1ZXN0GhQuc3lzbWwuUXVlcnlSZXNwb25zZRJTChBSdW5Eb2N1bWVudFF1ZXJ5Eh4uc3lzbWwuUnVuRG9jdW1lbnRRdWVyeVJlcXVlc3QaHy5zeXNtbC5SdW5Eb2N1bWVudFF1ZXJ5UmVzcG9uc2USTQoOUmVuZGVyRG9jdW1lbnQSHC5zeXNtbC5SZW5kZXJEb2N1bWVudFJlcXVlc3QaHS5zeXNtbC5SZW5kZXJEb2N1bWVudFJlc3BvbnNlQipaKGdpdGh1Yi5jb20vT3Blbi1NQkVFL09wZW5TeXNNTC9hcGkvcHJvdG9iBnByb3RvMw"); + fileDesc("CgtzeXNtbC5wcm90bxIFc3lzbWwihgMKB1ZlcmRpY3QSDAoEa2luZBgBIAEoCRISCgplbGVtZW50X2lkGAIgASgJEg8KB2VsZW1lbnQYAyABKAkSDQoFaG9sZHMYBCABKAgSEQoJY29uZGl0aW9uGAUgASgJEhMKC2luc3RhbmNlX2lkGAYgASgDEhgKEGluc3RhbmNlX3R5cGVfaWQYByABKAkSDQoFZXJyb3IYCCABKAkSLAoOZmFpbHVyZV9yZWFzb24YCSABKA4yFC5zeXNtbC5GYWlsdXJlUmVhc29uEhYKDnJlcXVpcmVtZW50X2lkGAogASgJEg4KBmVuZ2luZRgLIAEoCRIQCghzdHJlbmd0aBgMIAEoCRIcCgZib3VuZHMYDSADKAsyDC5zeXNtbC5Cb3VuZBIVCg1pbnN0YW5jZV9wYXRoGA4gASgJEhAKCHF1ZXN0aW9uGA8gASgJEg4KBnN0YXR1cxgQIAEoCRIpCgd3aXRuZXNzGBEgAygLMhguc3lzbWwuV2l0bmVzc0Fzc2lnbm1lbnQiXgoRV2l0bmVzc0Fzc2lnbm1lbnQSDwoHZmVhdHVyZRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlEgwKBHVuaXQYAyABKAkSDQoFZXhhY3QYBCABKAkiNQoFQm91bmQSDAoEbmFtZRgBIAEoCRINCgVsaW1pdBgCIAEoAxIPCgdyZWFjaGVkGAMgASgIIn0KF1ZlcmlmeUNvbnN0cmFpbnRSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJEhkKEXN1YmplY3Rfc3ltYm9sX2lkGAMgASgJEg4KBmVuZ2luZRgEIAEoCRIQCghxdWVzdGlvbhgFIAEoCSKWAQoYVmVyaWZ5Q29uc3RyYWludFJlc3BvbnNlEh8KB3ZlcmRpY3QYASABKAsyDi5zeXNtbC5WZXJkaWN0EiIKCWluc3RhbmNlcxgCIAMoCzIPLnN5c21sLkluc3RhbmNlEg0KBWVycm9yGAMgASgJEiYKC2RpYWdub3N0aWNzGAQgAygLMhEuc3lzbWwuRGlhZ25vc3RpYyJ+ChhWZXJpZnlSZXF1aXJlbWVudFJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkSGQoRc3ViamVjdF9zeW1ib2xfaWQYAyABKAkSDgoGZW5naW5lGAQgASgJEhAKCHF1ZXN0aW9uGAUgASgJIm0KE1ZlcmlmaWNhdGlvblZlcmRpY3QSDwoHY2FzZV9pZBgBIAEoCRIMCgRraW5kGAIgASgJEg4KBmRldGFpbBgDIAEoCRIPCgdzdWJjYXNlGAQgASgIEhYKDnJlcXVpcmVtZW50X2lkGAUgASgJItIBChlWZXJpZnlSZXF1aXJlbWVudFJlc3BvbnNlEh8KB3ZlcmRpY3QYASABKAsyDi5zeXNtbC5WZXJkaWN0EiIKCWluc3RhbmNlcxgCIAMoCzIPLnN5c21sLkluc3RhbmNlEg0KBWVycm9yGAMgASgJEiYKC2RpYWdub3N0aWNzGAQgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxI5ChV2ZXJpZmljYXRpb25fdmVyZGljdHMYBSADKAsyGi5zeXNtbC5WZXJpZmljYXRpb25WZXJkaWN0ImQKGVZlcmlmeVNhdGlzZmFjdGlvblJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkSDgoGZW5naW5lGAMgASgJEhAKCHF1ZXN0aW9uGAQgASgJIoICChpWZXJpZnlTYXRpc2ZhY3Rpb25SZXNwb25zZRIgCgh2ZXJkaWN0cxgBIAMoCzIOLnN5c21sLlZlcmRpY3QSIgoJaW5zdGFuY2VzGAIgAygLMg8uc3lzbWwuSW5zdGFuY2USDQoFZXJyb3IYAyABKAkSJgoLZGlhZ25vc3RpY3MYBCADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAUgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhI5ChV2ZXJpZmljYXRpb25fdmVyZGljdHMYBiADKAsyGi5zeXNtbC5WZXJpZmljYXRpb25WZXJkaWN0IlAKF1ZhbGlkYXRlSW5zdGFuY2VSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJEg4KBmVuZ2luZRgDIAEoCSKyAgoYVmFsaWRhdGVJbnN0YW5jZVJlc3BvbnNlEiAKCHZlcmRpY3RzGAEgAygLMg4uc3lzbWwuVmVyZGljdBIfCgdzdW1tYXJ5GAIgASgLMg4uc3lzbWwuVmVyZGljdBIiCglpbnN0YW5jZXMYAyADKAsyDy5zeXNtbC5JbnN0YW5jZRINCgVlcnJvchgEIAEoCRImCgtkaWFnbm9zdGljcxgFIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSLAoOZmFpbHVyZV9yZWFzb24YBiABKA4yFC5zeXNtbC5GYWlsdXJlUmVhc29uEjkKFXZlcmlmaWNhdGlvbl92ZXJkaWN0cxgHIAMoCzIaLnN5c21sLlZlcmlmaWNhdGlvblZlcmRpY3QSDwoHYm91bmRlZBgIIAEoCCJtChNFdmFsdWF0ZUNhbGNSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJEh8KCWFyZ3VtZW50cxgDIAMoCzIMLnN5c21sLlZhbHVlEg4KBmVuZ2luZRgEIAEoCSL9AQoURXZhbHVhdGVDYWxjUmVzcG9uc2USHAoGcmVzdWx0GAEgASgLMgwuc3lzbWwuVmFsdWUSIgoHb3V0cHV0cxgCIAMoCzIRLnN5c21sLkNhbGNPdXRwdXQSDQoFZXJyb3IYAyABKAkSJgoLZGlhZ25vc3RpY3MYBCADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAUgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhIOCgZlbmdpbmUYBiABKAkSEAoIc3RyZW5ndGgYByABKAkSHAoGYm91bmRzGAggAygLMgwuc3lzbWwuQm91bmQiNwoKQ2FsY091dHB1dBIMCgRuYW1lGAEgASgJEhsKBXZhbHVlGAIgASgLMgwuc3lzbWwuVmFsdWUikwEKDkNhc2VFdmFsdWF0aW9uEhMKC2Z1bmN0aW9uX2lkGAEgASgJEh8KCWFyZ3VtZW50cxgCIAMoCzIMLnN5c21sLlZhbHVlEhwKBnJlc3VsdBgDIAEoCzIMLnN5c21sLlZhbHVlEg0KBWVycm9yGAQgASgJEhAKCHNlbGVjdGVkGAUgASgIEgwKBHRpZWQYBiABKAgipgIKElJ1bkFuYWx5c2lzUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhEKCXN5bWJvbF9pZBgCIAEoCRIZChFzdWJqZWN0X3N5bWJvbF9pZBgDIAEoCRIfCglhcmd1bWVudHMYBCADKAsyDC5zeXNtbC5WYWx1ZRJGCg9uYW1lZF9hcmd1bWVudHMYBSADKAsyLS5zeXNtbC5SdW5BbmFseXNpc1JlcXVlc3QuTmFtZWRBcmd1bWVudHNFbnRyeRIQCghzY2hlZHVsZRgGIAEoCRIOCgZlbmdpbmUYByABKAkaQwoTTmFtZWRBcmd1bWVudHNFbnRyeRILCgNrZXkYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZToCOAEi3AMKE1J1bkFuYWx5c2lzUmVzcG9uc2USIgoHb3V0cHV0cxgBIAMoCzIRLnN5c21sLkNhbGNPdXRwdXQSIAoIdmVyZGljdHMYAiADKAsyDi5zeXNtbC5WZXJkaWN0EiIKCWluc3RhbmNlcxgDIAMoCzIPLnN5c21sLkluc3RhbmNlEg0KBWVycm9yGAQgASgJEiYKC2RpYWdub3N0aWNzGAUgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxIsCg5mYWlsdXJlX3JlYXNvbhgGIAEoDjIULnN5c21sLkZhaWx1cmVSZWFzb24SOQoVdmVyaWZpY2F0aW9uX3ZlcmRpY3RzGAcgAygLMhouc3lzbWwuVmVyaWZpY2F0aW9uVmVyZGljdBIgCghvdXRjb21lcxgIIAMoCzIOLnN5c21sLk91dGNvbWUSLQoLZXhwbG9yYXRpb24YCSABKAsyGC5zeXNtbC5FeHBsb3JhdGlvblN0YXR1cxIqCgtldmFsdWF0aW9ucxgKIAMoCzIVLnN5c21sLkNhc2VFdmFsdWF0aW9uEg4KBmVuZ2luZRgLIAEoCRIQCghzdHJlbmd0aBgMIAEoCRIcCgZib3VuZHMYDSADKAsyDC5zeXNtbC5Cb3VuZCIsChBQcm9iYWJpbGl0eVJhbmdlEgsKA21pbhgBIAEoARILCgNtYXgYAiABKAEiywIKB091dGNvbWUSLAoHb3V0cHV0cxgBIAMoCzIbLnN5c21sLk91dGNvbWUuT3V0cHV0c0VudHJ5EhMKC2ZpbmFsX3N0YXRlGAIgASgJEhYKDnN0YXRlc192aXNpdGVkGAMgAygJEg0KBWVycm9yGAQgASgJEhYKDmxpbmVhcml6YXRpb25zGAUgASgFEg8KB3dpdG5lc3MYBiADKAkSJgoLZGlhZ25vc3RpY3MYByADKAsyES5zeXNtbC5EaWFnbm9zdGljEhMKC3Byb2JhYmlsaXR5GAggASgBEjIKEXByb2JhYmlsaXR5X3JhbmdlGAkgASgLMhcuc3lzbWwuUHJvYmFiaWxpdHlSYW5nZRo8CgxPdXRwdXRzRW50cnkSCwoDa2V5GAEgASgJEhsKBXZhbHVlGAIgASgLMgwuc3lzbWwuVmFsdWU6AjgBIrUBChFFeHBsb3JhdGlvblN0YXR1cxIQCghjb21wbGV0ZRgBIAEoCBIMCgRydW5zGAIgASgFEhMKC2J1ZGdldHNfaGl0GAMgAygJEhMKC3J1bnNfYnVkZ2V0GAQgASgFEhQKDGRlcHRoX2J1ZGdldBgFIAEoBRIhChlwcm9iYWJpbGl0aWVzX2xvd2VyX2JvdW5kGAYgASgIEh0KFWZhaWxlZF9saW5lYXJpemF0aW9ucxgHIAEoBSIUChJMaXN0RW5naW5lc1JlcXVlc3Qi/AEKCkVuZ2luZUluZm8SDAoEbmFtZRgBIAEoCRIRCglhdXRob3JpdHkYAiABKAkSDwoHYW5zd2VycxgDIAMoCRIOCgZib3VuZHMYBCADKAkSDwoHcHJvY2VzcxgFIAEoCRIVCg1wcm9jZXNzX2ZvdW5kGAYgASgJEg0KBXJlYWR5GAcgASgIEhMKC3VuYXZhaWxhYmxlGAggASgJEgwKBGtpbmQYCSABKAkSEAoIcHJvdG9jb2wYCiABKAkSDgoGc291cmNlGAsgASgJEg8KB2NvbW1hbmQYDCABKAkSDwoHdmVyc2lvbhgNIAEoCRIOCgZzZXJ2ZWQYDiABKAgiOQoTTGlzdEVuZ2luZXNSZXNwb25zZRIiCgdlbmdpbmVzGAEgAygLMhEuc3lzbWwuRW5naW5lSW5mbyKMAQoQUGFyc2VGaWxlUmVxdWVzdBITCglmaWxlX3BhdGgYASABKAlIABIRCgdjb250ZW50GAIgASgJSAASGAoMY29udGVudF9oYXNoGAMgASgJQgIYARIQCghsYW5ndWFnZRgEIAEoCRIaChJzdHJpY3RfY29uZm9ybWFuY2UYBSABKAhCCAoGc291cmNlImIKDlNvdXJjZURvY3VtZW50EhMKCWZpbGVfcGF0aBgBIAEoCUgAEhEKB2NvbnRlbnQYAiABKAlIABIQCghsYW5ndWFnZRgDIAEoCRIMCgRuYW1lGAQgASgJQggKBnNvdXJjZSJbChNQYXJzZVNvdXJjZXNSZXF1ZXN0EigKCWRvY3VtZW50cxgBIAMoCzIVLnN5c21sLlNvdXJjZURvY3VtZW50EhoKEnN0cmljdF9jb25mb3JtYW5jZRgCIAEoCCKDAQoUUGFyc2VTb3VyY2VzUmVzcG9uc2USEgoKbW9kZWxfaGFzaBgBIAEoCRIgCgVyb290cxgCIAMoCzIRLnN5c21sLlN5bWJvbEluZm8SJgoLZGlhZ25vc3RpY3MYAyADKAsyES5zeXNtbC5EaWFnbm9zdGljEg0KBWVycm9yGAQgASgJIn8KEVBhcnNlRmlsZVJlc3BvbnNlEhIKCm1vZGVsX2hhc2gYASABKAkSHwoEcm9vdBgCIAEoCzIRLnN5c21sLlN5bWJvbEluZm8SJgoLZGlhZ25vc3RpY3MYAyADKAsyES5zeXNtbC5EaWFnbm9zdGljEg0KBWVycm9yGAQgASgJIjkKEEdldFN5bWJvbFJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkiQgoOU3ltYm9sUmVzcG9uc2USIQoGc3ltYm9sGAEgASgLMhEuc3lzbWwuU3ltYm9sSW5mbxINCgVlcnJvchgCIAEoCSIoChJEaWFnbm9zdGljc1JlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCSJMChNEaWFnbm9zdGljc1Jlc3BvbnNlEiYKC2RpYWdub3N0aWNzGAEgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxINCgVlcnJvchgCIAEoCSJvCg9FdmFsdWF0ZVJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRISCgpleHByZXNzaW9uGAIgASgJEhkKEWNvbnRleHRfc3ltYm9sX2lkGAMgASgJEhkKEXN1YmplY3Rfc3ltYm9sX2lkGAQgASgJImcKEEV2YWx1YXRlUmVzcG9uc2USHAoGcmVzdWx0GAEgASgLMgwuc3lzbWwuVmFsdWUSDQoFZXJyb3IYAiABKAkSJgoLZGlhZ25vc3RpY3MYAyADKAsyES5zeXNtbC5EaWFnbm9zdGljIsIBCghJbnN0YW5jZRIKCgJpZBgBIAEoAxIWCg50eXBlX3N5bWJvbF9pZBgCIAEoCRI6Cg5mZWF0dXJlX3ZhbHVlcxgEIAMoCzIiLnN5c21sLkluc3RhbmNlLkZlYXR1cmVWYWx1ZXNFbnRyeRpJChJGZWF0dXJlVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEiIKBXZhbHVlGAIgASgLMhMuc3lzbWwuRmVhdHVyZVZhbHVlOgI4AUoECAMQBFIFc2xvdHMihAEKDEZlYXR1cmVWYWx1ZRIUCgxmZWF0dXJlX25hbWUYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZRIcCgZ2YWx1ZXMYAyADKAsyDC5zeXNtbC5WYWx1ZRIUCgxtYXRlcmlhbGl6ZWQYBCABKAgSDQoFZXJyb3IYBSABKAkiOwoSSW5zdGFudGlhdGVSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJIpMBChNJbnN0YW50aWF0ZVJlc3BvbnNlEiEKCGluc3RhbmNlGAEgASgLMg8uc3lzbWwuSW5zdGFuY2USDQoFZXJyb3IYAiABKAkSJgoLZGlhZ25vc3RpY3MYAyADKAsyES5zeXNtbC5EaWFnbm9zdGljEiIKCWluc3RhbmNlcxgEIAMoCzIPLnN5c21sLkluc3RhbmNlIukBChRFeGVjdXRlQWN0aW9uUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhgKEGFjdGlvbl9zeW1ib2xfaWQYAiABKAkSNwoGaW5wdXRzGAMgAygLMicuc3lzbWwuRXhlY3V0ZUFjdGlvblJlcXVlc3QuSW5wdXRzRW50cnkSEAoIc2NoZWR1bGUYBCABKAkSGwoTcGVyZm9ybWVyX3N5bWJvbF9pZBgFIAEoCRo7CgtJbnB1dHNFbnRyeRILCgNrZXkYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZToCOAEizAMKFUV4ZWN1dGVBY3Rpb25SZXNwb25zZRI6CgdvdXRwdXRzGAEgAygLMikuc3lzbWwuRXhlY3V0ZUFjdGlvblJlc3BvbnNlLk91dHB1dHNFbnRyeRINCgVlcnJvchgCIAEoCRImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSIAoIb3V0Y29tZXMYBCADKAsyDi5zeXNtbC5PdXRjb21lEi0KC2V4cGxvcmF0aW9uGAUgASgLMhguc3lzbWwuRXhwbG9yYXRpb25TdGF0dXMSEgoKZmluYWxfdGltZRgGIAEoARJTChRwZXJmb3JtZXJfYXR0cmlidXRlcxgHIAMoCzI1LnN5c21sLkV4ZWN1dGVBY3Rpb25SZXNwb25zZS5QZXJmb3JtZXJBdHRyaWJ1dGVzRW50cnkaPAoMT3V0cHV0c0VudHJ5EgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlOgI4ARpIChhQZXJmb3JtZXJBdHRyaWJ1dGVzRW50cnkSCwoDa2V5GAEgASgJEhsKBXZhbHVlGAIgASgLMgwuc3lzbWwuVmFsdWU6AjgBIokBChNFeGVjdXRlU3RhdGVSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSHwoXc3RhdGVfbWFjaGluZV9zeW1ib2xfaWQYAiABKAkSDgoGZXZlbnRzGAMgAygJEhAKCHNjaGVkdWxlGAQgASgJEhsKE3BlcmZvcm1lcl9zeW1ib2xfaWQYBSABKAki0wIKFEV4ZWN1dGVTdGF0ZVJlc3BvbnNlEhYKDnN0YXRlc192aXNpdGVkGAEgAygJEkQKDWZpbmFsX2NvbnRleHQYAiADKAsyLS5zeXNtbC5FeGVjdXRlU3RhdGVSZXNwb25zZS5GaW5hbENvbnRleHRFbnRyeRINCgVlcnJvchgDIAEoCRImCgtkaWFnbm9zdGljcxgEIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSIAoIb3V0Y29tZXMYBSADKAsyDi5zeXNtbC5PdXRjb21lEi0KC2V4cGxvcmF0aW9uGAYgASgLMhguc3lzbWwuRXhwbG9yYXRpb25TdGF0dXMSEgoKZmluYWxfdGltZRgHIAEoARpBChFGaW5hbENvbnRleHRFbnRyeRILCgNrZXkYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZToCOAEisQEKDkNvbnZlcnRSZXF1ZXN0EhMKCWZpbGVfcGF0aBgBIAEoCUgAEhEKB2NvbnRlbnQYAiABKAlIABIUCgptb2RlbF9oYXNoGAYgASgJSAASEwoLZnJvbV9mb3JtYXQYAyABKAkSEQoJdG9fZm9ybWF0GAQgASgJEh4KFnRvbGVyYXRlX3N5bnRheF9lcnJvcnMYBSABKAgSDwoHaWRfZm9ybRgHIAEoCUIICgZzb3VyY2UitAEKD0NvbnZlcnRSZXNwb25zZRIPCgdjb250ZW50GAEgASgJEhMKC2Zyb21fZm9ybWF0GAIgASgJEhEKCXRvX2Zvcm1hdBgDIAEoCRINCgVlcnJvchgEIAEoCRImCgtkaWFnbm9zdGljcxgFIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSFAoMZXhwZXJpbWVudGFsGAYgASgIEhsKE2V4cGVyaW1lbnRhbF9ub3RpY2UYByABKAki7gEKDk1pZ3JhdGVSZXF1ZXN0EhMKCWZpbGVfcGF0aBgBIAEoCUgAEhEKB2NvbnRlbnQYAiABKAxIABITCgtmcm9tX2Zvcm1hdBgDIAEoCRIRCgl0b19mb3JtYXQYBCABKAkSDgoGcmVwb3J0GAUgASgIEg8KB3Jlc3VsdHMYBiABKAgSFQoLbGF5b3V0X3BhdGgYByABKAlIARIYCg5sYXlvdXRfY29udGVudBgIIAEoCUgBEhYKDmltYWdlX2Jhc2VfdXJsGAkgASgJEg4KBnN0cmljdBgKIAEoCEIICgZzb3VyY2VCCAoGbGF5b3V0IuoBCg9NaWdyYXRlUmVzcG9uc2USDwoHY29udGVudBgBIAEoCRITCgtmcm9tX2Zvcm1hdBgCIAEoCRIRCgl0b19mb3JtYXQYAyABKAkSDQoFZXJyb3IYBCABKAkSFAoMZXhwZXJpbWVudGFsGAUgASgIEhsKE2V4cGVyaW1lbnRhbF9ub3RpY2UYBiABKAkSJgoGcmVwb3J0GAcgASgLMhYuc3lzbWwuTWlncmF0aW9uUmVwb3J0Eg8KB3Jlc3VsdHMYCCABKAkSIwoFZmlsZXMYCSADKAsyFC5zeXNtbC5NaWdyYXRpb25GaWxlIsMBCg9NaWdyYXRpb25SZXBvcnQSDgoGc291cmNlGAEgASgJEhAKCGV4cG9ydGVyGAIgASgJEg8KB3N1bW1hcnkYAyABKAkSDgoGbWFwcGVkGAQgASgFEhQKDGFwcHJveGltYXRlZBgFIAEoBRIQCgh1bm1hcHBlZBgGIAEoBRIPCgdza2lwcGVkGAcgASgFEiYKB2VudHJpZXMYCCADKAsyFS5zeXNtbC5NaWdyYXRpb25FbnRyeRIMCgR0ZXh0GAkgASgJImcKDk1pZ3JhdGlvbkVudHJ5EgoKAmlkGAEgASgJEgwKBGtpbmQYAiABKAkSDAoEbmFtZRgDIAEoCRIOCgZ0YXJnZXQYBCABKAkSDwoHdmVyZGljdBgFIAEoCRIMCgRub3RlGAYgASgJIi4KDU1pZ3JhdGlvbkZpbGUSDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgMIn0KEUFwcGx5RWRpdHNSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSKAoKb3BlcmF0aW9ucxgCIAMoCzIULnN5c21sLkVkaXRPcGVyYXRpb24SEAoIZG9jdW1lbnQYAyABKAkSGAoQYWNjZXB0X2RvY3VtZW50cxgEIAEoCCLDBgoNRWRpdE9wZXJhdGlvbhIoCglzZXRfdmFsdWUYASABKAsyEy5zeXNtbC5TZXRWYWx1ZUVkaXRIABIjCgZyZW5hbWUYAiABKAsyES5zeXNtbC5SZW5hbWVFZGl0SAASKgoKYWRkX21lbWJlchgDIAEoCzIULnN5c21sLkFkZE1lbWJlckVkaXRIABIjCgZkZWxldGUYBCABKAsyES5zeXNtbC5EZWxldGVFZGl0SAASHwoEbW92ZRgFIAEoCzIPLnN5c21sLk1vdmVFZGl0SAASMgoOYWRkX2Nvbm5lY3Rpb24YBiABKAsyGC5zeXNtbC5BZGRDb25uZWN0aW9uRWRpdEgAEiwKC2FkZF9zYXRpc2Z5GAcgASgLMhUuc3lzbWwuQWRkU2F0aXNmeUVkaXRIABJJChphZGRfcmVxdWlyZW1lbnRfY29uc3RyYWludBgIIAEoCzIjLnN5c21sLkFkZFJlcXVpcmVtZW50Q29uc3RyYWludEVkaXRIABIyCg5hZGRfdHJhbnNpdGlvbhgJIAEoCzIYLnN5c21sLkFkZFRyYW5zaXRpb25FZGl0SAASKgoKYWRkX2ltcG9ydBgKIAEoCzIULnN5c21sLkFkZEltcG9ydEVkaXRIABI4ChFhZGRfZG9jdW1lbnRhdGlvbhgLIAEoCzIbLnN5c21sLkFkZERvY3VtZW50YXRpb25FZGl0SAASKgoKYWRkX3ZlcmlmeRgMIAEoCzIULnN5c21sLkFkZFZlcmlmeUVkaXRIABIuCgxhZGRfbWV0YWRhdGEYDSABKAsyFi5zeXNtbC5BZGRNZXRhZGF0YUVkaXRIABIuCgxhZGRfc2VxdWVuY2UYDiABKAsyFi5zeXNtbC5BZGRTZXF1ZW5jZUVkaXRIABI7ChNhZGRfbWV0YWRhdGFfcHJlZml4GA8gASgLMhwuc3lzbWwuQWRkTWV0YWRhdGFQcmVmaXhFZGl0SAASLAoLYWRkX2NvbW1lbnQYECABKAsyFS5zeXNtbC5BZGRDb21tZW50RWRpdEgAEiYKCGFkZF9ub3RlGBIgASgLMhIuc3lzbWwuQWRkTm90ZUVkaXRIAEILCglvcGVyYXRpb24ikgIKDUFkZE1lbWJlckVkaXQSDQoFb3duZXIYASABKAkSDAoEa2luZBgCIAEoCRIMCgRuYW1lGAMgASgJEgwKBHR5cGUYBCABKAkSFAoMbXVsdGlwbGljaXR5GAUgASgJEg0KBXZhbHVlGAYgASgJEhMKC3NwZWNpYWxpemVzGAcgAygJEhMKC2lzX2Fic3RyYWN0GAggASgIEhEKCXJlZGVmaW5lcxgJIAMoCRISCgppc19kZWZhdWx0GAogASgIEhEKCWRpcmVjdGlvbhgLIAEoCRIZChFtZXRhZGF0YV9wcmVmaXhlcxgOIAMoCRILCgNkb2MYDCABKAkSFwoPYm9keV9leHByZXNzaW9uGA0gASgJInkKDkFkZFNhdGlzZnlFZGl0Eg0KBW93bmVyGAEgASgJEhMKC3JlcXVpcmVtZW50GAIgASgJEhoKEnNhdGlzZnlpbmdfZmVhdHVyZRgDIAEoCRITCgtpc19hc3NlcnRlZBgEIAEoCBISCgppc19uZWdhdGVkGAUgASgIIl0KHEFkZFJlcXVpcmVtZW50Q29uc3RyYWludEVkaXQSDQoFb3duZXIYASABKAkSDAoEa2luZBgCIAEoCRISCgpleHByZXNzaW9uGAMgASgJEgwKBG5hbWUYBCABKAkikQEKEUFkZFRyYW5zaXRpb25FZGl0Eg0KBW93bmVyGAEgASgJEgwKBG5hbWUYAiABKAkSDgoGc291cmNlGAMgASgJEg4KBnRhcmdldBgEIAEoCRIPCgd0cmlnZ2VyGAUgASgJEg0KBWd1YXJkGAYgASgJEg4KBmVmZmVjdBgHIAEoCRIPCgdpbml0aWFsGAggASgIIjMKDUFkZFZlcmlmeUVkaXQSDQoFb3duZXIYASABKAkSEwoLcmVxdWlyZW1lbnQYAiABKAkiNgoUTWV0YWRhdGFGZWF0dXJlVmFsdWUSDwoHZmVhdHVyZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSKUAQoPQWRkTWV0YWRhdGFFZGl0Eg0KBW93bmVyGAEgASgJEhUKDW1ldGFkYXRhX3R5cGUYAiABKAkSDAoEbmFtZRgDIAEoCRINCgVhYm91dBgEIAMoCRIrCgZ2YWx1ZXMYBSADKAsyGy5zeXNtbC5NZXRhZGF0YUZlYXR1cmVWYWx1ZRIRCglzaG9ydGhhbmQYBiABKAgiPgoVQWRkTWV0YWRhdGFQcmVmaXhFZGl0Eg4KBnRhcmdldBgBIAEoCRIVCg1tZXRhZGF0YV90eXBlGAIgASgJIoABCg1BZGRJbXBvcnRFZGl0Eg0KBW93bmVyGAEgASgJEhIKCnZpc2liaWxpdHkYAiABKAkSDgoGdGFyZ2V0GAMgASgJEhQKDGlzX3JlY3Vyc2l2ZRgEIAEoCBIVCg1pc19pbXBvcnRfYWxsGAUgASgIEg8KB2ZpbHRlcnMYBiADKAkiYwoUQWRkRG9jdW1lbnRhdGlvbkVkaXQSDgoGdGFyZ2V0GAEgASgJEgwKBGJvZHkYAiABKAkSDAoEbmFtZRgDIAEoCRIOCgZsb2NhbGUYBCABKAkSDwoHcmVwbGFjZRgFIAEoCCJaCg5BZGRDb21tZW50RWRpdBINCgVvd25lchgBIAEoCRIMCgRib2R5GAIgASgJEgwKBG5hbWUYAyABKAkSDQoFYWJvdXQYBCADKAkSDgoGbG9jYWxlGAUgASgJIisKC0FkZE5vdGVFZGl0Eg4KBnRhcmdldBgBIAEoCRIMCgR0ZXh0GAIgASgJIs0CCg9BZGRTZXF1ZW5jZUVkaXQSDQoFb3duZXIYASABKAkSDwoHa2V5d29yZBgCIAEoCRILCgNyZWYYAyABKAkSEwoLbWVtYmVyX2tpbmQYBCABKAkSEwoLbWVtYmVyX25hbWUYBSABKAkSDAoEdHlwZRgGIAEoCRINCgVhZnRlchgHIAEoCRIRCgljb25kaXRpb24YCCABKAkSDQoFdmFsdWUYCSABKAkSDgoGdGFyZ2V0GAogASgJEgsKA3ZpYRgLIAEoCRINCgV1bnRpbBgMIAEoCRIkCgRib2R5GA0gAygLMhYuc3lzbWwuQWRkU2VxdWVuY2VFZGl0EikKCWVsc2VfYm9keRgOIAMoCzIWLnN5c21sLkFkZFNlcXVlbmNlRWRpdBIUCgxtdWx0aXBsaWNpdHkYDyABKAkSEQoJcGFyYW1ldGVyGBAgASgJIm4KEUFkZENvbm5lY3Rpb25FZGl0Eg0KBW93bmVyGAEgASgJEgwKBGtpbmQYAiABKAkSEAoIZnJvbV9lbmQYAyABKAkSDgoGdG9fZW5kGAQgASgJEgwKBG5hbWUYBSABKAkSDAoEdHlwZRgGIAEoCSItCgpEZWxldGVFZGl0Eg4KBnRhcmdldBgBIAEoCRIPCgdjYXNjYWRlGAIgASgIIikKCE1vdmVFZGl0Eg4KBnRhcmdldBgBIAEoCRINCgVvd25lchgCIAEoCSItCgxTZXRWYWx1ZUVkaXQSDgoGdGFyZ2V0GAEgASgJEg0KBXZhbHVlGAIgASgJIi4KClJlbmFtZUVkaXQSDgoGdGFyZ2V0GAEgASgJEhAKCG5ld19uYW1lGAIgASgJIpACChJBcHBseUVkaXRzUmVzcG9uc2USDwoHY29udGVudBgBIAEoCRIjCgdhcHBsaWVkGAIgAygLMhIuc3lzbWwuQXBwbGllZEVkaXQSDQoFZXJyb3IYAyABKAkSIwoHZmFpbHVyZRgEIAEoDjISLnN5c21sLkVkaXRGYWlsdXJlEiYKC2RpYWdub3N0aWNzGAUgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxIaChJyZWZlcnJpbmdfZWxlbWVudHMYBiADKAkSKAoJZG9jdW1lbnRzGAcgAygLMhUuc3lzbWwuRWRpdGVkRG9jdW1lbnQSIgoJcmVmZXJyZXJzGAggAygLMg8uc3lzbWwuUmVmZXJyZXIiLwoORWRpdGVkRG9jdW1lbnQSDAoEbmFtZRgBIAEoCRIPCgdjb250ZW50GAIgASgJIioKCFJlZmVycmVyEgwKBG5hbWUYASABKAkSEAoIZG9jdW1lbnQYAiABKAkijAEKC0FwcGxpZWRFZGl0EhcKD29wZXJhdGlvbl9pbmRleBgBIAEoBRIOCgZ0YXJnZXQYAiABKAkSDgoGb2Zmc2V0GAMgASgFEg4KBmxlbmd0aBgEIAEoBRIQCghvbGRfdGV4dBgFIAEoCRIQCghuZXdfdGV4dBgGIAEoCRIQCghkb2N1bWVudBgHIAEoCSL9AgoKU3ltYm9sSW5mbxIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGtpbmQYAyABKAkSMQoIbWV0YWRhdGEYBCADKAsyHy5zeXNtbC5TeW1ib2xJbmZvLk1ldGFkYXRhRW50cnkSEQoJY2hpbGRfaWRzGAUgAygJEigKCmF0dHJpYnV0ZXMYBiADKAsyFC5zeXNtbC5BdHRyaWJ1dGVJbmZvEiIKCXR5cGVfaW5mbxgHIAEoCzIPLnN5c21sLlR5cGVJbmZvEi0KDG11bHRpcGxpY2l0eRgIIAEoCzIXLnN5c21sLk11bHRpcGxpY2l0eUluZm8SLgoPc3BlY2lhbGl6YXRpb25zGAkgAygLMhUuc3lzbWwuU3BlY2lhbGl6YXRpb24SIwobd2l0aGhlbGRfbGlicmFyeV9hdHRyaWJ1dGVzGAogASgFGi8KDU1ldGFkYXRhRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJYCg5TcGVjaWFsaXphdGlvbhIMCgRraW5kGAEgASgJEhAKCGRlY2xhcmVkGAIgASgJEhEKCXRhcmdldF9pZBgDIAEoCRITCgt0YXJnZXRfa2luZBgEIAEoCSKVAQoIVHlwZUluZm8SEAoIZGVjbGFyZWQYASABKAkSEwoLcmVzb2x2ZWRfaWQYAiABKAkSFQoNcmVzb2x2ZWRfa2luZBgDIAEoCRIRCglwcmltaXRpdmUYBCABKAkSGAoQcHJpbWl0aXZlX3NvdXJjZRgFIAEoCRIQCghxdWFudGl0eRgGIAEoCBIMCgR1bml0GAcgASgJIjAKEE11bHRpcGxpY2l0eUluZm8SDQoFbG93ZXIYASABKAkSDQoFdXBwZXIYAiABKAkiVgoNQXR0cmlidXRlSW5mbxIMCgRuYW1lGAEgASgJEgwKBHR5cGUYAiABKAkSGwoFdmFsdWUYAyABKAsyDC5zeXNtbC5WYWx1ZRIMCgR1bml0GAQgASgJIocGCgVWYWx1ZRITCglpbnRfdmFsdWUYASABKANIABIUCgpyZWFsX3ZhbHVlGAIgASgBSAASFAoKYm9vbF92YWx1ZRgDIAEoCEgAEhYKDHN0cmluZ192YWx1ZRgEIAEoCUgAEhUKC2luc3RhbmNlX2lkGAUgASgDSAASKAoIc2VxdWVuY2UYBiABKAsyFC5zeXNtbC5WYWx1ZVNlcXVlbmNlSAASDgoEbnVsbBgHIAEoCUgAEiMKCHF1YW50aXR5GAggASgLMg8uc3lzbWwuUXVhbnRpdHlIABIqCgxlbnVtX2xpdGVyYWwYCSABKAsyEi5zeXNtbC5FbnVtTGl0ZXJhbEgAEg8KBXVuc2V0GAogASgISAASIQoHY29tcGxleBgLIAEoCzIOLnN5c21sLkNvbXBsZXhIABIdCgVhcnJheRgMIAEoCzIMLnN5c21sLkFycmF5SAASHwoGdmVjdG9yGA0gASgLMg0uc3lzbWwuVmVjdG9ySAASMAoPdmVjdG9yX3F1YW50aXR5GA4gASgLMhUuc3lzbWwuVmVjdG9yUXVhbnRpdHlIABIwCg9tZWFzdXJlbWVudF9yZWYYDyABKAsyFS5zeXNtbC5NZWFzdXJlbWVudFJlZkgAEhIKCGluZmluaXR5GBAgASgISAASIwoIZnVuY3Rpb24YESABKAsyDy5zeXNtbC5GdW5jdGlvbkgAEh4KA3NldBgSIAEoCzIPLnN5c21sLlZhbHVlU2V0SAASMAoPdGVuc29yX3F1YW50aXR5GBMgASgLMhUuc3lzbWwuVGVuc29yUXVhbnRpdHlIABInCgptZXRhb2JqZWN0GBQgASgLMhEuc3lzbWwuTWV0YW9iamVjdEgAEisKDHVuZGV0ZXJtaW5lZBgVIAEoCzITLnN5c21sLlVuZGV0ZXJtaW5lZEgAEhcKDWJpZ19pbnRfdmFsdWUYFiABKAlIABIpCg5yYXRpb25hbF92YWx1ZRgXIAEoCzIPLnN5c21sLlJhdGlvbmFsSABCBgoEa2luZCIyCghSYXRpb25hbBIRCgludW1lcmF0b3IYASABKAkSEwoLZGVub21pbmF0b3IYAiABKAkiNgoKTWV0YW9iamVjdBISCgplbGVtZW50X2lkGAEgASgJEhQKDG1ldGFjbGFzc19pZBgCIAEoCSJGCgxVbmRldGVybWluZWQSDgoGcmVhc29uGAEgASgJEiYKBWNvdW50GAIgASgLMhcuc3lzbWwuTXVsdGlwbGljaXR5SW5mbyIsCghGdW5jdGlvbhIPCgdjYWxjX2lkGAEgASgJEg8KB3NlbGZfaWQYAiABKAMiKgoIVmFsdWVTZXQSHgoIZWxlbWVudHMYASADKAsyDC5zeXNtbC5WYWx1ZSJJCg5UZW5zb3JRdWFudGl0eRISCgpkaW1lbnNpb25zGAEgAygDEiMKCmNvbXBvbmVudHMYAiADKAsyDy5zeXNtbC5RdWFudGl0eSI7CgVBcnJheRISCgpkaW1lbnNpb25zGAEgAygDEh4KCGVsZW1lbnRzGAIgAygLMgwuc3lzbWwuVmFsdWUiKgoGVmVjdG9yEiAKCmNvbXBvbmVudHMYASADKAsyDC5zeXNtbC5WYWx1ZSI1Cg5WZWN0b3JRdWFudGl0eRIjCgpjb21wb25lbnRzGAEgAygLMg8uc3lzbWwuUXVhbnRpdHkiKgoHQ29tcGxleBIMCgRyZWFsGAEgASgBEhEKCWltYWdpbmFyeRgCIAEoASJkCgtFbnVtTGl0ZXJhbBISCgpsaXRlcmFsX2lkGAEgASgJEhYKDmVudW1lcmF0aW9uX2lkGAIgASgJEgwKBG5hbWUYAyABKAkSGwoFdmFsdWUYBCABKAsyDC5zeXNtbC5WYWx1ZSIvCg1WYWx1ZVNlcXVlbmNlEh4KCGVsZW1lbnRzGAEgAygLMgwuc3lzbWwuVmFsdWUiyAEKCFF1YW50aXR5EhcKDWludF9tYWduaXR1ZGUYASABKANIABIYCg5yZWFsX21hZ25pdHVkZRgCIAEoAUgAEhsKEWJpZ19pbnRfbWFnbml0dWRlGAUgASgJSAASLQoScmF0aW9uYWxfbWFnbml0dWRlGAYgASgLMg8uc3lzbWwuUmF0aW9uYWxIABIMCgR1bml0GAMgASgJEiIKCXVuaXRfdGVybRgEIAEoCzIPLnN5c21sLlVuaXRUZXJtQgsKCW1hZ25pdHVkZSJTCg5NZWFzdXJlbWVudFJlZhIMCgR1bml0GAEgASgJEiIKCXVuaXRfdGVybRgCIAEoCzIPLnN5c21sLlVuaXRUZXJtEg8KB3VuaXRfaWQYAyABKAkiVAoIVW5pdFRlcm0SEQoJc2NhbGVfbnVtGAEgASgBEhEKCXNjYWxlX2RlbhgCIAEoARIiCgdmYWN0b3JzGAMgAygLMhEuc3lzbWwuVW5pdEZhY3RvciIvCgpVbml0RmFjdG9yEg8KB3VuaXRfaWQYASABKAkSEAoIZXhwb25lbnQYAiABKAEiWAoKRGlhZ25vc3RpYxIQCghzZXZlcml0eRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJEhkKBHNwYW4YAyABKAsyCy5zeXNtbC5TcGFuEgwKBGNvZGUYBCABKAkiXgoEU3BhbhIMCgRmaWxlGAEgASgJEhIKCnN0YXJ0X2xpbmUYAiABKAUSEQoJc3RhcnRfY29sGAMgASgFEhAKCGVuZF9saW5lGAQgASgFEg8KB2VuZF9jb2wYBSABKAUiEwoRU2VydmVySW5mb1JlcXVlc3QiOwoSU2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSFAoMY2FwYWJpbGl0aWVzGAIgAygJIlMKDFF1ZXJ5UmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhsKBXF1ZXJ5GAIgASgLMgwuc3lzbWwuUXVlcnkSEgoKb3NsY19xdWVyeRgDIAEoCSI8Cg1RdWVyeVJlc3BvbnNlEisKCGVsZW1lbnRzGAEgAygLMhkuc3lzbWwuUXVlcnlSZXN1bHRFbGVtZW50IkgKBVF1ZXJ5Eg0KBXNjb3BlGAEgAygJEg4KBnNlbGVjdBgCIAMoCRIgCgV3aGVyZRgDIAEoCzIRLnN5c21sLkNvbnN0cmFpbnQifAoKQ29uc3RyYWludBIvCglwcmltaXRpdmUYASABKAsyGi5zeXNtbC5QcmltaXRpdmVDb25zdHJhaW50SAASLwoJY29tcG9zaXRlGAIgASgLMhouc3lzbWwuQ29tcG9zaXRlQ29uc3RyYWludEgAQgwKCmNvbnN0cmFpbnQicwoTUHJpbWl0aXZlQ29uc3RyYWludBIPCgdpbnZlcnNlGAEgASgIEhAKCHByb3BlcnR5GAIgASgJEioKCG9wZXJhdG9yGAMgASgOMhguc3lzbWwuUHJpbWl0aXZlT3BlcmF0b3ISDQoFdmFsdWUYBCADKAkiaAoTQ29tcG9zaXRlQ29uc3RyYWludBIqCghvcGVyYXRvchgBIAEoDjIYLnN5c21sLkNvbXBvc2l0ZU9wZXJhdG9yEiUKCmNvbnN0cmFpbnQYAiADKAsyES5zeXNtbC5Db25zdHJhaW50IqABChJRdWVyeVJlc3VsdEVsZW1lbnQSCgoCaWQYASABKAkSDAoEdHlwZRgCIAEoCRI9Cgpwcm9wZXJ0aWVzGAMgAygLMikuc3lzbWwuUXVlcnlSZXN1bHRFbGVtZW50LlByb3BlcnRpZXNFbnRyeRoxCg9Qcm9wZXJ0aWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJzCgpTd2VlcFJhbmdlEhEKCXBhcmFtZXRlchgBIAEoCRIbCgVzdGFydBgCIAEoCzIMLnN5c21sLlZhbHVlEhkKA2VuZBgDIAEoCzIMLnN5c21sLlZhbHVlEhoKBHN0ZXAYBCABKAsyDC5zeXNtbC5WYWx1ZSLQAgoPUnVuU3dlZXBSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJEhkKEXN1YmplY3Rfc3ltYm9sX2lkGAMgASgJEh8KCWFyZ3VtZW50cxgEIAMoCzIMLnN5c21sLlZhbHVlEkMKD25hbWVkX2FyZ3VtZW50cxgFIAMoCzIqLnN5c21sLlJ1blN3ZWVwUmVxdWVzdC5OYW1lZEFyZ3VtZW50c0VudHJ5EiEKBnJhbmdlcxgGIAMoCzIRLnN5c21sLlN3ZWVwUmFuZ2USDwoHc2FtcGxlcxgHIAEoAxIMCgRzZWVkGAggASgEEg4KBmVuZ2luZRgJIAEoCRpDChNOYW1lZEFyZ3VtZW50c0VudHJ5EgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlOgI4ASL0AQoIU3dlZXBSb3cSIQoGaW5wdXRzGAEgAygLMhEuc3lzbWwuQ2FsY091dHB1dBIiCgdvdXRwdXRzGAIgAygLMhEuc3lzbWwuQ2FsY091dHB1dBIgCgh2ZXJkaWN0cxgDIAMoCzIOLnN5c21sLlZlcmRpY3QSFgoOZWxhcHNlZF9taWNyb3MYBCABKAMSDQoFZXJyb3IYBSABKAkSLAoOZmFpbHVyZV9yZWFzb24YBiABKA4yFC5zeXNtbC5GYWlsdXJlUmVhc29uEioKC2V2YWx1YXRpb25zGAcgAygLMhUuc3lzbWwuQ2FzZUV2YWx1YXRpb24irQIKEFJ1blN3ZWVwUmVzcG9uc2USHQoEcm93cxgBIAMoCzIPLnN5c21sLlN3ZWVwUm93EhIKCnBhcmFtZXRlcnMYAiADKAkSDwoHc2FtcGxlZBgDIAEoCBIMCgRzZWVkGAQgASgEEg0KBWVycm9yGAUgASgJEiYKC2RpYWdub3N0aWNzGAYgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxIsCg5mYWlsdXJlX3JlYXNvbhgHIAEoDjIULnN5c21sLkZhaWx1cmVSZWFzb24SIgoJaW5zdGFuY2VzGAggAygLMg8uc3lzbWwuSW5zdGFuY2USDgoGZW5naW5lGAkgASgJEhAKCHN0cmVuZ3RoGAogASgJEhwKBmJvdW5kcxgLIAMoCzIMLnN5c21sLkJvdW5kIm4KF1J1bkRvY3VtZW50UXVlcnlSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEAoIcXVlcnlfaWQYAiABKAkSLQoIYmluZGluZ3MYAyADKAsyGy5zeXNtbC5Eb2N1bWVudFF1ZXJ5QmluZGluZyJPChREb2N1bWVudFF1ZXJ5QmluZGluZxIRCglwYXJhbWV0ZXIYASABKAkSJAoGdmFsdWVzGAIgAygLMhQuc3lzbWwuRG9jdW1lbnRWYWx1ZSK7AwoNRG9jdW1lbnRWYWx1ZRIUCgplbGVtZW50X2lkGAEgASgJSAASFgoMc3RyaW5nX3ZhbHVlGAIgASgJSAASEwoJaW50X3ZhbHVlGAMgASgDSAASFAoKcmVhbF92YWx1ZRgEIAEoAUgAEhQKCmJvb2xfdmFsdWUYBSABKAhIABISCghpbmZpbml0eRgGIAEoCEgAEiMKCHF1YW50aXR5GAggASgLMg8uc3lzbWwuUXVhbnRpdHlIABIpCgd2ZXJkaWN0GAkgASgLMhYuc3lzbWwuRG9jdW1lbnRWZXJkaWN0SAASJwoGb2JqZWN0GAogASgLMhUuc3lzbWwuRG9jdW1lbnRPYmplY3RIABIlCgVzdGF0ZRgLIAEoCzIULnN5c21sLkRvY3VtZW50U3RhdGVIABIlCgVldmVudBgMIAEoCzIULnN5c21sLkRvY3VtZW50RXZlbnRIABIXCg1iaWdfaW50X3ZhbHVlGA0gASgJSAASKQoOcmF0aW9uYWxfdmFsdWUYDiABKAsyDy5zeXNtbC5SYXRpb25hbEgAEhQKDGVsZW1lbnRfdHlwZRgHIAEoCUIGCgRraW5kIloKDkRvY3VtZW50T2JqZWN0EhMKC2luc3RhbmNlX2lkGAEgASgDEgwKBHBhdGgYAiABKAkSJQoHZWxlbWVudBgDIAEoCzIULnN5c21sLkRvY3VtZW50VmFsdWUirgEKD0RvY3VtZW50VmVyZGljdBInCglhc3NlcnRpb24YASABKAsyFC5zeXNtbC5Eb2N1bWVudFZhbHVlEgwKBGtpbmQYAiABKAkSDAoEdGV4dBgDIAEoCRIMCgRwYXRoGAQgASgJEg8KB3ZlcmRpY3QYBSABKAkSEQoJY29uZGl0aW9uGAYgASgJEg4KBnJlYXNvbhgHIAEoCRIUCgx2ZXJpZmljYXRpb24YCCADKAkisQEKDURvY3VtZW50U3RhdGUSJQoGb2JqZWN0GAEgASgLMhUuc3lzbWwuRG9jdW1lbnRPYmplY3QSDwoHbWFjaGluZRgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnN0YXRlX3BhdGgYBCABKAkSIwoFc3RhdGUYBSABKAsyFC5zeXNtbC5Eb2N1bWVudFZhbHVlEg4KBnJlZ2lvbhgGIAEoCRIRCgllbmNsb3NpbmcYByADKAkinAIKDURvY3VtZW50RXZlbnQSDAoEa2luZBgBIAEoCRIiCgR0aW1lGAIgASgLMhQuc3lzbWwuRG9jdW1lbnRWYWx1ZRIlCgZvYmplY3QYAyABKAsyFS5zeXNtbC5Eb2N1bWVudE9iamVjdBIPCgdtYWNoaW5lGAQgASgJEg0KBXN0YXRlGAUgASgJEgwKBGZyb20YBiABKAkSCgoCdG8YByABKAkSJQoGdGFyZ2V0GAggASgLMhUuc3lzbWwuRG9jdW1lbnRPYmplY3QSDQoFZXZlbnQYCSABKAkSDwoHcGF5bG9hZBgKIAMoCRIUCgxhbHRlcm5hdGl2ZXMYCyADKAkSDQoFdGFrZW4YDCABKAkSDAoEdGV4dBgNIAEoCSIjChNEb2N1bWVudFF1ZXJ5Q29sdW1uEgwKBG5hbWUYASABKAkiOQoRRG9jdW1lbnRRdWVyeUNlbGwSJAoGdmFsdWVzGAEgAygLMhQuc3lzbWwuRG9jdW1lbnRWYWx1ZSJiChBEb2N1bWVudFF1ZXJ5Um93EiUKB2VsZW1lbnQYASABKAsyFC5zeXNtbC5Eb2N1bWVudFZhbHVlEicKBWNlbGxzGAIgAygLMhguc3lzbWwuRG9jdW1lbnRRdWVyeUNlbGwibgoYUnVuRG9jdW1lbnRRdWVyeVJlc3BvbnNlEisKB2NvbHVtbnMYASADKAsyGi5zeXNtbC5Eb2N1bWVudFF1ZXJ5Q29sdW1uEiUKBHJvd3MYAiADKAsyFy5zeXNtbC5Eb2N1bWVudFF1ZXJ5Um93Ik4KFVJlbmRlckRvY3VtZW50UmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhMKC2RvY3VtZW50X2lkGAIgASgJEgwKBGZvcm0YAyABKAkiOAoWUmVuZGVyRG9jdW1lbnRSZXNwb25zZRIQCghtYXJrZG93bhgBIAEoCRIMCgRodG1sGAIgASgJKrEBCg1GYWlsdXJlUmVhc29uEh4KGkZBSUxVUkVfUkVBU09OX1VOU1BFQ0lGSUVEEAASHQoZRkFJTFVSRV9SRUFTT05fRVZBTFVBVElPThABEh0KGUZBSUxVUkVfUkVBU09OX1dST05HX0tJTkQQAhIkCiBGQUlMVVJFX1JFQVNPTl9BTUJJR1VPVVNfU1VCSkVDVBADEhwKGEZBSUxVUkVfUkVBU09OX1VOREVDSURFRBAEKowFCgtFZGl0RmFpbHVyZRIcChhFRElUX0ZBSUxVUkVfVU5TUEVDSUZJRUQQABIeChpFRElUX0ZBSUxVUkVfTk9fT1BFUkFUSU9OUxABEh8KG0VESVRfRkFJTFVSRV9VTktOT1dOX1RBUkdFVBACEiEKHUVESVRfRkFJTFVSRV9BTUJJR1VPVVNfVEFSR0VUEAMSGwoXRURJVF9GQUlMVVJFX05PVF9WQUxVRUQQBBIeChpFRElUX0ZBSUxVUkVfSU5WQUxJRF9WQUxVRRAFEh0KGUVESVRfRkFJTFVSRV9JTlZBTElEX05BTUUQBhIaChZFRElUX0ZBSUxVUkVfTk9UX05BTUVEEAcSIgoeRURJVF9GQUlMVVJFX1JFTkFNRV9SRUZFUkVOQ0VEEAgSIgoeRURJVF9GQUlMVVJFX09WRVJMQVBQSU5HX0VESVRTEAkSHwobRURJVF9GQUlMVVJFX1JFU1VMVF9JTlZBTElEEAoSHgoaRURJVF9GQUlMVVJFX09XTkVSX1VOS05PV04QCxIkCiBFRElUX0ZBSUxVUkVfT1dORVJfTk9UX05BTUVTUEFDRRAMEh0KGUVESVRfRkFJTFVSRV9JTExFR0FMX0tJTkQQDRIiCh5FRElUX0ZBSUxVUkVfTUVNQkVSX05BTUVfVEFLRU4QDhIiCh5FRElUX0ZBSUxVUkVfREVMRVRFX1JFRkVSRU5DRUQQDxIkCiBFRElUX0ZBSUxVUkVfT1dORVJfSU5TSURFX1RBUkdFVBAQEiAKHEVESVRfRkFJTFVSRV9NT1ZFX1JFRkVSRU5DRUQQERIlCiFFRElUX0ZBSUxVUkVfUkVGRVJFTkNFRF9FTFNFV0hFUkUQEiqSAQoRUHJpbWl0aXZlT3BlcmF0b3ISIgoeUFJJTUlUSVZFX09QRVJBVE9SX1VOU1BFQ0lGSUVEEAASHAoYUFJJTUlUSVZFX09QRVJBVE9SX0VRVUFMEAESHgoaUFJJTUlUSVZFX09QRVJBVE9SX0dSRUFURVIQAhIbChdQUklNSVRJVkVfT1BFUkFUT1JfTEVTUxADKm4KEUNvbXBvc2l0ZU9wZXJhdG9yEiIKHkNPTVBPU0lURV9PUEVSQVRPUl9VTlNQRUNJRklFRBAAEhoKFkNPTVBPU0lURV9PUEVSQVRPUl9BTkQQARIZChVDT01QT1NJVEVfT1BFUkFUT1JfT1IQAjL5DAoMU3lzTUxTZXJ2aWNlEkQKDUdldFNlcnZlckluZm8SGC5zeXNtbC5TZXJ2ZXJJbmZvUmVxdWVzdBoZLnN5c21sLlNlcnZlckluZm9SZXNwb25zZRI+CglQYXJzZUZpbGUSFy5zeXNtbC5QYXJzZUZpbGVSZXF1ZXN0Ghguc3lzbWwuUGFyc2VGaWxlUmVzcG9uc2USRwoMUGFyc2VTb3VyY2VzEhouc3lzbWwuUGFyc2VTb3VyY2VzUmVxdWVzdBobLnN5c21sLlBhcnNlU291cmNlc1Jlc3BvbnNlEjsKCUdldFN5bWJvbBIXLnN5c21sLkdldFN5bWJvbFJlcXVlc3QaFS5zeXNtbC5TeW1ib2xSZXNwb25zZRJHCg5HZXREaWFnbm9zdGljcxIZLnN5c21sLkRpYWdub3N0aWNzUmVxdWVzdBoaLnN5c21sLkRpYWdub3N0aWNzUmVzcG9uc2USOwoIRXZhbHVhdGUSFi5zeXNtbC5FdmFsdWF0ZVJlcXVlc3QaFy5zeXNtbC5FdmFsdWF0ZVJlc3BvbnNlEkQKC0luc3RhbnRpYXRlEhkuc3lzbWwuSW5zdGFudGlhdGVSZXF1ZXN0Ghouc3lzbWwuSW5zdGFudGlhdGVSZXNwb25zZRJKCg1FeGVjdXRlQWN0aW9uEhsuc3lzbWwuRXhlY3V0ZUFjdGlvblJlcXVlc3QaHC5zeXNtbC5FeGVjdXRlQWN0aW9uUmVzcG9uc2USRwoMRXhlY3V0ZVN0YXRlEhouc3lzbWwuRXhlY3V0ZVN0YXRlUmVxdWVzdBobLnN5c21sLkV4ZWN1dGVTdGF0ZVJlc3BvbnNlEjgKB0NvbnZlcnQSFS5zeXNtbC5Db252ZXJ0UmVxdWVzdBoWLnN5c21sLkNvbnZlcnRSZXNwb25zZRI4CgdNaWdyYXRlEhUuc3lzbWwuTWlncmF0ZVJlcXVlc3QaFi5zeXNtbC5NaWdyYXRlUmVzcG9uc2USQQoKQXBwbHlFZGl0cxIYLnN5c21sLkFwcGx5RWRpdHNSZXF1ZXN0Ghkuc3lzbWwuQXBwbHlFZGl0c1Jlc3BvbnNlElMKEFZlcmlmeUNvbnN0cmFpbnQSHi5zeXNtbC5WZXJpZnlDb25zdHJhaW50UmVxdWVzdBofLnN5c21sLlZlcmlmeUNvbnN0cmFpbnRSZXNwb25zZRJWChFWZXJpZnlSZXF1aXJlbWVudBIfLnN5c21sLlZlcmlmeVJlcXVpcmVtZW50UmVxdWVzdBogLnN5c21sLlZlcmlmeVJlcXVpcmVtZW50UmVzcG9uc2USWQoSVmVyaWZ5U2F0aXNmYWN0aW9uEiAuc3lzbWwuVmVyaWZ5U2F0aXNmYWN0aW9uUmVxdWVzdBohLnN5c21sLlZlcmlmeVNhdGlzZmFjdGlvblJlc3BvbnNlElMKEFZhbGlkYXRlSW5zdGFuY2USHi5zeXNtbC5WYWxpZGF0ZUluc3RhbmNlUmVxdWVzdBofLnN5c21sLlZhbGlkYXRlSW5zdGFuY2VSZXNwb25zZRJHCgxFdmFsdWF0ZUNhbGMSGi5zeXNtbC5FdmFsdWF0ZUNhbGNSZXF1ZXN0Ghsuc3lzbWwuRXZhbHVhdGVDYWxjUmVzcG9uc2USRAoLUnVuQW5hbHlzaXMSGS5zeXNtbC5SdW5BbmFseXNpc1JlcXVlc3QaGi5zeXNtbC5SdW5BbmFseXNpc1Jlc3BvbnNlEjsKCFJ1blN3ZWVwEhYuc3lzbWwuUnVuU3dlZXBSZXF1ZXN0Ghcuc3lzbWwuUnVuU3dlZXBSZXNwb25zZRJECgtMaXN0RW5naW5lcxIZLnN5c21sLkxpc3RFbmdpbmVzUmVxdWVzdBoaLnN5c21sLkxpc3RFbmdpbmVzUmVzcG9uc2USMgoFUXVlcnkSEy5zeXNtbC5RdWVyeVJlcXVlc3QaFC5zeXNtbC5RdWVyeVJlc3BvbnNlElMKEFJ1bkRvY3VtZW50UXVlcnkSHi5zeXNtbC5SdW5Eb2N1bWVudFF1ZXJ5UmVxdWVzdBofLnN5c21sLlJ1bkRvY3VtZW50UXVlcnlSZXNwb25zZRJNCg5SZW5kZXJEb2N1bWVudBIcLnN5c21sLlJlbmRlckRvY3VtZW50UmVxdWVzdBodLnN5c21sLlJlbmRlckRvY3VtZW50UmVzcG9uc2VCKlooZ2l0aHViLmNvbS9PcGVuLU1CRUUvT3BlblN5c01ML2FwaS9wcm90b2IGcHJvdG8z"); /** * Verdict is one verification's answer: whether the condition held, and, when @@ -4407,6 +4407,15 @@ export type Value = Message<"sysml.Value"> & { */ value: string; case: "bigIntValue"; + } | { + /** + * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1` + * or `1 / 3` evaluates; one a double holds exactly crosses as real_value. + * + * @generated from field: sysml.Rational rational_value = 23; + */ + value: Rational; + case: "rationalValue"; } | { case: undefined; value?: undefined }; }; @@ -4417,6 +4426,32 @@ export type Value = Message<"sysml.Value"> & { export const ValueSchema: GenMessage = /*@__PURE__*/ messageDesc(file_sysml, 79); +/** + * Rational is an exact rational number in lowest terms: numerator over a + * positive denominator, each written as big_int_value is. A decoder rejects one + * not in lowest terms or one a double holds exactly, so no number has two spellings. + * + * @generated from message sysml.Rational + */ +export type Rational = Message<"sysml.Rational"> & { + /** + * @generated from field: string numerator = 1; + */ + numerator: string; + + /** + * @generated from field: string denominator = 2; + */ + denominator: string; +}; + +/** + * Describes the message sysml.Rational. + * Use `create(RationalSchema)` to create a new message. + */ +export const RationalSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sysml, 80); + /** * Metaobject is an element of the model held as an instance of its reflective * metaclass: what `x meta KerML::Feature`, or the last element of @@ -4452,7 +4487,7 @@ export type Metaobject = Message<"sysml.Metaobject"> & { * Use `create(MetaobjectSchema)` to create a new message. */ export const MetaobjectSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 80); + messageDesc(file_sysml, 81); /** * Undetermined is the model-level result of an expression the model does not @@ -4487,7 +4522,7 @@ export type Undetermined = Message<"sysml.Undetermined"> & { * Use `create(UndeterminedSchema)` to create a new message. */ export const UndeterminedSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 81); + messageDesc(file_sysml, 82); /** * Function is a calc held as a value: a calc definition, or a calc usage with @@ -4524,7 +4559,7 @@ export type Function = Message<"sysml.Function"> & { * Use `create(FunctionSchema)` to create a new message. */ export const FunctionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 82); + messageDesc(file_sysml, 83); /** * ValueSet is a unique, unordered collection — a Collections::Set's elements — @@ -4550,7 +4585,7 @@ export type ValueSet = Message<"sysml.ValueSet"> & { * Use `create(ValueSetSchema)` to create a new message. */ export const ValueSetSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 83); + messageDesc(file_sysml, 84); /** * TensorQuantity is a Quantities::TensorQuantityValue of any rank: its @@ -4582,7 +4617,7 @@ export type TensorQuantity = Message<"sysml.TensorQuantity"> & { * Use `create(TensorQuantitySchema)` to create a new message. */ export const TensorQuantitySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 84); + messageDesc(file_sysml, 85); /** * Array is a Collections::Array: its elements flattened in row-major order @@ -4612,7 +4647,7 @@ export type Array = Message<"sysml.Array"> & { * Use `create(ArraySchema)` to create a new message. */ export const ArraySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 85); + messageDesc(file_sysml, 86); /** * Vector is a VectorValues::NumericalVectorValue: its components in order, @@ -4635,7 +4670,7 @@ export type Vector = Message<"sysml.Vector"> & { * Use `create(VectorSchema)` to create a new message. */ export const VectorSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 86); + messageDesc(file_sysml, 87); /** * VectorQuantity is a Quantities::VectorQuantityValue: one Quantity per axis, @@ -4658,7 +4693,7 @@ export type VectorQuantity = Message<"sysml.VectorQuantity"> & { * Use `create(VectorQuantitySchema)` to create a new message. */ export const VectorQuantitySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 87); + messageDesc(file_sysml, 88); /** * Complex is one complex number in rectangular form. It crosses as one value @@ -4683,7 +4718,7 @@ export type Complex = Message<"sysml.Complex"> & { * Use `create(ComplexSchema)` to create a new message. */ export const ComplexSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 88); + messageDesc(file_sysml, 89); /** * EnumLiteral is one literal of an enumeration definition. A literal is its own @@ -4728,7 +4763,7 @@ export type EnumLiteral = Message<"sysml.EnumLiteral"> & { * Use `create(EnumLiteralSchema)` to create a new message. */ export const EnumLiteralSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 89); + messageDesc(file_sysml, 90); /** * @generated from message sysml.ValueSequence @@ -4745,7 +4780,7 @@ export type ValueSequence = Message<"sysml.ValueSequence"> & { * Use `create(ValueSequenceSchema)` to create a new message. */ export const ValueSequenceSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 90); + messageDesc(file_sysml, 91); /** * Quantity is a magnitude and the measurement reference it is expressed in, sent @@ -4779,6 +4814,14 @@ export type Quantity = Message<"sysml.Quantity"> & { */ value: string; case: "bigIntMagnitude"; + } | { + /** + * An exact Rational magnitude no double holds exactly, as Value.rational_value. + * + * @generated from field: sysml.Rational rational_magnitude = 6; + */ + value: Rational; + case: "rationalMagnitude"; } | { case: undefined; value?: undefined }; /** @@ -4804,7 +4847,7 @@ export type Quantity = Message<"sysml.Quantity"> & { * Use `create(QuantitySchema)` to create a new message. */ export const QuantitySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 91); + messageDesc(file_sysml, 92); /** * MeasurementRef is a MeasurementReferences::ScalarMeasurementReference held as @@ -4852,7 +4895,7 @@ export type MeasurementRef = Message<"sysml.MeasurementRef"> & { * Use `create(MeasurementRefSchema)` to create a new message. */ export const MeasurementRefSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 92); + messageDesc(file_sysml, 93); /** * UnitTerm is a unit reduced to a scale factor over base units: `km/h` reduces @@ -4886,7 +4929,7 @@ export type UnitTerm = Message<"sysml.UnitTerm"> & { * Use `create(UnitTermSchema)` to create a new message. */ export const UnitTermSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 93); + messageDesc(file_sysml, 94); /** * UnitFactor is one base unit raised to an exponent. @@ -4912,7 +4955,7 @@ export type UnitFactor = Message<"sysml.UnitFactor"> & { * Use `create(UnitFactorSchema)` to create a new message. */ export const UnitFactorSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 94); + messageDesc(file_sysml, 95); /** * Diagnostic represents a parse/semantic error or warning @@ -4951,7 +4994,7 @@ export type Diagnostic = Message<"sysml.Diagnostic"> & { * Use `create(DiagnosticSchema)` to create a new message. */ export const DiagnosticSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 95); + messageDesc(file_sysml, 96); /** * Span represents a source location @@ -4990,7 +5033,7 @@ export type Span = Message<"sysml.Span"> & { * Use `create(SpanSchema)` to create a new message. */ export const SpanSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 96); + messageDesc(file_sysml, 97); /** * ServerInfoRequest asks the service to describe itself. It carries no fields; @@ -5006,7 +5049,7 @@ export type ServerInfoRequest = Message<"sysml.ServerInfoRequest"> & { * Use `create(ServerInfoRequestSchema)` to create a new message. */ export const ServerInfoRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 97); + messageDesc(file_sysml, 98); /** * ServerInfoResponse describes the running service. @@ -5148,7 +5191,7 @@ export type ServerInfoResponse = Message<"sysml.ServerInfoResponse"> & { * Use `create(ServerInfoResponseSchema)` to create a new message. */ export const ServerInfoResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 98); + messageDesc(file_sysml, 99); /** * QueryRequest runs a Query against a model the service already parsed. @@ -5181,7 +5224,7 @@ export type QueryRequest = Message<"sysml.QueryRequest"> & { * Use `create(QueryRequestSchema)` to create a new message. */ export const QueryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 99); + messageDesc(file_sysml, 100); /** * QueryResponse contains the elements the query selected, in the order they are @@ -5203,7 +5246,7 @@ export type QueryResponse = Message<"sysml.QueryResponse"> & { * Use `create(QueryResponseSchema)` to create a new message. */ export const QueryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 100); + messageDesc(file_sysml, 101); /** * Query is the standard's Query resource (SysML v2 API & Services). Its `@type` @@ -5243,7 +5286,7 @@ export type Query = Message<"sysml.Query"> & { * Use `create(QuerySchema)` to create a new message. */ export const QuerySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 101); + messageDesc(file_sysml, 102); /** * Constraint is the standard's Constraint, whose `@type` discriminates between @@ -5275,7 +5318,7 @@ export type Constraint = Message<"sysml.Constraint"> & { * Use `create(ConstraintSchema)` to create a new message. */ export const ConstraintSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 102); + messageDesc(file_sysml, 103); /** * PrimitiveConstraint compares one property of an element against a value. @@ -5318,7 +5361,7 @@ export type PrimitiveConstraint = Message<"sysml.PrimitiveConstraint"> & { * Use `create(PrimitiveConstraintSchema)` to create a new message. */ export const PrimitiveConstraintSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 103); + messageDesc(file_sysml, 104); /** * CompositeConstraint combines constraints. An empty constraint list fails the @@ -5343,7 +5386,7 @@ export type CompositeConstraint = Message<"sysml.CompositeConstraint"> & { * Use `create(CompositeConstraintSchema)` to create a new message. */ export const CompositeConstraintSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 104); + messageDesc(file_sysml, 105); /** * QueryResultElement is one matched element. `id` and `type` are always @@ -5379,7 +5422,7 @@ export type QueryResultElement = Message<"sysml.QueryResultElement"> & { * Use `create(QueryResultElementSchema)` to create a new message. */ export const QueryResultElementSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 105); + messageDesc(file_sysml, 106); /** * SweepRange is one parameter's range: the endpoints a swept run advances @@ -5423,7 +5466,7 @@ export type SweepRange = Message<"sysml.SweepRange"> & { * Use `create(SweepRangeSchema)` to create a new message. */ export const SweepRangeSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 106); + messageDesc(file_sysml, 107); /** * RunSweepRequest runs one analysis case or calc once per row of a sweep. Every @@ -5506,7 +5549,7 @@ export type RunSweepRequest = Message<"sysml.RunSweepRequest"> & { * Use `create(RunSweepRequestSchema)` to create a new message. */ export const RunSweepRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 107); + messageDesc(file_sysml, 108); /** * SweepRow is one run of a sweep: what it bound, what it produced, how long it @@ -5574,7 +5617,7 @@ export type SweepRow = Message<"sysml.SweepRow"> & { * Use `create(SweepRowSchema)` to create a new message. */ export const SweepRowSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 108); + messageDesc(file_sysml, 109); /** * RunSweepResponse carries the table, one row per run, in the order the runs @@ -5664,7 +5707,7 @@ export type RunSweepResponse = Message<"sysml.RunSweepResponse"> & { * Use `create(RunSweepResponseSchema)` to create a new message. */ export const RunSweepResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 109); + messageDesc(file_sysml, 110); /** * RunDocumentQueryRequest runs a named document query — a calc def @@ -5706,7 +5749,7 @@ export type RunDocumentQueryRequest = Message<"sysml.RunDocumentQueryRequest"> & * Use `create(RunDocumentQueryRequestSchema)` to create a new message. */ export const RunDocumentQueryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 110); + messageDesc(file_sysml, 111); /** * DocumentQueryBinding binds one entry parameter of a document query. @@ -5730,7 +5773,7 @@ export type DocumentQueryBinding = Message<"sysml.DocumentQueryBinding"> & { * Use `create(DocumentQueryBindingSchema)` to create a new message. */ export const DocumentQueryBindingSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 111); + messageDesc(file_sysml, 112); /** * DocumentValue is one typed document-query value. A request binds a model @@ -5830,6 +5873,14 @@ export type DocumentValue = Message<"sysml.DocumentValue"> & { */ value: string; case: "bigIntValue"; + } | { + /** + * An exact Rational no double holds exactly, as Value.rational_value. + * + * @generated from field: sysml.Rational rational_value = 14; + */ + value: Rational; + case: "rationalValue"; } | { case: undefined; value?: undefined }; /** @@ -5845,7 +5896,7 @@ export type DocumentValue = Message<"sysml.DocumentValue"> & { * Use `create(DocumentValueSchema)` to create a new message. */ export const DocumentValueSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 112); + messageDesc(file_sysml, 113); /** * DocumentObject is an object the service holds for the model, created by @@ -5892,7 +5943,7 @@ export type DocumentObject = Message<"sysml.DocumentObject"> & { * Use `create(DocumentObjectSchema)` to create a new message. */ export const DocumentObjectSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 113); + messageDesc(file_sysml, 114); /** * DocumentVerdict is one row a `Verdicts` query answered: an assertion checked @@ -5968,7 +6019,7 @@ export type DocumentVerdict = Message<"sysml.DocumentVerdict"> & { * Use `create(DocumentVerdictSchema)` to create a new message. */ export const DocumentVerdictSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 114); + messageDesc(file_sysml, 115); /** * DocumentState is one row a `States` query answered: an active leaf state of @@ -6035,7 +6086,7 @@ export type DocumentState = Message<"sysml.DocumentState"> & { * Use `create(DocumentStateSchema)` to create a new message. */ export const DocumentStateSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 115); + messageDesc(file_sysml, 116); /** * DocumentEvent is one row an `Events` query answered: one record of the @@ -6141,7 +6192,7 @@ export type DocumentEvent = Message<"sysml.DocumentEvent"> & { * Use `create(DocumentEventSchema)` to create a new message. */ export const DocumentEventSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 116); + messageDesc(file_sysml, 117); /** * DocumentQueryColumn is one projected property, in projection order. @@ -6160,7 +6211,7 @@ export type DocumentQueryColumn = Message<"sysml.DocumentQueryColumn"> & { * Use `create(DocumentQueryColumnSchema)` to create a new message. */ export const DocumentQueryColumnSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 117); + messageDesc(file_sysml, 118); /** * DocumentQueryCell is one row's values for one column, in the query's order. @@ -6179,7 +6230,7 @@ export type DocumentQueryCell = Message<"sysml.DocumentQueryCell"> & { * Use `create(DocumentQueryCellSchema)` to create a new message. */ export const DocumentQueryCellSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 118); + messageDesc(file_sysml, 119); /** * DocumentQueryRow is one selected element and its projected cells, one per @@ -6209,7 +6260,7 @@ export type DocumentQueryRow = Message<"sysml.DocumentQueryRow"> & { * Use `create(DocumentQueryRowSchema)` to create a new message. */ export const DocumentQueryRowSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 119); + messageDesc(file_sysml, 120); /** * RunDocumentQueryResponse is the query's answer: its projected columns and its @@ -6236,7 +6287,7 @@ export type RunDocumentQueryResponse = Message<"sysml.RunDocumentQueryResponse"> * Use `create(RunDocumentQueryResponseSchema)` to create a new message. */ export const RunDocumentQueryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 120); + messageDesc(file_sysml, 121); /** * RenderDocumentRequest renders a named document — a part def specializing @@ -6277,7 +6328,7 @@ export type RenderDocumentRequest = Message<"sysml.RenderDocumentRequest"> & { * Use `create(RenderDocumentRequestSchema)` to create a new message. */ export const RenderDocumentRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 121); + messageDesc(file_sysml, 122); /** * RenderDocumentResponse carries the rendered document in the form requested, @@ -6303,7 +6354,7 @@ export type RenderDocumentResponse = Message<"sysml.RenderDocumentResponse"> & { * Use `create(RenderDocumentResponseSchema)` to create a new message. */ export const RenderDocumentResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 122); + messageDesc(file_sysml, 123); /** * FailureReason says what kind of failure an `error` reports, so a client acts diff --git a/client/opensysml/client.go b/client/opensysml/client.go index 39e73734cd..f9ae0ac4a3 100644 --- a/client/opensysml/client.go +++ b/client/opensysml/client.go @@ -610,6 +610,9 @@ func (c *client) requireValueCapabilities(ctx context.Context, values ...Value) if slices.ContainsFunc(values, carriesBigInt) { needed = append(needed, CapabilityBigIntValues) } + if slices.ContainsFunc(values, carriesRational) { + needed = append(needed, CapabilityRationalValues) + } if len(needed) == 0 { return nil } @@ -729,6 +732,33 @@ func isBigInt(n Number) bool { func quantityIsBigInt(q Quantity) bool { return isBigInt(q.Magnitude) } +// carriesRational reports whether a value, or any value nested in it, holds an +// exact Rational, as itself or as a quantity's magnitude. +func carriesRational(value Value) bool { + switch v := value.(type) { + case Rational: + return true + case Quantity: + return isRational(v.Magnitude) + case Vector: + return slices.ContainsFunc(v, isRational) + case VectorQuantity: + return slices.ContainsFunc(v, quantityIsRational) + case TensorQuantity: + return slices.ContainsFunc(v.Components, quantityIsRational) + case EnumLiteral: + return v.Value != nil && carriesRational(v.Value) + } + return slices.ContainsFunc(nestedValues(value), carriesRational) +} + +func isRational(n Number) bool { + _, ok := n.(Rational) + return ok +} + +func quantityIsRational(q Quantity) bool { return isRational(q.Magnitude) } + // nestedValues are the values a value holds: a sequence's or a set's elements, // an array's. func nestedValues(value Value) []Value { diff --git a/client/opensysml/convert.go b/client/opensysml/convert.go index cc12e67696..162e2582e3 100644 --- a/client/opensysml/convert.go +++ b/client/opensysml/convert.go @@ -2,6 +2,7 @@ package opensysml import ( "fmt" + "math" "math/big" pb "github.com/Open-MBEE/OpenSysML/api/proto" @@ -98,6 +99,12 @@ func valueFromProto(value *pb.Value) Value { return Null("unsupported: a big integer that is not decimal") } return NewInteger(n) + case *pb.Value_RationalValue: + rational, ok := rationalFromProto(kind.RationalValue) + if !ok { + return Null("unsupported: a rational that is not in lowest terms") + } + return rational case *pb.Value_RealValue: return Real(kind.RealValue) case *pb.Value_Complex: @@ -238,6 +245,11 @@ func valueToProto(value Value) (*pb.Value, error) { return &pb.Value{Kind: &pb.Value_IntValue{IntValue: n.Int64()}}, nil } return &pb.Value{Kind: &pb.Value_BigIntValue{BigIntValue: n.String()}}, nil + case Rational: + if f, exact := v.Rat().Float64(); exact && !math.IsInf(f, 0) { + return &pb.Value{Kind: &pb.Value_RealValue{RealValue: f}}, nil + } + return &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: rationalToProto(v)}}, nil case Real: return &pb.Value{Kind: &pb.Value_RealValue{RealValue: float64(v)}}, nil case Complex: @@ -386,6 +398,12 @@ func quantityToProto(quantity Quantity) (*pb.Quantity, error) { } else { out.Magnitude = &pb.Quantity_BigIntMagnitude{BigIntMagnitude: n.String()} } + case Rational: + if f, exact := magnitude.Rat().Float64(); exact && !math.IsInf(f, 0) { + out.Magnitude = &pb.Quantity_RealMagnitude{RealMagnitude: f} + break + } + out.Magnitude = &pb.Quantity_RationalMagnitude{RationalMagnitude: rationalToProto(magnitude)} case Real: out.Magnitude = &pb.Quantity_RealMagnitude{RealMagnitude: float64(magnitude)} default: @@ -454,6 +472,12 @@ func quantityFromProto(quantity *pb.Quantity) (Quantity, bool) { return Quantity{}, false } out.Magnitude = NewInteger(n) + case *pb.Quantity_RationalMagnitude: + rational, ok := rationalFromProto(magnitude.RationalMagnitude) + if !ok { + return Quantity{}, false + } + out.Magnitude = rational case *pb.Quantity_RealMagnitude: out.Magnitude = Real(magnitude.RealMagnitude) default: @@ -533,3 +557,19 @@ func migrationReportFromProto(report *pb.MigrationReport) *MigrationReport { } return out } + +// rationalToProto marshals a Rational that is no integer, in lowest terms. +func rationalToProto(q Rational) *pb.Rational { + r := q.Rat() + return &pb.Rational{Numerator: r.Num().String(), Denominator: r.Denom().String()} +} + +// rationalFromProto is false for a Rational not in lowest terms or whole, +// which the wire never spells. +func rationalFromProto(pr *pb.Rational) (Rational, bool) { + v, err := protoconv.ProtoToRational(pr) + if err != nil { + return Rational{}, false + } + return Rational{r: v.Rat()}, true +} diff --git a/client/opensysml/documents.go b/client/opensysml/documents.go index ea5a34e208..82e1c7f383 100644 --- a/client/opensysml/documents.go +++ b/client/opensysml/documents.go @@ -2,6 +2,7 @@ package opensysml import ( "context" + "math" "math/big" "slices" "strconv" @@ -135,6 +136,7 @@ func (DocumentEvent) isCell() { /* marker: closed Cell set */ } func (String) isCell() { /* marker: closed Cell set */ } func (Int) isCell() { /* marker: closed Cell set */ } func (BigInt) isCell() { /* marker: closed Cell set */ } +func (Rational) isCell() { /* marker: closed Cell set */ } func (Real) isCell() { /* marker: closed Cell set */ } func (Bool) isCell() { /* marker: closed Cell set */ } func (Quantity) isCell() { /* marker: closed Cell set */ } @@ -226,6 +228,12 @@ func bindingHoldsBigInt(binding *pb.DocumentQueryBinding) bool { return slices.ContainsFunc(binding.GetValues(), protoconv.DocumentValueHoldsBigInt) } +// bindingHoldsRational reports whether a binding sends an exact Rational, +// which only a service offering rational_values reads. +func bindingHoldsRational(binding *pb.DocumentQueryBinding) bool { + return slices.ContainsFunc(binding.GetValues(), protoconv.DocumentValueHoldsRational) +} + func (c *client) RunDocumentQuery( ctx context.Context, model *Model, @@ -253,6 +261,11 @@ func (c *client) RunDocumentQuery( return nil, err } } + if slices.ContainsFunc(req.Bindings, bindingHoldsRational) { + if err := c.requireCapabilities(ctx, CapabilityRationalValues); err != nil { + return nil, err + } + } resp, err := c.caller.runDocumentQuery(ctx, req) if err != nil { return nil, err @@ -331,6 +344,11 @@ func cellToProto(cell Cell) (*pb.DocumentValue, error) { return &pb.DocumentValue{Kind: &pb.DocumentValue_IntValue{IntValue: n.Int64()}}, nil } return &pb.DocumentValue{Kind: &pb.DocumentValue_BigIntValue{BigIntValue: value.String()}}, nil + case Rational: + if f, exact := value.Rat().Float64(); exact && !math.IsInf(f, 0) { + return &pb.DocumentValue{Kind: &pb.DocumentValue_RealValue{RealValue: f}}, nil + } + return &pb.DocumentValue{Kind: &pb.DocumentValue_RationalValue{RationalValue: rationalToProto(value)}}, nil case Real: return &pb.DocumentValue{Kind: &pb.DocumentValue_RealValue{RealValue: float64(value)}}, nil case Bool: @@ -383,6 +401,12 @@ func cellFromProto(value *pb.DocumentValue) Cell { return Int(n.Int64()) } return BigInt{n: n} + case *pb.DocumentValue_RationalValue: + rational, ok := rationalFromProto(kind.RationalValue) + if !ok { + return nil + } + return rational case *pb.DocumentValue_RealValue: return Real(kind.RealValue) case *pb.DocumentValue_BoolValue: @@ -476,6 +500,8 @@ func CellText(cell Cell) string { return strconv.FormatInt(int64(value), 10) case BigInt: return value.String() + case Rational: + return value.String() case Real: return strconv.FormatFloat(float64(value), 'g', -1, 64) case Bool: diff --git a/client/opensysml/edges_test.go b/client/opensysml/edges_test.go index 7c7e0e006b..7a9ee37f5b 100644 --- a/client/opensysml/edges_test.go +++ b/client/opensysml/edges_test.go @@ -36,14 +36,14 @@ func TestIntegerArithmeticBeyondInt64IsExact(t *testing.T) { } // TestRealOutsideItsRangeIsAFailure: a Real no float64 holds is reported, not -// read as an infinity. +// read as an infinity. The literal 1e400 is an exact Rational; ToReal is Real. func TestRealOutsideItsRangeIsAFailure(t *testing.T) { client := newClient(t) model := parseVehicle(t, client) - value, err := client.Evaluate(context.Background(), model, "1e400") + value, err := client.Evaluate(context.Background(), model, `RealFunctions::ToReal("1e400")`) if !errors.Is(err, opensysml.ErrFailure) { - t.Errorf("Evaluate(1e400) = %#v, %v; want a failure", value, err) + t.Errorf("Evaluate(ToReal(1e400)) = %#v, %v; want a failure", value, err) } } @@ -53,7 +53,7 @@ func TestRealsInRangeStillEvaluate(t *testing.T) { client := newClient(t) model := parseVehicle(t, client) - value, err := client.Evaluate(context.Background(), model, "1.5e308 / 2.0") + value, err := client.Evaluate(context.Background(), model, `RealFunctions::ToReal("1.5e308") / 2.0`) if err != nil { t.Fatalf("Evaluate: %v", err) } diff --git a/client/opensysml/equal.go b/client/opensysml/equal.go index 53021a174a..857a6fa37f 100644 --- a/client/opensysml/equal.go +++ b/client/opensysml/equal.go @@ -25,7 +25,7 @@ func Equal(a, b Value) bool { switch x := a.(type) { case nil: return b == nil - case Int, BigInt, Real, Complex: + case Int, BigInt, Rational, Real, Complex: return numbersEqual(a, b) case Sequence: y, ok := b.(Sequence) @@ -120,18 +120,31 @@ func numbersEqual(a, b Value) bool { } a, b = b, a } - x, ok := integerOf(a) + x, ok := exactOf(a) if !ok { return false } - switch y := b.(type) { - case Int, BigInt: - n, _ := integerOf(y) - return x.Cmp(n) == 0 - case Real: - return realIsInt(float64(y), x) + if y, ok := b.(Real); ok { + f := float64(y) + if math.IsInf(f, 0) || math.IsNaN(f) { + return false + } + return x.Cmp(new(big.Rat).SetFloat64(f)) == 0 } - return false + y, ok := exactOf(b) + return ok && x.Cmp(y) == 0 +} + +// exactOf is the exact number an Int, BigInt or Rational holds. +func exactOf(v Value) (*big.Rat, bool) { + if q, ok := v.(Rational); ok { + return q.Rat(), true + } + n, ok := integerOf(v) + if !ok { + return nil, false + } + return new(big.Rat).SetInt(n), true } // integerOf is the integer an Int or BigInt holds. @@ -145,14 +158,6 @@ func integerOf(v Value) (*big.Int, bool) { return nil, false } -// realIsInt reports whether r is exactly the integer n, never rounding n. -func realIsInt(r float64, n *big.Int) bool { - if math.IsInf(r, 0) || math.IsNaN(r) || r != math.Trunc(r) { - return false - } - return new(big.Float).SetFloat64(r).Cmp(new(big.Float).SetInt(n)) == 0 -} - func quantityEqual(a, b Quantity) bool { if a.Term == nil || b.Term == nil { return numbersEqual(a.Magnitude, b.Magnitude) && a.Unit == b.Unit && unitTermEqual(a.Term, b.Term) @@ -166,11 +171,11 @@ func quantityEqual(a, b Quantity) bool { return a.baseMagnitude() == b.baseMagnitude() } -// exactBaseMagnitudes expresses two Int magnitudes over their base units as +// exactBaseMagnitudes expresses two exact magnitudes over their base units as // rationals, which is exact only while both scales are whole. func exactBaseMagnitudes(a, b Quantity) (*big.Rat, *big.Rat, bool) { - x, xok := integerOf(a.Magnitude) - y, yok := integerOf(b.Magnitude) + x, xok := exactOf(a.Magnitude) + y, yok := exactOf(b.Magnitude) if !xok || !yok || !a.Term.whole() || !b.Term.whole() { return nil, nil, false } @@ -184,6 +189,8 @@ func (q Quantity) baseMagnitude() float64 { m = float64(x) case BigInt: m, _ = new(big.Float).SetInt(x.Int()).Float64() + case Rational: + m = x.Float64() case Real: m = float64(x) } @@ -247,9 +254,9 @@ func (t UnitTerm) whole() bool { return isWhole(t.ScaleNum) && isWhole(t.ScaleDe func isWhole(f float64) bool { return f == math.Trunc(f) && !math.IsInf(f, 0) } // scaled is magnitude times the term's scale, exactly; the scale must be whole. -func (t UnitTerm) scaled(magnitude *big.Int) *big.Rat { +func (t UnitTerm) scaled(magnitude *big.Rat) *big.Rat { num, den := new(big.Rat).SetFloat64(t.ScaleNum), new(big.Rat).SetFloat64(t.ScaleDen) - m := new(big.Rat).SetInt(magnitude) + m := new(big.Rat).Set(magnitude) return m.Mul(m, num.Quo(num, den)) } diff --git a/client/opensysml/types.go b/client/opensysml/types.go index 3fefb7c0ae..381b93eeed 100644 --- a/client/opensysml/types.go +++ b/client/opensysml/types.go @@ -64,6 +64,7 @@ const ( CapabilityEngines = sysmlgrpc.CapabilityEngines CapabilityUndeterminedValue = sysmlgrpc.CapabilityUndeterminedValue CapabilityBigIntValues = sysmlgrpc.CapabilityBigIntValues + CapabilityRationalValues = sysmlgrpc.CapabilityRationalValues ) // ServerInfo describes the implementation answering a Client's calls. diff --git a/client/opensysml/value.go b/client/opensysml/value.go index fb0f0ea1bf..64b991ecb8 100644 --- a/client/opensysml/value.go +++ b/client/opensysml/value.go @@ -6,10 +6,11 @@ import ( "strings" "github.com/Open-MBEE/OpenSysML/internal/exec/runtime" + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" ) // Value is one evaluated SysML value. It is a sealed sum: the concrete types -// are Int, BigInt, Real, Complex, Bool, String, InstanceID, Sequence, Null, Unset, +// are Int, BigInt, Rational, Real, Complex, Bool, String, InstanceID, Sequence, Null, Unset, // Quantity, EnumLiteral, Array, Vector, VectorQuantity, MeasurementRef, // Function, Set, TensorQuantity, Metaobject and Undetermined, and a type switch over // them is exhaustive. @@ -17,8 +18,9 @@ type Value interface { isValue() } -// Number is a Value that is a numeric magnitude: Int, BigInt or Real. Integers and -// reals stay apart end to end, so an integer compares exactly. +// Number is a Value that is a numeric magnitude: Int, BigInt, Rational or Real. +// Integers, rationals and reals stay apart end to end, so an exact number +// compares exactly. type Number interface { Value isNumber() @@ -51,6 +53,32 @@ func (b BigInt) Int() *big.Int { // String spells the integer in full decimal. func (b BigInt) String() string { return b.Int().String() } +// Rational is an exact rational value no Real holds exactly: KerML Rationals +// are exact, so 0.1 is one tenth and 1 / 3 one third. A Rational a binary64 +// holds exactly, such as 0.5, crosses the wire as that Real. +type Rational struct{ r *big.Rat } + +// NewRational is a Rational holding a copy of r. +func NewRational(r *big.Rat) Rational { return Rational{r: new(big.Rat).Set(r)} } + +// Rat returns a copy of the rational. +func (q Rational) Rat() *big.Rat { + if q.r == nil { + return new(big.Rat) + } + return new(big.Rat).Set(q.r) +} + +// Float64 is the binary64 nearest the rational. +func (q Rational) Float64() float64 { + f, _ := q.Rat().Float64() + return f +} + +// String spells the rational exactly, as the service prints it: a terminating +// decimal as `0.1`, any other as `1/3`. +func (q Rational) String() string { return semantics.RatValue(q.Rat()).FormatRational() } + // Real is a real value. type Real float64 @@ -369,6 +397,7 @@ func (Undetermined) String() string { func (Int) isValue() { /* marker: closed Value set */ } func (BigInt) isValue() { /* marker: closed Value set */ } +func (Rational) isValue() { /* marker: closed Value set */ } func (Real) isValue() { /* marker: closed Value set */ } func (Complex) isValue() { /* marker: closed Value set */ } func (Bool) isValue() { /* marker: closed Value set */ } @@ -389,6 +418,7 @@ func (TensorQuantity) isValue() { /* marker: closed Value set */ } func (Metaobject) isValue() { /* marker: closed Value set */ } func (Undetermined) isValue() { /* marker: closed Value set */ } -func (Int) isNumber() { /* marker: closed Number set */ } -func (BigInt) isNumber() { /* marker: closed Number set */ } -func (Real) isNumber() { /* marker: closed Number set */ } +func (Int) isNumber() { /* marker: closed Number set */ } +func (BigInt) isNumber() { /* marker: closed Number set */ } +func (Rational) isNumber() { /* marker: closed Number set */ } +func (Real) isNumber() { /* marker: closed Number set */ } diff --git a/client/python/opensysml/proto/sysml_pb2.py b/client/python/opensysml/proto/sysml_pb2.py index 23e478956c..eb9a2ad7ce 100644 --- a/client/python/opensysml/proto/sysml_pb2.py +++ b/client/python/opensysml/proto/sysml_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bsysml.proto\x12\x05sysml\"\x86\x03\n\x07Verdict\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x12\n\nelement_id\x18\x02 \x01(\t\x12\x0f\n\x07\x65lement\x18\x03 \x01(\t\x12\r\n\x05holds\x18\x04 \x01(\x08\x12\x11\n\tcondition\x18\x05 \x01(\t\x12\x13\n\x0binstance_id\x18\x06 \x01(\x03\x12\x18\n\x10instance_type_id\x18\x07 \x01(\t\x12\r\n\x05\x65rror\x18\x08 \x01(\t\x12,\n\x0e\x66\x61ilure_reason\x18\t \x01(\x0e\x32\x14.sysml.FailureReason\x12\x16\n\x0erequirement_id\x18\n \x01(\t\x12\x0e\n\x06\x65ngine\x18\x0b \x01(\t\x12\x10\n\x08strength\x18\x0c \x01(\t\x12\x1c\n\x06\x62ounds\x18\r \x03(\x0b\x32\x0c.sysml.Bound\x12\x15\n\rinstance_path\x18\x0e \x01(\t\x12\x10\n\x08question\x18\x0f \x01(\t\x12\x0e\n\x06status\x18\x10 \x01(\t\x12)\n\x07witness\x18\x11 \x03(\x0b\x32\x18.sysml.WitnessAssignment\"^\n\x11WitnessAssignment\x12\x0f\n\x07\x66\x65\x61ture\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\r\n\x05\x65xact\x18\x04 \x01(\t\"5\n\x05\x42ound\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x03\x12\x0f\n\x07reached\x18\x03 \x01(\x08\"}\n\x17VerifyConstraintRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x0e\n\x06\x65ngine\x18\x04 \x01(\t\x12\x10\n\x08question\x18\x05 \x01(\t\"\x96\x01\n\x18VerifyConstraintResponse\x12\x1f\n\x07verdict\x18\x01 \x01(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\"~\n\x18VerifyRequirementRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x0e\n\x06\x65ngine\x18\x04 \x01(\t\x12\x10\n\x08question\x18\x05 \x01(\t\"m\n\x13VerificationVerdict\x12\x0f\n\x07\x63\x61se_id\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x03 \x01(\t\x12\x0f\n\x07subcase\x18\x04 \x01(\x08\x12\x16\n\x0erequirement_id\x18\x05 \x01(\t\"\xd2\x01\n\x19VerifyRequirementResponse\x12\x1f\n\x07verdict\x18\x01 \x01(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x39\n\x15verification_verdicts\x18\x05 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\"d\n\x19VerifySatisfactionRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x0e\n\x06\x65ngine\x18\x03 \x01(\t\x12\x10\n\x08question\x18\x04 \x01(\t\"\x82\x02\n\x1aVerifySatisfactionResponse\x12 \n\x08verdicts\x18\x01 \x03(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x05 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x39\n\x15verification_verdicts\x18\x06 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\"P\n\x17ValidateInstanceRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x0e\n\x06\x65ngine\x18\x03 \x01(\t\"\xb2\x02\n\x18ValidateInstanceResponse\x12 \n\x08verdicts\x18\x01 \x03(\x0b\x32\x0e.sysml.Verdict\x12\x1f\n\x07summary\x18\x02 \x01(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x03 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x06 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x39\n\x15verification_verdicts\x18\x07 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\x12\x0f\n\x07\x62ounded\x18\x08 \x01(\x08\"m\n\x13\x45valuateCalcRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x1f\n\targuments\x18\x03 \x03(\x0b\x32\x0c.sysml.Value\x12\x0e\n\x06\x65ngine\x18\x04 \x01(\t\"\xfd\x01\n\x14\x45valuateCalcResponse\x12\x1c\n\x06result\x18\x01 \x01(\x0b\x32\x0c.sysml.Value\x12\"\n\x07outputs\x18\x02 \x03(\x0b\x32\x11.sysml.CalcOutput\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x05 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x0e\n\x06\x65ngine\x18\x06 \x01(\t\x12\x10\n\x08strength\x18\x07 \x01(\t\x12\x1c\n\x06\x62ounds\x18\x08 \x03(\x0b\x32\x0c.sysml.Bound\"7\n\nCalcOutput\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\"\x93\x01\n\x0e\x43\x61seEvaluation\x12\x13\n\x0b\x66unction_id\x18\x01 \x01(\t\x12\x1f\n\targuments\x18\x02 \x03(\x0b\x32\x0c.sysml.Value\x12\x1c\n\x06result\x18\x03 \x01(\x0b\x32\x0c.sysml.Value\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x10\n\x08selected\x18\x05 \x01(\x08\x12\x0c\n\x04tied\x18\x06 \x01(\x08\"\xa6\x02\n\x12RunAnalysisRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x1f\n\targuments\x18\x04 \x03(\x0b\x32\x0c.sysml.Value\x12\x46\n\x0fnamed_arguments\x18\x05 \x03(\x0b\x32-.sysml.RunAnalysisRequest.NamedArgumentsEntry\x12\x10\n\x08schedule\x18\x06 \x01(\t\x12\x0e\n\x06\x65ngine\x18\x07 \x01(\t\x1a\x43\n\x13NamedArgumentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xdc\x03\n\x13RunAnalysisResponse\x12\"\n\x07outputs\x18\x01 \x03(\x0b\x32\x11.sysml.CalcOutput\x12 \n\x08verdicts\x18\x02 \x03(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x03 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x06 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x39\n\x15verification_verdicts\x18\x07 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\x12 \n\x08outcomes\x18\x08 \x03(\x0b\x32\x0e.sysml.Outcome\x12-\n\x0b\x65xploration\x18\t \x01(\x0b\x32\x18.sysml.ExplorationStatus\x12*\n\x0b\x65valuations\x18\n \x03(\x0b\x32\x15.sysml.CaseEvaluation\x12\x0e\n\x06\x65ngine\x18\x0b \x01(\t\x12\x10\n\x08strength\x18\x0c \x01(\t\x12\x1c\n\x06\x62ounds\x18\r \x03(\x0b\x32\x0c.sysml.Bound\",\n\x10ProbabilityRange\x12\x0b\n\x03min\x18\x01 \x01(\x01\x12\x0b\n\x03max\x18\x02 \x01(\x01\"\xcb\x02\n\x07Outcome\x12,\n\x07outputs\x18\x01 \x03(\x0b\x32\x1b.sysml.Outcome.OutputsEntry\x12\x13\n\x0b\x66inal_state\x18\x02 \x01(\t\x12\x16\n\x0estates_visited\x18\x03 \x03(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x16\n\x0elinearizations\x18\x05 \x01(\x05\x12\x0f\n\x07witness\x18\x06 \x03(\t\x12&\n\x0b\x64iagnostics\x18\x07 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x13\n\x0bprobability\x18\x08 \x01(\x01\x12\x32\n\x11probability_range\x18\t \x01(\x0b\x32\x17.sysml.ProbabilityRange\x1a<\n\x0cOutputsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xb5\x01\n\x11\x45xplorationStatus\x12\x10\n\x08\x63omplete\x18\x01 \x01(\x08\x12\x0c\n\x04runs\x18\x02 \x01(\x05\x12\x13\n\x0b\x62udgets_hit\x18\x03 \x03(\t\x12\x13\n\x0bruns_budget\x18\x04 \x01(\x05\x12\x14\n\x0c\x64\x65pth_budget\x18\x05 \x01(\x05\x12!\n\x19probabilities_lower_bound\x18\x06 \x01(\x08\x12\x1d\n\x15\x66\x61iled_linearizations\x18\x07 \x01(\x05\"\x14\n\x12ListEnginesRequest\"\xfc\x01\n\nEngineInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\tauthority\x18\x02 \x01(\t\x12\x0f\n\x07\x61nswers\x18\x03 \x03(\t\x12\x0e\n\x06\x62ounds\x18\x04 \x03(\t\x12\x0f\n\x07process\x18\x05 \x01(\t\x12\x15\n\rprocess_found\x18\x06 \x01(\t\x12\r\n\x05ready\x18\x07 \x01(\x08\x12\x13\n\x0bunavailable\x18\x08 \x01(\t\x12\x0c\n\x04kind\x18\t \x01(\t\x12\x10\n\x08protocol\x18\n \x01(\t\x12\x0e\n\x06source\x18\x0b \x01(\t\x12\x0f\n\x07\x63ommand\x18\x0c \x01(\t\x12\x0f\n\x07version\x18\r \x01(\t\x12\x0e\n\x06served\x18\x0e \x01(\x08\"9\n\x13ListEnginesResponse\x12\"\n\x07\x65ngines\x18\x01 \x03(\x0b\x32\x11.sysml.EngineInfo\"\x8c\x01\n\x10ParseFileRequest\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x18\n\x0c\x63ontent_hash\x18\x03 \x01(\tB\x02\x18\x01\x12\x10\n\x08language\x18\x04 \x01(\t\x12\x1a\n\x12strict_conformance\x18\x05 \x01(\x08\x42\x08\n\x06source\"b\n\x0eSourceDocument\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x10\n\x08language\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x04 \x01(\tB\x08\n\x06source\"[\n\x13ParseSourcesRequest\x12(\n\tdocuments\x18\x01 \x03(\x0b\x32\x15.sysml.SourceDocument\x12\x1a\n\x12strict_conformance\x18\x02 \x01(\x08\"\x83\x01\n\x14ParseSourcesResponse\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12 \n\x05roots\x18\x02 \x03(\x0b\x32\x11.sysml.SymbolInfo\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\x7f\n\x11ParseFileResponse\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1f\n\x04root\x18\x02 \x01(\x0b\x32\x11.sysml.SymbolInfo\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"9\n\x10GetSymbolRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\"B\n\x0eSymbolResponse\x12!\n\x06symbol\x18\x01 \x01(\x0b\x32\x11.sysml.SymbolInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"(\n\x12\x44iagnosticsRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\"L\n\x13\x44iagnosticsResponse\x12&\n\x0b\x64iagnostics\x18\x01 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"o\n\x0f\x45valuateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x12\n\nexpression\x18\x02 \x01(\t\x12\x19\n\x11\x63ontext_symbol_id\x18\x03 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x04 \x01(\t\"g\n\x10\x45valuateResponse\x12\x1c\n\x06result\x18\x01 \x01(\x0b\x32\x0c.sysml.Value\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\"\xc2\x01\n\x08Instance\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x16\n\x0etype_symbol_id\x18\x02 \x01(\t\x12:\n\x0e\x66\x65\x61ture_values\x18\x04 \x03(\x0b\x32\".sysml.Instance.FeatureValuesEntry\x1aI\n\x12\x46\x65\x61tureValuesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.sysml.FeatureValue:\x02\x38\x01J\x04\x08\x03\x10\x04R\x05slots\"\x84\x01\n\x0c\x46\x65\x61tureValue\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\x12\x1c\n\x06values\x18\x03 \x03(\x0b\x32\x0c.sysml.Value\x12\x14\n\x0cmaterialized\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\";\n\x12InstantiateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\"\x93\x01\n\x13InstantiateResponse\x12!\n\x08instance\x18\x01 \x01(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\"\n\tinstances\x18\x04 \x03(\x0b\x32\x0f.sysml.Instance\"\xe9\x01\n\x14\x45xecuteActionRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x18\n\x10\x61\x63tion_symbol_id\x18\x02 \x01(\t\x12\x37\n\x06inputs\x18\x03 \x03(\x0b\x32\'.sysml.ExecuteActionRequest.InputsEntry\x12\x10\n\x08schedule\x18\x04 \x01(\t\x12\x1b\n\x13performer_symbol_id\x18\x05 \x01(\t\x1a;\n\x0bInputsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xcc\x03\n\x15\x45xecuteActionResponse\x12:\n\x07outputs\x18\x01 \x03(\x0b\x32).sysml.ExecuteActionResponse.OutputsEntry\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12 \n\x08outcomes\x18\x04 \x03(\x0b\x32\x0e.sysml.Outcome\x12-\n\x0b\x65xploration\x18\x05 \x01(\x0b\x32\x18.sysml.ExplorationStatus\x12\x12\n\nfinal_time\x18\x06 \x01(\x01\x12S\n\x14performer_attributes\x18\x07 \x03(\x0b\x32\x35.sysml.ExecuteActionResponse.PerformerAttributesEntry\x1a<\n\x0cOutputsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\x1aH\n\x18PerformerAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\x89\x01\n\x13\x45xecuteStateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1f\n\x17state_machine_symbol_id\x18\x02 \x01(\t\x12\x0e\n\x06\x65vents\x18\x03 \x03(\t\x12\x10\n\x08schedule\x18\x04 \x01(\t\x12\x1b\n\x13performer_symbol_id\x18\x05 \x01(\t\"\xd3\x02\n\x14\x45xecuteStateResponse\x12\x16\n\x0estates_visited\x18\x01 \x03(\t\x12\x44\n\rfinal_context\x18\x02 \x03(\x0b\x32-.sysml.ExecuteStateResponse.FinalContextEntry\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12 \n\x08outcomes\x18\x05 \x03(\x0b\x32\x0e.sysml.Outcome\x12-\n\x0b\x65xploration\x18\x06 \x01(\x0b\x32\x18.sysml.ExplorationStatus\x12\x12\n\nfinal_time\x18\x07 \x01(\x01\x1a\x41\n\x11\x46inalContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xb1\x01\n\x0e\x43onvertRequest\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x14\n\nmodel_hash\x18\x06 \x01(\tH\x00\x12\x13\n\x0b\x66rom_format\x18\x03 \x01(\t\x12\x11\n\tto_format\x18\x04 \x01(\t\x12\x1e\n\x16tolerate_syntax_errors\x18\x05 \x01(\x08\x12\x0f\n\x07id_form\x18\x07 \x01(\tB\x08\n\x06source\"\xb4\x01\n\x0f\x43onvertResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x13\n\x0b\x66rom_format\x18\x02 \x01(\t\x12\x11\n\tto_format\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x14\n\x0c\x65xperimental\x18\x06 \x01(\x08\x12\x1b\n\x13\x65xperimental_notice\x18\x07 \x01(\t\"\xee\x01\n\x0eMigrateRequest\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\x0cH\x00\x12\x13\n\x0b\x66rom_format\x18\x03 \x01(\t\x12\x11\n\tto_format\x18\x04 \x01(\t\x12\x0e\n\x06report\x18\x05 \x01(\x08\x12\x0f\n\x07results\x18\x06 \x01(\x08\x12\x15\n\x0blayout_path\x18\x07 \x01(\tH\x01\x12\x18\n\x0elayout_content\x18\x08 \x01(\tH\x01\x12\x16\n\x0eimage_base_url\x18\t \x01(\t\x12\x0e\n\x06strict\x18\n \x01(\x08\x42\x08\n\x06sourceB\x08\n\x06layout\"\xea\x01\n\x0fMigrateResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x13\n\x0b\x66rom_format\x18\x02 \x01(\t\x12\x11\n\tto_format\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x14\n\x0c\x65xperimental\x18\x05 \x01(\x08\x12\x1b\n\x13\x65xperimental_notice\x18\x06 \x01(\t\x12&\n\x06report\x18\x07 \x01(\x0b\x32\x16.sysml.MigrationReport\x12\x0f\n\x07results\x18\x08 \x01(\t\x12#\n\x05\x66iles\x18\t \x03(\x0b\x32\x14.sysml.MigrationFile\"\xc3\x01\n\x0fMigrationReport\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\x10\n\x08\x65xporter\x18\x02 \x01(\t\x12\x0f\n\x07summary\x18\x03 \x01(\t\x12\x0e\n\x06mapped\x18\x04 \x01(\x05\x12\x14\n\x0c\x61pproximated\x18\x05 \x01(\x05\x12\x10\n\x08unmapped\x18\x06 \x01(\x05\x12\x0f\n\x07skipped\x18\x07 \x01(\x05\x12&\n\x07\x65ntries\x18\x08 \x03(\x0b\x32\x15.sysml.MigrationEntry\x12\x0c\n\x04text\x18\t \x01(\t\"g\n\x0eMigrationEntry\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0e\n\x06target\x18\x04 \x01(\t\x12\x0f\n\x07verdict\x18\x05 \x01(\t\x12\x0c\n\x04note\x18\x06 \x01(\t\".\n\rMigrationFile\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\x0c\"}\n\x11\x41pplyEditsRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12(\n\noperations\x18\x02 \x03(\x0b\x32\x14.sysml.EditOperation\x12\x10\n\x08\x64ocument\x18\x03 \x01(\t\x12\x18\n\x10\x61\x63\x63\x65pt_documents\x18\x04 \x01(\x08\"\xc3\x06\n\rEditOperation\x12(\n\tset_value\x18\x01 \x01(\x0b\x32\x13.sysml.SetValueEditH\x00\x12#\n\x06rename\x18\x02 \x01(\x0b\x32\x11.sysml.RenameEditH\x00\x12*\n\nadd_member\x18\x03 \x01(\x0b\x32\x14.sysml.AddMemberEditH\x00\x12#\n\x06\x64\x65lete\x18\x04 \x01(\x0b\x32\x11.sysml.DeleteEditH\x00\x12\x1f\n\x04move\x18\x05 \x01(\x0b\x32\x0f.sysml.MoveEditH\x00\x12\x32\n\x0e\x61\x64\x64_connection\x18\x06 \x01(\x0b\x32\x18.sysml.AddConnectionEditH\x00\x12,\n\x0b\x61\x64\x64_satisfy\x18\x07 \x01(\x0b\x32\x15.sysml.AddSatisfyEditH\x00\x12I\n\x1a\x61\x64\x64_requirement_constraint\x18\x08 \x01(\x0b\x32#.sysml.AddRequirementConstraintEditH\x00\x12\x32\n\x0e\x61\x64\x64_transition\x18\t \x01(\x0b\x32\x18.sysml.AddTransitionEditH\x00\x12*\n\nadd_import\x18\n \x01(\x0b\x32\x14.sysml.AddImportEditH\x00\x12\x38\n\x11\x61\x64\x64_documentation\x18\x0b \x01(\x0b\x32\x1b.sysml.AddDocumentationEditH\x00\x12*\n\nadd_verify\x18\x0c \x01(\x0b\x32\x14.sysml.AddVerifyEditH\x00\x12.\n\x0c\x61\x64\x64_metadata\x18\r \x01(\x0b\x32\x16.sysml.AddMetadataEditH\x00\x12.\n\x0c\x61\x64\x64_sequence\x18\x0e \x01(\x0b\x32\x16.sysml.AddSequenceEditH\x00\x12;\n\x13\x61\x64\x64_metadata_prefix\x18\x0f \x01(\x0b\x32\x1c.sysml.AddMetadataPrefixEditH\x00\x12,\n\x0b\x61\x64\x64_comment\x18\x10 \x01(\x0b\x32\x15.sysml.AddCommentEditH\x00\x12&\n\x08\x61\x64\x64_note\x18\x12 \x01(\x0b\x32\x12.sysml.AddNoteEditH\x00\x42\x0b\n\toperation\"\x92\x02\n\rAddMemberEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\t\x12\x14\n\x0cmultiplicity\x18\x05 \x01(\t\x12\r\n\x05value\x18\x06 \x01(\t\x12\x13\n\x0bspecializes\x18\x07 \x03(\t\x12\x13\n\x0bis_abstract\x18\x08 \x01(\x08\x12\x11\n\tredefines\x18\t \x03(\t\x12\x12\n\nis_default\x18\n \x01(\x08\x12\x11\n\tdirection\x18\x0b \x01(\t\x12\x19\n\x11metadata_prefixes\x18\x0e \x03(\t\x12\x0b\n\x03\x64oc\x18\x0c \x01(\t\x12\x17\n\x0f\x62ody_expression\x18\r \x01(\t\"y\n\x0e\x41\x64\x64SatisfyEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x13\n\x0brequirement\x18\x02 \x01(\t\x12\x1a\n\x12satisfying_feature\x18\x03 \x01(\t\x12\x13\n\x0bis_asserted\x18\x04 \x01(\x08\x12\x12\n\nis_negated\x18\x05 \x01(\x08\"]\n\x1c\x41\x64\x64RequirementConstraintEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x12\n\nexpression\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x04 \x01(\t\"\x91\x01\n\x11\x41\x64\x64TransitionEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x0e\n\x06target\x18\x04 \x01(\t\x12\x0f\n\x07trigger\x18\x05 \x01(\t\x12\r\n\x05guard\x18\x06 \x01(\t\x12\x0e\n\x06\x65\x66\x66\x65\x63t\x18\x07 \x01(\t\x12\x0f\n\x07initial\x18\x08 \x01(\x08\"3\n\rAddVerifyEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x13\n\x0brequirement\x18\x02 \x01(\t\"6\n\x14MetadataFeatureValue\x12\x0f\n\x07\x66\x65\x61ture\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x94\x01\n\x0f\x41\x64\x64MetadataEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x15\n\rmetadata_type\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\r\n\x05\x61\x62out\x18\x04 \x03(\t\x12+\n\x06values\x18\x05 \x03(\x0b\x32\x1b.sysml.MetadataFeatureValue\x12\x11\n\tshorthand\x18\x06 \x01(\x08\">\n\x15\x41\x64\x64MetadataPrefixEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x15\n\rmetadata_type\x18\x02 \x01(\t\"\x80\x01\n\rAddImportEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x12\n\nvisibility\x18\x02 \x01(\t\x12\x0e\n\x06target\x18\x03 \x01(\t\x12\x14\n\x0cis_recursive\x18\x04 \x01(\x08\x12\x15\n\ris_import_all\x18\x05 \x01(\x08\x12\x0f\n\x07\x66ilters\x18\x06 \x03(\t\"c\n\x14\x41\x64\x64\x44ocumentationEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0e\n\x06locale\x18\x04 \x01(\t\x12\x0f\n\x07replace\x18\x05 \x01(\x08\"Z\n\x0e\x41\x64\x64\x43ommentEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\r\n\x05\x61\x62out\x18\x04 \x03(\t\x12\x0e\n\x06locale\x18\x05 \x01(\t\"+\n\x0b\x41\x64\x64NoteEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\"\xcd\x02\n\x0f\x41\x64\x64SequenceEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0f\n\x07keyword\x18\x02 \x01(\t\x12\x0b\n\x03ref\x18\x03 \x01(\t\x12\x13\n\x0bmember_kind\x18\x04 \x01(\t\x12\x13\n\x0bmember_name\x18\x05 \x01(\t\x12\x0c\n\x04type\x18\x06 \x01(\t\x12\r\n\x05\x61\x66ter\x18\x07 \x01(\t\x12\x11\n\tcondition\x18\x08 \x01(\t\x12\r\n\x05value\x18\t \x01(\t\x12\x0e\n\x06target\x18\n \x01(\t\x12\x0b\n\x03via\x18\x0b \x01(\t\x12\r\n\x05until\x18\x0c \x01(\t\x12$\n\x04\x62ody\x18\r \x03(\x0b\x32\x16.sysml.AddSequenceEdit\x12)\n\telse_body\x18\x0e \x03(\x0b\x32\x16.sysml.AddSequenceEdit\x12\x14\n\x0cmultiplicity\x18\x0f \x01(\t\x12\x11\n\tparameter\x18\x10 \x01(\t\"n\n\x11\x41\x64\x64\x43onnectionEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x10\n\x08\x66rom_end\x18\x03 \x01(\t\x12\x0e\n\x06to_end\x18\x04 \x01(\t\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x0c\n\x04type\x18\x06 \x01(\t\"-\n\nDeleteEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x0f\n\x07\x63\x61scade\x18\x02 \x01(\x08\")\n\x08MoveEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\r\n\x05owner\x18\x02 \x01(\t\"-\n\x0cSetValueEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\".\n\nRenameEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x10\n\x08new_name\x18\x02 \x01(\t\"\x90\x02\n\x12\x41pplyEditsResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12#\n\x07\x61pplied\x18\x02 \x03(\x0b\x32\x12.sysml.AppliedEdit\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12#\n\x07\x66\x61ilure\x18\x04 \x01(\x0e\x32\x12.sysml.EditFailure\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x1a\n\x12referring_elements\x18\x06 \x03(\t\x12(\n\tdocuments\x18\x07 \x03(\x0b\x32\x15.sysml.EditedDocument\x12\"\n\treferrers\x18\x08 \x03(\x0b\x32\x0f.sysml.Referrer\"/\n\x0e\x45\x64itedDocument\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\"*\n\x08Referrer\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08\x64ocument\x18\x02 \x01(\t\"\x8c\x01\n\x0b\x41ppliedEdit\x12\x17\n\x0foperation_index\x18\x01 \x01(\x05\x12\x0e\n\x06target\x18\x02 \x01(\t\x12\x0e\n\x06offset\x18\x03 \x01(\x05\x12\x0e\n\x06length\x18\x04 \x01(\x05\x12\x10\n\x08old_text\x18\x05 \x01(\t\x12\x10\n\x08new_text\x18\x06 \x01(\t\x12\x10\n\x08\x64ocument\x18\x07 \x01(\t\"\xfd\x02\n\nSymbolInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04kind\x18\x03 \x01(\t\x12\x31\n\x08metadata\x18\x04 \x03(\x0b\x32\x1f.sysml.SymbolInfo.MetadataEntry\x12\x11\n\tchild_ids\x18\x05 \x03(\t\x12(\n\nattributes\x18\x06 \x03(\x0b\x32\x14.sysml.AttributeInfo\x12\"\n\ttype_info\x18\x07 \x01(\x0b\x32\x0f.sysml.TypeInfo\x12-\n\x0cmultiplicity\x18\x08 \x01(\x0b\x32\x17.sysml.MultiplicityInfo\x12.\n\x0fspecializations\x18\t \x03(\x0b\x32\x15.sysml.Specialization\x12#\n\x1bwithheld_library_attributes\x18\n \x01(\x05\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"X\n\x0eSpecialization\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x10\n\x08\x64\x65\x63lared\x18\x02 \x01(\t\x12\x11\n\ttarget_id\x18\x03 \x01(\t\x12\x13\n\x0btarget_kind\x18\x04 \x01(\t\"\x95\x01\n\x08TypeInfo\x12\x10\n\x08\x64\x65\x63lared\x18\x01 \x01(\t\x12\x13\n\x0bresolved_id\x18\x02 \x01(\t\x12\x15\n\rresolved_kind\x18\x03 \x01(\t\x12\x11\n\tprimitive\x18\x04 \x01(\t\x12\x18\n\x10primitive_source\x18\x05 \x01(\t\x12\x10\n\x08quantity\x18\x06 \x01(\x08\x12\x0c\n\x04unit\x18\x07 \x01(\t\"0\n\x10MultiplicityInfo\x12\r\n\x05lower\x18\x01 \x01(\t\x12\r\n\x05upper\x18\x02 \x01(\t\"V\n\rAttributeInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x1b\n\x05value\x18\x03 \x01(\x0b\x32\x0c.sysml.Value\x12\x0c\n\x04unit\x18\x04 \x01(\t\"\xdc\x05\n\x05Value\x12\x13\n\tint_value\x18\x01 \x01(\x03H\x00\x12\x14\n\nreal_value\x18\x02 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x03 \x01(\x08H\x00\x12\x16\n\x0cstring_value\x18\x04 \x01(\tH\x00\x12\x15\n\x0binstance_id\x18\x05 \x01(\x03H\x00\x12(\n\x08sequence\x18\x06 \x01(\x0b\x32\x14.sysml.ValueSequenceH\x00\x12\x0e\n\x04null\x18\x07 \x01(\tH\x00\x12#\n\x08quantity\x18\x08 \x01(\x0b\x32\x0f.sysml.QuantityH\x00\x12*\n\x0c\x65num_literal\x18\t \x01(\x0b\x32\x12.sysml.EnumLiteralH\x00\x12\x0f\n\x05unset\x18\n \x01(\x08H\x00\x12!\n\x07\x63omplex\x18\x0b \x01(\x0b\x32\x0e.sysml.ComplexH\x00\x12\x1d\n\x05\x61rray\x18\x0c \x01(\x0b\x32\x0c.sysml.ArrayH\x00\x12\x1f\n\x06vector\x18\r \x01(\x0b\x32\r.sysml.VectorH\x00\x12\x30\n\x0fvector_quantity\x18\x0e \x01(\x0b\x32\x15.sysml.VectorQuantityH\x00\x12\x30\n\x0fmeasurement_ref\x18\x0f \x01(\x0b\x32\x15.sysml.MeasurementRefH\x00\x12\x12\n\x08infinity\x18\x10 \x01(\x08H\x00\x12#\n\x08\x66unction\x18\x11 \x01(\x0b\x32\x0f.sysml.FunctionH\x00\x12\x1e\n\x03set\x18\x12 \x01(\x0b\x32\x0f.sysml.ValueSetH\x00\x12\x30\n\x0ftensor_quantity\x18\x13 \x01(\x0b\x32\x15.sysml.TensorQuantityH\x00\x12\'\n\nmetaobject\x18\x14 \x01(\x0b\x32\x11.sysml.MetaobjectH\x00\x12+\n\x0cundetermined\x18\x15 \x01(\x0b\x32\x13.sysml.UndeterminedH\x00\x12\x17\n\rbig_int_value\x18\x16 \x01(\tH\x00\x42\x06\n\x04kind\"6\n\nMetaobject\x12\x12\n\nelement_id\x18\x01 \x01(\t\x12\x14\n\x0cmetaclass_id\x18\x02 \x01(\t\"F\n\x0cUndetermined\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12&\n\x05\x63ount\x18\x02 \x01(\x0b\x32\x17.sysml.MultiplicityInfo\",\n\x08\x46unction\x12\x0f\n\x07\x63\x61lc_id\x18\x01 \x01(\t\x12\x0f\n\x07self_id\x18\x02 \x01(\x03\"*\n\x08ValueSet\x12\x1e\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"I\n\x0eTensorQuantity\x12\x12\n\ndimensions\x18\x01 \x03(\x03\x12#\n\ncomponents\x18\x02 \x03(\x0b\x32\x0f.sysml.Quantity\";\n\x05\x41rray\x12\x12\n\ndimensions\x18\x01 \x03(\x03\x12\x1e\n\x08\x65lements\x18\x02 \x03(\x0b\x32\x0c.sysml.Value\"*\n\x06Vector\x12 \n\ncomponents\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"5\n\x0eVectorQuantity\x12#\n\ncomponents\x18\x01 \x03(\x0b\x32\x0f.sysml.Quantity\"*\n\x07\x43omplex\x12\x0c\n\x04real\x18\x01 \x01(\x01\x12\x11\n\timaginary\x18\x02 \x01(\x01\"d\n\x0b\x45numLiteral\x12\x12\n\nliteral_id\x18\x01 \x01(\t\x12\x16\n\x0e\x65numeration_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x1b\n\x05value\x18\x04 \x01(\x0b\x32\x0c.sysml.Value\"/\n\rValueSequence\x12\x1e\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"\x99\x01\n\x08Quantity\x12\x17\n\rint_magnitude\x18\x01 \x01(\x03H\x00\x12\x18\n\x0ereal_magnitude\x18\x02 \x01(\x01H\x00\x12\x1b\n\x11\x62ig_int_magnitude\x18\x05 \x01(\tH\x00\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\"\n\tunit_term\x18\x04 \x01(\x0b\x32\x0f.sysml.UnitTermB\x0b\n\tmagnitude\"S\n\x0eMeasurementRef\x12\x0c\n\x04unit\x18\x01 \x01(\t\x12\"\n\tunit_term\x18\x02 \x01(\x0b\x32\x0f.sysml.UnitTerm\x12\x0f\n\x07unit_id\x18\x03 \x01(\t\"T\n\x08UnitTerm\x12\x11\n\tscale_num\x18\x01 \x01(\x01\x12\x11\n\tscale_den\x18\x02 \x01(\x01\x12\"\n\x07\x66\x61\x63tors\x18\x03 \x03(\x0b\x32\x11.sysml.UnitFactor\"/\n\nUnitFactor\x12\x0f\n\x07unit_id\x18\x01 \x01(\t\x12\x10\n\x08\x65xponent\x18\x02 \x01(\x01\"X\n\nDiagnostic\x12\x10\n\x08severity\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x19\n\x04span\x18\x03 \x01(\x0b\x32\x0b.sysml.Span\x12\x0c\n\x04\x63ode\x18\x04 \x01(\t\"^\n\x04Span\x12\x0c\n\x04\x66ile\x18\x01 \x01(\t\x12\x12\n\nstart_line\x18\x02 \x01(\x05\x12\x11\n\tstart_col\x18\x03 \x01(\x05\x12\x10\n\x08\x65nd_line\x18\x04 \x01(\x05\x12\x0f\n\x07\x65nd_col\x18\x05 \x01(\x05\"\x13\n\x11ServerInfoRequest\";\n\x12ServerInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x02 \x03(\t\"S\n\x0cQueryRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1b\n\x05query\x18\x02 \x01(\x0b\x32\x0c.sysml.Query\x12\x12\n\noslc_query\x18\x03 \x01(\t\"<\n\rQueryResponse\x12+\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x19.sysml.QueryResultElement\"H\n\x05Query\x12\r\n\x05scope\x18\x01 \x03(\t\x12\x0e\n\x06select\x18\x02 \x03(\t\x12 \n\x05where\x18\x03 \x01(\x0b\x32\x11.sysml.Constraint\"|\n\nConstraint\x12/\n\tprimitive\x18\x01 \x01(\x0b\x32\x1a.sysml.PrimitiveConstraintH\x00\x12/\n\tcomposite\x18\x02 \x01(\x0b\x32\x1a.sysml.CompositeConstraintH\x00\x42\x0c\n\nconstraint\"s\n\x13PrimitiveConstraint\x12\x0f\n\x07inverse\x18\x01 \x01(\x08\x12\x10\n\x08property\x18\x02 \x01(\t\x12*\n\x08operator\x18\x03 \x01(\x0e\x32\x18.sysml.PrimitiveOperator\x12\r\n\x05value\x18\x04 \x03(\t\"h\n\x13\x43ompositeConstraint\x12*\n\x08operator\x18\x01 \x01(\x0e\x32\x18.sysml.CompositeOperator\x12%\n\nconstraint\x18\x02 \x03(\x0b\x32\x11.sysml.Constraint\"\xa0\x01\n\x12QueryResultElement\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12=\n\nproperties\x18\x03 \x03(\x0b\x32).sysml.QueryResultElement.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"s\n\nSweepRange\x12\x11\n\tparameter\x18\x01 \x01(\t\x12\x1b\n\x05start\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\x12\x19\n\x03\x65nd\x18\x03 \x01(\x0b\x32\x0c.sysml.Value\x12\x1a\n\x04step\x18\x04 \x01(\x0b\x32\x0c.sysml.Value\"\xd0\x02\n\x0fRunSweepRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x1f\n\targuments\x18\x04 \x03(\x0b\x32\x0c.sysml.Value\x12\x43\n\x0fnamed_arguments\x18\x05 \x03(\x0b\x32*.sysml.RunSweepRequest.NamedArgumentsEntry\x12!\n\x06ranges\x18\x06 \x03(\x0b\x32\x11.sysml.SweepRange\x12\x0f\n\x07samples\x18\x07 \x01(\x03\x12\x0c\n\x04seed\x18\x08 \x01(\x04\x12\x0e\n\x06\x65ngine\x18\t \x01(\t\x1a\x43\n\x13NamedArgumentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xf4\x01\n\x08SweepRow\x12!\n\x06inputs\x18\x01 \x03(\x0b\x32\x11.sysml.CalcOutput\x12\"\n\x07outputs\x18\x02 \x03(\x0b\x32\x11.sysml.CalcOutput\x12 \n\x08verdicts\x18\x03 \x03(\x0b\x32\x0e.sysml.Verdict\x12\x16\n\x0e\x65lapsed_micros\x18\x04 \x01(\x03\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12,\n\x0e\x66\x61ilure_reason\x18\x06 \x01(\x0e\x32\x14.sysml.FailureReason\x12*\n\x0b\x65valuations\x18\x07 \x03(\x0b\x32\x15.sysml.CaseEvaluation\"\xad\x02\n\x10RunSweepResponse\x12\x1d\n\x04rows\x18\x01 \x03(\x0b\x32\x0f.sysml.SweepRow\x12\x12\n\nparameters\x18\x02 \x03(\t\x12\x0f\n\x07sampled\x18\x03 \x01(\x08\x12\x0c\n\x04seed\x18\x04 \x01(\x04\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x06 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x07 \x01(\x0e\x32\x14.sysml.FailureReason\x12\"\n\tinstances\x18\x08 \x03(\x0b\x32\x0f.sysml.Instance\x12\x0e\n\x06\x65ngine\x18\t \x01(\t\x12\x10\n\x08strength\x18\n \x01(\t\x12\x1c\n\x06\x62ounds\x18\x0b \x03(\x0b\x32\x0c.sysml.Bound\"n\n\x17RunDocumentQueryRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x10\n\x08query_id\x18\x02 \x01(\t\x12-\n\x08\x62indings\x18\x03 \x03(\x0b\x32\x1b.sysml.DocumentQueryBinding\"O\n\x14\x44ocumentQueryBinding\x12\x11\n\tparameter\x18\x01 \x01(\t\x12$\n\x06values\x18\x02 \x03(\x0b\x32\x14.sysml.DocumentValue\"\x90\x03\n\rDocumentValue\x12\x14\n\nelement_id\x18\x01 \x01(\tH\x00\x12\x16\n\x0cstring_value\x18\x02 \x01(\tH\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x14\n\nreal_value\x18\x04 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x05 \x01(\x08H\x00\x12\x12\n\x08infinity\x18\x06 \x01(\x08H\x00\x12#\n\x08quantity\x18\x08 \x01(\x0b\x32\x0f.sysml.QuantityH\x00\x12)\n\x07verdict\x18\t \x01(\x0b\x32\x16.sysml.DocumentVerdictH\x00\x12\'\n\x06object\x18\n \x01(\x0b\x32\x15.sysml.DocumentObjectH\x00\x12%\n\x05state\x18\x0b \x01(\x0b\x32\x14.sysml.DocumentStateH\x00\x12%\n\x05\x65vent\x18\x0c \x01(\x0b\x32\x14.sysml.DocumentEventH\x00\x12\x17\n\rbig_int_value\x18\r \x01(\tH\x00\x12\x14\n\x0c\x65lement_type\x18\x07 \x01(\tB\x06\n\x04kind\"Z\n\x0e\x44ocumentObject\x12\x13\n\x0binstance_id\x18\x01 \x01(\x03\x12\x0c\n\x04path\x18\x02 \x01(\t\x12%\n\x07\x65lement\x18\x03 \x01(\x0b\x32\x14.sysml.DocumentValue\"\xae\x01\n\x0f\x44ocumentVerdict\x12\'\n\tassertion\x18\x01 \x01(\x0b\x32\x14.sysml.DocumentValue\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x0c\n\x04path\x18\x04 \x01(\t\x12\x0f\n\x07verdict\x18\x05 \x01(\t\x12\x11\n\tcondition\x18\x06 \x01(\t\x12\x0e\n\x06reason\x18\x07 \x01(\t\x12\x14\n\x0cverification\x18\x08 \x03(\t\"\xb1\x01\n\rDocumentState\x12%\n\x06object\x18\x01 \x01(\x0b\x32\x15.sysml.DocumentObject\x12\x0f\n\x07machine\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x12\n\nstate_path\x18\x04 \x01(\t\x12#\n\x05state\x18\x05 \x01(\x0b\x32\x14.sysml.DocumentValue\x12\x0e\n\x06region\x18\x06 \x01(\t\x12\x11\n\tenclosing\x18\x07 \x03(\t\"\x9c\x02\n\rDocumentEvent\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\"\n\x04time\x18\x02 \x01(\x0b\x32\x14.sysml.DocumentValue\x12%\n\x06object\x18\x03 \x01(\x0b\x32\x15.sysml.DocumentObject\x12\x0f\n\x07machine\x18\x04 \x01(\t\x12\r\n\x05state\x18\x05 \x01(\t\x12\x0c\n\x04\x66rom\x18\x06 \x01(\t\x12\n\n\x02to\x18\x07 \x01(\t\x12%\n\x06target\x18\x08 \x01(\x0b\x32\x15.sysml.DocumentObject\x12\r\n\x05\x65vent\x18\t \x01(\t\x12\x0f\n\x07payload\x18\n \x03(\t\x12\x14\n\x0c\x61lternatives\x18\x0b \x03(\t\x12\r\n\x05taken\x18\x0c \x01(\t\x12\x0c\n\x04text\x18\r \x01(\t\"#\n\x13\x44ocumentQueryColumn\x12\x0c\n\x04name\x18\x01 \x01(\t\"9\n\x11\x44ocumentQueryCell\x12$\n\x06values\x18\x01 \x03(\x0b\x32\x14.sysml.DocumentValue\"b\n\x10\x44ocumentQueryRow\x12%\n\x07\x65lement\x18\x01 \x01(\x0b\x32\x14.sysml.DocumentValue\x12\'\n\x05\x63\x65lls\x18\x02 \x03(\x0b\x32\x18.sysml.DocumentQueryCell\"n\n\x18RunDocumentQueryResponse\x12+\n\x07\x63olumns\x18\x01 \x03(\x0b\x32\x1a.sysml.DocumentQueryColumn\x12%\n\x04rows\x18\x02 \x03(\x0b\x32\x17.sysml.DocumentQueryRow\"N\n\x15RenderDocumentRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x02 \x01(\t\x12\x0c\n\x04\x66orm\x18\x03 \x01(\t\"8\n\x16RenderDocumentResponse\x12\x10\n\x08markdown\x18\x01 \x01(\t\x12\x0c\n\x04html\x18\x02 \x01(\t*\xb1\x01\n\rFailureReason\x12\x1e\n\x1a\x46\x41ILURE_REASON_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x46\x41ILURE_REASON_EVALUATION\x10\x01\x12\x1d\n\x19\x46\x41ILURE_REASON_WRONG_KIND\x10\x02\x12$\n FAILURE_REASON_AMBIGUOUS_SUBJECT\x10\x03\x12\x1c\n\x18\x46\x41ILURE_REASON_UNDECIDED\x10\x04*\x8c\x05\n\x0b\x45\x64itFailure\x12\x1c\n\x18\x45\x44IT_FAILURE_UNSPECIFIED\x10\x00\x12\x1e\n\x1a\x45\x44IT_FAILURE_NO_OPERATIONS\x10\x01\x12\x1f\n\x1b\x45\x44IT_FAILURE_UNKNOWN_TARGET\x10\x02\x12!\n\x1d\x45\x44IT_FAILURE_AMBIGUOUS_TARGET\x10\x03\x12\x1b\n\x17\x45\x44IT_FAILURE_NOT_VALUED\x10\x04\x12\x1e\n\x1a\x45\x44IT_FAILURE_INVALID_VALUE\x10\x05\x12\x1d\n\x19\x45\x44IT_FAILURE_INVALID_NAME\x10\x06\x12\x1a\n\x16\x45\x44IT_FAILURE_NOT_NAMED\x10\x07\x12\"\n\x1e\x45\x44IT_FAILURE_RENAME_REFERENCED\x10\x08\x12\"\n\x1e\x45\x44IT_FAILURE_OVERLAPPING_EDITS\x10\t\x12\x1f\n\x1b\x45\x44IT_FAILURE_RESULT_INVALID\x10\n\x12\x1e\n\x1a\x45\x44IT_FAILURE_OWNER_UNKNOWN\x10\x0b\x12$\n EDIT_FAILURE_OWNER_NOT_NAMESPACE\x10\x0c\x12\x1d\n\x19\x45\x44IT_FAILURE_ILLEGAL_KIND\x10\r\x12\"\n\x1e\x45\x44IT_FAILURE_MEMBER_NAME_TAKEN\x10\x0e\x12\"\n\x1e\x45\x44IT_FAILURE_DELETE_REFERENCED\x10\x0f\x12$\n EDIT_FAILURE_OWNER_INSIDE_TARGET\x10\x10\x12 \n\x1c\x45\x44IT_FAILURE_MOVE_REFERENCED\x10\x11\x12%\n!EDIT_FAILURE_REFERENCED_ELSEWHERE\x10\x12*\x92\x01\n\x11PrimitiveOperator\x12\"\n\x1ePRIMITIVE_OPERATOR_UNSPECIFIED\x10\x00\x12\x1c\n\x18PRIMITIVE_OPERATOR_EQUAL\x10\x01\x12\x1e\n\x1aPRIMITIVE_OPERATOR_GREATER\x10\x02\x12\x1b\n\x17PRIMITIVE_OPERATOR_LESS\x10\x03*n\n\x11\x43ompositeOperator\x12\"\n\x1e\x43OMPOSITE_OPERATOR_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x43OMPOSITE_OPERATOR_AND\x10\x01\x12\x19\n\x15\x43OMPOSITE_OPERATOR_OR\x10\x02\x32\xf9\x0c\n\x0cSysMLService\x12\x44\n\rGetServerInfo\x12\x18.sysml.ServerInfoRequest\x1a\x19.sysml.ServerInfoResponse\x12>\n\tParseFile\x12\x17.sysml.ParseFileRequest\x1a\x18.sysml.ParseFileResponse\x12G\n\x0cParseSources\x12\x1a.sysml.ParseSourcesRequest\x1a\x1b.sysml.ParseSourcesResponse\x12;\n\tGetSymbol\x12\x17.sysml.GetSymbolRequest\x1a\x15.sysml.SymbolResponse\x12G\n\x0eGetDiagnostics\x12\x19.sysml.DiagnosticsRequest\x1a\x1a.sysml.DiagnosticsResponse\x12;\n\x08\x45valuate\x12\x16.sysml.EvaluateRequest\x1a\x17.sysml.EvaluateResponse\x12\x44\n\x0bInstantiate\x12\x19.sysml.InstantiateRequest\x1a\x1a.sysml.InstantiateResponse\x12J\n\rExecuteAction\x12\x1b.sysml.ExecuteActionRequest\x1a\x1c.sysml.ExecuteActionResponse\x12G\n\x0c\x45xecuteState\x12\x1a.sysml.ExecuteStateRequest\x1a\x1b.sysml.ExecuteStateResponse\x12\x38\n\x07\x43onvert\x12\x15.sysml.ConvertRequest\x1a\x16.sysml.ConvertResponse\x12\x38\n\x07Migrate\x12\x15.sysml.MigrateRequest\x1a\x16.sysml.MigrateResponse\x12\x41\n\nApplyEdits\x12\x18.sysml.ApplyEditsRequest\x1a\x19.sysml.ApplyEditsResponse\x12S\n\x10VerifyConstraint\x12\x1e.sysml.VerifyConstraintRequest\x1a\x1f.sysml.VerifyConstraintResponse\x12V\n\x11VerifyRequirement\x12\x1f.sysml.VerifyRequirementRequest\x1a .sysml.VerifyRequirementResponse\x12Y\n\x12VerifySatisfaction\x12 .sysml.VerifySatisfactionRequest\x1a!.sysml.VerifySatisfactionResponse\x12S\n\x10ValidateInstance\x12\x1e.sysml.ValidateInstanceRequest\x1a\x1f.sysml.ValidateInstanceResponse\x12G\n\x0c\x45valuateCalc\x12\x1a.sysml.EvaluateCalcRequest\x1a\x1b.sysml.EvaluateCalcResponse\x12\x44\n\x0bRunAnalysis\x12\x19.sysml.RunAnalysisRequest\x1a\x1a.sysml.RunAnalysisResponse\x12;\n\x08RunSweep\x12\x16.sysml.RunSweepRequest\x1a\x17.sysml.RunSweepResponse\x12\x44\n\x0bListEngines\x12\x19.sysml.ListEnginesRequest\x1a\x1a.sysml.ListEnginesResponse\x12\x32\n\x05Query\x12\x13.sysml.QueryRequest\x1a\x14.sysml.QueryResponse\x12S\n\x10RunDocumentQuery\x12\x1e.sysml.RunDocumentQueryRequest\x1a\x1f.sysml.RunDocumentQueryResponse\x12M\n\x0eRenderDocument\x12\x1c.sysml.RenderDocumentRequest\x1a\x1d.sysml.RenderDocumentResponseB*Z(github.com/Open-MBEE/OpenSysML/api/protob\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bsysml.proto\x12\x05sysml\"\x86\x03\n\x07Verdict\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x12\n\nelement_id\x18\x02 \x01(\t\x12\x0f\n\x07\x65lement\x18\x03 \x01(\t\x12\r\n\x05holds\x18\x04 \x01(\x08\x12\x11\n\tcondition\x18\x05 \x01(\t\x12\x13\n\x0binstance_id\x18\x06 \x01(\x03\x12\x18\n\x10instance_type_id\x18\x07 \x01(\t\x12\r\n\x05\x65rror\x18\x08 \x01(\t\x12,\n\x0e\x66\x61ilure_reason\x18\t \x01(\x0e\x32\x14.sysml.FailureReason\x12\x16\n\x0erequirement_id\x18\n \x01(\t\x12\x0e\n\x06\x65ngine\x18\x0b \x01(\t\x12\x10\n\x08strength\x18\x0c \x01(\t\x12\x1c\n\x06\x62ounds\x18\r \x03(\x0b\x32\x0c.sysml.Bound\x12\x15\n\rinstance_path\x18\x0e \x01(\t\x12\x10\n\x08question\x18\x0f \x01(\t\x12\x0e\n\x06status\x18\x10 \x01(\t\x12)\n\x07witness\x18\x11 \x03(\x0b\x32\x18.sysml.WitnessAssignment\"^\n\x11WitnessAssignment\x12\x0f\n\x07\x66\x65\x61ture\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\r\n\x05\x65xact\x18\x04 \x01(\t\"5\n\x05\x42ound\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x03\x12\x0f\n\x07reached\x18\x03 \x01(\x08\"}\n\x17VerifyConstraintRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x0e\n\x06\x65ngine\x18\x04 \x01(\t\x12\x10\n\x08question\x18\x05 \x01(\t\"\x96\x01\n\x18VerifyConstraintResponse\x12\x1f\n\x07verdict\x18\x01 \x01(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\"~\n\x18VerifyRequirementRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x0e\n\x06\x65ngine\x18\x04 \x01(\t\x12\x10\n\x08question\x18\x05 \x01(\t\"m\n\x13VerificationVerdict\x12\x0f\n\x07\x63\x61se_id\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x03 \x01(\t\x12\x0f\n\x07subcase\x18\x04 \x01(\x08\x12\x16\n\x0erequirement_id\x18\x05 \x01(\t\"\xd2\x01\n\x19VerifyRequirementResponse\x12\x1f\n\x07verdict\x18\x01 \x01(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x39\n\x15verification_verdicts\x18\x05 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\"d\n\x19VerifySatisfactionRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x0e\n\x06\x65ngine\x18\x03 \x01(\t\x12\x10\n\x08question\x18\x04 \x01(\t\"\x82\x02\n\x1aVerifySatisfactionResponse\x12 \n\x08verdicts\x18\x01 \x03(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x05 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x39\n\x15verification_verdicts\x18\x06 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\"P\n\x17ValidateInstanceRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x0e\n\x06\x65ngine\x18\x03 \x01(\t\"\xb2\x02\n\x18ValidateInstanceResponse\x12 \n\x08verdicts\x18\x01 \x03(\x0b\x32\x0e.sysml.Verdict\x12\x1f\n\x07summary\x18\x02 \x01(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x03 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x06 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x39\n\x15verification_verdicts\x18\x07 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\x12\x0f\n\x07\x62ounded\x18\x08 \x01(\x08\"m\n\x13\x45valuateCalcRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x1f\n\targuments\x18\x03 \x03(\x0b\x32\x0c.sysml.Value\x12\x0e\n\x06\x65ngine\x18\x04 \x01(\t\"\xfd\x01\n\x14\x45valuateCalcResponse\x12\x1c\n\x06result\x18\x01 \x01(\x0b\x32\x0c.sysml.Value\x12\"\n\x07outputs\x18\x02 \x03(\x0b\x32\x11.sysml.CalcOutput\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x05 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x0e\n\x06\x65ngine\x18\x06 \x01(\t\x12\x10\n\x08strength\x18\x07 \x01(\t\x12\x1c\n\x06\x62ounds\x18\x08 \x03(\x0b\x32\x0c.sysml.Bound\"7\n\nCalcOutput\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\"\x93\x01\n\x0e\x43\x61seEvaluation\x12\x13\n\x0b\x66unction_id\x18\x01 \x01(\t\x12\x1f\n\targuments\x18\x02 \x03(\x0b\x32\x0c.sysml.Value\x12\x1c\n\x06result\x18\x03 \x01(\x0b\x32\x0c.sysml.Value\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x10\n\x08selected\x18\x05 \x01(\x08\x12\x0c\n\x04tied\x18\x06 \x01(\x08\"\xa6\x02\n\x12RunAnalysisRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x1f\n\targuments\x18\x04 \x03(\x0b\x32\x0c.sysml.Value\x12\x46\n\x0fnamed_arguments\x18\x05 \x03(\x0b\x32-.sysml.RunAnalysisRequest.NamedArgumentsEntry\x12\x10\n\x08schedule\x18\x06 \x01(\t\x12\x0e\n\x06\x65ngine\x18\x07 \x01(\t\x1a\x43\n\x13NamedArgumentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xdc\x03\n\x13RunAnalysisResponse\x12\"\n\x07outputs\x18\x01 \x03(\x0b\x32\x11.sysml.CalcOutput\x12 \n\x08verdicts\x18\x02 \x03(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x03 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x06 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x39\n\x15verification_verdicts\x18\x07 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\x12 \n\x08outcomes\x18\x08 \x03(\x0b\x32\x0e.sysml.Outcome\x12-\n\x0b\x65xploration\x18\t \x01(\x0b\x32\x18.sysml.ExplorationStatus\x12*\n\x0b\x65valuations\x18\n \x03(\x0b\x32\x15.sysml.CaseEvaluation\x12\x0e\n\x06\x65ngine\x18\x0b \x01(\t\x12\x10\n\x08strength\x18\x0c \x01(\t\x12\x1c\n\x06\x62ounds\x18\r \x03(\x0b\x32\x0c.sysml.Bound\",\n\x10ProbabilityRange\x12\x0b\n\x03min\x18\x01 \x01(\x01\x12\x0b\n\x03max\x18\x02 \x01(\x01\"\xcb\x02\n\x07Outcome\x12,\n\x07outputs\x18\x01 \x03(\x0b\x32\x1b.sysml.Outcome.OutputsEntry\x12\x13\n\x0b\x66inal_state\x18\x02 \x01(\t\x12\x16\n\x0estates_visited\x18\x03 \x03(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x16\n\x0elinearizations\x18\x05 \x01(\x05\x12\x0f\n\x07witness\x18\x06 \x03(\t\x12&\n\x0b\x64iagnostics\x18\x07 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x13\n\x0bprobability\x18\x08 \x01(\x01\x12\x32\n\x11probability_range\x18\t \x01(\x0b\x32\x17.sysml.ProbabilityRange\x1a<\n\x0cOutputsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xb5\x01\n\x11\x45xplorationStatus\x12\x10\n\x08\x63omplete\x18\x01 \x01(\x08\x12\x0c\n\x04runs\x18\x02 \x01(\x05\x12\x13\n\x0b\x62udgets_hit\x18\x03 \x03(\t\x12\x13\n\x0bruns_budget\x18\x04 \x01(\x05\x12\x14\n\x0c\x64\x65pth_budget\x18\x05 \x01(\x05\x12!\n\x19probabilities_lower_bound\x18\x06 \x01(\x08\x12\x1d\n\x15\x66\x61iled_linearizations\x18\x07 \x01(\x05\"\x14\n\x12ListEnginesRequest\"\xfc\x01\n\nEngineInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\tauthority\x18\x02 \x01(\t\x12\x0f\n\x07\x61nswers\x18\x03 \x03(\t\x12\x0e\n\x06\x62ounds\x18\x04 \x03(\t\x12\x0f\n\x07process\x18\x05 \x01(\t\x12\x15\n\rprocess_found\x18\x06 \x01(\t\x12\r\n\x05ready\x18\x07 \x01(\x08\x12\x13\n\x0bunavailable\x18\x08 \x01(\t\x12\x0c\n\x04kind\x18\t \x01(\t\x12\x10\n\x08protocol\x18\n \x01(\t\x12\x0e\n\x06source\x18\x0b \x01(\t\x12\x0f\n\x07\x63ommand\x18\x0c \x01(\t\x12\x0f\n\x07version\x18\r \x01(\t\x12\x0e\n\x06served\x18\x0e \x01(\x08\"9\n\x13ListEnginesResponse\x12\"\n\x07\x65ngines\x18\x01 \x03(\x0b\x32\x11.sysml.EngineInfo\"\x8c\x01\n\x10ParseFileRequest\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x18\n\x0c\x63ontent_hash\x18\x03 \x01(\tB\x02\x18\x01\x12\x10\n\x08language\x18\x04 \x01(\t\x12\x1a\n\x12strict_conformance\x18\x05 \x01(\x08\x42\x08\n\x06source\"b\n\x0eSourceDocument\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x10\n\x08language\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x04 \x01(\tB\x08\n\x06source\"[\n\x13ParseSourcesRequest\x12(\n\tdocuments\x18\x01 \x03(\x0b\x32\x15.sysml.SourceDocument\x12\x1a\n\x12strict_conformance\x18\x02 \x01(\x08\"\x83\x01\n\x14ParseSourcesResponse\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12 \n\x05roots\x18\x02 \x03(\x0b\x32\x11.sysml.SymbolInfo\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\x7f\n\x11ParseFileResponse\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1f\n\x04root\x18\x02 \x01(\x0b\x32\x11.sysml.SymbolInfo\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"9\n\x10GetSymbolRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\"B\n\x0eSymbolResponse\x12!\n\x06symbol\x18\x01 \x01(\x0b\x32\x11.sysml.SymbolInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"(\n\x12\x44iagnosticsRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\"L\n\x13\x44iagnosticsResponse\x12&\n\x0b\x64iagnostics\x18\x01 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"o\n\x0f\x45valuateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x12\n\nexpression\x18\x02 \x01(\t\x12\x19\n\x11\x63ontext_symbol_id\x18\x03 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x04 \x01(\t\"g\n\x10\x45valuateResponse\x12\x1c\n\x06result\x18\x01 \x01(\x0b\x32\x0c.sysml.Value\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\"\xc2\x01\n\x08Instance\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x16\n\x0etype_symbol_id\x18\x02 \x01(\t\x12:\n\x0e\x66\x65\x61ture_values\x18\x04 \x03(\x0b\x32\".sysml.Instance.FeatureValuesEntry\x1aI\n\x12\x46\x65\x61tureValuesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.sysml.FeatureValue:\x02\x38\x01J\x04\x08\x03\x10\x04R\x05slots\"\x84\x01\n\x0c\x46\x65\x61tureValue\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\x12\x1c\n\x06values\x18\x03 \x03(\x0b\x32\x0c.sysml.Value\x12\x14\n\x0cmaterialized\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\";\n\x12InstantiateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\"\x93\x01\n\x13InstantiateResponse\x12!\n\x08instance\x18\x01 \x01(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\"\n\tinstances\x18\x04 \x03(\x0b\x32\x0f.sysml.Instance\"\xe9\x01\n\x14\x45xecuteActionRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x18\n\x10\x61\x63tion_symbol_id\x18\x02 \x01(\t\x12\x37\n\x06inputs\x18\x03 \x03(\x0b\x32\'.sysml.ExecuteActionRequest.InputsEntry\x12\x10\n\x08schedule\x18\x04 \x01(\t\x12\x1b\n\x13performer_symbol_id\x18\x05 \x01(\t\x1a;\n\x0bInputsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xcc\x03\n\x15\x45xecuteActionResponse\x12:\n\x07outputs\x18\x01 \x03(\x0b\x32).sysml.ExecuteActionResponse.OutputsEntry\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12 \n\x08outcomes\x18\x04 \x03(\x0b\x32\x0e.sysml.Outcome\x12-\n\x0b\x65xploration\x18\x05 \x01(\x0b\x32\x18.sysml.ExplorationStatus\x12\x12\n\nfinal_time\x18\x06 \x01(\x01\x12S\n\x14performer_attributes\x18\x07 \x03(\x0b\x32\x35.sysml.ExecuteActionResponse.PerformerAttributesEntry\x1a<\n\x0cOutputsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\x1aH\n\x18PerformerAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\x89\x01\n\x13\x45xecuteStateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1f\n\x17state_machine_symbol_id\x18\x02 \x01(\t\x12\x0e\n\x06\x65vents\x18\x03 \x03(\t\x12\x10\n\x08schedule\x18\x04 \x01(\t\x12\x1b\n\x13performer_symbol_id\x18\x05 \x01(\t\"\xd3\x02\n\x14\x45xecuteStateResponse\x12\x16\n\x0estates_visited\x18\x01 \x03(\t\x12\x44\n\rfinal_context\x18\x02 \x03(\x0b\x32-.sysml.ExecuteStateResponse.FinalContextEntry\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12 \n\x08outcomes\x18\x05 \x03(\x0b\x32\x0e.sysml.Outcome\x12-\n\x0b\x65xploration\x18\x06 \x01(\x0b\x32\x18.sysml.ExplorationStatus\x12\x12\n\nfinal_time\x18\x07 \x01(\x01\x1a\x41\n\x11\x46inalContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xb1\x01\n\x0e\x43onvertRequest\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x14\n\nmodel_hash\x18\x06 \x01(\tH\x00\x12\x13\n\x0b\x66rom_format\x18\x03 \x01(\t\x12\x11\n\tto_format\x18\x04 \x01(\t\x12\x1e\n\x16tolerate_syntax_errors\x18\x05 \x01(\x08\x12\x0f\n\x07id_form\x18\x07 \x01(\tB\x08\n\x06source\"\xb4\x01\n\x0f\x43onvertResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x13\n\x0b\x66rom_format\x18\x02 \x01(\t\x12\x11\n\tto_format\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x14\n\x0c\x65xperimental\x18\x06 \x01(\x08\x12\x1b\n\x13\x65xperimental_notice\x18\x07 \x01(\t\"\xee\x01\n\x0eMigrateRequest\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\x0cH\x00\x12\x13\n\x0b\x66rom_format\x18\x03 \x01(\t\x12\x11\n\tto_format\x18\x04 \x01(\t\x12\x0e\n\x06report\x18\x05 \x01(\x08\x12\x0f\n\x07results\x18\x06 \x01(\x08\x12\x15\n\x0blayout_path\x18\x07 \x01(\tH\x01\x12\x18\n\x0elayout_content\x18\x08 \x01(\tH\x01\x12\x16\n\x0eimage_base_url\x18\t \x01(\t\x12\x0e\n\x06strict\x18\n \x01(\x08\x42\x08\n\x06sourceB\x08\n\x06layout\"\xea\x01\n\x0fMigrateResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x13\n\x0b\x66rom_format\x18\x02 \x01(\t\x12\x11\n\tto_format\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x14\n\x0c\x65xperimental\x18\x05 \x01(\x08\x12\x1b\n\x13\x65xperimental_notice\x18\x06 \x01(\t\x12&\n\x06report\x18\x07 \x01(\x0b\x32\x16.sysml.MigrationReport\x12\x0f\n\x07results\x18\x08 \x01(\t\x12#\n\x05\x66iles\x18\t \x03(\x0b\x32\x14.sysml.MigrationFile\"\xc3\x01\n\x0fMigrationReport\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\x10\n\x08\x65xporter\x18\x02 \x01(\t\x12\x0f\n\x07summary\x18\x03 \x01(\t\x12\x0e\n\x06mapped\x18\x04 \x01(\x05\x12\x14\n\x0c\x61pproximated\x18\x05 \x01(\x05\x12\x10\n\x08unmapped\x18\x06 \x01(\x05\x12\x0f\n\x07skipped\x18\x07 \x01(\x05\x12&\n\x07\x65ntries\x18\x08 \x03(\x0b\x32\x15.sysml.MigrationEntry\x12\x0c\n\x04text\x18\t \x01(\t\"g\n\x0eMigrationEntry\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0e\n\x06target\x18\x04 \x01(\t\x12\x0f\n\x07verdict\x18\x05 \x01(\t\x12\x0c\n\x04note\x18\x06 \x01(\t\".\n\rMigrationFile\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\x0c\"}\n\x11\x41pplyEditsRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12(\n\noperations\x18\x02 \x03(\x0b\x32\x14.sysml.EditOperation\x12\x10\n\x08\x64ocument\x18\x03 \x01(\t\x12\x18\n\x10\x61\x63\x63\x65pt_documents\x18\x04 \x01(\x08\"\xc3\x06\n\rEditOperation\x12(\n\tset_value\x18\x01 \x01(\x0b\x32\x13.sysml.SetValueEditH\x00\x12#\n\x06rename\x18\x02 \x01(\x0b\x32\x11.sysml.RenameEditH\x00\x12*\n\nadd_member\x18\x03 \x01(\x0b\x32\x14.sysml.AddMemberEditH\x00\x12#\n\x06\x64\x65lete\x18\x04 \x01(\x0b\x32\x11.sysml.DeleteEditH\x00\x12\x1f\n\x04move\x18\x05 \x01(\x0b\x32\x0f.sysml.MoveEditH\x00\x12\x32\n\x0e\x61\x64\x64_connection\x18\x06 \x01(\x0b\x32\x18.sysml.AddConnectionEditH\x00\x12,\n\x0b\x61\x64\x64_satisfy\x18\x07 \x01(\x0b\x32\x15.sysml.AddSatisfyEditH\x00\x12I\n\x1a\x61\x64\x64_requirement_constraint\x18\x08 \x01(\x0b\x32#.sysml.AddRequirementConstraintEditH\x00\x12\x32\n\x0e\x61\x64\x64_transition\x18\t \x01(\x0b\x32\x18.sysml.AddTransitionEditH\x00\x12*\n\nadd_import\x18\n \x01(\x0b\x32\x14.sysml.AddImportEditH\x00\x12\x38\n\x11\x61\x64\x64_documentation\x18\x0b \x01(\x0b\x32\x1b.sysml.AddDocumentationEditH\x00\x12*\n\nadd_verify\x18\x0c \x01(\x0b\x32\x14.sysml.AddVerifyEditH\x00\x12.\n\x0c\x61\x64\x64_metadata\x18\r \x01(\x0b\x32\x16.sysml.AddMetadataEditH\x00\x12.\n\x0c\x61\x64\x64_sequence\x18\x0e \x01(\x0b\x32\x16.sysml.AddSequenceEditH\x00\x12;\n\x13\x61\x64\x64_metadata_prefix\x18\x0f \x01(\x0b\x32\x1c.sysml.AddMetadataPrefixEditH\x00\x12,\n\x0b\x61\x64\x64_comment\x18\x10 \x01(\x0b\x32\x15.sysml.AddCommentEditH\x00\x12&\n\x08\x61\x64\x64_note\x18\x12 \x01(\x0b\x32\x12.sysml.AddNoteEditH\x00\x42\x0b\n\toperation\"\x92\x02\n\rAddMemberEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\t\x12\x14\n\x0cmultiplicity\x18\x05 \x01(\t\x12\r\n\x05value\x18\x06 \x01(\t\x12\x13\n\x0bspecializes\x18\x07 \x03(\t\x12\x13\n\x0bis_abstract\x18\x08 \x01(\x08\x12\x11\n\tredefines\x18\t \x03(\t\x12\x12\n\nis_default\x18\n \x01(\x08\x12\x11\n\tdirection\x18\x0b \x01(\t\x12\x19\n\x11metadata_prefixes\x18\x0e \x03(\t\x12\x0b\n\x03\x64oc\x18\x0c \x01(\t\x12\x17\n\x0f\x62ody_expression\x18\r \x01(\t\"y\n\x0e\x41\x64\x64SatisfyEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x13\n\x0brequirement\x18\x02 \x01(\t\x12\x1a\n\x12satisfying_feature\x18\x03 \x01(\t\x12\x13\n\x0bis_asserted\x18\x04 \x01(\x08\x12\x12\n\nis_negated\x18\x05 \x01(\x08\"]\n\x1c\x41\x64\x64RequirementConstraintEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x12\n\nexpression\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x04 \x01(\t\"\x91\x01\n\x11\x41\x64\x64TransitionEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x0e\n\x06target\x18\x04 \x01(\t\x12\x0f\n\x07trigger\x18\x05 \x01(\t\x12\r\n\x05guard\x18\x06 \x01(\t\x12\x0e\n\x06\x65\x66\x66\x65\x63t\x18\x07 \x01(\t\x12\x0f\n\x07initial\x18\x08 \x01(\x08\"3\n\rAddVerifyEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x13\n\x0brequirement\x18\x02 \x01(\t\"6\n\x14MetadataFeatureValue\x12\x0f\n\x07\x66\x65\x61ture\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x94\x01\n\x0f\x41\x64\x64MetadataEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x15\n\rmetadata_type\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\r\n\x05\x61\x62out\x18\x04 \x03(\t\x12+\n\x06values\x18\x05 \x03(\x0b\x32\x1b.sysml.MetadataFeatureValue\x12\x11\n\tshorthand\x18\x06 \x01(\x08\">\n\x15\x41\x64\x64MetadataPrefixEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x15\n\rmetadata_type\x18\x02 \x01(\t\"\x80\x01\n\rAddImportEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x12\n\nvisibility\x18\x02 \x01(\t\x12\x0e\n\x06target\x18\x03 \x01(\t\x12\x14\n\x0cis_recursive\x18\x04 \x01(\x08\x12\x15\n\ris_import_all\x18\x05 \x01(\x08\x12\x0f\n\x07\x66ilters\x18\x06 \x03(\t\"c\n\x14\x41\x64\x64\x44ocumentationEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0e\n\x06locale\x18\x04 \x01(\t\x12\x0f\n\x07replace\x18\x05 \x01(\x08\"Z\n\x0e\x41\x64\x64\x43ommentEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\r\n\x05\x61\x62out\x18\x04 \x03(\t\x12\x0e\n\x06locale\x18\x05 \x01(\t\"+\n\x0b\x41\x64\x64NoteEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\"\xcd\x02\n\x0f\x41\x64\x64SequenceEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0f\n\x07keyword\x18\x02 \x01(\t\x12\x0b\n\x03ref\x18\x03 \x01(\t\x12\x13\n\x0bmember_kind\x18\x04 \x01(\t\x12\x13\n\x0bmember_name\x18\x05 \x01(\t\x12\x0c\n\x04type\x18\x06 \x01(\t\x12\r\n\x05\x61\x66ter\x18\x07 \x01(\t\x12\x11\n\tcondition\x18\x08 \x01(\t\x12\r\n\x05value\x18\t \x01(\t\x12\x0e\n\x06target\x18\n \x01(\t\x12\x0b\n\x03via\x18\x0b \x01(\t\x12\r\n\x05until\x18\x0c \x01(\t\x12$\n\x04\x62ody\x18\r \x03(\x0b\x32\x16.sysml.AddSequenceEdit\x12)\n\telse_body\x18\x0e \x03(\x0b\x32\x16.sysml.AddSequenceEdit\x12\x14\n\x0cmultiplicity\x18\x0f \x01(\t\x12\x11\n\tparameter\x18\x10 \x01(\t\"n\n\x11\x41\x64\x64\x43onnectionEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x10\n\x08\x66rom_end\x18\x03 \x01(\t\x12\x0e\n\x06to_end\x18\x04 \x01(\t\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x0c\n\x04type\x18\x06 \x01(\t\"-\n\nDeleteEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x0f\n\x07\x63\x61scade\x18\x02 \x01(\x08\")\n\x08MoveEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\r\n\x05owner\x18\x02 \x01(\t\"-\n\x0cSetValueEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\".\n\nRenameEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x10\n\x08new_name\x18\x02 \x01(\t\"\x90\x02\n\x12\x41pplyEditsResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12#\n\x07\x61pplied\x18\x02 \x03(\x0b\x32\x12.sysml.AppliedEdit\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12#\n\x07\x66\x61ilure\x18\x04 \x01(\x0e\x32\x12.sysml.EditFailure\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x1a\n\x12referring_elements\x18\x06 \x03(\t\x12(\n\tdocuments\x18\x07 \x03(\x0b\x32\x15.sysml.EditedDocument\x12\"\n\treferrers\x18\x08 \x03(\x0b\x32\x0f.sysml.Referrer\"/\n\x0e\x45\x64itedDocument\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\"*\n\x08Referrer\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08\x64ocument\x18\x02 \x01(\t\"\x8c\x01\n\x0b\x41ppliedEdit\x12\x17\n\x0foperation_index\x18\x01 \x01(\x05\x12\x0e\n\x06target\x18\x02 \x01(\t\x12\x0e\n\x06offset\x18\x03 \x01(\x05\x12\x0e\n\x06length\x18\x04 \x01(\x05\x12\x10\n\x08old_text\x18\x05 \x01(\t\x12\x10\n\x08new_text\x18\x06 \x01(\t\x12\x10\n\x08\x64ocument\x18\x07 \x01(\t\"\xfd\x02\n\nSymbolInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04kind\x18\x03 \x01(\t\x12\x31\n\x08metadata\x18\x04 \x03(\x0b\x32\x1f.sysml.SymbolInfo.MetadataEntry\x12\x11\n\tchild_ids\x18\x05 \x03(\t\x12(\n\nattributes\x18\x06 \x03(\x0b\x32\x14.sysml.AttributeInfo\x12\"\n\ttype_info\x18\x07 \x01(\x0b\x32\x0f.sysml.TypeInfo\x12-\n\x0cmultiplicity\x18\x08 \x01(\x0b\x32\x17.sysml.MultiplicityInfo\x12.\n\x0fspecializations\x18\t \x03(\x0b\x32\x15.sysml.Specialization\x12#\n\x1bwithheld_library_attributes\x18\n \x01(\x05\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"X\n\x0eSpecialization\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x10\n\x08\x64\x65\x63lared\x18\x02 \x01(\t\x12\x11\n\ttarget_id\x18\x03 \x01(\t\x12\x13\n\x0btarget_kind\x18\x04 \x01(\t\"\x95\x01\n\x08TypeInfo\x12\x10\n\x08\x64\x65\x63lared\x18\x01 \x01(\t\x12\x13\n\x0bresolved_id\x18\x02 \x01(\t\x12\x15\n\rresolved_kind\x18\x03 \x01(\t\x12\x11\n\tprimitive\x18\x04 \x01(\t\x12\x18\n\x10primitive_source\x18\x05 \x01(\t\x12\x10\n\x08quantity\x18\x06 \x01(\x08\x12\x0c\n\x04unit\x18\x07 \x01(\t\"0\n\x10MultiplicityInfo\x12\r\n\x05lower\x18\x01 \x01(\t\x12\r\n\x05upper\x18\x02 \x01(\t\"V\n\rAttributeInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x1b\n\x05value\x18\x03 \x01(\x0b\x32\x0c.sysml.Value\x12\x0c\n\x04unit\x18\x04 \x01(\t\"\x87\x06\n\x05Value\x12\x13\n\tint_value\x18\x01 \x01(\x03H\x00\x12\x14\n\nreal_value\x18\x02 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x03 \x01(\x08H\x00\x12\x16\n\x0cstring_value\x18\x04 \x01(\tH\x00\x12\x15\n\x0binstance_id\x18\x05 \x01(\x03H\x00\x12(\n\x08sequence\x18\x06 \x01(\x0b\x32\x14.sysml.ValueSequenceH\x00\x12\x0e\n\x04null\x18\x07 \x01(\tH\x00\x12#\n\x08quantity\x18\x08 \x01(\x0b\x32\x0f.sysml.QuantityH\x00\x12*\n\x0c\x65num_literal\x18\t \x01(\x0b\x32\x12.sysml.EnumLiteralH\x00\x12\x0f\n\x05unset\x18\n \x01(\x08H\x00\x12!\n\x07\x63omplex\x18\x0b \x01(\x0b\x32\x0e.sysml.ComplexH\x00\x12\x1d\n\x05\x61rray\x18\x0c \x01(\x0b\x32\x0c.sysml.ArrayH\x00\x12\x1f\n\x06vector\x18\r \x01(\x0b\x32\r.sysml.VectorH\x00\x12\x30\n\x0fvector_quantity\x18\x0e \x01(\x0b\x32\x15.sysml.VectorQuantityH\x00\x12\x30\n\x0fmeasurement_ref\x18\x0f \x01(\x0b\x32\x15.sysml.MeasurementRefH\x00\x12\x12\n\x08infinity\x18\x10 \x01(\x08H\x00\x12#\n\x08\x66unction\x18\x11 \x01(\x0b\x32\x0f.sysml.FunctionH\x00\x12\x1e\n\x03set\x18\x12 \x01(\x0b\x32\x0f.sysml.ValueSetH\x00\x12\x30\n\x0ftensor_quantity\x18\x13 \x01(\x0b\x32\x15.sysml.TensorQuantityH\x00\x12\'\n\nmetaobject\x18\x14 \x01(\x0b\x32\x11.sysml.MetaobjectH\x00\x12+\n\x0cundetermined\x18\x15 \x01(\x0b\x32\x13.sysml.UndeterminedH\x00\x12\x17\n\rbig_int_value\x18\x16 \x01(\tH\x00\x12)\n\x0erational_value\x18\x17 \x01(\x0b\x32\x0f.sysml.RationalH\x00\x42\x06\n\x04kind\"2\n\x08Rational\x12\x11\n\tnumerator\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65nominator\x18\x02 \x01(\t\"6\n\nMetaobject\x12\x12\n\nelement_id\x18\x01 \x01(\t\x12\x14\n\x0cmetaclass_id\x18\x02 \x01(\t\"F\n\x0cUndetermined\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12&\n\x05\x63ount\x18\x02 \x01(\x0b\x32\x17.sysml.MultiplicityInfo\",\n\x08\x46unction\x12\x0f\n\x07\x63\x61lc_id\x18\x01 \x01(\t\x12\x0f\n\x07self_id\x18\x02 \x01(\x03\"*\n\x08ValueSet\x12\x1e\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"I\n\x0eTensorQuantity\x12\x12\n\ndimensions\x18\x01 \x03(\x03\x12#\n\ncomponents\x18\x02 \x03(\x0b\x32\x0f.sysml.Quantity\";\n\x05\x41rray\x12\x12\n\ndimensions\x18\x01 \x03(\x03\x12\x1e\n\x08\x65lements\x18\x02 \x03(\x0b\x32\x0c.sysml.Value\"*\n\x06Vector\x12 \n\ncomponents\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"5\n\x0eVectorQuantity\x12#\n\ncomponents\x18\x01 \x03(\x0b\x32\x0f.sysml.Quantity\"*\n\x07\x43omplex\x12\x0c\n\x04real\x18\x01 \x01(\x01\x12\x11\n\timaginary\x18\x02 \x01(\x01\"d\n\x0b\x45numLiteral\x12\x12\n\nliteral_id\x18\x01 \x01(\t\x12\x16\n\x0e\x65numeration_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x1b\n\x05value\x18\x04 \x01(\x0b\x32\x0c.sysml.Value\"/\n\rValueSequence\x12\x1e\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"\xc8\x01\n\x08Quantity\x12\x17\n\rint_magnitude\x18\x01 \x01(\x03H\x00\x12\x18\n\x0ereal_magnitude\x18\x02 \x01(\x01H\x00\x12\x1b\n\x11\x62ig_int_magnitude\x18\x05 \x01(\tH\x00\x12-\n\x12rational_magnitude\x18\x06 \x01(\x0b\x32\x0f.sysml.RationalH\x00\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\"\n\tunit_term\x18\x04 \x01(\x0b\x32\x0f.sysml.UnitTermB\x0b\n\tmagnitude\"S\n\x0eMeasurementRef\x12\x0c\n\x04unit\x18\x01 \x01(\t\x12\"\n\tunit_term\x18\x02 \x01(\x0b\x32\x0f.sysml.UnitTerm\x12\x0f\n\x07unit_id\x18\x03 \x01(\t\"T\n\x08UnitTerm\x12\x11\n\tscale_num\x18\x01 \x01(\x01\x12\x11\n\tscale_den\x18\x02 \x01(\x01\x12\"\n\x07\x66\x61\x63tors\x18\x03 \x03(\x0b\x32\x11.sysml.UnitFactor\"/\n\nUnitFactor\x12\x0f\n\x07unit_id\x18\x01 \x01(\t\x12\x10\n\x08\x65xponent\x18\x02 \x01(\x01\"X\n\nDiagnostic\x12\x10\n\x08severity\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x19\n\x04span\x18\x03 \x01(\x0b\x32\x0b.sysml.Span\x12\x0c\n\x04\x63ode\x18\x04 \x01(\t\"^\n\x04Span\x12\x0c\n\x04\x66ile\x18\x01 \x01(\t\x12\x12\n\nstart_line\x18\x02 \x01(\x05\x12\x11\n\tstart_col\x18\x03 \x01(\x05\x12\x10\n\x08\x65nd_line\x18\x04 \x01(\x05\x12\x0f\n\x07\x65nd_col\x18\x05 \x01(\x05\"\x13\n\x11ServerInfoRequest\";\n\x12ServerInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x02 \x03(\t\"S\n\x0cQueryRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1b\n\x05query\x18\x02 \x01(\x0b\x32\x0c.sysml.Query\x12\x12\n\noslc_query\x18\x03 \x01(\t\"<\n\rQueryResponse\x12+\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x19.sysml.QueryResultElement\"H\n\x05Query\x12\r\n\x05scope\x18\x01 \x03(\t\x12\x0e\n\x06select\x18\x02 \x03(\t\x12 \n\x05where\x18\x03 \x01(\x0b\x32\x11.sysml.Constraint\"|\n\nConstraint\x12/\n\tprimitive\x18\x01 \x01(\x0b\x32\x1a.sysml.PrimitiveConstraintH\x00\x12/\n\tcomposite\x18\x02 \x01(\x0b\x32\x1a.sysml.CompositeConstraintH\x00\x42\x0c\n\nconstraint\"s\n\x13PrimitiveConstraint\x12\x0f\n\x07inverse\x18\x01 \x01(\x08\x12\x10\n\x08property\x18\x02 \x01(\t\x12*\n\x08operator\x18\x03 \x01(\x0e\x32\x18.sysml.PrimitiveOperator\x12\r\n\x05value\x18\x04 \x03(\t\"h\n\x13\x43ompositeConstraint\x12*\n\x08operator\x18\x01 \x01(\x0e\x32\x18.sysml.CompositeOperator\x12%\n\nconstraint\x18\x02 \x03(\x0b\x32\x11.sysml.Constraint\"\xa0\x01\n\x12QueryResultElement\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12=\n\nproperties\x18\x03 \x03(\x0b\x32).sysml.QueryResultElement.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"s\n\nSweepRange\x12\x11\n\tparameter\x18\x01 \x01(\t\x12\x1b\n\x05start\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\x12\x19\n\x03\x65nd\x18\x03 \x01(\x0b\x32\x0c.sysml.Value\x12\x1a\n\x04step\x18\x04 \x01(\x0b\x32\x0c.sysml.Value\"\xd0\x02\n\x0fRunSweepRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x1f\n\targuments\x18\x04 \x03(\x0b\x32\x0c.sysml.Value\x12\x43\n\x0fnamed_arguments\x18\x05 \x03(\x0b\x32*.sysml.RunSweepRequest.NamedArgumentsEntry\x12!\n\x06ranges\x18\x06 \x03(\x0b\x32\x11.sysml.SweepRange\x12\x0f\n\x07samples\x18\x07 \x01(\x03\x12\x0c\n\x04seed\x18\x08 \x01(\x04\x12\x0e\n\x06\x65ngine\x18\t \x01(\t\x1a\x43\n\x13NamedArgumentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xf4\x01\n\x08SweepRow\x12!\n\x06inputs\x18\x01 \x03(\x0b\x32\x11.sysml.CalcOutput\x12\"\n\x07outputs\x18\x02 \x03(\x0b\x32\x11.sysml.CalcOutput\x12 \n\x08verdicts\x18\x03 \x03(\x0b\x32\x0e.sysml.Verdict\x12\x16\n\x0e\x65lapsed_micros\x18\x04 \x01(\x03\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12,\n\x0e\x66\x61ilure_reason\x18\x06 \x01(\x0e\x32\x14.sysml.FailureReason\x12*\n\x0b\x65valuations\x18\x07 \x03(\x0b\x32\x15.sysml.CaseEvaluation\"\xad\x02\n\x10RunSweepResponse\x12\x1d\n\x04rows\x18\x01 \x03(\x0b\x32\x0f.sysml.SweepRow\x12\x12\n\nparameters\x18\x02 \x03(\t\x12\x0f\n\x07sampled\x18\x03 \x01(\x08\x12\x0c\n\x04seed\x18\x04 \x01(\x04\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x06 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x07 \x01(\x0e\x32\x14.sysml.FailureReason\x12\"\n\tinstances\x18\x08 \x03(\x0b\x32\x0f.sysml.Instance\x12\x0e\n\x06\x65ngine\x18\t \x01(\t\x12\x10\n\x08strength\x18\n \x01(\t\x12\x1c\n\x06\x62ounds\x18\x0b \x03(\x0b\x32\x0c.sysml.Bound\"n\n\x17RunDocumentQueryRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x10\n\x08query_id\x18\x02 \x01(\t\x12-\n\x08\x62indings\x18\x03 \x03(\x0b\x32\x1b.sysml.DocumentQueryBinding\"O\n\x14\x44ocumentQueryBinding\x12\x11\n\tparameter\x18\x01 \x01(\t\x12$\n\x06values\x18\x02 \x03(\x0b\x32\x14.sysml.DocumentValue\"\xbb\x03\n\rDocumentValue\x12\x14\n\nelement_id\x18\x01 \x01(\tH\x00\x12\x16\n\x0cstring_value\x18\x02 \x01(\tH\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x14\n\nreal_value\x18\x04 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x05 \x01(\x08H\x00\x12\x12\n\x08infinity\x18\x06 \x01(\x08H\x00\x12#\n\x08quantity\x18\x08 \x01(\x0b\x32\x0f.sysml.QuantityH\x00\x12)\n\x07verdict\x18\t \x01(\x0b\x32\x16.sysml.DocumentVerdictH\x00\x12\'\n\x06object\x18\n \x01(\x0b\x32\x15.sysml.DocumentObjectH\x00\x12%\n\x05state\x18\x0b \x01(\x0b\x32\x14.sysml.DocumentStateH\x00\x12%\n\x05\x65vent\x18\x0c \x01(\x0b\x32\x14.sysml.DocumentEventH\x00\x12\x17\n\rbig_int_value\x18\r \x01(\tH\x00\x12)\n\x0erational_value\x18\x0e \x01(\x0b\x32\x0f.sysml.RationalH\x00\x12\x14\n\x0c\x65lement_type\x18\x07 \x01(\tB\x06\n\x04kind\"Z\n\x0e\x44ocumentObject\x12\x13\n\x0binstance_id\x18\x01 \x01(\x03\x12\x0c\n\x04path\x18\x02 \x01(\t\x12%\n\x07\x65lement\x18\x03 \x01(\x0b\x32\x14.sysml.DocumentValue\"\xae\x01\n\x0f\x44ocumentVerdict\x12\'\n\tassertion\x18\x01 \x01(\x0b\x32\x14.sysml.DocumentValue\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x0c\n\x04path\x18\x04 \x01(\t\x12\x0f\n\x07verdict\x18\x05 \x01(\t\x12\x11\n\tcondition\x18\x06 \x01(\t\x12\x0e\n\x06reason\x18\x07 \x01(\t\x12\x14\n\x0cverification\x18\x08 \x03(\t\"\xb1\x01\n\rDocumentState\x12%\n\x06object\x18\x01 \x01(\x0b\x32\x15.sysml.DocumentObject\x12\x0f\n\x07machine\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x12\n\nstate_path\x18\x04 \x01(\t\x12#\n\x05state\x18\x05 \x01(\x0b\x32\x14.sysml.DocumentValue\x12\x0e\n\x06region\x18\x06 \x01(\t\x12\x11\n\tenclosing\x18\x07 \x03(\t\"\x9c\x02\n\rDocumentEvent\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\"\n\x04time\x18\x02 \x01(\x0b\x32\x14.sysml.DocumentValue\x12%\n\x06object\x18\x03 \x01(\x0b\x32\x15.sysml.DocumentObject\x12\x0f\n\x07machine\x18\x04 \x01(\t\x12\r\n\x05state\x18\x05 \x01(\t\x12\x0c\n\x04\x66rom\x18\x06 \x01(\t\x12\n\n\x02to\x18\x07 \x01(\t\x12%\n\x06target\x18\x08 \x01(\x0b\x32\x15.sysml.DocumentObject\x12\r\n\x05\x65vent\x18\t \x01(\t\x12\x0f\n\x07payload\x18\n \x03(\t\x12\x14\n\x0c\x61lternatives\x18\x0b \x03(\t\x12\r\n\x05taken\x18\x0c \x01(\t\x12\x0c\n\x04text\x18\r \x01(\t\"#\n\x13\x44ocumentQueryColumn\x12\x0c\n\x04name\x18\x01 \x01(\t\"9\n\x11\x44ocumentQueryCell\x12$\n\x06values\x18\x01 \x03(\x0b\x32\x14.sysml.DocumentValue\"b\n\x10\x44ocumentQueryRow\x12%\n\x07\x65lement\x18\x01 \x01(\x0b\x32\x14.sysml.DocumentValue\x12\'\n\x05\x63\x65lls\x18\x02 \x03(\x0b\x32\x18.sysml.DocumentQueryCell\"n\n\x18RunDocumentQueryResponse\x12+\n\x07\x63olumns\x18\x01 \x03(\x0b\x32\x1a.sysml.DocumentQueryColumn\x12%\n\x04rows\x18\x02 \x03(\x0b\x32\x17.sysml.DocumentQueryRow\"N\n\x15RenderDocumentRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x02 \x01(\t\x12\x0c\n\x04\x66orm\x18\x03 \x01(\t\"8\n\x16RenderDocumentResponse\x12\x10\n\x08markdown\x18\x01 \x01(\t\x12\x0c\n\x04html\x18\x02 \x01(\t*\xb1\x01\n\rFailureReason\x12\x1e\n\x1a\x46\x41ILURE_REASON_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x46\x41ILURE_REASON_EVALUATION\x10\x01\x12\x1d\n\x19\x46\x41ILURE_REASON_WRONG_KIND\x10\x02\x12$\n FAILURE_REASON_AMBIGUOUS_SUBJECT\x10\x03\x12\x1c\n\x18\x46\x41ILURE_REASON_UNDECIDED\x10\x04*\x8c\x05\n\x0b\x45\x64itFailure\x12\x1c\n\x18\x45\x44IT_FAILURE_UNSPECIFIED\x10\x00\x12\x1e\n\x1a\x45\x44IT_FAILURE_NO_OPERATIONS\x10\x01\x12\x1f\n\x1b\x45\x44IT_FAILURE_UNKNOWN_TARGET\x10\x02\x12!\n\x1d\x45\x44IT_FAILURE_AMBIGUOUS_TARGET\x10\x03\x12\x1b\n\x17\x45\x44IT_FAILURE_NOT_VALUED\x10\x04\x12\x1e\n\x1a\x45\x44IT_FAILURE_INVALID_VALUE\x10\x05\x12\x1d\n\x19\x45\x44IT_FAILURE_INVALID_NAME\x10\x06\x12\x1a\n\x16\x45\x44IT_FAILURE_NOT_NAMED\x10\x07\x12\"\n\x1e\x45\x44IT_FAILURE_RENAME_REFERENCED\x10\x08\x12\"\n\x1e\x45\x44IT_FAILURE_OVERLAPPING_EDITS\x10\t\x12\x1f\n\x1b\x45\x44IT_FAILURE_RESULT_INVALID\x10\n\x12\x1e\n\x1a\x45\x44IT_FAILURE_OWNER_UNKNOWN\x10\x0b\x12$\n EDIT_FAILURE_OWNER_NOT_NAMESPACE\x10\x0c\x12\x1d\n\x19\x45\x44IT_FAILURE_ILLEGAL_KIND\x10\r\x12\"\n\x1e\x45\x44IT_FAILURE_MEMBER_NAME_TAKEN\x10\x0e\x12\"\n\x1e\x45\x44IT_FAILURE_DELETE_REFERENCED\x10\x0f\x12$\n EDIT_FAILURE_OWNER_INSIDE_TARGET\x10\x10\x12 \n\x1c\x45\x44IT_FAILURE_MOVE_REFERENCED\x10\x11\x12%\n!EDIT_FAILURE_REFERENCED_ELSEWHERE\x10\x12*\x92\x01\n\x11PrimitiveOperator\x12\"\n\x1ePRIMITIVE_OPERATOR_UNSPECIFIED\x10\x00\x12\x1c\n\x18PRIMITIVE_OPERATOR_EQUAL\x10\x01\x12\x1e\n\x1aPRIMITIVE_OPERATOR_GREATER\x10\x02\x12\x1b\n\x17PRIMITIVE_OPERATOR_LESS\x10\x03*n\n\x11\x43ompositeOperator\x12\"\n\x1e\x43OMPOSITE_OPERATOR_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x43OMPOSITE_OPERATOR_AND\x10\x01\x12\x19\n\x15\x43OMPOSITE_OPERATOR_OR\x10\x02\x32\xf9\x0c\n\x0cSysMLService\x12\x44\n\rGetServerInfo\x12\x18.sysml.ServerInfoRequest\x1a\x19.sysml.ServerInfoResponse\x12>\n\tParseFile\x12\x17.sysml.ParseFileRequest\x1a\x18.sysml.ParseFileResponse\x12G\n\x0cParseSources\x12\x1a.sysml.ParseSourcesRequest\x1a\x1b.sysml.ParseSourcesResponse\x12;\n\tGetSymbol\x12\x17.sysml.GetSymbolRequest\x1a\x15.sysml.SymbolResponse\x12G\n\x0eGetDiagnostics\x12\x19.sysml.DiagnosticsRequest\x1a\x1a.sysml.DiagnosticsResponse\x12;\n\x08\x45valuate\x12\x16.sysml.EvaluateRequest\x1a\x17.sysml.EvaluateResponse\x12\x44\n\x0bInstantiate\x12\x19.sysml.InstantiateRequest\x1a\x1a.sysml.InstantiateResponse\x12J\n\rExecuteAction\x12\x1b.sysml.ExecuteActionRequest\x1a\x1c.sysml.ExecuteActionResponse\x12G\n\x0c\x45xecuteState\x12\x1a.sysml.ExecuteStateRequest\x1a\x1b.sysml.ExecuteStateResponse\x12\x38\n\x07\x43onvert\x12\x15.sysml.ConvertRequest\x1a\x16.sysml.ConvertResponse\x12\x38\n\x07Migrate\x12\x15.sysml.MigrateRequest\x1a\x16.sysml.MigrateResponse\x12\x41\n\nApplyEdits\x12\x18.sysml.ApplyEditsRequest\x1a\x19.sysml.ApplyEditsResponse\x12S\n\x10VerifyConstraint\x12\x1e.sysml.VerifyConstraintRequest\x1a\x1f.sysml.VerifyConstraintResponse\x12V\n\x11VerifyRequirement\x12\x1f.sysml.VerifyRequirementRequest\x1a .sysml.VerifyRequirementResponse\x12Y\n\x12VerifySatisfaction\x12 .sysml.VerifySatisfactionRequest\x1a!.sysml.VerifySatisfactionResponse\x12S\n\x10ValidateInstance\x12\x1e.sysml.ValidateInstanceRequest\x1a\x1f.sysml.ValidateInstanceResponse\x12G\n\x0c\x45valuateCalc\x12\x1a.sysml.EvaluateCalcRequest\x1a\x1b.sysml.EvaluateCalcResponse\x12\x44\n\x0bRunAnalysis\x12\x19.sysml.RunAnalysisRequest\x1a\x1a.sysml.RunAnalysisResponse\x12;\n\x08RunSweep\x12\x16.sysml.RunSweepRequest\x1a\x17.sysml.RunSweepResponse\x12\x44\n\x0bListEngines\x12\x19.sysml.ListEnginesRequest\x1a\x1a.sysml.ListEnginesResponse\x12\x32\n\x05Query\x12\x13.sysml.QueryRequest\x1a\x14.sysml.QueryResponse\x12S\n\x10RunDocumentQuery\x12\x1e.sysml.RunDocumentQueryRequest\x1a\x1f.sysml.RunDocumentQueryResponse\x12M\n\x0eRenderDocument\x12\x1c.sysml.RenderDocumentRequest\x1a\x1d.sysml.RenderDocumentResponseB*Z(github.com/Open-MBEE/OpenSysML/api/protob\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -54,14 +54,14 @@ _globals['_QUERYRESULTELEMENT_PROPERTIESENTRY']._serialized_options = b'8\001' _globals['_RUNSWEEPREQUEST_NAMEDARGUMENTSENTRY']._loaded_options = None _globals['_RUNSWEEPREQUEST_NAMEDARGUMENTSENTRY']._serialized_options = b'8\001' - _globals['_FAILUREREASON']._serialized_start=18037 - _globals['_FAILUREREASON']._serialized_end=18214 - _globals['_EDITFAILURE']._serialized_start=18217 - _globals['_EDITFAILURE']._serialized_end=18869 - _globals['_PRIMITIVEOPERATOR']._serialized_start=18872 - _globals['_PRIMITIVEOPERATOR']._serialized_end=19018 - _globals['_COMPOSITEOPERATOR']._serialized_start=19020 - _globals['_COMPOSITEOPERATOR']._serialized_end=19130 + _globals['_FAILUREREASON']._serialized_start=18222 + _globals['_FAILUREREASON']._serialized_end=18399 + _globals['_EDITFAILURE']._serialized_start=18402 + _globals['_EDITFAILURE']._serialized_end=19054 + _globals['_PRIMITIVEOPERATOR']._serialized_start=19057 + _globals['_PRIMITIVEOPERATOR']._serialized_end=19203 + _globals['_COMPOSITEOPERATOR']._serialized_start=19205 + _globals['_COMPOSITEOPERATOR']._serialized_end=19315 _globals['_VERDICT']._serialized_start=23 _globals['_VERDICT']._serialized_end=413 _globals['_WITNESSASSIGNMENT']._serialized_start=415 @@ -237,97 +237,99 @@ _globals['_ATTRIBUTEINFO']._serialized_start=12403 _globals['_ATTRIBUTEINFO']._serialized_end=12489 _globals['_VALUE']._serialized_start=12492 - _globals['_VALUE']._serialized_end=13224 - _globals['_METAOBJECT']._serialized_start=13226 - _globals['_METAOBJECT']._serialized_end=13280 - _globals['_UNDETERMINED']._serialized_start=13282 - _globals['_UNDETERMINED']._serialized_end=13352 - _globals['_FUNCTION']._serialized_start=13354 - _globals['_FUNCTION']._serialized_end=13398 - _globals['_VALUESET']._serialized_start=13400 - _globals['_VALUESET']._serialized_end=13442 - _globals['_TENSORQUANTITY']._serialized_start=13444 - _globals['_TENSORQUANTITY']._serialized_end=13517 - _globals['_ARRAY']._serialized_start=13519 - _globals['_ARRAY']._serialized_end=13578 - _globals['_VECTOR']._serialized_start=13580 - _globals['_VECTOR']._serialized_end=13622 - _globals['_VECTORQUANTITY']._serialized_start=13624 - _globals['_VECTORQUANTITY']._serialized_end=13677 - _globals['_COMPLEX']._serialized_start=13679 - _globals['_COMPLEX']._serialized_end=13721 - _globals['_ENUMLITERAL']._serialized_start=13723 - _globals['_ENUMLITERAL']._serialized_end=13823 - _globals['_VALUESEQUENCE']._serialized_start=13825 - _globals['_VALUESEQUENCE']._serialized_end=13872 - _globals['_QUANTITY']._serialized_start=13875 - _globals['_QUANTITY']._serialized_end=14028 - _globals['_MEASUREMENTREF']._serialized_start=14030 - _globals['_MEASUREMENTREF']._serialized_end=14113 - _globals['_UNITTERM']._serialized_start=14115 - _globals['_UNITTERM']._serialized_end=14199 - _globals['_UNITFACTOR']._serialized_start=14201 - _globals['_UNITFACTOR']._serialized_end=14248 - _globals['_DIAGNOSTIC']._serialized_start=14250 - _globals['_DIAGNOSTIC']._serialized_end=14338 - _globals['_SPAN']._serialized_start=14340 - _globals['_SPAN']._serialized_end=14434 - _globals['_SERVERINFOREQUEST']._serialized_start=14436 - _globals['_SERVERINFOREQUEST']._serialized_end=14455 - _globals['_SERVERINFORESPONSE']._serialized_start=14457 - _globals['_SERVERINFORESPONSE']._serialized_end=14516 - _globals['_QUERYREQUEST']._serialized_start=14518 - _globals['_QUERYREQUEST']._serialized_end=14601 - _globals['_QUERYRESPONSE']._serialized_start=14603 - _globals['_QUERYRESPONSE']._serialized_end=14663 - _globals['_QUERY']._serialized_start=14665 - _globals['_QUERY']._serialized_end=14737 - _globals['_CONSTRAINT']._serialized_start=14739 - _globals['_CONSTRAINT']._serialized_end=14863 - _globals['_PRIMITIVECONSTRAINT']._serialized_start=14865 - _globals['_PRIMITIVECONSTRAINT']._serialized_end=14980 - _globals['_COMPOSITECONSTRAINT']._serialized_start=14982 - _globals['_COMPOSITECONSTRAINT']._serialized_end=15086 - _globals['_QUERYRESULTELEMENT']._serialized_start=15089 - _globals['_QUERYRESULTELEMENT']._serialized_end=15249 - _globals['_QUERYRESULTELEMENT_PROPERTIESENTRY']._serialized_start=15200 - _globals['_QUERYRESULTELEMENT_PROPERTIESENTRY']._serialized_end=15249 - _globals['_SWEEPRANGE']._serialized_start=15251 - _globals['_SWEEPRANGE']._serialized_end=15366 - _globals['_RUNSWEEPREQUEST']._serialized_start=15369 - _globals['_RUNSWEEPREQUEST']._serialized_end=15705 + _globals['_VALUE']._serialized_end=13267 + _globals['_RATIONAL']._serialized_start=13269 + _globals['_RATIONAL']._serialized_end=13319 + _globals['_METAOBJECT']._serialized_start=13321 + _globals['_METAOBJECT']._serialized_end=13375 + _globals['_UNDETERMINED']._serialized_start=13377 + _globals['_UNDETERMINED']._serialized_end=13447 + _globals['_FUNCTION']._serialized_start=13449 + _globals['_FUNCTION']._serialized_end=13493 + _globals['_VALUESET']._serialized_start=13495 + _globals['_VALUESET']._serialized_end=13537 + _globals['_TENSORQUANTITY']._serialized_start=13539 + _globals['_TENSORQUANTITY']._serialized_end=13612 + _globals['_ARRAY']._serialized_start=13614 + _globals['_ARRAY']._serialized_end=13673 + _globals['_VECTOR']._serialized_start=13675 + _globals['_VECTOR']._serialized_end=13717 + _globals['_VECTORQUANTITY']._serialized_start=13719 + _globals['_VECTORQUANTITY']._serialized_end=13772 + _globals['_COMPLEX']._serialized_start=13774 + _globals['_COMPLEX']._serialized_end=13816 + _globals['_ENUMLITERAL']._serialized_start=13818 + _globals['_ENUMLITERAL']._serialized_end=13918 + _globals['_VALUESEQUENCE']._serialized_start=13920 + _globals['_VALUESEQUENCE']._serialized_end=13967 + _globals['_QUANTITY']._serialized_start=13970 + _globals['_QUANTITY']._serialized_end=14170 + _globals['_MEASUREMENTREF']._serialized_start=14172 + _globals['_MEASUREMENTREF']._serialized_end=14255 + _globals['_UNITTERM']._serialized_start=14257 + _globals['_UNITTERM']._serialized_end=14341 + _globals['_UNITFACTOR']._serialized_start=14343 + _globals['_UNITFACTOR']._serialized_end=14390 + _globals['_DIAGNOSTIC']._serialized_start=14392 + _globals['_DIAGNOSTIC']._serialized_end=14480 + _globals['_SPAN']._serialized_start=14482 + _globals['_SPAN']._serialized_end=14576 + _globals['_SERVERINFOREQUEST']._serialized_start=14578 + _globals['_SERVERINFOREQUEST']._serialized_end=14597 + _globals['_SERVERINFORESPONSE']._serialized_start=14599 + _globals['_SERVERINFORESPONSE']._serialized_end=14658 + _globals['_QUERYREQUEST']._serialized_start=14660 + _globals['_QUERYREQUEST']._serialized_end=14743 + _globals['_QUERYRESPONSE']._serialized_start=14745 + _globals['_QUERYRESPONSE']._serialized_end=14805 + _globals['_QUERY']._serialized_start=14807 + _globals['_QUERY']._serialized_end=14879 + _globals['_CONSTRAINT']._serialized_start=14881 + _globals['_CONSTRAINT']._serialized_end=15005 + _globals['_PRIMITIVECONSTRAINT']._serialized_start=15007 + _globals['_PRIMITIVECONSTRAINT']._serialized_end=15122 + _globals['_COMPOSITECONSTRAINT']._serialized_start=15124 + _globals['_COMPOSITECONSTRAINT']._serialized_end=15228 + _globals['_QUERYRESULTELEMENT']._serialized_start=15231 + _globals['_QUERYRESULTELEMENT']._serialized_end=15391 + _globals['_QUERYRESULTELEMENT_PROPERTIESENTRY']._serialized_start=15342 + _globals['_QUERYRESULTELEMENT_PROPERTIESENTRY']._serialized_end=15391 + _globals['_SWEEPRANGE']._serialized_start=15393 + _globals['_SWEEPRANGE']._serialized_end=15508 + _globals['_RUNSWEEPREQUEST']._serialized_start=15511 + _globals['_RUNSWEEPREQUEST']._serialized_end=15847 _globals['_RUNSWEEPREQUEST_NAMEDARGUMENTSENTRY']._serialized_start=2854 _globals['_RUNSWEEPREQUEST_NAMEDARGUMENTSENTRY']._serialized_end=2921 - _globals['_SWEEPROW']._serialized_start=15708 - _globals['_SWEEPROW']._serialized_end=15952 - _globals['_RUNSWEEPRESPONSE']._serialized_start=15955 - _globals['_RUNSWEEPRESPONSE']._serialized_end=16256 - _globals['_RUNDOCUMENTQUERYREQUEST']._serialized_start=16258 - _globals['_RUNDOCUMENTQUERYREQUEST']._serialized_end=16368 - _globals['_DOCUMENTQUERYBINDING']._serialized_start=16370 - _globals['_DOCUMENTQUERYBINDING']._serialized_end=16449 - _globals['_DOCUMENTVALUE']._serialized_start=16452 - _globals['_DOCUMENTVALUE']._serialized_end=16852 - _globals['_DOCUMENTOBJECT']._serialized_start=16854 - _globals['_DOCUMENTOBJECT']._serialized_end=16944 - _globals['_DOCUMENTVERDICT']._serialized_start=16947 - _globals['_DOCUMENTVERDICT']._serialized_end=17121 - _globals['_DOCUMENTSTATE']._serialized_start=17124 - _globals['_DOCUMENTSTATE']._serialized_end=17301 - _globals['_DOCUMENTEVENT']._serialized_start=17304 - _globals['_DOCUMENTEVENT']._serialized_end=17588 - _globals['_DOCUMENTQUERYCOLUMN']._serialized_start=17590 - _globals['_DOCUMENTQUERYCOLUMN']._serialized_end=17625 - _globals['_DOCUMENTQUERYCELL']._serialized_start=17627 - _globals['_DOCUMENTQUERYCELL']._serialized_end=17684 - _globals['_DOCUMENTQUERYROW']._serialized_start=17686 - _globals['_DOCUMENTQUERYROW']._serialized_end=17784 - _globals['_RUNDOCUMENTQUERYRESPONSE']._serialized_start=17786 - _globals['_RUNDOCUMENTQUERYRESPONSE']._serialized_end=17896 - _globals['_RENDERDOCUMENTREQUEST']._serialized_start=17898 - _globals['_RENDERDOCUMENTREQUEST']._serialized_end=17976 - _globals['_RENDERDOCUMENTRESPONSE']._serialized_start=17978 - _globals['_RENDERDOCUMENTRESPONSE']._serialized_end=18034 - _globals['_SYSMLSERVICE']._serialized_start=19133 - _globals['_SYSMLSERVICE']._serialized_end=20790 + _globals['_SWEEPROW']._serialized_start=15850 + _globals['_SWEEPROW']._serialized_end=16094 + _globals['_RUNSWEEPRESPONSE']._serialized_start=16097 + _globals['_RUNSWEEPRESPONSE']._serialized_end=16398 + _globals['_RUNDOCUMENTQUERYREQUEST']._serialized_start=16400 + _globals['_RUNDOCUMENTQUERYREQUEST']._serialized_end=16510 + _globals['_DOCUMENTQUERYBINDING']._serialized_start=16512 + _globals['_DOCUMENTQUERYBINDING']._serialized_end=16591 + _globals['_DOCUMENTVALUE']._serialized_start=16594 + _globals['_DOCUMENTVALUE']._serialized_end=17037 + _globals['_DOCUMENTOBJECT']._serialized_start=17039 + _globals['_DOCUMENTOBJECT']._serialized_end=17129 + _globals['_DOCUMENTVERDICT']._serialized_start=17132 + _globals['_DOCUMENTVERDICT']._serialized_end=17306 + _globals['_DOCUMENTSTATE']._serialized_start=17309 + _globals['_DOCUMENTSTATE']._serialized_end=17486 + _globals['_DOCUMENTEVENT']._serialized_start=17489 + _globals['_DOCUMENTEVENT']._serialized_end=17773 + _globals['_DOCUMENTQUERYCOLUMN']._serialized_start=17775 + _globals['_DOCUMENTQUERYCOLUMN']._serialized_end=17810 + _globals['_DOCUMENTQUERYCELL']._serialized_start=17812 + _globals['_DOCUMENTQUERYCELL']._serialized_end=17869 + _globals['_DOCUMENTQUERYROW']._serialized_start=17871 + _globals['_DOCUMENTQUERYROW']._serialized_end=17969 + _globals['_RUNDOCUMENTQUERYRESPONSE']._serialized_start=17971 + _globals['_RUNDOCUMENTQUERYRESPONSE']._serialized_end=18081 + _globals['_RENDERDOCUMENTREQUEST']._serialized_start=18083 + _globals['_RENDERDOCUMENTREQUEST']._serialized_end=18161 + _globals['_RENDERDOCUMENTRESPONSE']._serialized_start=18163 + _globals['_RENDERDOCUMENTRESPONSE']._serialized_end=18219 + _globals['_SYSMLSERVICE']._serialized_start=19318 + _globals['_SYSMLSERVICE']._serialized_end=20975 # @@protoc_insertion_point(module_scope) diff --git a/client/python/opensysml/proto/sysml_pb2.pyi b/client/python/opensysml/proto/sysml_pb2.pyi index 0f26c028d2..f4c9b26455 100644 --- a/client/python/opensysml/proto/sysml_pb2.pyi +++ b/client/python/opensysml/proto/sysml_pb2.pyi @@ -1296,7 +1296,7 @@ class AttributeInfo(_message.Message): def __init__(self, name: _Optional[str] = ..., type: _Optional[str] = ..., value: _Optional[_Union[Value, _Mapping]] = ..., unit: _Optional[str] = ...) -> None: ... class Value(_message.Message): - __slots__ = ("int_value", "real_value", "bool_value", "string_value", "instance_id", "sequence", "null", "quantity", "enum_literal", "unset", "complex", "array", "vector", "vector_quantity", "measurement_ref", "infinity", "function", "set", "tensor_quantity", "metaobject", "undetermined", "big_int_value") + __slots__ = ("int_value", "real_value", "bool_value", "string_value", "instance_id", "sequence", "null", "quantity", "enum_literal", "unset", "complex", "array", "vector", "vector_quantity", "measurement_ref", "infinity", "function", "set", "tensor_quantity", "metaobject", "undetermined", "big_int_value", "rational_value") INT_VALUE_FIELD_NUMBER: _ClassVar[int] REAL_VALUE_FIELD_NUMBER: _ClassVar[int] BOOL_VALUE_FIELD_NUMBER: _ClassVar[int] @@ -1319,6 +1319,7 @@ class Value(_message.Message): METAOBJECT_FIELD_NUMBER: _ClassVar[int] UNDETERMINED_FIELD_NUMBER: _ClassVar[int] BIG_INT_VALUE_FIELD_NUMBER: _ClassVar[int] + RATIONAL_VALUE_FIELD_NUMBER: _ClassVar[int] int_value: int real_value: float bool_value: bool @@ -1341,7 +1342,16 @@ class Value(_message.Message): metaobject: Metaobject undetermined: Undetermined big_int_value: str - def __init__(self, int_value: _Optional[int] = ..., real_value: _Optional[float] = ..., bool_value: _Optional[bool] = ..., string_value: _Optional[str] = ..., instance_id: _Optional[int] = ..., sequence: _Optional[_Union[ValueSequence, _Mapping]] = ..., null: _Optional[str] = ..., quantity: _Optional[_Union[Quantity, _Mapping]] = ..., enum_literal: _Optional[_Union[EnumLiteral, _Mapping]] = ..., unset: _Optional[bool] = ..., complex: _Optional[_Union[Complex, _Mapping]] = ..., array: _Optional[_Union[Array, _Mapping]] = ..., vector: _Optional[_Union[Vector, _Mapping]] = ..., vector_quantity: _Optional[_Union[VectorQuantity, _Mapping]] = ..., measurement_ref: _Optional[_Union[MeasurementRef, _Mapping]] = ..., infinity: _Optional[bool] = ..., function: _Optional[_Union[Function, _Mapping]] = ..., set: _Optional[_Union[ValueSet, _Mapping]] = ..., tensor_quantity: _Optional[_Union[TensorQuantity, _Mapping]] = ..., metaobject: _Optional[_Union[Metaobject, _Mapping]] = ..., undetermined: _Optional[_Union[Undetermined, _Mapping]] = ..., big_int_value: _Optional[str] = ...) -> None: ... + rational_value: Rational + def __init__(self, int_value: _Optional[int] = ..., real_value: _Optional[float] = ..., bool_value: _Optional[bool] = ..., string_value: _Optional[str] = ..., instance_id: _Optional[int] = ..., sequence: _Optional[_Union[ValueSequence, _Mapping]] = ..., null: _Optional[str] = ..., quantity: _Optional[_Union[Quantity, _Mapping]] = ..., enum_literal: _Optional[_Union[EnumLiteral, _Mapping]] = ..., unset: _Optional[bool] = ..., complex: _Optional[_Union[Complex, _Mapping]] = ..., array: _Optional[_Union[Array, _Mapping]] = ..., vector: _Optional[_Union[Vector, _Mapping]] = ..., vector_quantity: _Optional[_Union[VectorQuantity, _Mapping]] = ..., measurement_ref: _Optional[_Union[MeasurementRef, _Mapping]] = ..., infinity: _Optional[bool] = ..., function: _Optional[_Union[Function, _Mapping]] = ..., set: _Optional[_Union[ValueSet, _Mapping]] = ..., tensor_quantity: _Optional[_Union[TensorQuantity, _Mapping]] = ..., metaobject: _Optional[_Union[Metaobject, _Mapping]] = ..., undetermined: _Optional[_Union[Undetermined, _Mapping]] = ..., big_int_value: _Optional[str] = ..., rational_value: _Optional[_Union[Rational, _Mapping]] = ...) -> None: ... + +class Rational(_message.Message): + __slots__ = ("numerator", "denominator") + NUMERATOR_FIELD_NUMBER: _ClassVar[int] + DENOMINATOR_FIELD_NUMBER: _ClassVar[int] + numerator: str + denominator: str + def __init__(self, numerator: _Optional[str] = ..., denominator: _Optional[str] = ...) -> None: ... class Metaobject(_message.Message): __slots__ = ("element_id", "metaclass_id") @@ -1428,18 +1438,20 @@ class ValueSequence(_message.Message): def __init__(self, elements: _Optional[_Iterable[_Union[Value, _Mapping]]] = ...) -> None: ... class Quantity(_message.Message): - __slots__ = ("int_magnitude", "real_magnitude", "big_int_magnitude", "unit", "unit_term") + __slots__ = ("int_magnitude", "real_magnitude", "big_int_magnitude", "rational_magnitude", "unit", "unit_term") INT_MAGNITUDE_FIELD_NUMBER: _ClassVar[int] REAL_MAGNITUDE_FIELD_NUMBER: _ClassVar[int] BIG_INT_MAGNITUDE_FIELD_NUMBER: _ClassVar[int] + RATIONAL_MAGNITUDE_FIELD_NUMBER: _ClassVar[int] UNIT_FIELD_NUMBER: _ClassVar[int] UNIT_TERM_FIELD_NUMBER: _ClassVar[int] int_magnitude: int real_magnitude: float big_int_magnitude: str + rational_magnitude: Rational unit: str unit_term: UnitTerm - def __init__(self, int_magnitude: _Optional[int] = ..., real_magnitude: _Optional[float] = ..., big_int_magnitude: _Optional[str] = ..., unit: _Optional[str] = ..., unit_term: _Optional[_Union[UnitTerm, _Mapping]] = ...) -> None: ... + def __init__(self, int_magnitude: _Optional[int] = ..., real_magnitude: _Optional[float] = ..., big_int_magnitude: _Optional[str] = ..., rational_magnitude: _Optional[_Union[Rational, _Mapping]] = ..., unit: _Optional[str] = ..., unit_term: _Optional[_Union[UnitTerm, _Mapping]] = ...) -> None: ... class MeasurementRef(_message.Message): __slots__ = ("unit", "unit_term", "unit_id") @@ -1682,7 +1694,7 @@ class DocumentQueryBinding(_message.Message): def __init__(self, parameter: _Optional[str] = ..., values: _Optional[_Iterable[_Union[DocumentValue, _Mapping]]] = ...) -> None: ... class DocumentValue(_message.Message): - __slots__ = ("element_id", "string_value", "int_value", "real_value", "bool_value", "infinity", "quantity", "verdict", "object", "state", "event", "big_int_value", "element_type") + __slots__ = ("element_id", "string_value", "int_value", "real_value", "bool_value", "infinity", "quantity", "verdict", "object", "state", "event", "big_int_value", "rational_value", "element_type") ELEMENT_ID_FIELD_NUMBER: _ClassVar[int] STRING_VALUE_FIELD_NUMBER: _ClassVar[int] INT_VALUE_FIELD_NUMBER: _ClassVar[int] @@ -1695,6 +1707,7 @@ class DocumentValue(_message.Message): STATE_FIELD_NUMBER: _ClassVar[int] EVENT_FIELD_NUMBER: _ClassVar[int] BIG_INT_VALUE_FIELD_NUMBER: _ClassVar[int] + RATIONAL_VALUE_FIELD_NUMBER: _ClassVar[int] ELEMENT_TYPE_FIELD_NUMBER: _ClassVar[int] element_id: str string_value: str @@ -1708,8 +1721,9 @@ class DocumentValue(_message.Message): state: DocumentState event: DocumentEvent big_int_value: str + rational_value: Rational element_type: str - def __init__(self, element_id: _Optional[str] = ..., string_value: _Optional[str] = ..., int_value: _Optional[int] = ..., real_value: _Optional[float] = ..., bool_value: _Optional[bool] = ..., infinity: _Optional[bool] = ..., quantity: _Optional[_Union[Quantity, _Mapping]] = ..., verdict: _Optional[_Union[DocumentVerdict, _Mapping]] = ..., object: _Optional[_Union[DocumentObject, _Mapping]] = ..., state: _Optional[_Union[DocumentState, _Mapping]] = ..., event: _Optional[_Union[DocumentEvent, _Mapping]] = ..., big_int_value: _Optional[str] = ..., element_type: _Optional[str] = ...) -> None: ... + def __init__(self, element_id: _Optional[str] = ..., string_value: _Optional[str] = ..., int_value: _Optional[int] = ..., real_value: _Optional[float] = ..., bool_value: _Optional[bool] = ..., infinity: _Optional[bool] = ..., quantity: _Optional[_Union[Quantity, _Mapping]] = ..., verdict: _Optional[_Union[DocumentVerdict, _Mapping]] = ..., object: _Optional[_Union[DocumentObject, _Mapping]] = ..., state: _Optional[_Union[DocumentState, _Mapping]] = ..., event: _Optional[_Union[DocumentEvent, _Mapping]] = ..., big_int_value: _Optional[str] = ..., rational_value: _Optional[_Union[Rational, _Mapping]] = ..., element_type: _Optional[str] = ...) -> None: ... class DocumentObject(_message.Message): __slots__ = ("instance_id", "path", "element") diff --git a/client/python/opensysml/proto/sysml_pb2_grpc.py b/client/python/opensysml/proto/sysml_pb2_grpc.py index b3ec296a3f..b03ac7c4f4 100644 --- a/client/python/opensysml/proto/sysml_pb2_grpc.py +++ b/client/python/opensysml/proto/sysml_pb2_grpc.py @@ -5,7 +5,7 @@ from . import sysml_pb2 as sysml__pb2 -GRPC_GENERATED_VERSION = '1.83.0' +GRPC_GENERATED_VERSION = '1.84.0' GRPC_VERSION = grpc.__version__ _version_not_supported = False diff --git a/client/rust/conformance/sysml.descriptor.binpb b/client/rust/conformance/sysml.descriptor.binpb index aa5d56aab43b11785a53bcdf9b5af6bf1845207d..81d91db4c299f93c5b28c52edd02ddb0e5586e08 100644 GIT binary patch delta 12420 zcmaKydz4hgwZ{7tP1DoUr_Wnt#sMwZFZ0 z?b@|#*EzH4p`yo^7PZMNUHVL$)BC7jrdM_Ev%XDJczz~VSJ6;aTT@Xz;$Qz z^3EB>=XyQrC)JOu?lCAHyfQJ!wSq%Mu1_kHytS%kbhn>o(pD(!oGGel99M~8ZCytP zMMZ-OKZN;A@#xB$+HqAi(YVSx9s>XP+p51_Hn@$Nujcgo$*<4fgB+Sm9Nhf3hI!Y+E&E=zgdC|IaV??-YDHzg1UV*D=Vf*xoMFE^M1fygsG2 zs-?EdY5q~>Xt7h)JumZr;JDLXf9G-MZ0DPn{*OBYikcQWnPjrRY9A)QE>50YHp|KP zuE|&4UNNd6Z{w3c<7bt1SM|$Z(4%LMbNM^}%zRC4espc)&DE9pv9;Bs>kXlLQulmC zeSYNkdVGImCSO~ZANl?KIr*L=^Odqh6%Cd37v*bfD)SZpz-=m!^P}o&>+37)MMqtw zUDzI(jGJjI9;PA}vlco6-A)#^5!f}K`Nqkpy&-!6sA#yYCAU@^`<;4 ze{*GBzN#i)T|2R|z9HXGSvRi!LiycfM85Vm#w#-U@wN3;4OO>Q=Kmoby5}n^Mvcu+ ztgC8hsH_p!Z>}0MqN=9Bq-DOUzDK?{gi*DlQBqfVOXaABdWk}Y7&hdqn*Q0WMD72E zs1Tv<`TAN3eeCjBtTjxm&DW2wtgf!A8Bw)1+8I#JGfMBZN`Xk;n=LAdlmbzDZ?=7h z%-bqrGCHRv`K&X<=`~P^KXVFH@n=r9=!BT+K%JBAcw%O+!kk0Roz_<%Hg;NHhsuKO(*6JB48D|S^z16=5np!|^oXy!Y+w;a&n^jU zBd%T>rryaW-qRob$r)dIErRc4)49we;_9{HYOAh##raKNo-xsCO#m^`YE1xPwpv$# zm}nJO7fCp-3sWCv6MxY^-QX-Jxemz>W$FG46W57}kMxlZ&h349#>7X~1P~J+*@y!n zeqJTgZpj$yFS_#KueXd?ziQSQKtD5XnQfh5=D}NNQNWh{)h!ed8wQN{F&Y4;P5y&Eag3 zsnv`^Je)1-l<6H2RhT@Y-`wO}QhtL{5RVjyl1N8v1Q>;QL?RGvts4X$)mLnG<~Q-o ziX63N0SNP`WVW2LKoK>uQ^^~{)Uj;hPS=T!l^c;fmQ9}!h4n_6|8ZLtKrB1ZY*-*h zj@!%vLOm|Y+exf|v2t9pcbP zEl&R?Afvx>(`IRhi4W7#V7Gk=qgRRcMreV=aOfOJH@pQM?`Xr&B?w*v-43DgWX1K{y z%KHC{F6-N7@jrG~Q5W08wlBR+n-Moj!=lB-fN7F|I5Wd_Pl_WVi#x;Z_MOcAQW}OS z)@+uZyVaRl&NC61<;MF02y>PjZz&+mSuP3nrePx7tgqYV?C3R2wPmfF-FWGNFq_?C z(-i<=HoJ5MhvWnp9;P006Hm5`*zVLirNa?@$fbOha_%a*3CX7=e+QxT zCYjPo-TO`F>L#9<(n_lnh~!E)?!18LTQmx~f~*h50@e^_BTXt|0Y!o!jm7y!0j|i_WNt36RBm)-5(Y6A;DEy6w9}2?Ip& zvu^(6OdqB>B1}E+CSKH2cRQDqj6m>tmxfMSmJu?|HTsR+&J|5O^fx^~_;- znH{oRqa?O_^saZEt>rvpYmbdA5Yat0`+?ZnBYr-8al~YFOUv$7r+e2+M@0?bmR!6w zfoR&2D>l&qifk@4Cl(M*TXM8cL-1~Nn0h@&uX_34oB^ey5qv$z-R5*mj27p1NB>|%*_kd{HRd7OjDR5Bpc)?C-qtT0wub7!8F zX%QQviy*Z}#iLCC=#mpxUj8l92@Q961tPLRH{KT#h5aK7f z&@>!CY6eK2}Jf_uH(su8qNA(?vztwDuag#R7oOW z2eW2lqAWU8;L1!73HLe{Wo(%GI+vL0>A$s6gG$Dt=<8g%6Ll_`94j6j)fGwg-Ey9Z z-qD;gr4@+e(OkP+-QIUj$&^GwDE_slPU_Y+-|doak#%Gw1pHbSQyeJ2_PTV9 zDWK_|I$0<`{FWk9=htZJXo#9)kEusiE|f^zxpow}_RNe0P;x-5v3AfK@%H((fqw0PMorJwjn40S)9@IUH)Bu03jd~mLO-E7B zHv!O~ZR~-}P>X^ShDDgat%7&aCi?trevdZ3NI{zCvGGeI5@|k!JE1QNLKPE>?Y;cqTU&dvWCk%9t155r7aEdvVuri#W2x#v8;^ z0Zl6fgu28dz!F~|)Fl$052PI58m5+ciPfG{_PWXso>*Bqeo#ftn3joI)z2xt73MOJ z(llMR)~zy&r}S=D{i>X2{Cmp!2SoHKFYc0o74)@c2o@#51B zC}UoHx&dK6V`p}&%xt9>w{{?A1~dmF5b8=VZK4T;x>BMkNqDQERh}|OItVJTyXDHZ zSmn{ONmLk)q&+|LyhNosO#Q)2yx#Jfr^=nHs}cQ!mo{g0wM1c!r_6}~A`U=v!UB<9 z%5|!hMgj8uTu>qVie?^7KW!u57b0twD~FMhEI zM9+FJuzDmm>qXCyNDsUD6+NUx4J@x#NrUT=ie_zToYBm-;kc;g`TND^Y(MGSmsqsJ*ZItrD=_-m4-mD)iRf}*Th1ShBCx9?F zdvU`Fgt^(H;hbZb?dV-~%?av0qSp@Q)7h-rx1z4*N~qY$@?$v31bj0;n5dWlc;>h@||$v7n6^ysAJ)%-Zgq__0k4r)Lj zo{7g>HXcBx_?C?a5aL@l9zaaICGn_<^f0(bpV3j>f*vWhdkT7_3)oZ8BU!e`i|?EW zqi2uDom1vGPLixuztK_kN00RNtpz<2@>Z|dv^$KVr`0PlZ-s&AY4v!0`GZIg=zU#z zqUu>bE;?i1x61>B_`b($2-$W(5%o?i%S4~X#);7n^=l`p1u*3%@u7_}5ax$ok?A#n zFhBI@HF`xRk=^Ufd1U$bAWAOn^^|%04urVZ^G!1kgt*tEnSVesVw_CuV|~0#&AyCh z?0;OCbV|m&_|+7nDEindGshYbMIU=-oSE4y>s%A2KJya4^!2Vz>XqR&h<@g!J4U6b zMi%;W&Y)=0Agln*c?iVH=U)7x90>Juuhi5RAXYw?gJ}}p)=1nB=)ZPWjeU4V&w+v- zDJTbQH^ZoidVFt{NCVMxKvG7Ini^T^gZim1s&7x8As+PNrVa@4pcglFK!^t=(DKH= zM$lLK)UIl9InNNkvMU6H_?6v2KoOM!Y5Uvv~PAcrU99 zGHC*YdDNx}5av-ylPSh~3db?sJ+GSj)~I&4aID}$I}{x&ETGhpV_vCUZK)&2WRLAL ziYO#^`TC2z`iMA4CArIw3pfz!U4C3ifKc!9sU#*2MYN}W{F+7E;}#g7|m zAk-;-srj~8qcXhc^}kKP4E_TzvN6msfvJ96{t-siR6j764j`(g`epIgwAwIrub+6J zWo9?k;*{1Rdas{8DZ2mGN}kX3<^EgFGycuA{sEDk>Bk2n5T!GH4#p@sY9;4p>lLS{ zhha)0&n_?}k!RbPF$#0G&sE_*iD#{3aJdNj zY|9T$R~IvhK~Y7X)ML+9=R+)l_@tjS zJu(pDlYV>w210yNHohTJLze5EhA0(dd4VVuV|jrn6=S&&`$ZOu!{iG6!|v)z(yy4P zt|%;KF~k*i3or_Ch2N=5)Q?P1S*`*;|0r|WzxaalpZ+W6pTFA_KHHoZC0iEXQENJX zjNrmEe*DiZKwNmn=bu|{ihOs&yrGfLO`@s$RMSUd4$W7Y)zS`A`XF{;akB>ee z%uT*;b}|s=CfUiNlnJNN)4F3M~<(^)izkdgHlj;rTqHbca<6 zM0AJmn=AyPbcfG-hpQtZgKzjMEc@}C@ppFCYkI0P%j=^F@vGT#z(^>b5D*P-$b_O^ zv0f&$TX!s{SLB&c>@F~6Lc47!7=^i8n9(QNdV%lgkIU8ko;<^Rr@)lz@=k#%)#V*w z$~Lc;c|Ir*rNVq*y9P!fejr@~eW}={ z#O&1_exUx`muF_M*PcK?6z%on7hpgX?e*i=-ar)X^=W}#l{{z&Q~UhHQQhr)^{0{s zB=^brBj4v6Bn|fKug_Ok_2d~(_xtgc9f;(9+e`q_xnFXtIwCUog`RYQ8Vga<`3pNM zpp4mo0wI1O0X;1uviJvd>4mBrM2YT!0#O$KfL%gHAs&z=lvlb9f)47c3)M9cW$h0Z zh_dzv?dia%h|1n;jEF?;u>PM5)dYxg?LBN$9SHHT@0$`2gm_pgfe8*qj_Br#qTtAq zA1PRrB|lUT|2}Z+Z}KK}I84GDa@jjk0%V1p2HW ztNt)$^Uf$RWdUad@!gV9m@@+IA96-FN_x!<^zt983z`5C`Zh?if-?(2l1)2Pf+USh zqfG6-!qjBZfM&ll2=%_8$h^G*LcK5G?NwbwW!K&x@EUt`{+h9s`P!T1pBeMIvbP%E zvoY!v7&TwyfM~kE5Hs1g_e;#o8q+B>>#usNYaz;gqq*RMbPCOdc}u6zEc3o0a)C}^ zu5Rk1hW2SxMTBo|5Vx*C^vn(7!U06j+#ocy9*CZ~0Ud{NfW~2-?r^EPmhj2dV_rc| z20il%dZdq-7sLe!Vf4%kPX10DP5OxWLHw?%G3q3MHk#5&%r8tuI*Iu~P?EVInhKr7 zfqsn3xXsOZxEGVp@bN~wiKaB1Lw}q*NLE;g8O<#3&|Jx8;7{o8gZiGR-p@z+KWR)EF| z5G&6G#pZnu5bATmN#^wc5G&6Gd~FneCyBq$2l1B(5SoBillc35P~0hVmIRRDNV>Cm zQw~Jc^8s(l_evSPBTTIg60hpXKT+?O-ht%WAl)Up_ue7?t_z}jFVEz`I-3VTB-aJ; zy%&hibpeM;)WqB&{;k)Weo7O=GtBh`rg*bHi2vo6QJCwci1M*EnY=@a=*E`$KU0Cz z3jlRvfhuY@+T>#t>PAVv4`m|12vb{w#OwO~eyX+U7f5am(p{r){X!N=NngMqOerD(Ug_**jO(OC3w27m06GvO~uFZU1Y?XB{A2@XEWNj&{oj&g+ z>muu`T;G?hK1KF>1sRp>6QriviJ7@?U$fq}O8Qtqa;lvUGW|2jWFM6XlE2bBa#o*8 z#%%z$N@uc}HZq0euk50d7#0X@cUI;j6*8H9JU8HVYk*bNM_EuG&r>a#w2RtCR150k zcKZ&QLkfTT2C3P0;@RB(_0|xpq%Weg?R05oI9mIPKXY~84c3rK#weW|D+MAs*Di8H zr9hOQXoo~+a;x$775SoirJO9 z%igkjT2%n3D{WO6$%;QK?X+15vrt#cN?Mcn(=SM^u@kT5RySKWsv-RlT_gV3XzeHd zthH5_P!A{>!b}WV^;)~wc*HE!wYJ|Op2f;qyHn>(lWao&Ahp3xY|IVVWL;r(=#S_I zJKZj`NMhMv%rx1myzDpngLkdl^r?5O8>{&%=J>NQ2V_xAu{j_*nry#gXby?#lwJH^$d5{^59)NVVmS1)+qT2OKalDlOS{|6Iy zh>1P==Uc3Ydosqvp4bEs6MN!_10n8-BM!vG9*OuuF>z;*+HWU5)@@oXU4JK%`z3KD z8}Aeo2jWu(BE~>dK7hy`u#2)`J^;~r!1nxz%H%;?ofS>W`2faL3F|?-$Q<80!>}H- z+qTQtUx`O|1*yY!;)L$@fpxOvE<_L8>5@!eJi1FfI-lG52JgWcjp>;Dv@#birDBc{ki;S<#LOg1hb=4=On2LAU0r#XQV>7O z6D5&8izC1+#LpxG;UT+A;OF|;t=4n(jM~Fjl^h?ESq&c~Fr0 z#!fu0Ten#+wi|@#H&TTLvgSeQM36ddtEswSyVbiM$n0r5ZMJrh`0%Zr>=gPAA|}A1 z7$W9psW63?W`K4l7zP*8$}xkvmskt8ZA5tvmtv{`^*KY_b?hR7UXA`UR z%#W;JmE4Qu^H~z*c9h;L3t6GN?6GdEXUsxY#5#dUuE<7b9Ei>pSvlh&k;xacQF*vG zOa!1Q55REtd#bGDMWNmtQd!8#tg0xxvb=g|?TGTp<44uimg}Q?tRC0js}vi?tVt;# zAzGO&Hhl^Z#VfPzI)_^ZMDfaO`PrFjmN_^`{UMuJt%vTlt}Ypj;2*LyQPL$0mSw)8 zU)^inRL_`Yz7qQiMDmq5bU;+TBB7II4i3W+^_0I`mzE5HxJJCY5F5xS5wEiHAto*XvpPtQRX8V|9JrsvO7lar!eWqLTiX5R?0Y)SKDF zCjG!atnT&qA^2uCZHG&_Ph!xNjru_lN`Yo6KxCU@r9hN6iBgHeed6m|aVmnK0!=3e z3}*?olu#hlw1QomHmt0O0~-uGFQsvr+TOEZn!VG@mo%Fb@rq@9Eb4*0j>0h`pCh zci@^+$zefiOE$4l4?AF8RXU8BE!lKgW~Xe|Fp2F}{ig%g)=I|M+8Rd|i0IZh`+?Zn zDt@lMDr7R-oLhU)>fYu0VWul|qN52!Q?pZSq5~9JCooq75KYYvJ<0&Q8y=+IaT4#( zKS%W`9gf^P4xba&yF29x>^Wa(#iNF0UFM<^Z!M z^?+z<$vYvt-Xc!SmrLS)I!Gt6BRAhto;BotM7KH7O@F_vY`dc_3Nrx2o&!x?1LE&? zr`QxSAk^(nm-Ay`tZa9FaA77VRz}c6I*9|hWP!TWDjk97Zil6PDw{h(%-B-Np6 z#u(Wf8vzo}z3~nKVeWMTbF%|6ve)5ezbn+k=BfDrdPS#uVF5cfNr#qh)7fgp9rNgUC$ zQ))^514tfnIJFKsACPwDpN{&}DF_vTnf|9tODa4dA^5~en&5#5Ezm3zi0mg$`?C!- zT=gf;x#vYxCJ*PSl10F?E{=p{rHAueS?OWn-pQ_v3{uCO#8-}0c1!u(OL~{+&kNO0 zOGd&t=A=7P?~=)p;@NS1OV#gB zR?pWnhIt}RNubO*(VYNa)k-H%BDE_^UZO>+~!)~Acr|I|N-=rou8mP~w5Ry5s>f)672Kofi* zveVrt_&{t=ck#qfnVjLO^TKp^Fg$@k6Kx=xX1Gxs1Pq1Sm1oN2dI6(qhD)#{+yy~u zrki+5ds)@To#{r=E)d_I&=az%Tm2Jm6lVU&+MW<*Tf$r*v3k-~ohu5i9bvRbq~g***rI@#y}+JxkaWg0kZCSE`5nKmZQYtd_Bii zJ?j}moS!Gk3g)}fR|2yT=etn{F-m-EjAIWX*g(@(0iia!1Y2SagxV+(J0P`rbdXx? zCVuByWjj=PzbPX|)Z9LF{K#B?PxZ1&N5fp~Ql_4e<270qwoI?})vxtnSGDhC>L7nc z<1&{zIM0Z|dO?p5)a?4v;l{t>p zgboOGg@jJBZ?vEnU1jca5L96Im@5b3MVDSpLcnw=?RuH@5`r;7>SZ_aPHt6+s*5m#BqZzH=;;oKo^`Gt>ye186Fmb$J(SAV^`&R2zPK(Qov*u5uLp$qy4&{L zC?e9_z3z6qKx&J)j<*|h?kx4o>lveFL*9cnDB2L$0A^9N!EI+sIS@q~q?&MZy6n^Vm!dbYbI=Cu`w zp6xErf|rGQKs$AHXVs%}Y<)A zz2gu);HKM$1!$aX^g(W(aM2*F0L}FW#L7W8dVmH(J?NI2sshByLAj0|VV&b7?uYbW z%hlv+#^^be*CS=*kQ;rnFe{>NV2%(FJ%=P^6Tm6ZC@@&Ydm`C$W z+1sP>-ZBgGsF;-UJx<&^<|@+xgU|zvqbHkvEKYD{(R9phXF5b6nvS_NwUQ;{B#Do^ zYVM$>i}7A|7i7`|2=jQHCP0|SB~7Lo?_H|m>#P``9b&4boBl?jqxHh5YhJ6>|SK@a|cdh&Y4 zEV03hvINKq8@%GK;RytysKG10AciPv@Gk038C1y$LF!2_@f+R!LUmjH1O%V-(w)K` zm>{c~+}pBw6U5?Eo-#Kh2={G>Yb zjeN4z$O3qv?XG@U&lvX`z36=#h-9M|b>Tp4HhTPMD(=^a``U|M)j?2!#(iKoTWk^r z2vvL0t9p%W%d=k6tPKPeSZ`kB!Em4T_+XI>0SNb5$p&K<^qiM8jYv&cZ-B8`sW;E% zJ(ha&T;5|M{M)=)DL6p0S2etV!~JcXgkZS8&6`DdftNIwRE=t5V9b>gvmnkwW}z;S zEbI{mik)1jQ$JD{_oxXg%t9||I!hqLg8#ouc}A_ZoVe(*=?N{CWX7J11y zC{j@td8OvIWftNhkK6XTkVrvUtats0FNzwKhPc>Enrj*eaj{ooezO2VTqHnlZT@JAb;u0@udSoEPC0_KJ0))84qXZZt)nut&ZHQ7kmgb34JC^2&QahFk zu~%rZSbop$_+xc57K@4M^898NLtGvo0cIgC_d0gY3=WAl{k(2}iMqS0M%fTo#K#T@ zafKKC*bjubLZZ4w+V_dH?_T1K-0vF(pvqy$#r^dIQ`XUvXUaNSgc<(uI7wiuzVQn69L`Dy(VAzznW7GM8meYdIQn0O{#aFPy;dDp})LR^tvwjy={_&|Oa>yk&x@{dAUgL+N{tPPOdinxd$oEH zqGa)bcvV1|ivtRTct8Sren@2VKi0L^s0xS@-H-D`+5C^=Eo2tr$Fha;ep)N&kZ!p~ z^@k{Xe<)9sy+0IJWM)NFj@;ysNaPOd-q)&$5apxuaGc>lh=)DTlvNR2 z|NGi7II`tO@)l*wkHkAy8*cd#**SSKu9dTSRM-AY4d}@j^GD;21ES|>ym3JE9Q6WI zo`E=URLb+OW#j6C)aM=#R`zx3vC=vuKbMUggo!%YxRahuJ@W$$5b7z9pIXEJQ>l}a`?aUPsaB0pWtm^c z`2vLcb(}9is9#IIG)gkp2|VrTcdl1+DgiirI`6RT-|0AGmH#5&4lNMonS7As(4LVXN%K%A zOPlIPM*##CXwEwj>Qp~^Yy(1_>hoeWDWr00r}?}U-(TMU!4c&XhK(LEysliobA!6K zM_qV3Gi!eS0-|Y}ADuBEnx^^0%Go&jU69*(hX4J#<#wLov+lb? z7r31p^gnJ?1FP#)5#ej_qjnL9o(4ZE96__$w6wipLj;Exmn%TdoqHv{pdA% zvK*c{e)JO=h@}9HQXsN(Vx>To&hbf%L7d2kg4BGU*V#TlR|5w>gy4Li-x(!k9ulRE ze$=Id2p7=wn?Mq((T}=xAk;>mE?s&AAT}C(n)FH7m=dHG_=)8@d5aoXIt9T6K92zl z<$v?0h>@ka8MmnRRy_cvOZ}(^03y58k9q(gN|#D9iF;GzbT0E%x6mR8_khMdAexr> z(SMl$L*dR9u`rsJ`RARVSt@Km44bh);}!%t@5Ma_CQzx z8Y@7otn!P^YYGtRD!;4wI0s^7mCtW`;_t)a?@NC4><>Z{FxDjgzT_8o%v>k|WIB}Y zWS*jdsCvogDf*C<(MN*RYCrL3ec)ELzw{9#SNrMC;kV`^;_s`zd~068m^^qj&I2Hl zulmuqCJ>#k`dlhu6Z44p$BT7`ve|mh1*)Cy*;`GmCqtdmY;ZAU(;9ZYxgLUZ~5si zVc;H>B{lna^!aV7^B~id^GE2-e)L<|qoR9bd;maL1{$Y<;cRrOfKWGLd047u(uBGR OJ$8OskW44qC;lJYt(E-% diff --git a/client/rust/opensysml/src/proto/sysml/sysml.rs b/client/rust/opensysml/src/proto/sysml/sysml.rs index 44c74c0125..6be81a420c 100644 --- a/client/rust/opensysml/src/proto/sysml/sysml.rs +++ b/client/rust/opensysml/src/proto/sysml/sysml.rs @@ -1847,7 +1847,7 @@ pub struct AttributeInfo { /// Value represents a runtime-evaluable value #[derive(Clone, PartialEq, ::prost::Message)] pub struct Value { - #[prost(oneof="value::Kind", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22")] + #[prost(oneof="value::Kind", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23")] pub kind: ::core::option::Option, } /// Nested message and enum types in `Value`. @@ -1919,8 +1919,22 @@ pub mod value { /// zeros). An Integer that fits int64 is always int_value, never this. #[prost(string, tag="22")] BigIntValue(::prost::alloc::string::String), + /// An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1` + /// or `1 / 3` evaluates; one a double holds exactly crosses as real_value. + #[prost(message, tag="23")] + RationalValue(super::Rational), } } +/// Rational is an exact rational number in lowest terms: numerator over a +/// positive denominator, each written as big_int_value is. A decoder rejects one +/// not in lowest terms or one a double holds exactly, so no number has two spellings. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Rational { + #[prost(string, tag="1")] + pub numerator: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub denominator: ::prost::alloc::string::String, +} /// Metaobject is an element of the model held as an instance of its reflective /// metaclass: what `x meta KerML::Feature`, or the last element of /// `x.metadata`, evaluates to (KerML 7.4.9.2, 8.3.4.8.15). It crosses as the @@ -2080,7 +2094,7 @@ pub struct Quantity { #[prost(message, optional, tag="4")] pub unit_term: ::core::option::Option, /// Magnitude, keeping Integer and Real apart as the rest of Value does. - #[prost(oneof="quantity::Magnitude", tags="1, 2, 5")] + #[prost(oneof="quantity::Magnitude", tags="1, 2, 5, 6")] pub magnitude: ::core::option::Option, } /// Nested message and enum types in `Quantity`. @@ -2095,6 +2109,9 @@ pub mod quantity { /// An Integer magnitude beyond int64, in decimal, as Value.big_int_value. #[prost(string, tag="5")] BigIntMagnitude(::prost::alloc::string::String), + /// An exact Rational magnitude no double holds exactly, as Value.rational_value. + #[prost(message, tag="6")] + RationalMagnitude(super::Rational), } } /// MeasurementRef is a MeasurementReferences::ScalarMeasurementReference held as @@ -2566,7 +2583,7 @@ pub struct DocumentValue { /// Metamodel type of element_id ("PartUsage", ...); answered, ignored when bound. #[prost(string, tag="7")] pub element_type: ::prost::alloc::string::String, - #[prost(oneof="document_value::Kind", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13")] + #[prost(oneof="document_value::Kind", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14")] pub kind: ::core::option::Option, } /// Nested message and enum types in `DocumentValue`. @@ -2604,6 +2621,9 @@ pub mod document_value { /// An Integer beyond int64, in decimal, as Value.big_int_value. #[prost(string, tag="13")] BigIntValue(::prost::alloc::string::String), + /// An exact Rational no double holds exactly, as Value.rational_value. + #[prost(message, tag="14")] + RationalValue(super::Rational), } } /// DocumentObject is an object the service holds for the model, created by diff --git a/internal/check/passes/typecheck_expr.go b/internal/check/passes/typecheck_expr.go index 1b563f3ce9..fa6eca957e 100644 --- a/internal/check/passes/typecheck_expr.go +++ b/internal/check/passes/typecheck_expr.go @@ -170,7 +170,7 @@ func (ec *exprChecker) checkBoundValue(valueScope, declScope *symbols.Scope, d f // Uniqueness is judged last, as the run time judges it: a value refused for // its type, dimension or count is not also refused for repeating an element. if !ec.errorsSince(reported) { - ec.checkValueUniqueness(valueScope, declScope, d, value) + ec.checkValueUniqueness(valueScope, declScope, d, value, want) } } diff --git a/internal/check/passes/typecheck_metadata_body_test.go b/internal/check/passes/typecheck_metadata_body_test.go index 35282979fe..b5f7cfecd4 100644 --- a/internal/check/passes/typecheck_metadata_body_test.go +++ b/internal/check/passes/typecheck_metadata_body_test.go @@ -80,7 +80,7 @@ func TestMetadataBodyValueIsBoundByTheRestatedFeatureWhole(t *testing.T) { "2 value(s) bound to a feature with multiplicity upper bound 1", "2 value(s) bound to a feature with multiplicity upper bound 1", "2 value(s) bound to a feature with multiplicity upper bound 1", - "0.3 (a Real) is written at positions 1 and 2 of a unique feature", + "0.3 (a Rational) is written at positions 1 and 2 of a unique feature", "cannot bind Natural value to a feature typed by Level", "cannot bind Natural value to a feature typed by Wheel") } diff --git a/internal/check/passes/typecheck_value.go b/internal/check/passes/typecheck_value.go index c0ebb8cbfe..510afb434f 100644 --- a/internal/check/passes/typecheck_value.go +++ b/internal/check/passes/typecheck_value.go @@ -127,6 +127,7 @@ func (ec *exprChecker) checkEnumeratedValue(scope *symbols.Scope, enum *symbols. if !ok { return } + got = heldBy(ec.model.PrimTypeOf(enum), got) literals := ec.model.EnumeratedValuesOf(enum) if len(literals) == 0 { ec.errorf(value.Span(), "cannot bind %s to a feature typed by %s, which enumerates no values", got.text, enum.Name) @@ -144,6 +145,7 @@ func (ec *exprChecker) checkEnumeratedValue(scope *symbols.Scope, enum *symbols. if !ok { return } + want = heldBy(ec.model.PrimTypeOf(enum), want) if want.equal(got) { return } @@ -203,7 +205,7 @@ func (ec *exprChecker) checkValueCount(valueScope, declScope *symbols.Scope, d f // checkValueUniqueness reports a literal writing one const-decidable value twice to // a unique, sequence-held feature (KerML 7.3.4.4); other equality is the runtime's. -func (ec *exprChecker) checkValueUniqueness(valueScope, declScope *symbols.Scope, d featureDecl, value ast.Node) { +func (ec *exprChecker) checkValueUniqueness(valueScope, declScope *symbols.Scope, d featureDecl, value ast.Node, want semantics.PrimType) { if _, ok := value.(*ast.SequenceExpr); !ok { return } @@ -217,6 +219,7 @@ func (ec *exprChecker) checkValueUniqueness(valueScope, declScope *symbols.Scope if !ok { continue } + el = heldBy(want, el) el.position = i + 1 for _, prior := range seen { if prior.equal(el) { @@ -292,11 +295,28 @@ func (ec *exprChecker) constElement(scope *symbols.Scope, element ast.Node) (con return constElement{}, false } +// heldBy is el as a feature of scalar type prim holds it: an exact Rational a +// Real-typed feature holds is its nearest binary64, as at run time. +func heldBy(prim semantics.PrimType, el constElement) constElement { + if el.scalar.Kind != semantics.ValRational || prim != semantics.PrimReal { + return el + } + real, err := semantics.RealOf(el.scalar) + if err != nil { + return el + } + el.scalar = real + el.text = semantics.FormatConst(real) + " (" + constKind(real) + ")" + return el +} + // constKind names a constant's kind as the runtime describes one. func constKind(v semantics.Value) string { switch v.Kind { case semantics.ValInt: return "an Integer" + case semantics.ValRational: + return "a Rational" case semantics.ValReal: return "a Real" default: diff --git a/internal/doc/docir/evaluate.go b/internal/doc/docir/evaluate.go index af99f3d45e..1d115f6f58 100644 --- a/internal/doc/docir/evaluate.go +++ b/internal/doc/docir/evaluate.go @@ -844,6 +844,9 @@ func (e *evaluator) executionValue(value docplan.BindingValue) queryexec.Value { if integer, ok := value.IntegerConst(); ok { return queryexec.IntegerOf(integer) } + if rational, ok := value.Rational(); ok { + return queryexec.RationalOf(rational) + } if realVal, ok := value.Real(); ok { return queryexec.RealValue(realVal) } @@ -897,6 +900,9 @@ func (e *evaluator) valueText(value queryexec.Value) string { if integer, ok := value.IntegerConst(); ok { return integer.FormatInt() } + if rational, ok := value.Rational(); ok { + return queryexec.RationalText(rational) + } if realVal, ok := value.Real(); ok { return strconv.FormatFloat(realVal, 'g', -1, 64) } diff --git a/internal/doc/docrender/markdown.go b/internal/doc/docrender/markdown.go index a5ed27dba5..e1318a3b5e 100644 --- a/internal/doc/docrender/markdown.go +++ b/internal/doc/docrender/markdown.go @@ -717,6 +717,9 @@ func valueText(names namer, value queryexec.Value) string { if integer, ok := value.IntegerConst(); ok { return integer.FormatInt() } + if rational, ok := value.Rational(); ok { + return queryexec.RationalText(rational) + } if realVal, ok := value.Real(); ok { return strconv.FormatFloat(realVal, 'g', -1, 64) } diff --git a/internal/doc/queryexec/computed.go b/internal/doc/queryexec/computed.go index 82708e6af0..aafc5d5b93 100644 --- a/internal/doc/queryexec/computed.go +++ b/internal/doc/queryexec/computed.go @@ -568,6 +568,12 @@ func (e *executor) applyColumnOperator( integer = semantics.IntNeg(integer) } return IntegerOf(integer), nil + case ValueRational: + rational, _ := values[0].Rational() + if operator == "-" { + rational = semantics.RatNeg(rational) + } + return RationalOf(rational), nil case ValueReal: realVal, _ := values[0].Real() if operator == "-" { @@ -607,6 +613,21 @@ func (e *executor) applyColumnOperator( return IntegerOf(q), nil } } + if exactKind(left.Kind()) && exactKind(right.Kind()) { + op, ok := map[string]ast.OperatorKind{"+": ast.OpAdd, "-": ast.OpSub, "*": ast.OpMul, "/": ast.OpDiv}[operator] + if ok { + l, r := exactOperand(left), exactOperand(right) + if operator == "/" && r.RatSign() == 0 { + return Value{}, e.columnError( + ErrorColumnDivisionByZero, column, row, expression.Origin(), operator, "") + } + result, err := semantics.RatArith(op, l, r, semantics.DefaultMaxIntegerBits) + if err != nil { + return Value{}, err + } + return numberOf(result), nil + } + } l := realOperand(left) r := realOperand(right) switch operator { @@ -627,7 +648,28 @@ func (e *executor) applyColumnOperator( } func arithmeticKind(kind ValueKind) bool { - return kind == ValueInteger || kind == ValueReal + return kind == ValueInteger || kind == ValueRational || kind == ValueReal +} + +func exactKind(kind ValueKind) bool { + return kind == ValueInteger || kind == ValueRational +} + +// exactOperand is an Integer or Rational operand's exact value. +func exactOperand(value Value) semantics.Value { + if integer, ok := value.IntegerConst(); ok { + return integer + } + rational, _ := value.Rational() + return rational +} + +// numberOf is the query value of an exact Integer or Rational result. +func numberOf(value semantics.Value) Value { + if value.Kind == semantics.ValInt { + return IntegerOf(value) + } + return RationalOf(value) } func hasQuantity(values []Value) bool { @@ -701,6 +743,9 @@ func quantityOperand(value Value) (semantics.Quantity, bool) { case ValueInteger: integer, _ := value.IntegerConst() return semantics.Quantity{Num: integer, Unit: semantics.UnitOne()}, true + case ValueRational: + rational, _ := value.Rational() + return semantics.Quantity{Num: rational, Unit: semantics.UnitOne()}, true case ValueReal: realVal, _ := value.Real() return semantics.Quantity{Num: semantics.Value{Kind: semantics.ValReal, Real: realVal}, Unit: semantics.UnitOne()}, true @@ -729,8 +774,8 @@ func columnOperatorKind(operator string, unary bool) (ast.OperatorKind, bool) { } func realOperand(value Value) float64 { - if integer, ok := value.IntegerConst(); ok { - return integer.AsReal() + if exactKind(value.Kind()) { + return exactOperand(value).AsReal() } realVal, _ := value.Real() return realVal diff --git a/internal/doc/queryexec/derived.go b/internal/doc/queryexec/derived.go index 3ffe0f4bf9..a88426fa0a 100644 --- a/internal/doc/queryexec/derived.go +++ b/internal/doc/queryexec/derived.go @@ -113,6 +113,8 @@ func constValue(value semantics.Value) (Value, bool) { return BooleanValue(value.Bool), true case semantics.ValInt: return IntegerOf(value), true + case semantics.ValRational: + return RationalOf(value), true case semantics.ValReal: return RealValue(value.Real), true default: diff --git a/internal/doc/queryexec/events.go b/internal/doc/queryexec/events.go index 7260dd7e88..7af5d6a06d 100644 --- a/internal/doc/queryexec/events.go +++ b/internal/doc/queryexec/events.go @@ -198,6 +198,8 @@ func (e *executor) instantArgument(expression queryplan.Expression, name string) switch { case bound.kind == ValueInteger: instant = bound.integer.AsReal() + case bound.kind == ValueRational: + instant = bound.integer.AsReal() case bound.kind == ValueReal: instant = bound.real case bound.kind == ValueQuantity: diff --git a/internal/doc/queryexec/execute.go b/internal/doc/queryexec/execute.go index 881645c415..4931728b05 100644 --- a/internal/doc/queryexec/execute.go +++ b/internal/doc/queryexec/execute.go @@ -374,6 +374,8 @@ func scalarValueType(value Value) (semantics.PrimType, bool) { return semantics.PrimString, true case ValueInteger: return semantics.PrimInteger, true + case ValueRational: + return semantics.PrimRational, true case ValueReal: return semantics.PrimReal, true default: diff --git a/internal/doc/queryexec/memberpath_test.go b/internal/doc/queryexec/memberpath_test.go index 7b96556d39..e9e57fc2ea 100644 --- a/internal/doc/queryexec/memberpath_test.go +++ b/internal/doc/queryexec/memberpath_test.go @@ -67,6 +67,10 @@ func cellNumbers(t *testing.T, result *RowSet, column string) [][]float64 { nums = append(nums, float64(integer)) continue } + if rational, ok := value.Rational(); ok { + nums = append(nums, rational.AsReal()) + continue + } number, ok := value.Real() if !ok { t.Fatalf("%s cell = %+v, want numbers", column, row.Cells()[position].Values()) diff --git a/internal/doc/queryexec/operations.go b/internal/doc/queryexec/operations.go index 0df4bda0c4..3646b14f0f 100644 --- a/internal/doc/queryexec/operations.go +++ b/internal/doc/queryexec/operations.go @@ -815,6 +815,9 @@ func (e *executor) filterValue(value symbols.FilterValue, sym *symbols.Symbol) ( result = BooleanValue(value.Bool) case symbols.FilterValueInt: result = IntegerOf(semantics.FilterInteger(value)) + case symbols.FilterValueRational: + number, _ := semantics.FilterNumber(value) + result = RationalOf(number) case symbols.FilterValueReal: result = RealValue(value.Real) case symbols.FilterValueString: @@ -924,7 +927,7 @@ func compareValue(actual Value, operator, expected string) (bool, error) { default: return false, errComparison } - case ValueInteger, ValueReal, ValueInfinity: + case ValueInteger, ValueRational, ValueReal, ValueInfinity: want, err := parseNumericValue(expected) if err != nil { return false, err @@ -950,6 +953,9 @@ func parseNumericValue(text string) (Value, error) { if integer, ok := semantics.ParseInteger(text); ok { return IntegerOf(integer), nil } + if rational, err := semantics.ParseRational(text, semantics.DefaultMaxIntegerBits); err == nil { + return numberOf(rational), nil + } realVal, err := strconv.ParseFloat(text, 64) if err != nil || math.IsNaN(realVal) || math.IsInf(realVal, 0) { return Value{}, strconv.ErrSyntax @@ -1021,21 +1027,18 @@ func compareNumeric(left, right Value) int { if right.Kind() == ValueInfinity { return -1 } - if left.Kind() == ValueInteger && right.Kind() == ValueReal { - l, _ := left.IntegerConst() + if exactKind(left.Kind()) && exactKind(right.Kind()) { + return semantics.CompareRat(exactOperand(left), exactOperand(right)) + } + if exactKind(left.Kind()) && right.Kind() == ValueReal { r, _ := right.Real() - return semantics.CompareIntReal(l, r) + return semantics.CompareExactReal(exactOperand(left), r) } - if left.Kind() == ValueReal && right.Kind() == ValueInteger { + if left.Kind() == ValueReal && exactKind(right.Kind()) { l, _ := left.Real() - r, _ := right.IntegerConst() - return -semantics.CompareIntReal(r, l) + return -semantics.CompareExactReal(exactOperand(right), l) } switch left.Kind() { - case ValueInteger: - l, _ := left.IntegerConst() - r, _ := right.IntegerConst() - return semantics.CompareInt(l, r) case ValueReal: l, _ := left.Real() r, _ := right.Real() @@ -1070,7 +1073,7 @@ func (e *executor) orderedKeysCompatible(left, right Value) bool { } func numericKind(kind ValueKind) bool { - return kind == ValueInteger || kind == ValueReal || kind == ValueInfinity + return kind == ValueInteger || kind == ValueRational || kind == ValueReal || kind == ValueInfinity } // resolveType resolves a type a filter names: a qualified name as the element diff --git a/internal/doc/queryexec/quantity_test.go b/internal/doc/queryexec/quantity_test.go index 91968faa55..e27c6f0dd9 100644 --- a/internal/doc/queryexec/quantity_test.go +++ b/internal/doc/queryexec/quantity_test.go @@ -100,6 +100,10 @@ func cellTexts(t *testing.T, result *RowSet, column int) []string { out = append(out, semantics.FormatReal(realVal)) continue } + if rational, ok := value.Rational(); ok { + out = append(out, rational.FormatRational()) + continue + } if sym, ok := value.Element(); ok { out = append(out, symbols.FQNOf(sym)) continue @@ -413,7 +417,7 @@ calc def Derived :> Query { {1, []string{"4580000 [kg]", "238000 [kg]"}}, {2, []string{"2290.0 [kg]", "119.0 [kg]"}}, {3, []string{"1145000.0 [kg]", "59500.0 [kg]"}}, - {4, []string{"54523.80952380953 [SI::'kg⋅m⁻¹']", "6611.111111111111 [SI::'kg⋅m⁻¹']"}}, + {4, []string{"1145000/21 [SI::'kg⋅m⁻¹']", "59500/9 [SI::'kg⋅m⁻¹']"}}, {5, []string{"1.0", "1.0"}}, {6, []string{"4580000 [kg]", "238000 [kg]"}}, } diff --git a/internal/doc/queryexec/value.go b/internal/doc/queryexec/value.go index c2ef5abef5..72185a96b8 100644 --- a/internal/doc/queryexec/value.go +++ b/internal/doc/queryexec/value.go @@ -5,6 +5,7 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/exec/runtime" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "strings" ) // ValueKind classifies one scalar query value. @@ -23,10 +24,12 @@ const ( ValueState ValueKind = "state" // ValueEvent is one record of a session's trace, carried as a row whose // declaration is the behavior that made it. - ValueEvent ValueKind = "event" - ValueString ValueKind = "string" - ValueInteger ValueKind = "integer" - ValueReal ValueKind = "real" + ValueEvent ValueKind = "event" + ValueString ValueKind = "string" + ValueInteger ValueKind = "integer" + ValueReal ValueKind = "real" + // ValueRational is an exact non-integer Rational, `1/3` or `0.1`. + ValueRational ValueKind = "rational" ValueBoolean ValueKind = "boolean" ValueInfinity ValueKind = "infinity" // ValueQuantity is a magnitude in a measurement unit, `2290000 [kg]`. @@ -89,6 +92,11 @@ func IntegerOf(value semantics.Value) Value { return Value{kind: ValueInteger, integer: value} } +// RationalOf constructs the value of the exact Rational constant value. +func RationalOf(value semantics.Value) Value { + return Value{kind: ValueRational, integer: value} +} + // RealValue constructs a real value. func RealValue(value float64) Value { return Value{kind: ValueReal, real: value} @@ -112,6 +120,8 @@ func constantValue(constant semantics.Value) (Value, bool) { switch constant.Kind { case semantics.ValInt: return IntegerOf(constant), true + case semantics.ValRational: + return RationalOf(constant), true case semantics.ValReal: return RealValue(constant.Real), true case semantics.ValBool: @@ -204,6 +214,15 @@ func (v Value) Integer() (int64, bool) { // integer value. func (v Value) IntegerConst() (semantics.Value, bool) { return v.integer, v.kind == ValueInteger } +// RationalText spells an exact Rational as a document spells a number: a terminating +// one in decimal, as strconv's shortest 'g' form spells a Real, else `numer/denom`. +func RationalText(value semantics.Value) string { + return strings.TrimSuffix(value.FormatRational(), ".0") +} + +// Rational returns the value's exact Rational and whether it is a rational value. +func (v Value) Rational() (semantics.Value, bool) { return v.integer, v.kind == ValueRational } + // Real returns the value's real and whether it is a real value. func (v Value) Real() (float64, bool) { return v.real, v.kind == ValueReal } diff --git a/internal/exec/analysis/solve_test.go b/internal/exec/analysis/solve_test.go index 53419b0171..f6c5966569 100644 --- a/internal/exec/analysis/solve_test.go +++ b/internal/exec/analysis/solve_test.go @@ -108,7 +108,10 @@ func TestRegistrySolveIsTheSolversAnswer(t *testing.T) { func TestSolveGradesEachVerdict(t *testing.T) { exact := intQuery("C", 2, 5) rounded := intQuery("R", 2, 5) - rounded.Assertions = append(rounded.Assertions, solve.Assertion{Term: solve.Binary(solve.OpGt, solve.Bool, solve.ToReal(solve.VarTerm(rounded.Vars[0])), solve.IntTerm(0))}) + x := &solve.Var{Name: "test::R::x", Sort: solve.Real, Binary64: true} + rounded.Vars = append(rounded.Vars, x) + sum := solve.Binary(solve.OpAdd, solve.Real, solve.VarTerm(x), solve.ToReal(solve.VarTerm(rounded.Vars[0]))) + rounded.Assertions = append(rounded.Assertions, solve.Assertion{Term: solve.Binary(solve.OpGt, solve.Bool, sum, solve.IntTerm(0))}) if !rounded.Rounded() { t.Fatal("the widened query must round") } diff --git a/internal/exec/analysis/tool.go b/internal/exec/analysis/tool.go index 0f9ea9545a..99a85419e5 100644 --- a/internal/exec/analysis/tool.go +++ b/internal/exec/analysis/tool.go @@ -531,6 +531,13 @@ func encodeValue(v runtime.ToolValue) (json.RawMessage, error) { return nil, fmt.Errorf("%v is not a JSON number", v.Value.Real) } return json.RawMessage(strconv.FormatFloat(v.Value.Real, 'g', -1, 64)), nil + case semantics.ValRational: + // A JSON number is decimal, so it carries a terminating Rational exactly and no other. + text := v.Value.FormatRational() + if strings.Contains(text, "/") { + return nil, fmt.Errorf("%s has no exact JSON number", text) + } + return json.RawMessage(text), nil case semantics.ValBool: return json.RawMessage(strconv.FormatBool(v.Value.Bool)), nil case semantics.ValInvalid: diff --git a/internal/exec/fmi/runner.go b/internal/exec/fmi/runner.go index df68120a80..65585656c6 100644 --- a/internal/exec/fmi/runner.go +++ b/internal/exec/fmi/runner.go @@ -144,7 +144,7 @@ func reservedNumber(call *runtime.ToolCall, in runtime.ToolInput, unit string) ( switch value.Kind { case semantics.ValReal: return value.Real, nil - case semantics.ValInt: + case semantics.ValInt, semantics.ValRational: return value.AsReal(), nil } return 0, &runtime.ToolError{Tool: call.ToolName, Kind: runtime.ToolUnsentInput, @@ -210,7 +210,8 @@ func scalarValue(call *runtime.ToolCall, in runtime.ToolInput, d *Description, v switch tv.Value.Kind { case semantics.ValReal: return tv.Value.Real, nil - case semantics.ValInt: + // An FMU Real variable is a binary64, so an exact Rational crosses it rounded once. + case semantics.ValInt, semantics.ValRational: return tv.Value.AsReal(), nil } case KindInteger, KindEnumeration: diff --git a/internal/exec/runtime/action_activation_test.go b/internal/exec/runtime/action_activation_test.go index d17293bd07..6574a10c03 100644 --- a/internal/exec/runtime/action_activation_test.go +++ b/internal/exec/runtime/action_activation_test.go @@ -35,7 +35,7 @@ func TestActionBodyActivationEndsWithTheBody(t *testing.T) { if err != nil { t.Fatalf("ExecuteAction: %v", err) } - if got := out["v"].Const.Real; got != 6.0 { + if got := out["v"].Const.AsReal(); got != 6.0 { t.Errorf("v = %v, want 6", got) } if len(ctx.run.calcUsageRuns) != 0 { diff --git a/internal/exec/runtime/action_body_local_usage_test.go b/internal/exec/runtime/action_body_local_usage_test.go index 6cf51a902d..882edd2b84 100644 --- a/internal/exec/runtime/action_body_local_usage_test.go +++ b/internal/exec/runtime/action_body_local_usage_test.go @@ -41,7 +41,7 @@ func TestActionBodyLocalUsageBindsCurrentValues(t *testing.T) { } // 2.0 is the declared default of v doubled, which the assignment before the // usage replaced. - if got := out["r"].Const.Real; got != 4.0 { + if got := out["r"].Const.AsReal(); got != 4.0 { t.Errorf("r = %v, want 4", got) } } @@ -81,7 +81,7 @@ func TestActionBodyLocalUsageBindsPerIteration(t *testing.T) { if err != nil { t.Fatalf("ExecuteAction: %v", err) } - if got := out["acc"].Const.Real; got != 12.0 { + if got := out["acc"].Const.AsReal(); got != 12.0 { t.Errorf("acc = %v, want 12 (2+4+6)", got) } } diff --git a/internal/exec/runtime/action_executor.go b/internal/exec/runtime/action_executor.go index 2f9599c03c..41179dabc5 100644 --- a/internal/exec/runtime/action_executor.go +++ b/internal/exec/runtime/action_executor.go @@ -1178,6 +1178,9 @@ func (e *ActionExecutor) initializeAttributes() error { if err != nil { return fmt.Errorf("eval attribute default %s: %w", attr.Name, err) } + if err := e.holdAttributeAsReal(attr, &value); err != nil { + return fmt.Errorf("eval attribute default %s: %w", attr.Name, err) + } if value, err = e.mirrorOccurrence(attr.Name, value); err != nil { return err } @@ -1190,6 +1193,19 @@ func (e *ActionExecutor) initializeAttributes() error { return nil } +// holdAttributeAsReal holds an attribute default as the Real its declaration states. +func (e *ActionExecutor) holdAttributeAsReal(attr lower.Attribute, value *Value) error { + scope := attr.Scope + if scope == nil { + scope = e.graph.Scope + } + sym, ok := resolve.FeatureSymbolInScope(scope, []string{attr.Name}) + if !ok { + return nil + } + return e.ctx.holdAsReal(value, e.ctx.extractType(sym)) +} + // bindContextDefault binds an unbound `in ref` parameter of a behavior started on an // object to that object, when the object is of the type the parameter declares: a // performance started on an instance answers `in ref :>> context` with the instance diff --git a/internal/exec/runtime/action_results_parts_test.go b/internal/exec/runtime/action_results_parts_test.go index 046c4b1492..8543986b04 100644 --- a/internal/exec/runtime/action_results_parts_test.go +++ b/internal/exec/runtime/action_results_parts_test.go @@ -37,14 +37,14 @@ func TestResultsReportPartsOfEachActivation(t *testing.T) { return exec } first := run(1) - if got, ok := first.Results()["target.total"]; !ok || got.Const.Real != 1 { + if got, ok := first.Results()["target.total"]; !ok || got.Const.AsReal() != 1 { t.Fatalf("first target.total = %v, %v; want 1", got, ok) } second := run(2) - if got, ok := second.Results()["target.total"]; !ok || got.Const.Real != 2 { + if got, ok := second.Results()["target.total"]; !ok || got.Const.AsReal() != 2 { t.Fatalf("second target.total = %v, %v; want 2", got, ok) } - if got, ok := first.Results()["target.total"]; !ok || got.Const.Real != 2 { + if got, ok := first.Results()["target.total"]; !ok || got.Const.AsReal() != 2 { t.Fatalf("first target.total after the second run = %v, %v; want 2", got, ok) } } diff --git a/internal/exec/runtime/body_scope_test.go b/internal/exec/runtime/body_scope_test.go index 065207ad07..097b7ed414 100644 --- a/internal/exec/runtime/body_scope_test.go +++ b/internal/exec/runtime/body_scope_test.go @@ -54,17 +54,17 @@ func TestBodyScopeImportSpellings(t *testing.T) { if got, want := val.Quantity().Unit.String(), "s"; got != want { t.Errorf("%s import: unit of t = %q, want %q", visibility, got, want) } - if got, want := val.Quantity().Num.Real, 0.5; got != want { + if got, want := val.Quantity().Num.AsReal(), 0.5; got != want { t.Errorf("%s import: magnitude of t = %v, want %v", visibility, got, want) } outputs[visibility] = val } private, public := outputs["private"], outputs["public"] - if private.Quantity().Num.Real != public.Quantity().Num.Real || + if private.Quantity().Num.AsReal() != public.Quantity().Num.AsReal() || private.Quantity().Unit.String() != public.Quantity().Unit.String() { t.Errorf("private import gave %v %s, public import gave %v %s; want the same", - private.Quantity().Num.Real, private.Quantity().Unit, - public.Quantity().Num.Real, public.Quantity().Unit) + private.Quantity().Num.AsReal(), private.Quantity().Unit, + public.Quantity().Num.AsReal(), public.Quantity().Unit) } } diff --git a/internal/exec/runtime/budget.go b/internal/exec/runtime/budget.go index 3ea0f1a59a..be6269cc89 100644 --- a/internal/exec/runtime/budget.go +++ b/internal/exec/runtime/budget.go @@ -200,7 +200,7 @@ func budgetFromValue(v budgetVar, raw string) (int64, error) { // integerSizeHint names the variable raising the Integer size budget on an // error reporting it spent; any other error passes through. func integerSizeHint(err error) error { - if errors.Is(err, ErrIntegerSizeLimit) { + if errors.Is(err, ErrIntegerSizeLimit) || errors.Is(err, semantics.ErrRationalSizeLimit) { return fmt.Errorf("%w (raise %s to allow more)", err, MaxIntegerBitsEnvVar) } return err diff --git a/internal/exec/runtime/builtins.go b/internal/exec/runtime/builtins.go index 4af55d2d4d..30eca46ba4 100644 --- a/internal/exec/runtime/builtins.go +++ b/internal/exec/runtime/builtins.go @@ -81,8 +81,8 @@ func init() { "IntegerFunctions::sum": builtinNumericalSum, "IntegerFunctions::product": builtinNumericalProduct, - "RationalFunctions::sum": builtinRealSum, - "RationalFunctions::product": builtinRealProduct, + "RationalFunctions::sum": builtinRationalSum, + "RationalFunctions::product": builtinRationalProduct, "RealFunctions::sum": builtinRealSum, "RealFunctions::product": builtinRealProduct, } diff --git a/internal/exec/runtime/builtins_named.go b/internal/exec/runtime/builtins_named.go index 09a48f2663..e204afd032 100644 --- a/internal/exec/runtime/builtins_named.go +++ b/internal/exec/runtime/builtins_named.go @@ -186,7 +186,7 @@ func (ctx *Context) aggregateWithIdentity(op, param string, args []Value, operat if len(elementsOf(args[0])) == 0 { return identity, nil } - return ctx.aggregate(op, args[:1], operator, false) + return ctx.aggregate(op, args[:1], operator, aggregateNumber) } // isIdentityElement reports whether val is the additive (0) or multiplicative diff --git a/internal/exec/runtime/builtins_named_test.go b/internal/exec/runtime/builtins_named_test.go index 82f2395f75..8fa6accef5 100644 --- a/internal/exec/runtime/builtins_named_test.go +++ b/internal/exec/runtime/builtins_named_test.go @@ -141,7 +141,7 @@ func TestAggregationsWithIdentityErrors(t *testing.T) { {"NumericalFunctions::sum0((1, 2), 1)", ErrTypeMismatch, "isZero(zero)"}, {"NumericalFunctions::product1((1, 2), 0)", ErrTypeMismatch, "isUnit(one)"}, {`NumericalFunctions::sum0(("a", "b"), 0)`, ErrTypeMismatch, "numeric elements"}, - {"NumericalFunctions::product1((1e200, 1e200), 1)", semantics.ErrArithmeticOverflow, "finite"}, + {"NumericalFunctions::product1((1e200 * (1.0 ** 0.5), 1e200), 1)", semantics.ErrArithmeticOverflow, "finite"}, {"NumericalFunctions::sum0((1, 2))", ErrCalcArity, "sum0"}, } for _, tc := range cases { diff --git a/internal/exec/runtime/calc_usage_body_local_test.go b/internal/exec/runtime/calc_usage_body_local_test.go index b8fd41de51..ccc70774bb 100644 --- a/internal/exec/runtime/calc_usage_body_local_test.go +++ b/internal/exec/runtime/calc_usage_body_local_test.go @@ -78,8 +78,8 @@ func TestBodyLocalCalcUsageInLoopBindsPerIteration(t *testing.T) { if err != nil { t.Fatalf("Propagate(%d): %v", want.n, err) } - if got.Const.Real != want.value { - t.Errorf("Propagate(%d) = %v, want %v", want.n, got.Const.Real, want.value) + if got.Const.AsReal() != want.value { + t.Errorf("Propagate(%d) = %v, want %v", want.n, got.Const.AsReal(), want.value) } } } @@ -97,8 +97,8 @@ func TestBodyLocalCalcUsageInBranch(t *testing.T) { if err != nil { t.Fatalf("Branch(3): %v", err) } - if taken.Const.Real != 3.0 { - t.Errorf("Branch(3) = %v, want 3", taken.Const.Real) + if taken.Const.AsReal() != 3.0 { + t.Errorf("Branch(3) = %v, want 3", taken.Const.AsReal()) } // k = -2: the else branch's usage binds pv = 1, so dx = 1. @@ -106,8 +106,8 @@ func TestBodyLocalCalcUsageInBranch(t *testing.T) { if err != nil { t.Fatalf("Branch(-2): %v", err) } - if other.Const.Real != 1.0 { - t.Errorf("Branch(-2) = %v, want 1", other.Const.Real) + if other.Const.AsReal() != 1.0 { + t.Errorf("Branch(-2) = %v, want 1", other.Const.AsReal()) } } @@ -123,8 +123,8 @@ func TestBodyLocalCalcUsageInNestedBodies(t *testing.T) { if err != nil { t.Fatalf("NestedBodies(3): %v", err) } - if got.Const.Real != 2.0 { - t.Errorf("NestedBodies(3) = %v, want 2", got.Const.Real) + if got.Const.AsReal() != 2.0 { + t.Errorf("NestedBodies(3) = %v, want 2", got.Const.AsReal()) } } diff --git a/internal/exec/runtime/calc_usage_nested_test.go b/internal/exec/runtime/calc_usage_nested_test.go index 7865c6a2f8..3064b4e854 100644 --- a/internal/exec/runtime/calc_usage_nested_test.go +++ b/internal/exec/runtime/calc_usage_nested_test.go @@ -60,8 +60,8 @@ func wantReal(t *testing.T, outputs []CalcOutputValue, name string, want float64 if value.Kind != ValConst || value.Const.Kind != semantics.ValReal { t.Fatalf("%s = %s, want a real", name, FormatTraceValue(value)) } - if diff := value.Const.Real - want; diff > 1e-9 || diff < -1e-9 { - t.Errorf("%s = %v, want %v", name, value.Const.Real, want) + if diff := value.Const.AsReal() - want; diff > 1e-9 || diff < -1e-9 { + t.Errorf("%s = %v, want %v", name, value.Const.AsReal(), want) } } diff --git a/internal/exec/runtime/calc_usage_snapshot_test.go b/internal/exec/runtime/calc_usage_snapshot_test.go index c11e756880..fa085c449c 100644 --- a/internal/exec/runtime/calc_usage_snapshot_test.go +++ b/internal/exec/runtime/calc_usage_snapshot_test.go @@ -54,12 +54,12 @@ func TestCalcUsageOutputsShareOneInputBinding(t *testing.T) { if err != nil { t.Fatalf("Snapshotted: %v", err) } - if got.Const.Real != want.Const.Real { + if got.Const.AsReal() != want.Const.AsReal() { t.Errorf("Interleaved = %v, Snapshotted = %v; reading order must not be observable", - got.Const.Real, want.Const.Real) + got.Const.AsReal(), want.Const.AsReal()) } - if want.Const.Real != 1010.0 { - t.Errorf("Snapshotted = %v, want 1010 (both outputs from k = 1.0)", want.Const.Real) + if want.Const.AsReal() != 1010.0 { + t.Errorf("Snapshotted = %v, want 1010 (both outputs from k = 1.0)", want.Const.AsReal()) } } @@ -110,8 +110,8 @@ func TestCalcUsageOutputsInAssignmentLoop(t *testing.T) { if err != nil { t.Fatalf("Step(1): %v", err) } - if one.Const.Real != 1000000.0-5000.0+100.0+1.0 { - t.Errorf("Step(1) = %v, want %v", one.Const.Real, 1000000.0-5000.0+100.0+1.0) + if one.Const.AsReal() != 1000000.0-5000.0+100.0+1.0 { + t.Errorf("Step(1) = %v, want %v", one.Const.AsReal(), 1000000.0-5000.0+100.0+1.0) } // Two iterations: the second reads x = 1, v = -0.5 — the values the first @@ -121,8 +121,8 @@ func TestCalcUsageOutputsInAssignmentLoop(t *testing.T) { t.Fatalf("Step(2): %v", err) } want := 0.75*1000000.0 + -1.0*10000.0 + 1.5*100.0 + 2.0 - if two.Const.Real != want { - t.Errorf("Step(2) = %v, want %v (each iteration binds its own state)", two.Const.Real, want) + if two.Const.AsReal() != want { + t.Errorf("Step(2) = %v, want %v (each iteration binds its own state)", two.Const.AsReal(), want) } } @@ -158,8 +158,8 @@ func TestCalcUsageMemoDistinguishesEnclosingArguments(t *testing.T) { if err != nil { t.Fatalf("Both: %v", err) } - if got.Const.Real != 6010.0 { - t.Errorf("Both = %v, want 6010 (Outer(3) = 6, Outer(5) = 10)", got.Const.Real) + if got.Const.AsReal() != 6010.0 { + t.Errorf("Both = %v, want 6010 (Outer(3) = 6, Outer(5) = 10)", got.Const.AsReal()) } } diff --git a/internal/exec/runtime/calc_usage_step_test.go b/internal/exec/runtime/calc_usage_step_test.go index 0e115a34b7..9c5a8c3fea 100644 --- a/internal/exec/runtime/calc_usage_step_test.go +++ b/internal/exec/runtime/calc_usage_step_test.go @@ -50,7 +50,7 @@ func TestDecisionGuardReadsCalcUsagePerStep(t *testing.T) { if got := out["i"].Const.Int; got != 3 { t.Errorf("i = %v, want 3", got) } - if got := out["log"].Const.Real; got != 12.0 { + if got := out["log"].Const.AsReal(); got != 12.0 { t.Errorf("log = %v, want 12 (2 + 4 + 6)", got) } } @@ -125,7 +125,7 @@ func TestPartChainReadBelongsToTheReadingActivation(t *testing.T) { } ctx.SetTrace(nil) - if got := out["acc"].Const.Real; got != 18.0 { + if got := out["acc"].Const.AsReal(); got != 18.0 { t.Errorf("acc = %v, want 18 (three reads of 6)", got) } if got := strings.Count(tr.String(), "enter calc test::lander::mass"); got != 3 { diff --git a/internal/exec/runtime/classification.go b/internal/exec/runtime/classification.go index b8f59001bc..ec508f8327 100644 --- a/internal/exec/runtime/classification.go +++ b/internal/exec/runtime/classification.go @@ -188,7 +188,7 @@ func (ctx *Context) valueTypes(scope *symbols.Scope, value Value) ([]*symbols.Sy // holds a quantity of dimension one and no unit as the bare number (ToDimensionOneValue). func (ctx *Context) numberTypes(value Value, scalar *symbols.Symbol) []*symbols.Symbol { types := append(make([]*symbols.Symbol, 0, 2), scalar) - if value.Kind != ValConst || (value.Const.Kind != semantics.ValInt && value.Const.Kind != semantics.ValReal) { + if value.Kind != ValConst || !value.Const.IsNumeric() { return types } if dimOne := ctx.librarySymbol(dimensionOneValueFQN); dimOne != nil { @@ -260,6 +260,8 @@ func representationPrim(value Value) semantics.PrimType { return semantics.PrimBoolean case semantics.ValInt: return semantics.PrimInteger + case semantics.ValRational: + return semantics.PrimRational case semantics.ValReal: return realRepresentationPrim(value.Const.Real) } diff --git a/internal/exec/runtime/classifier_behavior_test.go b/internal/exec/runtime/classifier_behavior_test.go index 710268ec7a..94a93dd104 100644 --- a/internal/exec/runtime/classifier_behavior_test.go +++ b/internal/exec/runtime/classifier_behavior_test.go @@ -2682,7 +2682,7 @@ func TestCalcCallBindsContextBeforeItsInputs(t *testing.T) { if err != nil { t.Fatalf("run Run on Plant: %v", err) } - if got := results["result"]; got.Const.Real != 3.0 { + if got := results["result"]; got.Const.AsReal() != 3.0 { t.Errorf("result = %v, want 3.0: the tank instance bound context, 2.0 bound factor", got) } } diff --git a/internal/exec/runtime/clock.go b/internal/exec/runtime/clock.go index 53e07e7ad1..748119b590 100644 --- a/internal/exec/runtime/clock.go +++ b/internal/exec/runtime/clock.go @@ -314,10 +314,8 @@ func (ctx *Context) timeMagnitude(val Value, what string) (float64, error) { switch val.Kind { case ValConst: switch val.Const.Kind { - case semantics.ValInt: + case semantics.ValInt, semantics.ValRational, semantics.ValReal: return val.Const.AsReal(), nil - case semantics.ValReal: - return val.Const.Real, nil default: return 0, fmt.Errorf("%s must be numeric, got %v", what, val.Const.Kind) } diff --git a/internal/exec/runtime/clock_step_test.go b/internal/exec/runtime/clock_step_test.go index f0d6e23eaa..861e91cc0b 100644 --- a/internal/exec/runtime/clock_step_test.go +++ b/internal/exec/runtime/clock_step_test.go @@ -50,7 +50,7 @@ func realOutputs(t *testing.T, out map[string]Value, names ...string) []float64 if v.Kind != ValConst || v.Const.Kind != semantics.ValReal { t.Fatalf("%s = %v, want a Real", name, v) } - got[i] = v.Const.Real + got[i] = v.Const.AsReal() } return got } diff --git a/internal/exec/runtime/collections.go b/internal/exec/runtime/collections.go index 9ac2ea4ef0..04d55135ce 100644 --- a/internal/exec/runtime/collections.go +++ b/internal/exec/runtime/collections.go @@ -242,6 +242,10 @@ func wholeBeyondInt64(v semantics.Value) (semantics.Value, bool) { switch v.Kind { case semantics.ValInt: return v, v.IsBigInt() + case semantics.ValRational: + if n := v.RatNumer(); v.RatIsWhole() && n.IsBigInt() { + return n, true + } case semantics.ValReal: if n, ok := semantics.IntegerOfReal(v.Real); ok && n.IsBigInt() { return n, true @@ -280,6 +284,8 @@ func describeValue(val Value) string { switch val.Const.Kind { case semantics.ValInt: return "an Integer" + case semantics.ValRational: + return "a Rational" case semantics.ValReal: return "a Real" case semantics.ValBool: @@ -1323,8 +1329,7 @@ func (ec *EvalContext) extremum(op string, args []Value, least bool) (Value, err best = val continue } - if (toReal(val.Const) < toReal(best.Const)) == least && - toReal(val.Const) != toReal(best.Const) { + if order := compareNumbers(val.Const, best.Const); (order < 0) == least && order != 0 { best = val } } @@ -1456,47 +1461,99 @@ func truthOf(op string, args []Value, universal bool) (Value, error) { // kind: Integer elements sum to an Integer (IntegerFunctions::sum returns // `Integer[1]`), a Real anywhere makes the sum a Real. func builtinNumericalSum(ec *EvalContext, args []Value) (Value, error) { - return ec.ctx.aggregate("NumericalFunctions::sum", args, ast.OpAdd, false) + return ec.ctx.aggregate("NumericalFunctions::sum", args, ast.OpAdd, aggregateNumber) } // builtinNumericalProduct is NumericalFunctions::product, with the // multiplicative identity for an empty collection (`product1(collection, 1)`). func builtinNumericalProduct(ec *EvalContext, args []Value) (Value, error) { - return ec.ctx.aggregate("NumericalFunctions::product", args, ast.OpMul, false) + return ec.ctx.aggregate("NumericalFunctions::product", args, ast.OpMul, aggregateNumber) +} + +// builtinRationalSum is RationalFunctions::sum, whose identity the library +// writes `0.0` (`sum0(collection, 0.0)`), the Rational 0: an empty collection +// sums to 0.0, not the Integer 0, and the sum is exact. +func builtinRationalSum(ec *EvalContext, args []Value) (Value, error) { + return ec.ctx.aggregate("RationalFunctions::sum", args, ast.OpAdd, aggregateRational) +} + +// builtinRationalProduct is RationalFunctions::product (`product1(collection, 1.0)`). +func builtinRationalProduct(ec *EvalContext, args []Value) (Value, error) { + return ec.ctx.aggregate("RationalFunctions::product", args, ast.OpMul, aggregateRational) } -// builtinRealSum is RealFunctions::sum and RationalFunctions::sum, whose -// identity the library declares Real (`sum0(collection, 0.0)`): an empty -// collection sums to 0.0, not 0. +// builtinRealSum is RealFunctions::sum, from the library's identity `0.0` +// (`sum0(collection, 0.0)`): RealFunctions declares its operands and result +// Real, so an exact element is rounded once to binary64. func builtinRealSum(ec *EvalContext, args []Value) (Value, error) { - return ec.ctx.aggregate("RealFunctions::sum", args, ast.OpAdd, true) + return ec.ctx.aggregate("RealFunctions::sum", args, ast.OpAdd, aggregateReal) } -// builtinRealProduct is RealFunctions::product and RationalFunctions::product -// (`product1(collection, 1.0)`). +// builtinRealProduct is RealFunctions::product (`product1(collection, 1.0)`). func builtinRealProduct(ec *EvalContext, args []Value) (Value, error) { - return ec.ctx.aggregate("RealFunctions::product", args, ast.OpMul, true) + return ec.ctx.aggregate("RealFunctions::product", args, ast.OpMul, aggregateReal) +} + +// aggregateKind is the number kind an aggregation's library declares its +// collection over. +type aggregateKind int + +const ( + // aggregateNumber keeps the elements' kinds, as NumericalFunctions does. + aggregateNumber aggregateKind = iota + // aggregateRational folds from a Rational identity, reading a finite + // binary64 Real as the Rational it equals. + aggregateRational + // aggregateReal folds binary64 Reals from a Real identity, as RealFunctions + // declares its operands and result Real. + aggregateReal +) + +// identity is the aggregation's identity element of kind k. +func (k aggregateKind) identity(n int64) semantics.Value { + switch k { + case aggregateRational: + return semantics.RatOf(semantics.IntValue(n)) + case aggregateReal: + return semantics.Value{Kind: semantics.ValReal, Real: float64(n)} + } + return semantics.IntValue(n) +} + +// element is a numeric element as the declared collection holds it: a finite +// binary64 Real is read exactly for RationalFunctions, and an exact number is +// rounded once to binary64 for RealFunctions. +func (k aggregateKind) element(v semantics.Value) semantics.Value { + switch { + case k == aggregateRational && v.Kind == semantics.ValReal: + if r, ok := semantics.RationalOfReal(v.Real); ok { + return r + } + case k == aggregateReal && v.IsExact(): + return semantics.Value{Kind: semantics.ValReal, Real: v.AsReal()} + } + return v } // aggregate folds the collection's numeric elements with op, starting from its -// identity element: 0 for a sum, 1 for a product, a Real where asReal says so. -// A non-numeric element is reported rather than skipped or coerced. The sum of -// no elements read from a quantity-typed declaration is that quantity's zero. -func (ctx *Context) aggregate(op string, args []Value, operator ast.OperatorKind, asReal bool) (Value, error) { +// identity element of kind: 0 for a sum, 1 for a product. A non-numeric element +// is reported rather than skipped or coerced. The sum of no elements read from +// a quantity-typed declaration is that quantity's zero. +func (ctx *Context) aggregate(op string, args []Value, operator ast.OperatorKind, kind aggregateKind) (Value, error) { if err := checkArity(op, args, 1); err != nil { return Value{}, err } elements := elementsOf(args[0]) if operator == ast.OpAdd && len(elements) == 0 { if unit, ok := args[0].Sequence().ElementUnit(); ok { - return typedZero(unit, asReal), nil + return typedZero(unit, kind), nil } } // A quantity carries its unit through an aggregation as through the folded // operator, so a collection of measured values aggregates to one. for _, elem := range elements { if elem.Kind == ValQuantity { - return ctx.aggregateQuantities(op, elements, operator) + return ctx.aggregateQuantities(op, elements, operator, kind) } } // A Complex is a Number, so a collection holding one folds as ComplexFunctions'. @@ -1507,23 +1564,21 @@ func (ctx *Context) aggregate(op string, args []Value, operator ast.OperatorKind if operator == ast.OpMul { identity = 1 } - acc := semantics.Value{Kind: semantics.ValInt, Int: identity} - if asReal { - acc = semantics.Value{Kind: semantics.ValReal, Real: float64(identity)} - } + acc := kind.identity(identity) for _, elem := range elements { if elem.Kind != ValConst || !elem.Const.IsNumeric() { return Value{}, fmt.Errorf("%w: %s requires numeric elements, got %s", ErrTypeMismatch, op, describeValue(elem)) } + x := kind.element(elem.Const) if a, ok := acc.Int64(); ok && operator == ast.OpAdd { - if b, ok := elem.Const.Int64(); ok { + if b, ok := x.Int64(); ok { if sum := a + b; (sum^a)&(sum^b) >= 0 { acc = semantics.IntValue(sum) continue } } } - next, err := foldNumeric(op, operator, acc, elem.Const, ctx.maxIntegerBits) + next, err := foldNumeric(op, operator, acc, x, ctx.maxIntegerBits) if err != nil { return Value{}, err } @@ -1532,27 +1587,29 @@ func (ctx *Context) aggregate(op string, args []Value, operator ast.OperatorKind return Value{Kind: ValConst, Const: acc}, nil } -// typedZero is the additive identity of the quantities measured in unit: an -// Integer 0 in that unit, or a Real one where asReal says so. -func typedZero(unit Unit, asReal bool) Value { - num := semantics.Value{Kind: semantics.ValInt, Int: 0} - if asReal { - num = semantics.Value{Kind: semantics.ValReal, Real: 0} - } - return NewQuantityValue(&Quantity{Num: num, Unit: unit}) +// typedZero is the additive identity of the quantities measured in unit: the +// zero of kind in that unit. +func typedZero(unit Unit, kind aggregateKind) Value { + return NewQuantityValue(&Quantity{Num: kind.identity(0), Unit: unit}) } // aggregateQuantities folds a collection holding a quantity in the unit of its // first element, as the binary operator does. A bare number is a magnitude of // dimension one, so mixing one in reports incommensurable units. Points on a // measurement scale have no sum or product, so a fold over them is refused. -func (ctx *Context) aggregateQuantities(op string, elements []Value, operator ast.OperatorKind) (Value, error) { +func (ctx *Context) aggregateQuantities(op string, elements []Value, operator ast.OperatorKind, kind aggregateKind) (Value, error) { var acc Value for i, elem := range elements { q, ok := asQuantity(elem) if !ok { return Value{}, fmt.Errorf("%w: %s requires numeric elements, got %s", ErrTypeMismatch, op, describeValue(elem)) } + // An Integer magnitude keeps its kind, as the quantity operators keep it. + if num := kind.element(q.Num); q.Num.Kind != semantics.ValInt && num != q.Num { + held := *q + held.Num = num + q = &held + } if err := ctx.refusePoints(op, "points have no sum or product to fold; fold their differences from one point instead", q); err != nil { return Value{}, fmt.Errorf("%s: %w", op, err) } @@ -1578,8 +1635,8 @@ func (ctx *Context) aggregateQuantities(op string, elements []Value, operator as return acc, nil } -// foldNumeric applies one step of an aggregation, keeping Integer arithmetic -// exact where both operands are Integers, refused only beyond maxBits. +// foldNumeric applies one step of an aggregation, keeping Integer and Rational +// arithmetic exact, refused only beyond maxBits. func foldNumeric(op string, operator ast.OperatorKind, acc, elem semantics.Value, maxBits int64) (semantics.Value, error) { if acc.Kind == semantics.ValInt && elem.Kind == semantics.ValInt { res, err := semantics.IntArith(operator, acc, elem, maxBits) @@ -1593,6 +1650,13 @@ func foldNumeric(op string, operator ast.OperatorKind, acc, elem semantics.Value if acc.Kind == semantics.ValInfinity || elem.Kind == semantics.ValInfinity { return semantics.Value{}, fmt.Errorf("%w: %s requires finite elements", ErrTypeMismatch, op) } + if acc.IsExact() && elem.IsExact() { + res, err := semantics.RatArith(operator, acc, elem, maxBits) + if err != nil { + return semantics.Value{}, fmt.Errorf("%s: %w", op, integerSizeHint(err)) + } + return res, nil + } var result float64 switch operator { case ast.OpAdd: diff --git a/internal/exec/runtime/compile.go b/internal/exec/runtime/compile.go index 812bfcf9ae..353acf5171 100644 --- a/internal/exec/runtime/compile.go +++ b/internal/exec/runtime/compile.go @@ -103,6 +103,8 @@ type scalarCheck struct { countOK bool // Whether the declared type holds a value of each lattice type a scalar has. boolOK, naturalOK, integerOK, rationalOK, realOK bool + // holdsReal: the declared type is Real, which holds a Rational as its nearest binary64. + holdsReal bool // least is the smallest integer a Natural-holding declaration takes: 1 for Positive. least int64 } @@ -136,6 +138,23 @@ func (c *scalarCheck) acceptsReal(v scalar) bool { return false } +// held is v as the declaration holds it: a Rational a Real declaration takes as its +// nearest binary64, refused when no finite Real is; what, if any, names the declaration. +func (c *scalarCheck) held(v scalar, what func() string) (scalar, error) { + if !c.holdsReal || v.kind != scalarRational { + return v, nil + } + real, err := semantics.RealOf(v.exact) + if err != nil { + err = fmt.Errorf("%s as a Real: %w", v.exact.FormatRational(), err) + if what != nil { + err = fmt.Errorf("%s: %w", what(), err) + } + return scalar{}, err + } + return realScalar(real.Real), nil +} + // refuse is the evaluator's verdict on a value accepts declined, so a refusal // carries the reference's message and a value it would take is taken. func (c *scalarCheck) refuse(ctx *Context, v scalar, what func() string) error { @@ -389,6 +408,7 @@ func (c *calcCompiler) scalarCheckFor(decl *calcMemberDecl) (scalarCheck, bool) check.integerOK = holds(semantics.PrimInteger) check.rationalOK = holds(semantics.PrimRational) check.realOK = holds(semantics.PrimReal) + check.holdsReal = prim == semantics.PrimReal if c.ctx.positiveScalar(decl.Target.typ) { check.least = 1 } @@ -428,11 +448,13 @@ func (c *calcCompiler) compileNode(n ast.Node, scope *symbols.Scope, layout *fra s, _ := scalarOfConst(v) return constNode(s), nil case *ast.LiteralReal: - v, err := semantics.ParseReal(e.Value) + // One beyond the least budget is left to the evaluator, which checks it against the run's. + v, err := semantics.ParseRational(e.Value, semantics.MinMaxIntegerBits) if err != nil { - return nil, ineligible(fmt.Sprintf("real literal %s outside the range", e.Value)) + return nil, ineligible(fmt.Sprintf("rational literal %s beyond the least size budget", e.Value)) } - return constNode(realScalar(v)), nil + s, _ := scalarOfConst(v) + return constNode(s), nil case *ast.LiteralBool: return constNode(boolScalar(e.Value)), nil case *ast.FeatureReference: @@ -798,23 +820,35 @@ func (c *compiledCalc) invoke(ctx *Context, base int, bound paramSet) (scalar, e source = "default" frame[i] = v } + what := func() string { + return fmt.Sprintf("%s %s: %s for parameter %q", c.kind, c.name, source, p.name) + } if !p.check.accepts(v) { - err := p.check.refuse(ctx, v, func() string { - return fmt.Sprintf("%s %s: %s for parameter %q", c.kind, c.name, source, p.name) - }) - if err != nil { + if err := p.check.refuse(ctx, v, what); err != nil { ctx.leaveCalc() return scalar{}, err } } + held, err := p.check.held(v, what) + if err != nil { + ctx.leaveCalc() + return scalar{}, err + } + frame[i] = held } result, err := c.body(ctx, frame) ctx.leaveCalc() if err != nil { return scalar{}, calcFrame(c.kind, c.name, fmt.Errorf("%s%w", c.bodyErr, err)) } - if c.result != nil && !c.result.accepts(result) { - if err := c.result.refuse(ctx, result, func() string { return c.resultWhat }); err != nil { + if c.result != nil { + what := func() string { return c.resultWhat } + if !c.result.accepts(result) { + if err := c.result.refuse(ctx, result, what); err != nil { + return scalar{}, calcFrame(c.kind, c.name, err) + } + } + if result, err = c.result.held(result, what); err != nil { return scalar{}, calcFrame(c.kind, c.name, err) } } diff --git a/internal/exec/runtime/compile_constructs_test.go b/internal/exec/runtime/compile_constructs_test.go index d1242175a2..441e68292e 100644 --- a/internal/exec/runtime/compile_constructs_test.go +++ b/internal/exec/runtime/compile_constructs_test.go @@ -193,7 +193,7 @@ func wantOutcomeReal(t *testing.T, name string, got calcOutcome, want float64) { if got.err != nil { t.Fatalf("%s: %v", name, got.err) } - if got.value.Kind != ValConst || got.value.Const.Kind != semantics.ValReal || got.value.Const.Real != want { + if got.value.Kind != ValConst || got.value.Const.Kind != semantics.ValReal || got.value.Const.AsReal() != want { t.Fatalf("%s = %s, want %g", name, FormatTraceValue(got.value), want) } } diff --git a/internal/exec/runtime/compile_test.go b/internal/exec/runtime/compile_test.go index 32d2fdc6bb..c83f782f10 100644 --- a/internal/exec/runtime/compile_test.go +++ b/internal/exec/runtime/compile_test.go @@ -152,7 +152,7 @@ func wantOutcomesEqual(t *testing.T, name string, compiled, reference calcOutcom func outcomeValuesIdentical(compiled, reference Value) bool { if compiled.Kind == ValConst && reference.Kind == ValConst && compiled.Const.Kind == semantics.ValReal && reference.Const.Kind == semantics.ValReal { - return math.Float64bits(compiled.Const.Real) == math.Float64bits(reference.Const.Real) + return math.Float64bits(compiled.Const.AsReal()) == math.Float64bits(reference.Const.AsReal()) } return valueIdentical(compiled, reference) } @@ -295,11 +295,11 @@ func TestCompiledCalcErrorParity(t *testing.T) { // Defaults literal in the declaration bind as the evaluator binds them. func TestCompiledCalcDefaults(t *testing.T) { full := wantSameOutcome(t, "Dflt", intArg(1), intArg(2), realArg(3)) - if full.err != nil || full.value.Const.Real != 7 { + if full.err != nil || full.value.Const.AsReal() != 7 { t.Fatalf("Dflt(1, 2, 3) = %s, %v", FormatTraceValue(full.value), full.err) } partial := wantSameOutcome(t, "Dflt", intArg(1)) - if partial.err != nil || partial.value.Const.Real != 26 { + if partial.err != nil || partial.value.Const.AsReal() != 26 { t.Fatalf("Dflt(1) = %s, %v", FormatTraceValue(partial.value), partial.err) } // An inherited body binds the flattened parameters, a redeclared one in the @@ -307,7 +307,7 @@ func TestCompiledCalcDefaults(t *testing.T) { inherited := wantSameOutcome(t, "Inherits", intArg(2), intArg(3)) wantOutcomeInt(t, "Inherits(2, 3)", inherited, 5) redeclared := wantSameOutcome(t, "Redeclares", intArg(1)) - if redeclared.err != nil || redeclared.value.Const.Real != 8.5 { + if redeclared.err != nil || redeclared.value.Const.AsReal() != 8.5 { t.Fatalf("Redeclares(1) = %s, %v", FormatTraceValue(redeclared.value), redeclared.err) } } diff --git a/internal/exec/runtime/compiled_ops.go b/internal/exec/runtime/compiled_ops.go index fa5f41590d..b5cc1468f0 100644 --- a/internal/exec/runtime/compiled_ops.go +++ b/internal/exec/runtime/compiled_ops.go @@ -10,13 +10,15 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) -// scalar is an unboxed Integer, Real or Boolean, the values the compiled calc -// tier computes over; it is boxed into a Value only at an invocation boundary. -// An Integer beyond int64 is held in big, immutable, with bits zero. +// scalar is an unboxed Integer, Rational, Real or Boolean, the values the +// compiled calc tier computes over; it is boxed into a Value only at an +// invocation boundary. An Integer beyond int64 is held in big, immutable, with +// bits zero; a Rational is held exactly in exact. type scalar struct { - kind scalarKind - bits uint64 - big *big.Int + kind scalarKind + bits uint64 + big *big.Int + exact semantics.Value } type scalarKind uint8 @@ -25,6 +27,7 @@ const ( scalarInt scalarKind = iota scalarReal scalarBool + scalarRational ) // #nosec G115 -- the Integer's two's-complement bits are stored, not its magnitude. @@ -61,6 +64,8 @@ func (s scalar) semantic() semantics.Value { return semantics.IntValue(s.int()) case scalarReal: return semantics.Value{Kind: semantics.ValReal, Real: s.real()} + case scalarRational: + return s.exact default: return semantics.Value{Kind: semantics.ValBool, Bool: s.truth()} } @@ -81,6 +86,8 @@ func scalarOfConst(c semantics.Value) (scalar, bool) { return scalar{kind: scalarInt, big: c.BigIntView()}, true case semantics.ValReal: return realScalar(c.Real), true + case semantics.ValRational: + return scalar{kind: scalarRational, exact: c}, true case semantics.ValBool: return boolScalar(c.Bool), true } diff --git a/internal/exec/runtime/compiled_stmts.go b/internal/exec/runtime/compiled_stmts.go index 6fc3ff5ab7..0ebf833bae 100644 --- a/internal/exec/runtime/compiled_stmts.go +++ b/internal/exec/runtime/compiled_stmts.go @@ -221,9 +221,14 @@ func declareStmt(s lower.Declare, slot int, value compiledExpr, check *scalarChe if err != nil { return scalar{}, false, fmt.Errorf("eval declaration %s: %w", s.Name, err) } - if check != nil && !check.accepts(v) { - boxed := v.boxed() - if err := ctx.checkBodyDeclaration(s.Scope, calcBodyDescription, s.Name, &boxed); err != nil { + if check != nil { + if !check.accepts(v) { + boxed := v.boxed() + if err := ctx.checkBodyDeclaration(s.Scope, calcBodyDescription, s.Name, &boxed); err != nil { + return scalar{}, false, err + } + } + if v, err = check.held(v, nil); err != nil { return scalar{}, false, err } } @@ -240,8 +245,14 @@ func returnStmt(value compiledExpr, result *scalarCheck) compiledStmt { if err != nil { return scalar{}, false, fmt.Errorf("evaluating the returned expression: %w", err) } - if result != nil && !result.accepts(v) { - if err := result.refuse(ctx, v, func() string { return "result" }); err != nil { + if result != nil { + what := func() string { return "result" } + if !result.accepts(v) { + if err := result.refuse(ctx, v, what); err != nil { + return scalar{}, false, err + } + } + if v, err = result.held(v, what); err != nil { return scalar{}, false, err } } diff --git a/internal/exec/runtime/conformance_test.go b/internal/exec/runtime/conformance_test.go index 8c030cec9a..03bdbb8890 100644 --- a/internal/exec/runtime/conformance_test.go +++ b/internal/exec/runtime/conformance_test.go @@ -2102,6 +2102,17 @@ func expectedInteger(v any) (semantics.Value, bool) { return semantics.Value{}, false } +// expectedRational reads a case's Rational, written as the exact text ToString +// prints (`"0.1"`, `"1/3"`) so no decimal is read through a binary64. +func expectedRational(v any) (semantics.Value, bool) { + s, ok := v.(string) + if !ok { + return semantics.Value{}, false + } + r, err := semantics.ParseRationalText(s, semantics.DefaultMaxIntegerBits) + return r, err == nil +} + // expectedToRuntimeValue converts ExpectedValue to runtime Value func expectedToRuntimeValue(t *testing.T, ev ExpectedValue) Value { switch ev.Type { @@ -2116,6 +2127,11 @@ func expectedToRuntimeValue(t *testing.T, ev ExpectedValue) Value { return Value{Kind: ValConst, Const: semantics.Value{Kind: semantics.ValReal, Real: v}} } t.Fatalf("invalid Real value type: %T", ev.Value) + case "Rational": + if r, ok := expectedRational(ev.Value); ok { + return Value{Kind: ValConst, Const: r} + } + t.Fatalf("invalid Rational value %v (%T)", ev.Value, ev.Value) case "Boolean": if v, ok := ev.Value.(bool); ok { return Value{Kind: ValConst, Const: semantics.Value{Kind: semantics.ValBool, Bool: v}} @@ -2258,8 +2274,21 @@ func validateValue(t reporter, ctx *Context, name string, expected ExpectedValue return } want := expected.Value.(float64) - if actual.Const.Real != want { - t.Errorf("%s: value = %f, want %f", name, actual.Const.Real, want) + if actual.Const.AsReal() != want { + t.Errorf("%s: value = %f, want %f", name, actual.Const.AsReal(), want) + } + case "Rational": + if actual.Kind != ValConst || actual.Const.Kind != semantics.ValRational { + t.Errorf("%s: type = %v (Const.Kind=%v), want Rational", name, actual.Kind, actual.Const.Kind) + return + } + want, ok := expectedRational(expected.Value) + if !ok { + t.Errorf("%s: invalid Rational value %v (%T)", name, expected.Value, expected.Value) + return + } + if semantics.CompareRat(actual.Const, want) != 0 { + t.Errorf("%s: value = %s, want %s", name, actual.Const.FormatRational(), want.FormatRational()) } case "Boolean": if actual.Kind != ValConst || actual.Const.Kind != semantics.ValBool { @@ -2338,6 +2367,10 @@ func validateValue(t reporter, ctx *Context, name string, expected ExpectedValue if float64(got.Int) != want { t.Errorf("%s: magnitude = %d, want %v", name, got.Int, want) } + case semantics.ValRational: + if f := got.AsReal(); math.Abs(f-want) > 1e-9*math.Max(1, math.Abs(want)) { + t.Errorf("%s: magnitude = %s, want %v", name, got.FormatRational(), want) + } default: t.Errorf("%s: magnitude kind = %v, want a number", name, got.Kind) } diff --git a/internal/exec/runtime/connector_test.go b/internal/exec/runtime/connector_test.go index 2415ebe15c..da12465a48 100644 --- a/internal/exec/runtime/connector_test.go +++ b/internal/exec/runtime/connector_test.go @@ -100,7 +100,7 @@ func TestWritingAConnectedPortIsReadThroughTheEnd(t *testing.T) { if err != nil { t.Fatalf("GetFeatureValue rate through the end: %v", err) } - if read.Value.Const.Real != 9.5 { + if read.Value.Const.AsReal() != 9.5 { t.Errorf("link.source.rate = %v, want the 9.5 written on a.p", read.Value) } } diff --git a/internal/exec/runtime/declared_test.go b/internal/exec/runtime/declared_test.go index ba020235fa..1347198d64 100644 --- a/internal/exec/runtime/declared_test.go +++ b/internal/exec/runtime/declared_test.go @@ -237,7 +237,7 @@ func TestDeclaredReaderSelectsCallsThroughTheArgumentTyper(t *testing.T) { if err != nil { t.Fatalf("Invoking.n: %v", err) } - if val.Kind != ValConst || val.Const.Kind != semantics.ValReal || val.Const.Real != 6.0 { + if val.Kind != ValConst || val.Const.Kind != semantics.ValReal || val.Const.AsReal() != 6.0 { t.Errorf("Invoking.n = %s, want 6.0", describeValue(val)) } diff --git a/internal/exec/runtime/division_test.go b/internal/exec/runtime/division_test.go index 004d4dcefa..f1336b0404 100644 --- a/internal/exec/runtime/division_test.go +++ b/internal/exec/runtime/division_test.go @@ -11,8 +11,8 @@ import ( // the constant folder's. const quotientModel = `calc def quotient { in a : Integer; in b : Integer; return : Rational = a / b; }` -// TestWholeNumberQuotientIsRational: the quotient of two whole numbers is a -// Rational for every sign combination, exact or not, never truncated. +// TestWholeNumberQuotientIsRational: the quotient of two whole numbers is the exact +// Rational for every sign combination, whole or not, never truncated. func TestWholeNumberQuotientIsRational(t *testing.T) { model, resolver, root := parseAndBuildModel(t, quotientModel) ctx := NewContext(typedModel(model, resolver), 1000) @@ -20,52 +20,52 @@ func TestWholeNumberQuotientIsRational(t *testing.T) { cases := []struct { a, b int64 - want float64 + want string }{ - {5, 2, 2.5}, - {9, 3, 3.0}, - {-5, -1, 5.0}, - {-7, 2, -3.5}, - {7, -2, -3.5}, - {10, 4, 2.5}, - {7, 2, 3.5}, - {0, -3, 0.0}, + {5, 2, "2.5"}, + {9, 3, "3"}, + {-5, -1, "5"}, + {-7, 2, "-3.5"}, + {7, -2, "-3.5"}, + {10, 4, "2.5"}, + {7, 2, "3.5"}, + {0, -3, "0"}, } for _, tc := range cases { got, err := ctx.InvokeCalc(quotient, []Value{constInt(tc.a), constInt(tc.b)}, root) if err != nil { t.Fatalf("%d / %d: %v", tc.a, tc.b, err) } - if got.Const.Kind != semantics.ValReal || got.Const.Real != tc.want { - t.Errorf("%d / %d = %+v, want the Real %v", tc.a, tc.b, got.Const, tc.want) + if want := constRat(tc.want).Const; got.Const.Kind != semantics.ValRational || semantics.CompareRat(got.Const, want) != 0 { + t.Errorf("%d / %d = %s, want the Rational %s", tc.a, tc.b, FormatValue(got), tc.want) } } } -// TestQuotientBeyondFloatExactRangeRoundsOnce: operands above 2^53 divide as -// exact rationals rounded once, not through pre-rounded float64 operands. -func TestQuotientBeyondFloatExactRangeRoundsOnce(t *testing.T) { +// TestQuotientBeyondFloatExactRangeIsExact: operands above 2^53 divide as exact +// rationals, not through rounded float64 operands or a rounded quotient. +func TestQuotientBeyondFloatExactRangeIsExact(t *testing.T) { model, resolver, root := parseAndBuildModel(t, quotientModel) ctx := NewContext(typedModel(model, resolver), 1000) quotient := resolveSymbol(t, root, "quotient") cases := []struct { a, b int64 - want float64 + want string }{ - {9007199254740993, 3, 3002399751580331}, // 2^53+1: rounding a first loses the exact thirds - {-9007199254740993, 3, -3002399751580331}, // the sign does not change the rounding - {9007199254740993, 1, 9007199254740992}, // 2^53+1 itself is not a float64; ties round to even - {9007199254740993, 2, 4503599627370496}, // an inexact half rounds once, to even - {-9223372036854775808, -1, 9223372036854775808}, // MinInt64 / -1 is the Real 2^63 + {9007199254740993, 3, "3002399751580331"}, // 2^53+1: rounding a first loses the exact thirds + {-9007199254740993, 3, "-3002399751580331"}, // the sign does not change the quotient + {9007199254740993, 1, "9007199254740993"}, // 2^53+1 itself is not a float64 + {9007199254740993, 2, "9007199254740993/2"}, // no half is rounded + {-9223372036854775808, -1, "9223372036854775808"}, // MinInt64 / -1 is the Rational 2^63 } for _, tc := range cases { got, err := ctx.InvokeCalc(quotient, []Value{constInt(tc.a), constInt(tc.b)}, root) if err != nil { t.Fatalf("%d / %d: %v", tc.a, tc.b, err) } - if got.Const.Kind != semantics.ValReal || got.Const.Real != tc.want { - t.Errorf("%d / %d = %+v, want the Real %v", tc.a, tc.b, got.Const, tc.want) + if want := constRat(tc.want).Const; got.Const.Kind != semantics.ValRational || semantics.CompareRat(got.Const, want) != 0 { + t.Errorf("%d / %d = %s, want the Rational %s", tc.a, tc.b, FormatValue(got), tc.want) } } } diff --git a/internal/exec/runtime/draw_policy_test.go b/internal/exec/runtime/draw_policy_test.go index a6ec7d192f..de58fc4513 100644 --- a/internal/exec/runtime/draw_policy_test.go +++ b/internal/exec/runtime/draw_policy_test.go @@ -33,7 +33,7 @@ func realOut(t *testing.T, out map[string]Value, name string) float64 { if !ok || v.Const.Kind != semantics.ValReal { t.Fatalf("%s = %v, want a Real", name, v) } - return v.Const.Real + return v.Const.AsReal() } // formatDraws spells draws one per line, as a witness lists them. diff --git a/internal/exec/runtime/eval.go b/internal/exec/runtime/eval.go index 1a9a01acfb..d55049f9f5 100644 --- a/internal/exec/runtime/eval.go +++ b/internal/exec/runtime/eval.go @@ -465,18 +465,22 @@ func (ec *EvalContext) evalLiteralInteger(n *ast.LiteralInteger) (Value, error) return Value{Kind: ValConst, Const: val}, nil } -// evalLiteralReal evaluates a real literal, reporting one outside the Real -// range rather than carrying it as an infinity. +// evalLiteralReal evaluates a decimal literal to the exact Rational it denotes +// (KerML 1.0 §8.4.4.9.2), refusing one beyond the size budget. func (ec *EvalContext) evalLiteralReal(n *ast.LiteralReal) (Value, error) { val, ok := ec.ctx.model.realLiterals[n] if !ok { var err error - if val, err = semantics.ParseReal(n.Value); err != nil { - return Value{}, fmt.Errorf("%w: literal %s is outside the Real range", err, n.Value) + if val, err = semantics.ParseRational(n.Value, ec.ctx.maxIntegerBits); err != nil { + return Value{}, fmt.Errorf("literal %s: %w", n.Value, integerSizeHint(err)) } ec.ctx.model.realLiterals[n] = val } - return Value{Kind: ValConst, Const: semantics.Value{Kind: semantics.ValReal, Real: val}}, nil + if bits := val.RatBitLen(); bits > ec.ctx.maxIntegerBits { + return Value{}, fmt.Errorf("literal %s: %w", n.Value, + integerSizeHint(semantics.RationalSizeExceeded(bits, ec.ctx.maxIntegerBits))) + } + return Value{Kind: ValConst, Const: val}, nil } // evalLiteralBool evaluates a boolean literal. @@ -1612,6 +1616,9 @@ func (ec *EvalContext) enumLiteralValue(sym *symbols.Symbol) (Value, error) { if err != nil { return Value{}, fmt.Errorf("enumeration literal %s: %w", sym.Name, err) } + if err := ec.ctx.holdAsReal(&val, semantics.EnumerationOwning(sym)); err != nil { + return Value{}, fmt.Errorf("enumeration literal %s: %w", sym.Name, err) + } return val.ofLiteral(sym), nil } @@ -1843,7 +1850,7 @@ func (ctx *Context) directValueType(scope *symbols.Scope, value Value) (*symbols switch value.Kind { case ValConst: switch value.Const.Kind { - case semantics.ValInt, semantics.ValReal, semantics.ValBool: + case semantics.ValInt, semantics.ValRational, semantics.ValReal, semantics.ValBool: return ctx.scalarValueType(scope, value) case semantics.ValInfinity: // `*` is the natural number exceeding every other (KerML 8.4.4.6). @@ -2262,17 +2269,10 @@ func constArithmetic(op ast.OperatorKind, left, right semantics.Value, maxBits i return res, integerSizeHint(err) } - // Integer arithmetic is exact, in int64 while the result fits it. - if left.Kind == semantics.ValInt && right.Kind == semantics.ValInt { + // Integer arithmetic is exact, in int64 while the result fits it; an + // Integer quotient is the exact Rational IntegerFunctions::'/' declares. + if left.Kind == semantics.ValInt && right.Kind == semantics.ValInt && op != ast.OpDiv { switch op { - case ast.OpDiv: - // A quotient is a Rational: the exact ratio, rounded once to float64 - // so operands beyond 2^53 are not rounded before dividing. - q, ok := semantics.IntQuotient(left, right) - if !ok { - return semantics.Value{}, ErrDivisionByZero - } - return semantics.RealResult(q) case ast.OpMod: r, ok := semantics.IntRem(left, right) if !ok { @@ -2283,8 +2283,12 @@ func constArithmetic(op ast.OperatorKind, left, right semantics.Value, maxBits i res, err := semantics.IntArith(op, left, right, maxBits) return res, integerSizeHint(err) } + if left.IsExact() && right.IsExact() { + res, err := semantics.RatArith(op, left, right, maxBits) + return res, integerSizeHint(err) + } - // Real arithmetic (coerce int to real if needed) + // Real arithmetic, an exact operand rounded once to the nearest binary64 leftReal := toReal(left) rightReal := toReal(right) var result float64 @@ -2546,13 +2550,17 @@ func constComparison(op ast.OperatorKind, left, right semantics.Value) (bool, er return res, nil } - // An Integer orders against a Real exactly, neither rounded to the other. - if left.Kind == semantics.ValInt && right.Kind == semantics.ValReal && !math.IsNaN(right.Real) { - res, _ := semantics.OrderSatisfies(op, semantics.CompareIntReal(left, right.Real)) + if left.IsExact() && right.IsExact() { + res, _ := semantics.OrderSatisfies(op, semantics.CompareRat(left, right)) + return res, nil + } + // An exact number orders against a Real exactly, neither rounded to the other. + if left.IsExact() && right.Kind == semantics.ValReal && !math.IsNaN(right.Real) { + res, _ := semantics.OrderSatisfies(op, semantics.CompareExactReal(left, right.Real)) return res, nil } - if left.Kind == semantics.ValReal && right.Kind == semantics.ValInt && !math.IsNaN(left.Real) { - res, _ := semantics.OrderSatisfies(op, -semantics.CompareIntReal(right, left.Real)) + if left.Kind == semantics.ValReal && right.IsExact() && !math.IsNaN(left.Real) { + res, _ := semantics.OrderSatisfies(op, -semantics.CompareExactReal(right, left.Real)) return res, nil } diff --git a/internal/exec/runtime/eval_cache_test.go b/internal/exec/runtime/eval_cache_test.go index a3a2ad2f33..13a7aa55e5 100644 --- a/internal/exec/runtime/eval_cache_test.go +++ b/internal/exec/runtime/eval_cache_test.go @@ -22,7 +22,7 @@ func parseExpr(t *testing.T, src string) ast.Node { // TestRepeatedLiteralEvaluationAnswersAlike: a literal evaluated again in one // context, memoized or not, answers the same value, one beyond int64 included, -// and one outside its range reports the same error each time rather than a +// and one beyond the size budget reports the same error each time rather than a // cached success. func TestRepeatedLiteralEvaluationAnswersAlike(t *testing.T) { model, resolver, _ := parseAndBuildModel(t, sumModel) @@ -33,7 +33,7 @@ func TestRepeatedLiteralEvaluationAnswersAlike(t *testing.T) { want semantics.Value }{ {"42", semantics.Value{Kind: semantics.ValInt, Int: 42}}, - {"2.5", semantics.Value{Kind: semantics.ValReal, Real: 2.5}}, + {"2.5", constRat("2.5").Const}, {"9223372036854775808", bigConst(t, "9223372036854775808").Const}, } { expr := parseExpr(t, tc.src) @@ -45,13 +45,13 @@ func TestRepeatedLiteralEvaluationAnswersAlike(t *testing.T) { } } - for _, src := range []string{"1e400", "-1e400"} { + for _, src := range []string{"1e400000", "-1e400000"} { expr := parseExpr(t, src) var first string for i := 0; i < 3; i++ { got, err := ctx.Eval(expr) - if !errors.Is(err, semantics.ErrArithmeticOverflow) { - t.Fatalf("eval %d of %s = %+v, %v; want ErrArithmeticOverflow", i, src, got, err) + if !errors.Is(err, semantics.ErrRationalSizeLimit) { + t.Fatalf("eval %d of %s = %+v, %v; want ErrRationalSizeLimit", i, src, got, err) } if i == 0 { first = err.Error() @@ -96,9 +96,9 @@ func TestNestedInvocationArgumentsStayDistinct(t *testing.T) { } } - failing := parseExpr(t, "sub(fib(5), 1e400)") - if _, err := ec.Eval(failing); !errors.Is(err, semantics.ErrArithmeticOverflow) { - t.Fatalf("eval of a failing second argument: err = %v; want ErrArithmeticOverflow", err) + failing := parseExpr(t, "sub(fib(5), 1 / 0)") + if _, err := ec.Eval(failing); !errors.Is(err, ErrDivisionByZero) { + t.Fatalf("eval of a failing second argument: err = %v; want ErrDivisionByZero", err) } if len(ctx.argStack) != 0 { t.Fatalf("a failing argument left %d arguments on the stack", len(ctx.argStack)) @@ -152,11 +152,11 @@ func TestRepeatedInvocationEvaluationAnswersAlike(t *testing.T) { // An argument that fails is reported before the target is judged, so a // resolved and an unresolved call alike answer with the argument's error. - for _, src := range []string{"twice(1e400)", "nowhere(1e400)"} { + for _, src := range []string{"twice(1 / 0)", "nowhere(1 / 0)"} { expr := parseExpr(t, src) for i := 0; i < 2; i++ { - if _, err := ec.Eval(expr); !errors.Is(err, semantics.ErrArithmeticOverflow) { - t.Fatalf("eval %d of %s: err = %v; want the argument's ErrArithmeticOverflow", i, src, err) + if _, err := ec.Eval(expr); !errors.Is(err, ErrDivisionByZero) { + t.Fatalf("eval %d of %s: err = %v; want the argument's ErrDivisionByZero", i, src, err) } } } diff --git a/internal/exec/runtime/eval_literal_arith_test.go b/internal/exec/runtime/eval_literal_arith_test.go index 2081da69a6..5c117125b8 100644 --- a/internal/exec/runtime/eval_literal_arith_test.go +++ b/internal/exec/runtime/eval_literal_arith_test.go @@ -62,9 +62,9 @@ func TestIntegerArithmeticBeyondInt64IsExact(t *testing.T) { } // TestRealArithmeticOverflowIsReported requires an arithmetic result that is no -// finite Real to be an error rather than an infinity. +// finite Real to be an error rather than an infinity; 4.0 ** 0.5 is a Real. func TestRealArithmeticOverflowIsReported(t *testing.T) { - value, err := evalStringExpr(t, "1.0e308 * 10.0") + value, err := evalStringExpr(t, "(4.0 ** 0.5) * 1.0e308") if err == nil { t.Fatalf("eval = %v, want an overflow error", value) } diff --git a/internal/exec/runtime/eval_test.go b/internal/exec/runtime/eval_test.go index c35fca318a..e5316f5569 100644 --- a/internal/exec/runtime/eval_test.go +++ b/internal/exec/runtime/eval_test.go @@ -443,13 +443,13 @@ func TestEval_NegationArithmetic(t *testing.T) { } } -func TestEval_NegationArithmeticReal(t *testing.T) { +func TestEval_NegationArithmeticRational(t *testing.T) { tests := []struct { src string - expected float64 + expected string }{ - {"-3.14", -3.14}, - {"-(-2.5)", 2.5}, + {"-3.14", "-3.14"}, + {"-(-2.5)", "2.5"}, } for _, tt := range tests { t.Run(tt.src, func(t *testing.T) { @@ -461,11 +461,11 @@ func TestEval_NegationArithmeticReal(t *testing.T) { if err != nil { t.Fatalf("Eval failed: %v", err) } - if result.Kind != ValConst || result.Const.Kind != semantics.ValReal { - t.Fatalf("expected real, got %v", result) + if result.Kind != ValConst || result.Const.Kind != semantics.ValRational { + t.Fatalf("expected rational, got %v", result) } - if result.Const.Real != tt.expected { - t.Errorf("expected %v, got %v", tt.expected, result.Const.Real) + if got := result.Const.FormatRational(); got != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, got) } }) } diff --git a/internal/exec/runtime/evaluation_log.go b/internal/exec/runtime/evaluation_log.go index ed3e7bbeec..e49fdbc149 100644 --- a/internal/exec/runtime/evaluation_log.go +++ b/internal/exec/runtime/evaluation_log.go @@ -37,13 +37,20 @@ func (ctx *Context) endEvaluationLog(log *evaluationLog) { ctx.evaluations = log.enclosing } -// record notes one application of fn to args, when fn is a calc of the case -// being run and no application binding the same arguments was noted before. -func (log *evaluationLog) record(fn *functionValue, args calcArgs, result Value, err error) { +// record notes one application of fn to args, as its parameters hold them, when fn is +// a calc of the case being run and no application binding the same arguments was noted before. +func (log *evaluationLog) record(ctx *Context, fn *functionValue, args calcArgs, result Value, err error) { if log == nil || fn.shape == nil || !log.calcs[fn.shape.Sym] { return } 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 { + arguments[i] = fn.shape.argumentsByPosition(args)[i] + } + } + } if log.seen(fn.shape.Sym, arguments) { return } diff --git a/internal/exec/runtime/exponentiation_test.go b/internal/exec/runtime/exponentiation_test.go index 12fa6cfe20..f27069904d 100644 --- a/internal/exec/runtime/exponentiation_test.go +++ b/internal/exec/runtime/exponentiation_test.go @@ -37,10 +37,10 @@ func TestExponentiationRuntimeMatchesFolding(t *testing.T) { {"2 ** 10", []Value{constInt(2), constInt(10)}, semantics.Value{Kind: semantics.ValInt, Int: 1024}}, {"2 ^ 3", []Value{constInt(2), constInt(3)}, semantics.Value{Kind: semantics.ValInt, Int: 8}}, {"7 ** 0", []Value{constInt(7), constInt(0)}, semantics.Value{Kind: semantics.ValInt, Int: 1}}, - {"2 ** -1", []Value{constInt(2), constInt(-1)}, semantics.Value{Kind: semantics.ValReal, Real: 0.5}}, + {"2 ** -1", []Value{constInt(2), constInt(-1)}, constRat("1/2").Const}, {"2.0 ** 0.5", []Value{constReal(2), constReal(0.5)}, semantics.Value{Kind: semantics.ValReal, Real: math.Sqrt2}}, {"9 ** 0.5", []Value{constInt(9), constReal(0.5)}, semantics.Value{Kind: semantics.ValReal, Real: 3}}, - {"2.5 ** 2", []Value{constReal(2.5), constInt(2)}, semantics.Value{Kind: semantics.ValReal, Real: 6.25}}, + {"2.5 ** 2", []Value{constRat("2.5"), constInt(2)}, constRat("25/4").Const}, } model, resolver, root := parseAndBuildModel(t, powerModel) diff --git a/internal/exec/runtime/function_value.go b/internal/exec/runtime/function_value.go index 0b19ed5732..6bc1aee6c1 100644 --- a/internal/exec/runtime/function_value.go +++ b/internal/exec/runtime/function_value.go @@ -239,6 +239,6 @@ func (ec *EvalContext) invokeFunction(callee string, val Value, args calcArgs) ( } else { result, err = ec.ctx.invokeCalcShapeIn(fn.shape, args, fn.scope, fn.self, fn.enclosing) } - ec.ctx.evaluations.record(fn, args, result, err) + ec.ctx.evaluations.record(ec.ctx, fn, args, result, err) return result, err } diff --git a/internal/exec/runtime/instance_test.go b/internal/exec/runtime/instance_test.go index 23c48cff4b..d4262aab85 100644 --- a/internal/exec/runtime/instance_test.go +++ b/internal/exec/runtime/instance_test.go @@ -37,8 +37,8 @@ func TestInstantiate_SimplePartDef(t *testing.T) { if diameterFeatureValue.Value.Kind != ValConst { t.Errorf("expected ValConst, got %v", diameterFeatureValue.Value.Kind) } - if diameterFeatureValue.Value.Const.Real != 0.5 { - t.Errorf("expected Real=0.5, got %f", diameterFeatureValue.Value.Const.Real) + if diameterFeatureValue.Value.Const.AsReal() != 0.5 { + t.Errorf("expected Real=0.5, got %f", diameterFeatureValue.Value.Const.AsReal()) } } @@ -160,7 +160,7 @@ func TestMultiValuedDefaultMaterializes(t *testing.T) { t.Fatalf("Values.Kind = %v, want a sequence", fv.Values.Kind) } elements := fv.Values.Sequence().Elements() - if len(elements) != 1 || elements[0].Const.Real != 200.0 { + if len(elements) != 1 || elements[0].Const.AsReal() != 200.0 { t.Errorf("doubles = %v, want [200]", elements) } } @@ -436,7 +436,7 @@ func nestedReal(t *testing.T, ctx *Context, inst *Instance, featureName, nestedN if err != nil { t.Fatalf("GetFeatureValue(%q) failed: %v", nestedName, err) } - return nestedFeatureValue.Value.Const.Real + return nestedFeatureValue.Value.Const.AsReal() } // A default that is no value at all holds nothing, so what the multiplicity diff --git a/internal/exec/runtime/integer_promotion_test.go b/internal/exec/runtime/integer_promotion_test.go index 7360cf8a0a..66a73aba59 100644 --- a/internal/exec/runtime/integer_promotion_test.go +++ b/internal/exec/runtime/integer_promotion_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) // sumModel and productModel are calcs over parameters, so their arithmetic @@ -108,15 +109,22 @@ func TestLiteralBeyondInt64IsRead(t *testing.T) { } wantInteger(t, src, got, src) } - for _, src := range []string{ - "1e400", - "1e400 + 1.0", - "1e308 + 1e308", - "1e200 * 1e200", + for src, want := range map[string]string{ + "1e400": "1e400", + "1e400 + 1.0": "1e400", + "1e308 + 1e308": "2e308", + "1e200 * 1e200": "1e400", } { got, err := evalLiteral(t, ctx, src) - if !errors.Is(err, semantics.ErrArithmeticOverflow) { - t.Fatalf("%s = %+v, %v; want ErrArithmeticOverflow", src, got, err) + if err != nil { + t.Fatalf("%s: %v", src, err) + } + exact, _ := semantics.ParseRationalText(want, semantics.DefaultMaxIntegerBits) + if src == "1e400 + 1.0" { + exact, _ = semantics.RatArith(ast.OpAdd, exact, semantics.IntValue(1), semantics.DefaultMaxIntegerBits) + } + if got.Const.Kind != semantics.ValRational || semantics.CompareRat(got.Const, exact) != 0 { + t.Fatalf("%s = %s, want the exact Rational", src, FormatValue(got)) } } } @@ -136,8 +144,8 @@ func TestLeastInt64LiteralIsRead(t *testing.T) { } // TestNegatingTheLeastInt64: the negation of the least int64 is 2^63, an -// Integer beyond int64. Dividing it by -1 is a quotient, a Rational, so it -// answers the Real 2^63. +// Integer beyond int64. Dividing it by -1 is a quotient, so it answers the +// Rational 2^63. func TestNegatingTheLeastInt64(t *testing.T) { model, resolver, root := parseAndBuildModel(t, sumModel) ctx := NewContext(typedModel(model, resolver), 1000) @@ -153,8 +161,8 @@ func TestNegatingTheLeastInt64(t *testing.T) { if err != nil { t.Fatalf("-9223372036854775808 / -1: %v", err) } - if got.Const.Kind != semantics.ValReal || got.Const.Real != -float64(math.MinInt64) { - t.Errorf("-9223372036854775808 / -1 = %+v, want the Real %v", got.Const, -float64(math.MinInt64)) + if got.Const.Kind != semantics.ValRational || got.Const.FormatRational() != "9223372036854775808.0" { + t.Errorf("-9223372036854775808 / -1 = %s, want the Rational 2^63", FormatValue(got)) } args := []Value{constInt(math.MinInt64), constInt(0)} diff --git a/internal/exec/runtime/library_census_probes_test.go b/internal/exec/runtime/library_census_probes_test.go index be2563e35e..9812b47daa 100644 --- a/internal/exec/runtime/library_census_probes_test.go +++ b/internal/exec/runtime/library_census_probes_test.go @@ -28,6 +28,24 @@ func probeModel(imports, body string) string { func expectReal(v float64) ExpectedValue { return ExpectedValue{Type: "Real", Value: v} } +func expectRational(text string) ExpectedValue { return ExpectedValue{Type: "Rational", Value: text} } + +func expectRationals(texts ...string) []ExpectedValue { + elements := make([]ExpectedValue, len(texts)) + for i, text := range texts { + elements[i] = expectRational(text) + } + return elements +} + +func expectRationalVector(texts ...string) ExpectedValue { + return ExpectedValue{Type: "Vector", Elements: expectRationals(texts...)} +} + +func expectRationalSequence(texts ...string) ExpectedValue { + return ExpectedValue{Type: "Sequence", Elements: expectRationals(texts...)} +} + func expectBool(v bool) ExpectedValue { return ExpectedValue{Type: "Boolean", Value: v} } func expectInt(v int64) ExpectedValue { return ExpectedValue{Type: "Integer", Value: float64(v)} } @@ -58,13 +76,13 @@ var sampledFunctionProbes = []libraryProbe{ decl: "SampledFunctions::Domain", model: probeModel("SampledFunctions", sampledPairs+"\tattribute r = Domain(s);\n"), read: "test::r", - want: expectSequence(1.0, 3.0), + want: expectRationalSequence("1.0", "3.0"), }, { decl: "SampledFunctions::Range", model: probeModel("SampledFunctions", sampledPairs+"\tattribute r = Range(s);\n"), read: "test::r", - want: expectSequence(2.0, 4.0), + want: expectRationalSequence("2.0", "4.0"), }, { decl: "SampledFunctions::Sample", @@ -80,14 +98,14 @@ var sampledFunctionProbes = []libraryProbe{ attribute r = First(s, 9.0); `), read: "test::r", - want: expectReal(2.0), + want: expectRational("2.0"), }, { // The library's Linear formula: f = (2-1)/(1-3) = -0.5, 4 + f*(2-4) = 5. decl: "SampledFunctions::interpolateLinear", model: probeModel("SampledFunctions", sampledPairs+"\tattribute r = interpolateLinear(s, 2.0);\n"), read: "test::r", - want: expectReal(5.0), + want: expectRational("5.0"), }, } @@ -300,14 +318,14 @@ func stateSpaceAction(name string) libraryProbe { var vectorFunctionProbes = []libraryProbe{ vectorProbe("isZeroVector", "isZeroVector(CartesianVectorOf((0.0, 0.0)))", expectBool(true)), - vectorProbe("+", "VectorFunctions::'+'((1.0, 2.0), (3.0, 4.0))", expectVector(4.0, 6.0)), - vectorProbe("-", "VectorFunctions::'-'((1.0, 2.0), (3.0, 4.0))", expectVector(-2.0, -2.0)), + vectorProbe("+", "VectorFunctions::'+'((1.0, 2.0), (3.0, 4.0))", expectRationalVector("4.0", "6.0")), + vectorProbe("-", "VectorFunctions::'-'((1.0, 2.0), (3.0, 4.0))", expectRationalVector("-2.0", "-2.0")), vectorProbe("sum0", "sum0((CartesianVectorOf((1.0, 2.0)), CartesianVectorOf((3.0, 4.0))), CartesianVectorOf((0.0, 0.0)))", expectVector(4.0, 6.0)), - vectorProbe("VectorOf", "VectorOf((1.0, 2.0, 3.0))", expectVector(1.0, 2.0, 3.0)), - vectorProbe("scalarVectorMult", "scalarVectorMult(2.0, (1.0, 2.0))", expectVector(2.0, 4.0)), - vectorProbe("vectorScalarMult", "vectorScalarMult((1.0, 2.0), 3.0)", expectVector(3.0, 6.0)), + vectorProbe("VectorOf", "VectorOf((1.0, 2.0, 3.0))", expectRationalVector("1.0", "2.0", "3.0")), + vectorProbe("scalarVectorMult", "scalarVectorMult(2.0, (1.0, 2.0))", expectRationalVector("2.0", "4.0")), + vectorProbe("vectorScalarMult", "vectorScalarMult((1.0, 2.0), 3.0)", expectRationalVector("3.0", "6.0")), vectorProbe("vectorScalarDiv", "vectorScalarDiv((2.0, 4.0), 2.0)", expectVector(1.0, 2.0)), - vectorProbe("inner", "inner((1.0, 2.0), (3.0, 4.0))", expectReal(11.0)), + vectorProbe("inner", "inner((1.0, 2.0), (3.0, 4.0))", expectRational("11.0")), vectorProbe("norm", "norm((3.0, 4.0))", expectReal(5.0)), vectorProbe("angle", "angle((1.0, 0.0), (0.0, 1.0))", expectReal(math.Pi/2)), vectorProbe("CartesianVectorOf", "CartesianVectorOf((1.0, 2.0))", expectVector(1.0, 2.0)), @@ -357,14 +375,23 @@ const ( // invariantProbe invokes a function's invariant where its parameters are bound: from the // result of a specialization, which is 1 when the invariant holds and 0 when it does not. func invariantProbe(fn, inv, params, args, marker string, holds bool) libraryProbe { - one, zero, want := "1.0", "0.0", expectReal(0.0) + one, zero, result := "1.0", "0.0", "0.0" + if holds { + result = "1.0" + } + // A Cartesian result is Real; the others hold the exact literal they return. + want := expectRational(result) if marker != "" { - one, zero, want = marker+"(1.0)", marker+"(0.0)", expectVector(0.0) + one, zero, want = marker+"(1.0)", marker+"(0.0)", expectRationalVector(result) } - if holds { - want = expectReal(1.0) + if strings.Contains(fn, "cartesian") { + real := expectReal(0.0) + if holds { + real = expectReal(1.0) + } + want = real if marker != "" { - want = expectVector(1.0) + want = ExpectedValue{Type: "Vector", Elements: []ExpectedValue{real}} } } return libraryProbe{ diff --git a/internal/exec/runtime/library_conversions.go b/internal/exec/runtime/library_conversions.go index fe911edaea..66ef3fb5d4 100644 --- a/internal/exec/runtime/library_conversions.go +++ b/internal/exec/runtime/library_conversions.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) // ErrInvalidNotation reports a String a conversion function cannot read as a @@ -16,7 +17,7 @@ var ErrInvalidNotation = errors.New("string is not a valid notation") // registerConversionFunctions registers the Kernel Function Library's // conversions between String, Boolean and the numeric scalar types, plus the -// Rational functions the float64 representation can answer. +// RationalFunctions over the exact Rational. func registerConversionFunctions() { registerValueFunction("BaseFunctions::ToString", []string{"x"}, 0, anythingToString) registerValueFunction("BooleanFunctions::ToString", []string{"x"}, 1, booleanToString) @@ -34,7 +35,7 @@ func registerConversionFunctions() { registerValueFunction("IntegerFunctions::ToNatural", []string{"x"}, 1, integerToNatural) registerValueFunction("RationalFunctions::ToInteger", []string{"x"}, 1, numberToInteger) registerValueFunction("RealFunctions::ToInteger", []string{"x"}, 1, numberToInteger) - registerValueFunction("RealFunctions::ToRational", []string{"x"}, 1, realItself) + registerValueFunction("RealFunctions::ToRational", []string{"x"}, 1, realToRational) // A Real is a Complex with a zero imaginary part, so the Complex functions // answer `re` and `im`; the library gives `arg` the body `0.0`. @@ -45,7 +46,7 @@ func registerConversionFunctions() { registerLibraryFunction("RationalFunctions::floor", []string{"x"}, floorToInteger) registerLibraryFunction("RationalFunctions::round", []string{"x"}, roundToInteger) registerLibraryFunction("RationalFunctions::gcd", []string{"x", "y"}, rationalGCD, wholeDomain, wholeDomain) - registerLibraryFunction("RationalFunctions::rat", []string{"numer", "denum"}, integersToRational, integerDomain, denominatorDomain) + registerContextFunction("RationalFunctions::rat", []string{"numer", "denum"}, integersToRational, integerDomain, denominatorDomain) registerLibraryFunction("RationalFunctions::numer", []string{"rat"}, rationalNumerator) registerLibraryFunction("RationalFunctions::denom", []string{"rat"}, rationalDenominator) } @@ -115,7 +116,9 @@ func naturalToString(name string, _ *Context, args []Value) (Value, error) { // numberToString is RealFunctions::ToString and RationalFunctions::ToString: // the shortest decimal that reads back as the same value, as the REPL prints -// it, so ToReal(ToString(x)) == x. An Integer argument keeps its digits. +// it, so ToReal(ToString(x)) == x for a Real and ToRational(ToString(x)) == x +// for a Rational, a non-terminating one written `numer/denom`. An Integer +// argument keeps its digits. func numberToString(name string, _ *Context, args []Value) (Value, error) { x, err := numericArg(name, "x", args[0]) if err != nil { @@ -177,14 +180,23 @@ func stringToReal(name string, _ *Context, args []Value) (Value, error) { return parseReal(name, s, "Real") } -// stringToRational is RationalFunctions::ToRational, which reads the same -// decimal notation as ToReal since a Rational is a float64 here. -func stringToRational(name string, _ *Context, args []Value) (Value, error) { +// stringToRational is RationalFunctions::ToRational: the exact Rational a +// decimal notation, or the `numer/denom` form ToString writes, denotes. +func stringToRational(name string, ctx *Context, args []Value) (Value, error) { s, err := stringArg(name, "x", args[0]) if err != nil { return Value{}, err } - return parseReal(name, s, "Rational") + x, err := semantics.ParseRationalText(s, ctx.maxIntegerBits) + switch { + case errors.Is(err, semantics.ErrRealNotation): + return Value{}, invalidNotation(name, s, "Rational") + case errors.Is(err, semantics.ErrDivisionByZero): + return Value{}, fmt.Errorf("%w: function %s: %s", ErrDivisionByZero, name, strconv.Quote(s)) + case err != nil: + return Value{}, fmt.Errorf("function %s: %w", name, integerSizeHint(err)) + } + return Value{Kind: ValConst, Const: x}, nil } // integerToNatural is IntegerFunctions::ToNatural; a negative Integer is no @@ -205,8 +217,8 @@ func numberToInteger(name string, _ *Context, args []Value) (Value, error) { if err != nil { return Value{}, err } - if x.Kind == semantics.ValInt { - return Value{Kind: ValConst, Const: x}, nil + if x.IsExact() { + return Value{Kind: ValConst, Const: semantics.RatTrunc(x)}, nil } result, err := integerResult(math.Trunc(x.Real)) if err != nil { @@ -215,9 +227,8 @@ func numberToInteger(name string, _ *Context, args []Value) (Value, error) { return Value{Kind: ValConst, Const: result}, nil } -// realItself answers the Real x is bound as, an Integer argument widened: it is -// RealFunctions::ToRational (a Rational is held as a float64) and RealFunctions::re -// (a Real is its own real part). +// realItself answers the Real x is bound as, an exact argument widened: it is +// RealFunctions::re (a Real is its own real part). func realItself(name string, _ *Context, args []Value) (Value, error) { x, err := numericArg(name, "x", args[0]) if err != nil { @@ -226,6 +237,23 @@ func realItself(name string, _ *Context, args []Value) (Value, error) { return checkedReal(asReal(x)) } +// realToRational is RealFunctions::ToRational: the Rational a finite binary64 +// Real exactly is, an exact argument itself. +func realToRational(name string, _ *Context, args []Value) (Value, error) { + x, err := numericArg(name, "x", args[0]) + if err != nil { + return Value{}, err + } + if x.IsExact() { + return Value{Kind: ValConst, Const: semantics.RatOf(x)}, nil + } + r, ok := semantics.RationalOfReal(x.Real) + if !ok { + return Value{}, fmt.Errorf("%w: function %s: %s has no Rational", semantics.ErrArithmeticDomain, name, semantics.FormatConst(x)) + } + return Value{Kind: ValConst, Const: r}, nil +} + // realZero answers 0.0 for every Real: RealFunctions::im and RealFunctions::arg, // whose vendored bodies are both `0.0`. func realZero(name string, _ *Context, args []Value) (Value, error) { @@ -265,11 +293,11 @@ func denominatorDomain(v semantics.Value) error { return nil } -// integersToRational is RationalFunctions::rat: numer/denum rounded once to the -// float64 a Rational is here, exactly as `/` computes it. -func integersToRational(args []semantics.Value) (semantics.Value, error) { - q, _ := semantics.IntQuotient(args[0], args[1]) - return semantics.RealResult(q) +// integersToRational is RationalFunctions::rat: the exact Rational numer/denum, +// as `/` computes it. +func integersToRational(ctx *Context, _ string, args []semantics.Value) (semantics.Value, error) { + q, err := semantics.RatArith(ast.OpDiv, args[0], args[1], ctx.maxIntegerBits) + return q, integerSizeHint(err) } // rationalNumerator is RationalFunctions::numer, the numerator of the ratio in @@ -285,11 +313,12 @@ func rationalDenominator(args []semantics.Value) (semantics.Value, error) { } // exactRationalTerm reads one term of the exact ratio x holds: an Integer is -// itself over 1, a float64 the dyadic ratio its bits denote. +// itself over 1, a Rational its lowest terms, a binary64 Real the dyadic ratio +// its bits denote. func exactRationalTerm(fn string, x semantics.Value, term func(*big.Rat) *big.Int) (semantics.Value, error) { var ratio *big.Rat - if x.Kind == semantics.ValInt { - ratio = new(big.Rat).SetInt(x.BigInt()) + if x.IsExact() { + ratio = x.Rat() } else if ratio = new(big.Rat).SetFloat64(x.Real); ratio == nil { return semantics.Value{}, fmt.Errorf("%w: %s(%s) has no finite ratio", semantics.ErrArithmeticDomain, fn, semantics.FormatConst(x)) } @@ -302,6 +331,11 @@ func wholeInteger(v semantics.Value) (*big.Int, bool) { switch v.Kind { case semantics.ValInt: return v.BigInt(), true + case semantics.ValRational: + if !v.RatIsWhole() { + return nil, false + } + return v.RatNumer().BigInt(), true case semantics.ValReal: if math.IsInf(v.Real, 0) || math.IsNaN(v.Real) || v.Real != math.Trunc(v.Real) { return nil, false diff --git a/internal/exec/runtime/library_conversions_test.go b/internal/exec/runtime/library_conversions_test.go index 151dc7821b..4b00afecdc 100644 --- a/internal/exec/runtime/library_conversions_test.go +++ b/internal/exec/runtime/library_conversions_test.go @@ -70,15 +70,17 @@ func TestConversionFunctionValues(t *testing.T) { {"RealFunctions::ToReal", []Value{NewStringValue("0e-400")}, constReal(0)}, {"RealFunctions::ToReal", []Value{NewStringValue("-0.000")}, constReal(0)}, {"RealFunctions::ToReal", []Value{NewStringValue("4.9e-324")}, constReal(math.SmallestNonzeroFloat64)}, - {"RationalFunctions::ToRational", []Value{NewStringValue("0.25")}, constReal(0.25)}, + {"RationalFunctions::ToRational", []Value{NewStringValue("0.25")}, constRat("0.25")}, + {"RationalFunctions::ToRational", []Value{NewStringValue("1/3")}, constRat("1/3")}, + {"RationalFunctions::ToRational", []Value{NewStringValue("1e-400")}, constRat("1e-400")}, {"IntegerFunctions::ToNatural", []Value{constInt(5)}, constInt(5)}, {"RealFunctions::ToInteger", []Value{constReal(2.7)}, constInt(2)}, {"RealFunctions::ToInteger", []Value{constReal(-2.7)}, constInt(-2)}, {"RealFunctions::ToInteger", []Value{constInt(4)}, constInt(4)}, {"RationalFunctions::ToInteger", []Value{constReal(0.5)}, constInt(0)}, - {"RealFunctions::ToRational", []Value{constReal(0.75)}, constReal(0.75)}, - {"RealFunctions::ToRational", []Value{constInt(2)}, constReal(2)}, + {"RealFunctions::ToRational", []Value{constReal(0.75)}, constRat("0.75")}, + {"RealFunctions::ToRational", []Value{constInt(2)}, constRat("2")}, {"RealFunctions::re", []Value{constReal(2.5)}, constReal(2.5)}, {"RealFunctions::re", []Value{constInt(2)}, constReal(2)}, @@ -103,15 +105,15 @@ func TestConversionFunctionValues(t *testing.T) { {"RationalFunctions::gcd", []Value{constReal(1e20), constReal(6e18)}, constInt(2e18)}, {"RationalFunctions::gcd", []Value{constReal(-1e300), constInt(1 << 62)}, constInt(1 << 62)}, - {"RationalFunctions::rat", []Value{constInt(1), constInt(3)}, constReal(1.0 / 3.0)}, - {"RationalFunctions::rat", []Value{constInt(6), constInt(4)}, constReal(1.5)}, - {"RationalFunctions::rat", []Value{constInt(-3), constInt(4)}, constReal(-0.75)}, - {"RationalFunctions::rat", []Value{constInt(3), constInt(-4)}, constReal(-0.75)}, - {"RationalFunctions::rat", []Value{constInt(0), constInt(1)}, constReal(0)}, - {"RationalFunctions::rat", []Value{constInt(4), constInt(2)}, constReal(2)}, - {"RationalFunctions::rat", []Value{constInt(3602879701896397), constInt(1 << 55)}, constReal(0.1)}, - {"RationalFunctions::rat", []Value{constInt(math.MinInt64), constInt(math.MinInt64)}, constReal(1)}, - {"RationalFunctions::rat", []Value{constInt(math.MaxInt64), constInt(1)}, constReal(math.Ldexp(1, 63))}, + {"RationalFunctions::rat", []Value{constInt(1), constInt(3)}, constRat("1/3")}, + {"RationalFunctions::rat", []Value{constInt(6), constInt(4)}, constRat("1.5")}, + {"RationalFunctions::rat", []Value{constInt(-3), constInt(4)}, constRat("-0.75")}, + {"RationalFunctions::rat", []Value{constInt(3), constInt(-4)}, constRat("-0.75")}, + {"RationalFunctions::rat", []Value{constInt(0), constInt(1)}, constRat("0")}, + {"RationalFunctions::rat", []Value{constInt(4), constInt(2)}, constRat("2")}, + {"RationalFunctions::rat", []Value{constInt(3602879701896397), constInt(1 << 55)}, constRat("3602879701896397/36028797018963968")}, + {"RationalFunctions::rat", []Value{constInt(math.MinInt64), constInt(math.MinInt64)}, constRat("1")}, + {"RationalFunctions::rat", []Value{constInt(math.MaxInt64), constInt(1)}, constRat("9223372036854775807")}, {"RationalFunctions::numer", []Value{constReal(0.75)}, constInt(3)}, {"RationalFunctions::denom", []Value{constReal(0.75)}, constInt(4)}, {"RationalFunctions::numer", []Value{constReal(-0.75)}, constInt(-3)}, @@ -152,10 +154,8 @@ func TestConversionFunctionValues(t *testing.T) { } } -// rat(numer(x), denom(x)) is x for every finite Rational whose exact terms are -// Integers: the terms are the ratio x holds, of any magnitude, and rat rounds -// that ratio to x. An Integer x beyond 2^53 that no Real holds exactly is -// left out: rat answers the Real nearest it, which compares unequal to it. +// rat(numer(x), denom(x)) is x, as an exact Rational, for every finite number: +// the terms are the ratio x holds, of any magnitude. func TestRationalTermsRoundTrip(t *testing.T) { for _, x := range []Value{ constReal(0), constReal(math.Copysign(0, -1)), constReal(1), constReal(-1), constReal(2), constReal(-7), @@ -184,8 +184,12 @@ func TestRationalTermsRoundTrip(t *testing.T) { if err != nil { t.Fatalf("rat(%s, %s) = error %v", FormatValue(numer), FormatValue(denom), err) } - if back.Const.Kind != semantics.ValReal || back.Const.Real != asReal(x.Const) { - t.Fatalf("rat(numer(%s), denom(%s)) = %s, want %v", FormatValue(x), FormatValue(x), FormatValue(back), asReal(x.Const)) + exact := x.Const + if exact.Kind == semantics.ValReal { + exact, _ = semantics.RationalOfReal(exact.Real) + } + if back.Const.Kind != semantics.ValRational || semantics.CompareRat(back.Const, exact) != 0 { + t.Fatalf("rat(numer(%s), denom(%s)) = %s, want %s exactly", FormatValue(x), FormatValue(x), FormatValue(back), FormatValue(x)) } same, err := applyLibrary(t, "RationalFunctions::==", back, x) if err != nil || !valueIdentical(same, constBool(true)) { @@ -205,7 +209,7 @@ func TestRealToStringRoundTrips(t *testing.T) { if err != nil { t.Fatalf("ToReal(%q) = error %v", text.Str(), err) } - if back.Const.Kind != semantics.ValReal || back.Const.Real != x { + if back.Const.Kind != semantics.ValReal || back.Const.AsReal() != x { t.Fatalf("ToReal(ToString(%v)) = %s, want %v", x, FormatValue(back), x) } } @@ -246,9 +250,10 @@ func TestConversionFunctionErrors(t *testing.T) { {"RealFunctions::ToReal", []Value{NewStringValue("1e-400")}, semantics.ErrArithmeticOverflow}, {"RealFunctions::ToReal", []Value{NewStringValue("-2e-324")}, semantics.ErrArithmeticOverflow}, {"RealFunctions::ToReal", []Value{NewStringValue("0." + strings.Repeat("0", 330) + "1")}, semantics.ErrArithmeticOverflow}, - {"RationalFunctions::ToRational", []Value{NewStringValue("1e-400")}, semantics.ErrArithmeticOverflow}, + {"RationalFunctions::ToRational", []Value{NewStringValue("1e-400000")}, semantics.ErrRationalSizeLimit}, {"RealFunctions::ToReal", []Value{constReal(1)}, ErrTypeMismatch}, - {"RationalFunctions::ToRational", []Value{NewStringValue("1/3")}, ErrInvalidNotation}, + {"RationalFunctions::ToRational", []Value{NewStringValue("1/0")}, ErrDivisionByZero}, + {"RationalFunctions::ToRational", []Value{NewStringValue("1/x")}, ErrInvalidNotation}, {"RealFunctions::ToInteger", []Value{NewStringValue("1")}, ErrTypeMismatch}, {"RealFunctions::re", []Value{NewStringValue("1")}, ErrTypeMismatch}, {"NaturalFunctions::ToString", []Value{constInt(-1)}, ErrTypeMismatch}, diff --git a/internal/exec/runtime/library_functions.go b/internal/exec/runtime/library_functions.go index 272ade13be..cbdd440752 100644 --- a/internal/exec/runtime/library_functions.go +++ b/internal/exec/runtime/library_functions.go @@ -148,28 +148,28 @@ func init() { // compute alike over that representation, so both are registered. func registerVectorFunctions() { registerValueFunction("VectorFunctions::isZeroVector", []string{"v"}, 1, vectorIsZero) - registerValueFunction("VectorFunctions::isCartesianZeroVector", []string{"v"}, 1, vectorIsZero) + registerValueFunction("VectorFunctions::isCartesianZeroVector", []string{"v"}, 1, cartesian(vectorIsZero)) registerValueFunction("VectorFunctions::+", []string{"v", "w"}, 1, vectorAdd) - registerValueFunction("VectorFunctions::cartesian+", []string{"v", "w"}, 1, vectorAdd) + registerValueFunction("VectorFunctions::cartesian+", []string{"v", "w"}, 1, cartesian(vectorAdd)) registerValueFunction("VectorFunctions::-", []string{"v", "w"}, 1, vectorSubtract) - registerValueFunction("VectorFunctions::cartesian-", []string{"v", "w"}, 1, vectorSubtract) + registerValueFunction("VectorFunctions::cartesian-", []string{"v", "w"}, 1, cartesian(vectorSubtract)) registerValueFunction("VectorFunctions::VectorOf", []string{"components"}, 1, vectorOf) registerValueFunction("VectorFunctions::CartesianVectorOf", []string{"components"}, 0, cartesianVectorOf) registerValueFunction("VectorFunctions::CartesianThreeVectorOf", []string{"components"}, 1, cartesianThreeVectorOf) registerValueFunction("VectorFunctions::inner", []string{"v", "w"}, 2, vectorInner) - registerValueFunction("VectorFunctions::cartesianInner", []string{"v", "w"}, 2, vectorInner) + registerValueFunction("VectorFunctions::cartesianInner", []string{"v", "w"}, 2, cartesian(vectorInner)) registerValueFunction("VectorFunctions::norm", []string{"v"}, 1, vectorNorm) - registerValueFunction("VectorFunctions::cartesianNorm", []string{"v"}, 1, vectorNorm) + registerValueFunction("VectorFunctions::cartesianNorm", []string{"v"}, 1, cartesian(vectorNorm)) registerValueFunction("VectorFunctions::angle", []string{"v", "w"}, 2, vectorAngle) - registerValueFunction("VectorFunctions::cartesianAngle", []string{"v", "w"}, 2, vectorAngle) + registerValueFunction("VectorFunctions::cartesianAngle", []string{"v", "w"}, 2, cartesian(vectorAngle)) // scalarVectorMult takes the scalar first and vectorScalarMult the vector, // and the library aliases '*' for the former. registerValueFunction("VectorFunctions::scalarVectorMult", []string{"x", "v"}, 2, scalarVectorMult) registerValueFunction("VectorFunctions::*", []string{"x", "v"}, 2, scalarVectorMult) - registerValueFunction("VectorFunctions::cartesianScalarVectorMult", []string{"x", "v"}, 2, scalarVectorMult) + registerValueFunction("VectorFunctions::cartesianScalarVectorMult", []string{"x", "v"}, 2, cartesian(scalarVectorMult)) registerValueFunction("VectorFunctions::vectorScalarMult", []string{"v", "x"}, 2, vectorScalarMult) - registerValueFunction("VectorFunctions::cartesianVectorScalarMult", []string{"v", "x"}, 2, vectorScalarMult) + registerValueFunction("VectorFunctions::cartesianVectorScalarMult", []string{"v", "x"}, 2, cartesian(vectorScalarMult)) registerValueFunction("VectorFunctions::vectorScalarDiv", []string{"v", "x"}, 2, vectorScalarDiv) registerDeclaredFunction("VectorFunctions::sum0", []declaredParam{optionalParam("coll"), param("zero")}, vectorSum0) @@ -678,27 +678,37 @@ func atan2Real(args []semantics.Value) (semantics.Value, error) { return semantics.RealResult(math.Atan2(y, x)) } -// floorToInteger is RealFunctions::floor, which returns Integer. +// floorToInteger is RealFunctions::floor and RationalFunctions::floor, which +// return Integer; an exact argument is floored exactly. func floorToInteger(args []semantics.Value) (semantics.Value, error) { + if args[0].IsExact() { + return semantics.RatFloor(args[0]), nil + } return integerResult(math.Floor(asReal(args[0]))) } // ceilingToInteger is OpenSysMLMathFunctions::ceiling, which returns Integer. func ceilingToInteger(args []semantics.Value) (semantics.Value, error) { + if args[0].IsExact() { + return semantics.IntNeg(semantics.RatFloor(semantics.RatNeg(args[0]))), nil + } return integerResult(math.Ceil(asReal(args[0]))) } -// roundToInteger is RealFunctions::round, which returns Integer. Halves round -// away from zero, as math.Round does. +// roundToInteger is RealFunctions::round and RationalFunctions::round, which +// return Integer. Halves round away from zero, as math.Round does. func roundToInteger(args []semantics.Value) (semantics.Value, error) { + if args[0].IsExact() { + return semantics.RatRound(args[0]), nil + } return integerResult(math.Round(asReal(args[0]))) } // numericAbs is the kind-preserving absolute value NumericalFunctions declares -// over NumericalValue: an Integer argument gives an Integer. +// over NumericalValue: an Integer argument gives an Integer, a Rational a Rational. func numericAbs(args []semantics.Value) (semantics.Value, error) { - if args[0].Kind == semantics.ValInt { - return integerAbs(args) + if args[0].IsExact() { + return semantics.RatAbs(args[0]), nil } return semantics.RealResult(math.Abs(args[0].Real)) } @@ -717,6 +727,12 @@ func numericExtremum(larger bool) func([]semantics.Value) (semantics.Value, erro if args[0].Kind == semantics.ValInt && args[1].Kind == semantics.ValInt { return integerExtremum(larger)(args) } + if args[0].IsExact() && args[1].IsExact() { + if order := semantics.CompareRat(args[0], args[1]); (larger && order >= 0) || (!larger && order <= 0) { + return semantics.RatOf(args[0]), nil + } + return semantics.RatOf(args[1]), nil + } return semantics.RealResult(pickReal(larger, asReal(args[0]), asReal(args[1]))) } } @@ -764,11 +780,19 @@ func naturalDomain(v semantics.Value) error { // isZero and isUnit are the NumericalFunctions predicates the library's sum0 // and product1 assert on their identity element. func isZero(args []semantics.Value) (semantics.Value, error) { - return semantics.Value{Kind: semantics.ValBool, Bool: asReal(args[0]) == 0}, nil + return semantics.Value{Kind: semantics.ValBool, Bool: numberEquals(args[0], 0)}, nil } func isUnit(args []semantics.Value) (semantics.Value, error) { - return semantics.Value{Kind: semantics.ValBool, Bool: asReal(args[0]) == 1}, nil + return semantics.Value{Kind: semantics.ValBool, Bool: numberEquals(args[0], 1)}, nil +} + +// numberEquals reports whether the number x is n, an exact x compared exactly. +func numberEquals(x semantics.Value, n int64) bool { + if x.IsExact() { + return semantics.CompareRat(x, semantics.IntValue(n)) == 0 + } + return asReal(x) == float64(n) } // pickReal returns the larger or the smaller of two reals. @@ -791,7 +815,8 @@ func asReal(v semantics.Value) float64 { // not ask for. func asInteger(v semantics.Value) (semantics.Value, error) { if v.Kind != semantics.ValInt { - return semantics.Value{}, fmt.Errorf("%w: requires an Integer argument, got a Real", ErrTypeMismatch) + return semantics.Value{}, fmt.Errorf("%w: requires an Integer argument, got %s", ErrTypeMismatch, + describeValue(Value{Kind: ValConst, Const: v})) } return v, nil } @@ -1089,7 +1114,7 @@ func complexProduct(name string, ctx *Context, args []Value) (Value, error) { func (ctx *Context) aggregateComplex(name string, collection Value, operator ast.OperatorKind) (Value, error) { elements := elementsOf(collection) if len(elements) > 0 && !holdsComplex(elements) { - return ctx.aggregate(name, []Value{collection}, operator, false) + return ctx.aggregate(name, []Value{collection}, operator, aggregateNumber) } acc := complex(0, 0) if operator == ast.OpMul { diff --git a/internal/exec/runtime/library_functions_test.go b/internal/exec/runtime/library_functions_test.go index 55019e54db..48b834b1bb 100644 --- a/internal/exec/runtime/library_functions_test.go +++ b/internal/exec/runtime/library_functions_test.go @@ -36,6 +36,15 @@ func constReal(f float64) Value { return Value{Kind: ValConst, Const: semantics.Value{Kind: semantics.ValReal, Real: f}} } +// constRat is the exact Rational a decimal or numer/denom text denotes. +func constRat(text string) Value { + r, err := semantics.ParseRationalText(text, semantics.DefaultMaxIntegerBits) + if err != nil { + panic(err) + } + return Value{Kind: ValConst, Const: r} +} + // applyLibrary applies the function of that fully-qualified name to positional // arguments. func applyLibrary(t *testing.T, name string, args ...Value) (Value, error) { @@ -204,7 +213,7 @@ func TestLibraryFunctionErrors(t *testing.T) { func TestLibraryFunctionNamedArguments(t *testing.T) { fn, _ := libraryFunctionByName("TrigFunctions::sin") got, err := fn.invoke(libCtx(t), calcArgs{named: map[string]Value{"theta": constReal(0)}}) - if err != nil || got.Const.Real != 0 { + if err != nil || got.Const.AsReal() != 0 { t.Fatalf("sin(theta = 0.0) = %+v, %v", got, err) } @@ -218,7 +227,7 @@ func TestLibraryFunctionNamedArguments(t *testing.T) { func TestLibraryFunctionAtan2NamedArguments(t *testing.T) { fn, _ := libraryFunctionByName("OpenSysMLMathFunctions::atan2") got, err := fn.invoke(libCtx(t), calcArgs{named: map[string]Value{"x": constReal(-1), "y": constReal(1)}}) - if err != nil || got.Const.Real != 3*math.Pi/4 { + if err != nil || got.Const.AsReal() != 3*math.Pi/4 { t.Fatalf("atan2(x = -1.0, y = 1.0) = %+v, %v; want 3pi/4", got, err) } } @@ -342,7 +351,7 @@ func TestLibraryFunctionDispatchByResolvedSymbol(t *testing.T) { t.Fatalf("RealFunctions::sqrt did not dispatch to its built-in implementation") } got, err := ctx.InvokeCalc(sym, []Value{constReal(25)}, nil) - if err != nil || got.Const.Real != 5 { + if err != nil || got.Const.AsReal() != 5 { t.Fatalf("InvokeCalc(sqrt, 25.0) = %+v, %v", got, err) } } @@ -391,13 +400,13 @@ package mine { t.Fatalf("a declaration with a body dispatched to the built-in implementation") } got, err := ctx.InvokeCalc(libSym, []Value{constReal(25)}, nil) - if err != nil || got.Const.Real != 25 { + if err != nil || got.Const.AsReal() != 25 { t.Fatalf("InvokeCalc(RealFunctions::sqrt, 25.0) = %+v, %v; want the declared body", got, err) } ownSym := lookupOne(t, idx, "mine::sqrt") got, err = ctx.InvokeCalc(ownSym, []Value{constReal(25)}, nil) - if err != nil || got.Const.Real != 42 { + if err != nil || got.Const.AsReal() != 42 { t.Fatalf("InvokeCalc(mine::sqrt, 25.0) = %+v, %v; want the declared body", got, err) } } @@ -414,7 +423,7 @@ func TestLibraryFunctionDoesNotHijackAnOutputAssignedInABody(t *testing.T) { t.Fatalf("a declaration assigning its output dispatched to the built-in implementation") } got, err := ctx.InvokeCalc(sym, []Value{constReal(25)}, nil) - if err != nil || got.Const.Real != 42 { + if err != nil || got.Const.AsReal() != 42 { t.Fatalf("InvokeCalc(RealFunctions::sqrt, 25.0) = %+v, %v; want the declared body", got, err) } } @@ -432,7 +441,7 @@ func TestLibraryFunctionDispatchByLibraryDeclaration(t *testing.T) { t.Fatalf("the library RealFunctions::sqrt did not dispatch to its built-in implementation") } got, err := ctx.InvokeCalc(fnSym, []Value{constReal(25)}, nil) - if err != nil || got.Const.Real != 5 { + if err != nil || got.Const.AsReal() != 5 { t.Fatalf("InvokeCalc(RealFunctions::sqrt, 25.0) = %+v, %v; want the built-in", got, err) } attrSym := lookupOne(t, idx, "RealFunctions::tolerance") @@ -616,7 +625,7 @@ func TestVectorNormAndAngleOfLargeComponents(t *testing.T) { if err != nil { t.Fatalf("norm = error %v", err) } - if got := norm.Const.Real; math.Abs(got-5e200) > 1e185 { + if got := norm.Const.AsReal(); math.Abs(got-5e200) > 1e185 { t.Fatalf("norm = %g, want 5e200", got) } cases := []struct { @@ -639,7 +648,7 @@ func TestVectorNormAndAngleOfLargeComponents(t *testing.T) { t.Fatalf("angle = error %v", err) } // The arc cosine magnifies a rounding of the cosine near ±1 to ~1e-8. - if got.Kind != ValConst || math.Abs(got.Const.Real-tc.want) > 1e-7 { + if got.Kind != ValConst || math.Abs(got.Const.AsReal()-tc.want) > 1e-7 { t.Fatalf("angle = %s, want %g", FormatValue(got), tc.want) } }) @@ -782,7 +791,7 @@ func TestComplexFunctionsRejectNumericPairs(t *testing.T) { } // A one-element collection is the scalar it holds, here a Real on the real axis. got, err := applyLibrary(t, "ComplexFunctions::re", realVec(1)) - if err != nil || got.Kind != ValConst || !got.Const.IsNumeric() || got.Const.Real != 1 { + if err != nil || got.Kind != ValConst || !got.Const.IsNumeric() || got.Const.AsReal() != 1 { t.Errorf("re((1.0)) = (%v, %v), want 1.0", got, err) } } @@ -791,15 +800,15 @@ func TestComplexFunctionsRejectNumericPairs(t *testing.T) { // which is what their library bodies read. func TestTrigDegreesAndRadians(t *testing.T) { got, err := applyLibrary(t, "TrigFunctions::deg", constReal(math.Pi)) - if err != nil || got.Const.Real != 180 { + if err != nil || got.Const.AsReal() != 180 { t.Fatalf("deg(pi) = %+v, %v; want 180.0", got, err) } got, err = applyLibrary(t, "TrigFunctions::rad", constReal(180)) - if err != nil || got.Const.Real != math.Pi { + if err != nil || got.Const.AsReal() != math.Pi { t.Fatalf("rad(180.0) = %+v, %v; want pi", got, err) } got, err = applyLibrary(t, "TrigFunctions::rad", constInt(0)) - if err != nil || got.Const.Real != 0 { + if err != nil || got.Const.AsReal() != 0 { t.Fatalf("rad(0) = %+v, %v; want 0.0", got, err) } } @@ -1402,7 +1411,7 @@ func TestLibraryFeatureNameReadFromItsLibraryDeclaration(t *testing.T) { if err != nil { t.Fatalf("2 * TrigFunctions::pi = error %v", err) } - if got.Kind != ValConst || got.Const.Real != 2*math.Pi { + if got.Kind != ValConst || got.Const.AsReal() != 2*math.Pi { t.Fatalf("2 * TrigFunctions::pi = %+v, want %v", got, 2*math.Pi) } } @@ -1431,7 +1440,7 @@ func TestLibraryFeatureValue(t *testing.T) { if err != nil || !ok { t.Fatalf("libraryFeatureValue(TrigFunctions::pi) = %+v, %v, %v", got, ok, err) } - if got.Kind != ValConst || got.Const.Real != math.Pi { + if got.Kind != ValConst || got.Const.AsReal() != math.Pi { t.Fatalf("TrigFunctions::pi = %+v, want %v", got, math.Pi) } } @@ -1444,7 +1453,7 @@ func TestLibraryFeatureValueOverridesADeclaredLibraryValue(t *testing.T) { }`) got, ok, err := ctx.libraryFeatureValue(lookupOne(t, idx, "TrigFunctions::pi")) - if err != nil || !ok || got.Const.Real != math.Pi { + if err != nil || !ok || got.Const.AsReal() != math.Pi { t.Fatalf("TrigFunctions::pi = %+v, %v, %v; want pi", got, ok, err) } } @@ -1543,7 +1552,7 @@ func TestLibraryFunctionAnswersALibraryDeclarationWithABody(t *testing.T) { t.Fatalf("the library's own deg did not dispatch to its built-in implementation") } got, err := ctx.InvokeCalc(sym, []Value{constReal(math.Pi)}, nil) - if err != nil || got.Const.Real != 180 { + if err != nil || got.Const.AsReal() != 180 { t.Fatalf("InvokeCalc(TrigFunctions::deg, pi) = %+v, %v; want 180.0", got, err) } } diff --git a/internal/exec/runtime/library_operators.go b/internal/exec/runtime/library_operators.go index 7f1f97f0ae..42980c1489 100644 --- a/internal/exec/runtime/library_operators.go +++ b/internal/exec/runtime/library_operators.go @@ -2,6 +2,7 @@ package runtime import ( "fmt" + "math" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -206,10 +207,10 @@ func naturalDivision(name string, ctx *Context, args []Value) (Value, error) { return Value{}, fmt.Errorf("%w: function %s: %s / 0", ErrDivisionByZero, name, x.FormatInt()) } if rem.IntSign() != 0 { - q, _ := semantics.IntQuotient(x, y) + q, _ := semantics.RatArith(ast.OpDiv, x, y, math.MaxInt64) return Value{}, fmt.Errorf( "%w: function %s has no Natural result for %s / %s; the quotient is %s", - semantics.ErrArithmeticDomain, name, x.FormatInt(), y.FormatInt(), semantics.FormatReal(q), + semantics.ErrArithmeticDomain, name, x.FormatInt(), y.FormatInt(), q.FormatRational(), ) } q, _ := semantics.IntDivTrunc(x, y) @@ -402,11 +403,17 @@ func realOperand(_ *Context, name, param string, val Value) (Value, error) { return Value{}, operandMismatch(name, param, "a Real", val) } -// rationalOperand admits an Integer or a Real as a Rational; an Integer keeps -// its kind, as RationalFunctions::abs/max/min keep it. +// rationalOperand binds an Integer, a Rational or a finite Real to a Rational +// parameter as the Rational it equals, so the operator answers the exact +// Rational RationalFunctions declares. func rationalOperand(_ *Context, name, param string, val Value) (Value, error) { - if val.Kind == ValConst && val.Const.IsNumeric() { - return val, nil + if val.Kind == ValConst && val.Const.IsExact() { + return Value{Kind: ValConst, Const: semantics.RatOf(val.Const)}, nil + } + if val.Kind == ValConst && val.Const.Kind == semantics.ValReal { + if r, ok := semantics.RationalOfReal(val.Const.Real); ok { + return Value{Kind: ValConst, Const: r}, nil + } } return Value{}, operandMismatch(name, param, "a Rational", val) } diff --git a/internal/exec/runtime/library_operators_test.go b/internal/exec/runtime/library_operators_test.go index e77da96a3b..e3a0fed5ca 100644 --- a/internal/exec/runtime/library_operators_test.go +++ b/internal/exec/runtime/library_operators_test.go @@ -20,7 +20,7 @@ func TestOperatorFunctionValues(t *testing.T) { {"IntegerFunctions::-", []Value{constInt(5)}, constInt(-5)}, {"IntegerFunctions::-", []Value{constInt(5), constInt(7)}, constInt(-2)}, {"IntegerFunctions::*", []Value{constInt(3), constInt(4)}, constInt(12)}, - {"IntegerFunctions::/", []Value{constInt(1), constInt(4)}, constReal(0.25)}, + {"IntegerFunctions::/", []Value{constInt(1), constInt(4)}, constRat("0.25")}, {"IntegerFunctions::%", []Value{constInt(7), constInt(3)}, constInt(1)}, {"IntegerFunctions::**", []Value{constInt(2), constInt(10)}, constInt(1024)}, {"IntegerFunctions::^", []Value{constInt(2), constInt(0)}, constInt(1)}, @@ -58,11 +58,11 @@ func TestOperatorFunctionValues(t *testing.T) { {"RealFunctions::<", []Value{constInt(1), constInt(2)}, constBool(true)}, {"RealFunctions::==", []Value{constInt(2), constInt(2)}, constBool(true)}, {"RealFunctions::-", []Value{constReal(1.5)}, constReal(-1.5)}, - {"RationalFunctions::+", []Value{constInt(1), constInt(2)}, constInt(3)}, - {"RationalFunctions::/", []Value{constInt(1), constInt(4)}, constReal(0.25)}, + {"RationalFunctions::+", []Value{constInt(1), constInt(2)}, constRat("3")}, + {"RationalFunctions::/", []Value{constInt(1), constInt(4)}, constRat("0.25")}, {"RealFunctions::**", []Value{constReal(2), constReal(-1)}, constReal(0.5)}, {"RealFunctions::<=", []Value{constReal(2), constInt(2)}, constBool(true)}, - {"RationalFunctions::*", []Value{constReal(0.5), constReal(0.5)}, constReal(0.25)}, + {"RationalFunctions::*", []Value{constReal(0.5), constReal(0.5)}, constRat("0.25")}, {"RationalFunctions::>", []Value{constReal(0.5), constReal(0.25)}, constBool(true)}, {"NumericalFunctions::+", []Value{constInt(1), constInt(2)}, constInt(3)}, {"NumericalFunctions::%", []Value{constReal(7.5), constInt(2)}, constReal(1.5)}, diff --git a/internal/exec/runtime/library_shape_test.go b/internal/exec/runtime/library_shape_test.go index 8d8e1f2ca7..e36d00a1ec 100644 --- a/internal/exec/runtime/library_shape_test.go +++ b/internal/exec/runtime/library_shape_test.go @@ -457,7 +457,7 @@ func TestSubjectRedefinesEveryInheritedSubject(t *testing.T) { if err != nil { t.Fatalf("Both.%s: %v", name, err) } - if got.Kind != ValConst || got.Const.Real != 2.5 { + if got.Kind != ValConst || got.Const.AsReal() != 2.5 { t.Errorf("Both.%s = %s, want 2.5 through the one subject", name, FormatValue(got)) } } diff --git a/internal/exec/runtime/measurement_ref_test.go b/internal/exec/runtime/measurement_ref_test.go index 3f43c10f13..659dfef6ae 100644 --- a/internal/exec/runtime/measurement_ref_test.go +++ b/internal/exec/runtime/measurement_ref_test.go @@ -358,7 +358,7 @@ func TestMeasurementRefReport(t *testing.T) { {"m - s", ErrTypeMismatch, "operator '-' is not defined for a measurement reference and a measurement reference"}, {"m * 3", ErrTypeMismatch, "operator '*' is not defined for a measurement reference and an Integer"}, {"3 * m", ErrTypeMismatch, "operator '*' is not defined for an Integer and a measurement reference"}, - {"m / 2.0", ErrTypeMismatch, "operator '/' is not defined for a measurement reference and a Real"}, + {"m / 2.0", ErrTypeMismatch, "operator '/' is not defined for a measurement reference and a Rational"}, {"m ** s", ErrTypeMismatch, "operator '**' is not defined for a measurement reference and a measurement reference"}, {"-m", ErrTypeMismatch, "unary '-' requires numeric operand, got measurement reference"}, {"m < s", ErrTypeMismatch, "operator '<' is not defined for a measurement reference and a measurement reference; DataFunctions::'<' is abstract and no library function declares '<' for a measurement reference, which is no ScalarValue"}, diff --git a/internal/exec/runtime/metaobject.go b/internal/exec/runtime/metaobject.go index 28fcf5f389..899494ddc3 100644 --- a/internal/exec/runtime/metaobject.go +++ b/internal/exec/runtime/metaobject.go @@ -221,6 +221,9 @@ func (ec *EvalContext) valueOfFilterValue(fv symbols.FilterValue) (Value, error) return Value{Kind: ValConst, Const: semantics.FilterInteger(fv)}, nil case symbols.FilterValueReal: return realConst(fv.Real), nil + case symbols.FilterValueRational: + n, _ := semantics.FilterNumber(fv) + return Value{Kind: ValConst, Const: n}, nil case symbols.FilterValueString: return NewStringValue(fv.Str), nil case symbols.FilterValueEmpty: diff --git a/internal/exec/runtime/model.go b/internal/exec/runtime/model.go index 9db942e250..0e11997d94 100644 --- a/internal/exec/runtime/model.go +++ b/internal/exec/runtime/model.go @@ -104,7 +104,7 @@ type Model struct { // integerLiterals and realLiterals memoize the value each numeric literal // node spells, so a literal in a recursion is parsed once per model. integerLiterals map[*ast.LiteralInteger]semantics.Value - realLiterals map[*ast.LiteralReal]float64 + realLiterals map[*ast.LiteralReal]semantics.Value // census memoizes the object usages the model's namespaces declare; see modelUsages. census *usageCensus @@ -212,7 +212,7 @@ func NewModel(sem *semantics.Model, resolver *resolve.Resolver) *Model { libraryPerformances: make(map[*symbols.Symbol]*libraryPerformance), invocationTargets: make(map[invocationKey]*invocationTarget), integerLiterals: make(map[*ast.LiteralInteger]semantics.Value), - realLiterals: make(map[*ast.LiteralReal]float64), + realLiterals: make(map[*ast.LiteralReal]semantics.Value), behaving: make(map[*symbols.Symbol]bool), behavingFeatures: make(map[*symbols.Symbol][]int), redefGroups: make(map[*symbols.Symbol][][]string), diff --git a/internal/exec/runtime/modeled.go b/internal/exec/runtime/modeled.go index eb6ae6198d..fa425479e5 100644 --- a/internal/exec/runtime/modeled.go +++ b/internal/exec/runtime/modeled.go @@ -225,8 +225,11 @@ func (d DrawTaken) Describe() string { // formatDrawn spells a drawn number so it reads back as the kind it is: a Real // always carries a point or an exponent. func formatDrawn(v semantics.Value) string { - if v.Kind == semantics.ValInt { + switch v.Kind { + case semantics.ValInt: return v.FormatInt() + case semantics.ValRational: + return v.FormatRational() } s := strconv.FormatFloat(v.Real, 'g', -1, 64) if !strings.ContainsAny(s, ".eIN") { diff --git a/internal/exec/runtime/montecarlo_test.go b/internal/exec/runtime/montecarlo_test.go index a4e66339f3..f776164edf 100644 --- a/internal/exec/runtime/montecarlo_test.go +++ b/internal/exec/runtime/montecarlo_test.go @@ -260,7 +260,7 @@ func TestMonteCarloSampleTakesQuantitiesInTheFirstRunsUnit(t *testing.T) { if stats.Runs != 3 || stats.Mean != 5.0/3 || stats.Unit == nil || stats.Unit.Text != "SI::km" { t.Errorf("stats = %+v; want 3 runs with mean 5/3 in SI::km", stats) } - if got := stats.statistic(stats.Mean).Quantity(); got == nil || got.Num.Real != 5.0/3 || got.Unit.Text != "SI::km" { + if got := stats.statistic(stats.Mean).Quantity(); got == nil || got.Num.AsReal() != 5.0/3 || got.Unit.Text != "SI::km" { t.Errorf("Mean = %s; want 1.6666666666666667 [SI::km]", FormatValue(stats.statistic(stats.Mean))) } diff --git a/internal/exec/runtime/optional_feature_test.go b/internal/exec/runtime/optional_feature_test.go index a8d4d52157..0f1bc4afd2 100644 --- a/internal/exec/runtime/optional_feature_test.go +++ b/internal/exec/runtime/optional_feature_test.go @@ -493,7 +493,7 @@ func TestOptionalOccurrenceDeclarationIsUndeterminedUntilInstantiated(t *testing t.Errorf("%s = %s, %v; want the one object", src, FormatValue(val), err) } } - if val, err := evalIn(t, ctx, pkg.Scope, "fitted.radius"); err != nil || val.Kind != ValConst || val.Const.Real != 0.3 { + if val, err := evalIn(t, ctx, pkg.Scope, "fitted.radius"); err != nil || val.Kind != ValConst || val.Const.AsReal() != 0.3 { t.Errorf("fitted.radius = %s, %v; want 0.3", FormatValue(val), err) } car, err := ctx.Instantiate(oneSymbol(t, idx, "test::Car")) diff --git a/internal/exec/runtime/part_feature_chain_test.go b/internal/exec/runtime/part_feature_chain_test.go index c0db982675..ebed371c24 100644 --- a/internal/exec/runtime/part_feature_chain_test.go +++ b/internal/exec/runtime/part_feature_chain_test.go @@ -67,7 +67,7 @@ func TestCalcUsageReadThroughPartChain(t *testing.T) { t.Errorf("%s: %v", tt.name, err) continue } - if got.Const.Real != tt.want { + if got.Const.AsReal() != tt.want { t.Errorf("%s = %s, want %v", tt.name, FormatTraceValue(got), tt.want) } } @@ -113,7 +113,7 @@ func TestPartChainReadsTheObjectInHand(t *testing.T) { if err != nil { t.Fatalf("Car.r: %v", err) } - if fv.Value.Const.Real != 5.0 { + if fv.Value.Const.AsReal() != 5.0 { t.Errorf("Car.r = %s, want 5 (the radius this car's wheel carries)", FormatTraceValue(fv.Value)) } } @@ -138,7 +138,7 @@ func TestPartChainThroughSeveralOccurrences(t *testing.T) { t.Fatalf("wheels.radius = %s, want the radius of each of the four wheels", FormatValue(got)) } for i, radius := range got.Sequence().Elements() { - if radius.Kind != ValConst || radius.Const.Real != 1.0 { + if radius.Kind != ValConst || radius.Const.AsReal() != 1.0 { t.Errorf("wheels.radius #%d = %s, want 1.0", i+1, FormatValue(radius)) } } diff --git a/internal/exec/runtime/quantity_functions.go b/internal/exec/runtime/quantity_functions.go index 771a67f98d..37419a8b2e 100644 --- a/internal/exec/runtime/quantity_functions.go +++ b/internal/exec/runtime/quantity_functions.go @@ -42,7 +42,7 @@ func registerQuantityCalculations() { registerValueFunction("QuantityCalculations::round", []string{"x"}, 1, quantityMagnitudeUnary(roundToInteger)) registerValueFunction("QuantityCalculations::ToString", []string{"x"}, 1, quantityToString) registerValueFunction("QuantityCalculations::ToInteger", []string{"x"}, 1, quantityToInteger) - registerValueFunction("QuantityCalculations::ToRational", []string{"x"}, 1, quantityToReal) + registerValueFunction("QuantityCalculations::ToRational", []string{"x"}, 1, quantityToRational) registerValueFunction("QuantityCalculations::ToReal", []string{"x"}, 1, quantityToReal) registerValueFunction("QuantityCalculations::ToDimensionOneValue", []string{"x"}, 1, toDimensionOneValue) registerValueFunction("QuantityCalculations::sum", []string{"collection"}, 0, quantityAggregate(ast.OpAdd)) @@ -182,13 +182,13 @@ func quantityArg(name, param string, val Value) (*Quantity, error) { // quantityPredicate is isZero or isUnit: whether the magnitude is the given // number, as the library bodies test `x.num`. -func quantityPredicate(magnitude float64) libraryApply { +func quantityPredicate(magnitude int64) libraryApply { return func(name string, _ *Context, args []Value) (Value, error) { q, err := quantityArg(name, "x", args[0]) if err != nil { return Value{}, err } - return boolValue(toReal(q.Num) == magnitude), nil + return boolValue(numberEquals(q.Num, magnitude)), nil } } @@ -335,8 +335,8 @@ func quantityToInteger(name string, _ *Context, args []Value) (Value, error) { if err != nil { return Value{}, err } - if x.Num.Kind == semantics.ValInt { - return Value{Kind: ValConst, Const: x.Num}, nil + if x.Num.IsExact() { + return Value{Kind: ValConst, Const: semantics.RatTrunc(x.Num)}, nil } num, err := integerResult(math.Trunc(x.Num.Real)) if err != nil { @@ -345,7 +345,17 @@ func quantityToInteger(name string, _ *Context, args []Value) (Value, error) { return Value{Kind: ValConst, Const: num}, nil } -// quantityToReal is ToReal and ToRational: the magnitude as a Real. +// quantityToRational is QuantityCalculations::ToRational: an exact magnitude +// itself, a Real one the Rational it exactly is. +func quantityToRational(name string, ctx *Context, args []Value) (Value, error) { + x, err := quantityArg(name, "x", args[0]) + if err != nil { + return Value{}, err + } + return realToRational(name, ctx, []Value{{Kind: ValConst, Const: x.Num}}) +} + +// quantityToReal is ToReal: the magnitude as a Real. func quantityToReal(name string, _ *Context, args []Value) (Value, error) { x, err := quantityArg(name, "x", args[0]) if err != nil { @@ -368,7 +378,7 @@ func toDimensionOneValue(name string, _ *Context, args []Value) (Value, error) { // unit; an empty collection has no unit, so it is the dimensionless 0 or 1. func quantityAggregate(op ast.OperatorKind) libraryApply { return func(name string, ctx *Context, args []Value) (Value, error) { - return ctx.aggregate(name, args, op, false) + return ctx.aggregate(name, args, op, aggregateNumber) } } diff --git a/internal/exec/runtime/quantity_functions_test.go b/internal/exec/runtime/quantity_functions_test.go index 8324945a89..069d0094c7 100644 --- a/internal/exec/runtime/quantity_functions_test.go +++ b/internal/exec/runtime/quantity_functions_test.go @@ -370,7 +370,7 @@ func TestTrigFunctionsTakeAngles(t *testing.T) { if err != nil { t.Fatalf("%s: %v", tc.src, err) } - if got.Kind != ValConst || got.Const.Kind != semantics.ValReal || math.Abs(got.Const.Real-tc.want) > 1e-12 { + if got.Kind != ValConst || got.Const.Kind != semantics.ValReal || math.Abs(got.Const.AsReal()-tc.want) > 1e-12 { t.Errorf("%s = %s, want %v", tc.src, FormatValue(got), tc.want) } }) @@ -523,7 +523,7 @@ func TestQuantityPowerAndProductAgree(t *testing.T) { {"side * side", "side ** 2", "9 [SI::'m²']"}, {"side * side / side", "side ** 2 / side", "3.0 [SI::m]"}, {"2.5 [m] * 2.5 [m]", "2.5 [m] ** 2", "6.25 [SI::'m²']"}, - {"2 [km/h] * 2 [km/h]", "2 [km/h] ** 2", "0.30864197530864196 [SI::'m²⋅s⁻²']"}, + {"2 [km/h] * 2 [km/h]", "2 [km/h] ** 2", "25/81 [SI::'m²⋅s⁻²']"}, } for _, tc := range cases { t.Run(tc.power, func(t *testing.T) { diff --git a/internal/exec/runtime/quantity_test.go b/internal/exec/runtime/quantity_test.go index ef5a24a137..c994a0804f 100644 --- a/internal/exec/runtime/quantity_test.go +++ b/internal/exec/runtime/quantity_test.go @@ -74,20 +74,24 @@ func TestQuantityEvaluation(t *testing.T) { } } -// TestQuantityArithmeticReportsOverflow: a magnitude no Real holds is reported -// for a quantity as it is for a bare Real, rather than carried as an infinity. +// TestQuantityArithmeticReportsOverflow: a Real magnitude no Real holds is reported +// for a quantity as it is for a bare Real, rather than carried as an infinity, +// while exact magnitudes stay exact. 1.0 ** 0.5 is the Real 1.0. func TestQuantityArithmeticReportsOverflow(t *testing.T) { ctx, scope := quantityContext(t) - for _, src := range []string{ - "1e308 [m] + 1e308 [m]", - "1e200 [m] * 1e200 [s]", - "1e308 [m] / 1e-308 [s]", - "1e308 [m] / 1e-308 [m]", + for src, exact := range map[string]string{ + "1e308 [m] + 1e308 [m]": "2e+308 [m]", + "1e200 [m] * 1e200 [s]": "1e+400 [m*s]", + "1e308 [m] / 1e-308 [m]": "1e+616", } { got, err := evalIn(t, ctx, scope, src) - if !errors.Is(err, semantics.ErrArithmeticOverflow) { - t.Errorf("%s = %+v, %v; want ErrArithmeticOverflow", src, got, err) + if err != nil || FormatValue(got) != exact { + t.Errorf("%s = %s, %v; want %s", src, FormatValue(got), err, exact) + } + real := "(1.0 ** 0.5) * " + src + if got, err := evalIn(t, ctx, scope, real); !errors.Is(err, semantics.ErrArithmeticOverflow) { + t.Errorf("%s = %+v, %v; want ErrArithmeticOverflow", real, got, err) } } } @@ -209,9 +213,9 @@ func TestQuantityExponentiation(t *testing.T) { wantKind semantics.ValueKind }{ {"(2 [m]) ** 3", "8 [SI::'m³']", semantics.ValInt}, - {"(2.0 [m]) ** 3", "8.0 [SI::'m³']", semantics.ValReal}, + {"(2.0 [m]) ** 3", "8.0 [SI::'m³']", semantics.ValRational}, {"(3.0 [m/s]) ** 2.0", "9.0 [SI::'m²⋅s⁻²']", semantics.ValReal}, - {"(2.0 [m]) ** -1", "0.5 [SI::'m⁻¹']", semantics.ValReal}, + {"(2.0 [m]) ** -1", "0.5 [SI::'m⁻¹']", semantics.ValRational}, } for _, tc := range cases { t.Run(tc.src, func(t *testing.T) { @@ -305,7 +309,7 @@ func TestComposedUnitCanonical(t *testing.T) { {"1 [N] * 2 [m]", "2 [SI::'kg⋅m²⋅s⁻²']"}, {"2 [N*m]", "2 [N*m]"}, {"1 [N*m] * 2 [m]", "2 [kg*m**3/s**2]"}, - {"36 [km/h] / 2 [h]", "0.001388888888888889 [SI::'m⋅s⁻²']"}, + {"36 [km/h] / 2 [h]", "1/720 [SI::'m⋅s⁻²']"}, {"1 [m/s] * 1 [kg/s]", "1 [SI::N]"}, {"1 [m/s] / 1 [kg/s]", "1.0 [m/kg]"}, {"6 [m] / 2 [s] / 3 [kg]", "1.0 [m/(kg*s)]"}, @@ -439,8 +443,8 @@ func TestSequenceIndexIsNotAQuantity(t *testing.T) { if err != nil { t.Fatalf("speeds#(2): %v", err) } - if got.Kind != ValConst || got.Const.Kind != semantics.ValReal || got.Const.Real != 2.0 { - t.Errorf("speeds#(2) = %v, want the real 2.0", got) + if got.Kind != ValConst || got.Const.Kind != semantics.ValRational || got.Const.FormatRational() != "2.0" { + t.Errorf("speeds#(2) = %v, want the Rational 2.0", got) } if _, err := evalIn(t, ctx, scope, "speeds#(4)"); !errors.Is(err, ErrIndexOutOfRange) { diff --git a/internal/exec/runtime/robustness_performed_action_inputs_test.go b/internal/exec/runtime/robustness_performed_action_inputs_test.go index c05d4071c7..4bf7cfb39c 100644 --- a/internal/exec/runtime/robustness_performed_action_inputs_test.go +++ b/internal/exec/runtime/robustness_performed_action_inputs_test.go @@ -89,7 +89,7 @@ func testPerformedActionUnboundInput(t *testing.T, mult string, required bool) { if err != nil { t.Fatalf("read cycleTime: %v", err) } - if got := fv.HeldValue(); got.Kind != ValQuantity || got.Quantity() == nil || got.Quantity().Num.Real != 200.0 { + if got := fv.HeldValue(); got.Kind != ValQuantity || got.Quantity() == nil || got.Quantity().Num.AsReal() != 200.0 { t.Fatalf("cycleTime = %v, want 200.0 [s]", FormatValue(got)) } behavior, ok := inst.Behavior("toastBread") @@ -130,7 +130,7 @@ func testPerformedUsageUnboundInput(t *testing.T, mult string, required bool) { if err != nil { t.Fatalf("read cycleTime: %v", err) } - if got := fv.HeldValue(); got.Kind != ValQuantity || got.Quantity() == nil || got.Quantity().Num.Real != 200.0 { + if got := fv.HeldValue(); got.Kind != ValQuantity || got.Quantity() == nil || got.Quantity().Num.AsReal() != 200.0 { t.Fatalf("cycleTime = %v, want 200.0 [s]", FormatValue(got)) } behavior, ok := inst.Behavior("applyHeat") diff --git a/internal/exec/runtime/robustness_test.go b/internal/exec/runtime/robustness_test.go index a81394359d..8082f31e26 100644 --- a/internal/exec/runtime/robustness_test.go +++ b/internal/exec/runtime/robustness_test.go @@ -2441,7 +2441,7 @@ func testNamedLibraryCallThatHasNoValue(t *testing.T) { {`RealFunctions::ToReal("NaN")`, ErrInvalidNotation}, {`RealFunctions::ToReal(" 1.5 ")`, ErrInvalidNotation}, {`IntegerFunctions::ToInteger(" 7")`, ErrInvalidNotation}, - {`RationalFunctions::ToRational("1/3")`, ErrInvalidNotation}, + {`RationalFunctions::ToRational("1/x")`, ErrInvalidNotation}, {`IntegerFunctions::ToInteger("2.0")`, ErrInvalidNotation}, {`BooleanFunctions::ToBoolean("yes")`, ErrInvalidNotation}, {`IntegerFunctions::ToNatural(-1)`, semantics.ErrArithmeticDomain}, @@ -2734,17 +2734,17 @@ func testStructuredValueOutsideTheDeclaredShape(t *testing.T) { "TwoAsThreeVector": "it declares dimension = 3", "TwoAsIntThree": "it declares dimension = 3", "ThreeAsFixed2": "it declares dimension = 2", - "RealsAsIntVec": "it declares elements : Integer, got element 1.5 (a Real)", - "RealsAsIntThree": "it declares elements : Integer, got element 1.5 (a Real)", + "RealsAsIntVec": "it declares elements : Integer, got element 1.5 (a Rational)", + "RealsAsIntThree": "it declares elements : Integer, got element 1.5 (a Rational)", "VectorAsGrid": "cannot write ⟨1, 2, 3, 4⟩ (vector) to a feature typed by Grid", "VectorAsString": "cannot write ⟨1, 2⟩ (vector) to a feature typed by String", "WideAsGrid": "it declares dimensions = [2, 2]", "SquareAsRow4": "it declares rank = 1", "SquareAsOneDim": "it declares dimension : Positive[0..1], got 2 dimension(s)", - "RealsAsIntArray": "it declares elements : Integer, got element 1.5 (a Real)", + "RealsAsIntArray": "it declares elements : Integer, got element 1.5 (a Rational)", "RealsAsFour": "it declares elements : Integer[4], got 2 element(s)", "TwoAsVel3": "it declares num : Real[3], got 2 element(s)", - "RealsAsIntVQ": "it declares num : Integer, got element 1.5 (a Real)", + "RealsAsIntVQ": "it declares num : Integer, got element 1.5 (a Rational)", "TwoAsScalar": "to a feature typed by ScalarQuantityValue: it is a ScalarValue, which holds one scalar", "TwoAsLength": "to a feature typed by LengthValue: it is a ScalarValue, which holds one scalar", } { @@ -2818,15 +2818,21 @@ func testBodyByReferenceThatCannotBeApplied(t *testing.T) { } } -// testRealLiteralThatUnderflows: a nonzero Real literal too small for a Real is -// reported rather than read as zero. +// testRealLiteralThatUnderflows: a decimal literal beyond the binary64 range is the +// exact Rational, one beyond the size budget is reported, and a Real too small for +// a binary64 is reported rather than read as zero. func testRealLiteralThatUnderflows(t *testing.T) { + for _, src := range []string{`1.0e-400`, `1.0e400`} { + got, err := evalCollectionExpr(t, src) + if err != nil || got.Kind != ValConst || got.Const.Kind != semantics.ValRational || got.Const.RatSign() != 1 { + t.Errorf("%s = (%v, %v), want the exact Rational", src, got, err) + } + } for _, tt := range []struct { expr string want error }{ - {`1.0e-400`, semantics.ErrArithmeticOverflow}, - {`1.0e400`, semantics.ErrArithmeticOverflow}, + {`1.0e-400000`, semantics.ErrRationalSizeLimit}, {`RealFunctions::ToReal("1e-400")`, semantics.ErrArithmeticOverflow}, } { got, err := evalCollectionExpr(t, tt.expr) @@ -2835,8 +2841,8 @@ func testRealLiteralThatUnderflows(t *testing.T) { } } got, err := evalCollectionExpr(t, `0.0e-400`) - if err != nil || got.Kind != ValConst || got.Const.Kind != semantics.ValReal || got.Const.Real != 0 { - t.Errorf("0.0e-400 = (%v, %v), want the Real 0", got, err) + if err != nil || got.Kind != ValConst || got.Const.Kind != semantics.ValRational || got.Const.RatSign() != 0 { + t.Errorf("0.0e-400 = (%v, %v), want the Rational 0", got, err) } } @@ -12191,7 +12197,7 @@ func testVariantOutsideAVariation(t *testing.T) { if err != nil { t.Fatalf("widget.misplaced: %v", err) } - if fv.Value.Kind != ValConst || fv.Value.Const.Real != 1.0 { + if fv.Value.Kind != ValConst || fv.Value.Const.AsReal() != 1.0 { t.Errorf("widget.misplaced = %v, want 1", fv.Value) } } @@ -12234,7 +12240,7 @@ func testVariantUnderARedefinedVariation(t *testing.T) { if err != nil { t.Fatalf("sedan.engine.power: %v", err) } - if power.Value.Kind != ValConst || power.Value.Const.Real != 150.0 { + if power.Value.Kind != ValConst || power.Value.Const.AsReal() != 150.0 { t.Errorf("sedan.engine.power = %v, want 150", power.Value) } } @@ -12279,7 +12285,7 @@ func testDeepSpecializationChainOfRedefinitions(t *testing.T) { if err != nil { t.Fatalf("t = %v", err) } - if got.Kind != ValConst || got.Const.Real != 7.0 { + if got.Kind != ValConst || got.Const.AsReal() != 7.0 { t.Errorf("t = %+v, want the base's 7.0", got) } } @@ -12300,7 +12306,7 @@ func testConflictingRedefinitionsAtSeveralLevels(t *testing.T) { if err != nil { t.Fatalf("t = %v", err) } - if got.Kind != ValConst || got.Const.Real != 321.0 { + if got.Kind != ValConst || got.Const.AsReal() != 321.0 { t.Errorf("t = %+v, want 321.0 (innermost c, middle b, base a)", got) } } @@ -13070,7 +13076,7 @@ func testWriteOfAWrongTypedValueLeavesTheFeature(t *testing.T) { if err != nil { t.Fatalf("read reading after the rejected write: %v", err) } - if got := fv.HeldValue(); got.Kind != ValConst || got.Const.Real != 0.5 { + if got := fv.HeldValue(); got.Kind != ValConst || got.Const.AsReal() != 0.5 { t.Errorf("reading = %v, want the 0.5 it held before the rejected write", FormatValue(got)) } } @@ -13229,7 +13235,7 @@ func testWriteOfNoValueWhereOneIsRequired(t *testing.T) { if err != nil { t.Fatalf("read reading after the rejected write: %v", err) } - if got := fv.HeldValue(); got.Kind != ValConst || got.Const.Real != 0.5 { + if got := fv.HeldValue(); got.Kind != ValConst || got.Const.AsReal() != 0.5 { t.Errorf("reading = %v, want the 0.5 it held before the rejected write", FormatValue(got)) } } diff --git a/internal/exec/runtime/run_state_test.go b/internal/exec/runtime/run_state_test.go index 77b6809e63..1bb3464ca0 100644 --- a/internal/exec/runtime/run_state_test.go +++ b/internal/exec/runtime/run_state_test.go @@ -285,7 +285,7 @@ func TestPausedActivationKeepsItsCalcUsageEvaluations(t *testing.T) { if err := exec.RunToCompletion(); err != nil { t.Fatalf("resume: %v", err) } - if total := exec.Results()["total"]; total.Const.Real != 13.0 { + if total := exec.Results()["total"]; total.Const.AsReal() != 13.0 { t.Errorf("total = %v, want 13", total) } if got := strings.Count(trace.String(), "enter calc test::outer::t"); got != 1 { diff --git a/internal/exec/runtime/runtime_integration_test.go b/internal/exec/runtime/runtime_integration_test.go index 81b01b82f9..da22de54b1 100644 --- a/internal/exec/runtime/runtime_integration_test.go +++ b/internal/exec/runtime/runtime_integration_test.go @@ -26,7 +26,7 @@ func TestIntegration_ParseAndInstantiate(t *testing.T) { t.Fatalf("GetFeatureValue failed: %v", err) } - if fv.Value.Const.Real != 0.5 { - t.Errorf("expected diameter=0.5, got %v", fv.Value.Const.Real) + if fv.Value.Const.AsReal() != 0.5 { + t.Errorf("expected diameter=0.5, got %v", fv.Value.Const.AsReal()) } } diff --git a/internal/exec/runtime/satisfy_test.go b/internal/exec/runtime/satisfy_test.go index 79e338dc11..171a4a4c61 100644 --- a/internal/exec/runtime/satisfy_test.go +++ b/internal/exec/runtime/satisfy_test.go @@ -212,7 +212,8 @@ func TestReadInSatisfactionReadsThroughTheSubject(t *testing.T) { if err != nil { t.Fatalf("ReadInSatisfaction: %v", err) } - if value.Kind != ValConst || value.Const.Kind != semantics.ValReal || value.Const.Real != 7.0 { + // Unresolved without the libraries, Real does not hold the literal as a binary64. + if value.Kind != ValConst || value.Const.Kind != semantics.ValRational || value.Const.FormatRational() != "7.0" { t.Fatalf("ReadInSatisfaction = %+v, want 7.0", value) } } diff --git a/internal/exec/runtime/set_order.go b/internal/exec/runtime/set_order.go index 6cb30f1498..2982fa504d 100644 --- a/internal/exec/runtime/set_order.go +++ b/internal/exec/runtime/set_order.go @@ -276,23 +276,23 @@ func canonicalClass(v Value) int { // compareNumbers orders the numeric constants exactly, infinity above every finite number. func compareNumbers(a, b semantics.Value) int { switch { - case a.Kind == semantics.ValInt && b.Kind == semantics.ValInt: - return semantics.CompareInt(a, b) - case a.Kind == semantics.ValInt && b.Kind == semantics.ValReal: + case a.IsExact() && b.IsExact(): + return semantics.CompareRat(a, b) + case a.IsExact() && b.Kind == semantics.ValReal: return compareIntReal(a, b.Real) - case a.Kind == semantics.ValReal && b.Kind == semantics.ValInt: + case a.Kind == semantics.ValReal && b.IsExact(): return -compareIntReal(b, a.Real) } return cmp.Compare(numberOf(a), numberOf(b)) } -// compareIntReal orders an Integer against a Real without rounding either, a -// NaN below every number as cmp.Compare puts it. +// compareIntReal orders an exact number against a Real without rounding either, +// a NaN below every number as cmp.Compare puts it. func compareIntReal(i semantics.Value, r float64) int { if math.IsNaN(r) { return cmp.Compare(0.0, r) } - return semantics.CompareIntReal(i, r) + return semantics.CompareExactReal(i, r) } func numberOf(v semantics.Value) float64 { diff --git a/internal/exec/runtime/signal_test.go b/internal/exec/runtime/signal_test.go index 98628f6ce5..62b6e05ebb 100644 --- a/internal/exec/runtime/signal_test.go +++ b/internal/exec/runtime/signal_test.go @@ -2351,7 +2351,7 @@ func testActionSendCallsLibraryTypedFeature(t *testing.T, decls string) { if err != nil { t.Fatalf("execute action: %v", err) } - if got := outputs["got"]; got.Const.Kind != semantics.ValReal || got.Const.Real != 4 { + if got := outputs["got"]; got.Const.Kind != semantics.ValReal || got.Const.AsReal() != 4 { t.Errorf("got = %+v, want 4.0", got) } if pending := ctx.PendingMessages(); len(pending) != 0 { diff --git a/internal/exec/runtime/stochastic_test.go b/internal/exec/runtime/stochastic_test.go index 473860cecc..88cdb8c762 100644 --- a/internal/exec/runtime/stochastic_test.go +++ b/internal/exec/runtime/stochastic_test.go @@ -222,14 +222,14 @@ func TestModelSeedIsIndependentOfTheScheduleSeed(t *testing.T) { if err != nil { t.Fatal(err) } - if a["d"].Const.Real != b["d"].Const.Real || a["d"].Const.Real != c["d"].Const.Real { + if a["d"].Const.AsReal() != b["d"].Const.AsReal() || a["d"].Const.AsReal() != c["d"].Const.AsReal() { t.Errorf("model seed 7 drew %v, %v, %v under three schedules", a["d"], b["d"], c["d"]) } _, d, err := runAction(t, m, "draw", "seed:7") if err != nil { t.Fatal(err) } - if d["d"].Const.Real != a["d"].Const.Real { + if d["d"].Const.AsReal() != a["d"].Const.AsReal() { t.Errorf("`seed:7` drew %v, model seed 7 %v; want one modeled stream per seed", d["d"], a["d"]) } } @@ -738,16 +738,16 @@ func TestRandomFunctionsDrawWithinTheirSupport(t *testing.T) { if err != nil { t.Fatal(err) } - if u := out["u"].Const.Real; u < 2 || u >= 3 { + if u := out["u"].Const.AsReal(); u < 2 || u >= 3 { t.Errorf("uniform(2.0, 3.0) drew %v", u) } - if x := out["tri"].Const.Real; x < 0 || x > 4 { + if x := out["tri"].Const.AsReal(); x < 0 || x > 4 { t.Errorf("triangular(0.0, 1.0, 4.0) drew %v", x) } - if x := out["flat"].Const.Real; x != 5 { + if x := out["flat"].Const.AsReal(); x != 5 { t.Errorf("normal(5.0, 0.0) drew %v, want the mean", x) } - if x := out["g"].Const.Real; math.IsNaN(x) || math.IsInf(x, 0) { + if x := out["g"].Const.AsReal(); math.IsNaN(x) || math.IsInf(x, 0) { t.Errorf("normal(0.0, 1.0) drew %v", x) } last = out @@ -757,7 +757,7 @@ func TestRandomFunctionsDrawWithinTheirSupport(t *testing.T) { t.Fatal(err) } for _, name := range []string{"u", "tri", "g"} { - if again[name].Const.Real != last[name].Const.Real { + if again[name].Const.AsReal() != last[name].Const.AsReal() { t.Errorf("seed 30 drew %s = %v then %v", name, last[name], again[name]) } } @@ -785,7 +785,7 @@ func TestNormalDrawsStayFiniteNearTheLargestReal(t *testing.T) { if err != nil { t.Fatalf("seed %d: %v", seed, err) } - x := out["x"].Const.Real + x := out["x"].Const.AsReal() if math.IsInf(x, 0) || math.IsNaN(x) { t.Fatalf("seed %d drew normal(1.0e308, 1.0e308) = %v", seed, x) } @@ -799,8 +799,8 @@ func TestNormalDrawsStayFiniteNearTheLargestReal(t *testing.T) { if err != nil { t.Fatalf("seed %d: replay: %v\n%s", seed, err, w) } - if got["x"].Const.Real != x { - t.Errorf("seed %d: replay gave x = %v, want %v", seed, got["x"].Const.Real, x) + if got["x"].Const.AsReal() != x { + t.Errorf("seed %d: replay gave x = %v, want %v", seed, got["x"].Const.AsReal(), x) } } if overflowed == 0 { @@ -835,7 +835,7 @@ func TestWideBoundsStillBoundTheDraws(t *testing.T) { t.Fatalf("seed %d: %v", seed, err) } for name, bound := range bounds { - x := out[name].Const.Real + x := out[name].Const.AsReal() if math.IsInf(x, 0) || math.IsNaN(x) || x < bound[0] || x > bound[1] || (name == "u" && x == bound[1]) { t.Fatalf("seed %d drew %s = %v, outside its bounds %v", seed, name, x, bound) } @@ -854,8 +854,8 @@ func TestWideBoundsStillBoundTheDraws(t *testing.T) { t.Fatalf("seed %d: replay: %v\n%s", seed, err, w) } for name := range bounds { - if got[name].Const.Real != out[name].Const.Real { - t.Errorf("seed %d: replay gave %s = %v, want %v", seed, name, got[name].Const.Real, out[name].Const.Real) + if got[name].Const.AsReal() != out[name].Const.AsReal() { + t.Errorf("seed %d: replay gave %s = %v, want %v", seed, name, got[name].Const.AsReal(), out[name].Const.AsReal()) } } } diff --git a/internal/exec/runtime/subject_report_test.go b/internal/exec/runtime/subject_report_test.go index a940b3906c..777efa332d 100644 --- a/internal/exec/runtime/subject_report_test.go +++ b/internal/exec/runtime/subject_report_test.go @@ -102,7 +102,7 @@ func TestCheckReportsTheChosenSubject(t *testing.T) { if err != nil { t.Fatalf("value of the subject: %v", err) } - if fv == nil || fv.Value.Const.Real != 99.0 { + if fv == nil || fv.Value.Const.AsReal() != 99.0 { t.Errorf("subject's value = %v, want the redefined 99", fv) } } diff --git a/internal/exec/runtime/sweep_test.go b/internal/exec/runtime/sweep_test.go index 4ec9c9da42..cbac97ecdb 100644 --- a/internal/exec/runtime/sweep_test.go +++ b/internal/exec/runtime/sweep_test.go @@ -907,7 +907,7 @@ func TestSweepOverAQuantityParameterConvertsToTheFirstEndpointsUnit(t *testing.T } for i, m := range want { q := table.Rows[i].Bindings[0].Value.Quantity() - if q == nil || q.Num.Kind != semantics.ValReal || q.Num.Real != m || q.Unit.Text != "SI::km" { + if q == nil || q.Num.Kind != semantics.ValReal || q.Num.AsReal() != m || q.Unit.Text != "SI::km" { t.Errorf("row %d bound %s; want the Real %v [SI::km]", i, FormatValue(table.Rows[i].Bindings[0].Value), m) } if table.Rows[i].Err != nil { @@ -1341,7 +1341,7 @@ func TestSweepRealRangeStopsAtItsEnd(t *testing.T) { len(table.Rows), len(want)) } for i, value := range want { - if got := table.Rows[i].Bindings[0].Value.Const.Real; got != value { + if got := table.Rows[i].Bindings[0].Value.Const.AsReal(); got != value { t.Errorf("row %d ran a = %v; want %v", i, got, value) } } @@ -1359,7 +1359,7 @@ func TestSamplesOverTheWidestRealRangeStayInIt(t *testing.T) { Sampled: true, Samples: 32, Seed: 5, }) for i := range table.Rows { - drawn := table.Rows[i].Bindings[0].Value.Const.Real + drawn := table.Rows[i].Bindings[0].Value.Const.AsReal() if math.IsNaN(drawn) || math.IsInf(drawn, 0) { t.Fatalf("row %d drew %v; want a finite value", i, drawn) } @@ -1424,7 +1424,7 @@ func TestSweepRunsToTheOutermostRealEndpoints(t *testing.T) { t.Fatalf("the range took %d run(s); want %d", len(table.Rows), len(tc.want)) } for i, value := range tc.want { - if got := table.Rows[i].Bindings[0].Value.Const.Real; got != value { + if got := table.Rows[i].Bindings[0].Value.Const.AsReal(); got != value { t.Errorf("row %d ran a = %v; want %v", i, got, value) } } diff --git a/internal/exec/runtime/testdata/conformance/accept_binds_signal_occurrence.expected.json b/internal/exec/runtime/testdata/conformance/accept_binds_signal_occurrence.expected.json index 59e09ee5ac..fb13ec3f19 100644 --- a/internal/exec/runtime/testdata/conformance/accept_binds_signal_occurrence.expected.json +++ b/internal/exec/runtime/testdata/conformance/accept_binds_signal_occurrence.expected.json @@ -12,7 +12,7 @@ "finalState": "acknowledged", "stateVisits": ["standingBy", "logged", "acknowledged"], "slots": { - "frames": {"type": "Real", "value": 3.0}, + "frames": {"type": "Rational", "value": "3.0"}, "pinged": {"type": "Integer", "value": 1} } } diff --git a/internal/exec/runtime/testdata/conformance/action_block_flow_if_branch.expected.json b/internal/exec/runtime/testdata/conformance/action_block_flow_if_branch.expected.json index 1f85763e38..1294049be9 100644 --- a/internal/exec/runtime/testdata/conformance/action_block_flow_if_branch.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_block_flow_if_branch.expected.json @@ -2,7 +2,7 @@ "type": "action", "outputs": { "result": {"type": "Integer", "value": 8}, - "choose.Halve.halved": {"type": "Real", "value": 4.0}, + "choose.Halve.halved": {"type": "Rational", "value": "4.0"}, "untaken": {"type": "Integer", "value": 0} } } diff --git a/internal/exec/runtime/testdata/conformance/action_body_declarer_scope.expected.json b/internal/exec/runtime/testdata/conformance/action_body_declarer_scope.expected.json index 3b7552732a..aa25b42820 100644 --- a/internal/exec/runtime/testdata/conformance/action_body_declarer_scope.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_body_declarer_scope.expected.json @@ -1,6 +1,6 @@ { "type": "action", "outputs": { - "result": {"type": "Real", "value": 4.0} + "result": {"type": "Rational", "value": "4.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/action_body_local_calc_usage.expected.json b/internal/exec/runtime/testdata/conformance/action_body_local_calc_usage.expected.json index 119b24fdbf..d7a5885485 100644 --- a/internal/exec/runtime/testdata/conformance/action_body_local_calc_usage.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_body_local_calc_usage.expected.json @@ -1,9 +1,9 @@ { "type": "action", "outputs": { - "v": {"type": "Real", "value": 2.0}, - "doubled": {"type": "Real", "value": 4.0}, + "v": {"type": "Rational", "value": "2.0"}, + "doubled": {"type": "Rational", "value": "4.0"}, "i": {"type": "Integer", "value": 3}, - "acc": {"type": "Real", "value": 12.0} + "acc": {"type": "Rational", "value": "12.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/action_body_package_member.expected.json b/internal/exec/runtime/testdata/conformance/action_body_package_member.expected.json index 9e4949198e..e2a606d744 100644 --- a/internal/exec/runtime/testdata/conformance/action_body_package_member.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_body_package_member.expected.json @@ -2,6 +2,6 @@ "type": "action", "evaluate": "test::owner::byPkgRef", "outputs": { - "r": {"type": "Real", "value": 8.0} + "r": {"type": "Rational", "value": "8.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/action_body_shadows_enclosing_scope.expected.json b/internal/exec/runtime/testdata/conformance/action_body_shadows_enclosing_scope.expected.json index f603a66f9c..472016d800 100644 --- a/internal/exec/runtime/testdata/conformance/action_body_shadows_enclosing_scope.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_body_shadows_enclosing_scope.expected.json @@ -1,9 +1,9 @@ { "type": "action", "outputs": { - "h": {"type": "Real", "value": 1.0}, - "seen": {"type": "Real", "value": 1.0}, - "inBlock": {"type": "Real", "value": 50.0}, - "afterBlock": {"type": "Real", "value": 1.0} + "h": {"type": "Rational", "value": "1.0"}, + "seen": {"type": "Rational", "value": "1.0"}, + "inBlock": {"type": "Rational", "value": "50.0"}, + "afterBlock": {"type": "Rational", "value": "1.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/action_choice_chained_write_conflict.expected.json b/internal/exec/runtime/testdata/conformance/action_choice_chained_write_conflict.expected.json index e502e4fac7..110c4df65e 100644 --- a/internal/exec/runtime/testdata/conformance/action_choice_chained_write_conflict.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_choice_chained_write_conflict.expected.json @@ -3,8 +3,8 @@ "evaluate": "test::calibrate", "trace": true, "outcomes": [ - {"outputs": {"seen": {"type": "Real", "value": 1.5}}}, - {"outputs": {"seen": {"type": "Real", "value": 2.5}}} + {"outputs": {"seen": {"type": "Rational", "value": "1.5"}}}, + {"outputs": {"seen": {"type": "Rational", "value": "2.5"}}} ], "admissible": "Concurrent branches writing one feature: the value is open, the writes are not" } diff --git a/internal/exec/runtime/testdata/conformance/action_for_loop.expected.json b/internal/exec/runtime/testdata/conformance/action_for_loop.expected.json index 33b6c7c4e8..5f0972324e 100644 --- a/internal/exec/runtime/testdata/conformance/action_for_loop.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_for_loop.expected.json @@ -2,8 +2,8 @@ "type": "action", "outputs": { "total": { - "type": "Real", - "value": 6.5 + "type": "Rational", + "value": "6.5" }, "visited": { "type": "Integer", diff --git a/internal/exec/runtime/testdata/conformance/action_for_over_a_part_collection.expected.json b/internal/exec/runtime/testdata/conformance/action_for_over_a_part_collection.expected.json index 78a906be3f..009f338f79 100644 --- a/internal/exec/runtime/testdata/conformance/action_for_over_a_part_collection.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_for_over_a_part_collection.expected.json @@ -3,7 +3,7 @@ "evaluate": "test::p::a", "trace": true, "outputs": { - "total": {"type": "Real", "value": 6.5}, + "total": {"type": "Rational", "value": "6.5"}, "visited": {"type": "Integer", "value": 3} } } diff --git a/internal/exec/runtime/testdata/conformance/action_guard_reads_calc_usage.expected.json b/internal/exec/runtime/testdata/conformance/action_guard_reads_calc_usage.expected.json index 3131e503bd..6a5dba101e 100644 --- a/internal/exec/runtime/testdata/conformance/action_guard_reads_calc_usage.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_guard_reads_calc_usage.expected.json @@ -2,6 +2,6 @@ "type": "action", "outputs": { "i": {"type": "Integer", "value": 3}, - "log": {"type": "Real", "value": 12.0} + "log": {"type": "Rational", "value": "12.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/action_part_attributes_in_outcome.expected.json b/internal/exec/runtime/testdata/conformance/action_part_attributes_in_outcome.expected.json index 40aa4cd533..7dcdafc311 100644 --- a/internal/exec/runtime/testdata/conformance/action_part_attributes_in_outcome.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_part_attributes_in_outcome.expected.json @@ -2,7 +2,7 @@ "type": "action", "evaluate": "test::Cfg", "outputs": { - "target.total": {"type": "Real", "value": 2.5}, + "target.total": {"type": "Rational", "value": "2.5"}, "target.tries": {"type": "Integer", "value": 1} } } diff --git a/internal/exec/runtime/testdata/conformance/action_unbraced_body_edges_first.expected.json b/internal/exec/runtime/testdata/conformance/action_unbraced_body_edges_first.expected.json index e299ff64d0..f9f623fc7d 100644 --- a/internal/exec/runtime/testdata/conformance/action_unbraced_body_edges_first.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_unbraced_body_edges_first.expected.json @@ -1,6 +1,6 @@ { "type": "action", "outputs": { - "total": {"type": "Real", "value": 1.62} + "total": {"type": "Rational", "value": "1.62"} } } diff --git a/internal/exec/runtime/testdata/conformance/action_unbraced_body_successions.expected.json b/internal/exec/runtime/testdata/conformance/action_unbraced_body_successions.expected.json index e299ff64d0..f9f623fc7d 100644 --- a/internal/exec/runtime/testdata/conformance/action_unbraced_body_successions.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_unbraced_body_successions.expected.json @@ -1,6 +1,6 @@ { "type": "action", "outputs": { - "total": {"type": "Real", "value": 1.62} + "total": {"type": "Rational", "value": "1.62"} } } diff --git a/internal/exec/runtime/testdata/conformance/assign_chain_depth_two.expected.json b/internal/exec/runtime/testdata/conformance/assign_chain_depth_two.expected.json index 4c567ee044..31f2778406 100644 --- a/internal/exec/runtime/testdata/conformance/assign_chain_depth_two.expected.json +++ b/internal/exec/runtime/testdata/conformance/assign_chain_depth_two.expected.json @@ -3,6 +3,6 @@ "evaluate": "test::write", "trace": true, "outputs": { - "seen": {"type": "Real", "value": 4.5} + "seen": {"type": "Rational", "value": "4.5"} } } diff --git a/internal/exec/runtime/testdata/conformance/assign_chain_inherited_feature.expected.json b/internal/exec/runtime/testdata/conformance/assign_chain_inherited_feature.expected.json index ea9dcdf0e9..d6e5fbe734 100644 --- a/internal/exec/runtime/testdata/conformance/assign_chain_inherited_feature.expected.json +++ b/internal/exec/runtime/testdata/conformance/assign_chain_inherited_feature.expected.json @@ -2,6 +2,6 @@ "type": "action", "evaluate": "test::write", "outputs": { - "seen": {"type": "Real", "value": 6.5} + "seen": {"type": "Rational", "value": "6.5"} } } diff --git a/internal/exec/runtime/testdata/conformance/assign_chain_state_do_and_exit.expected.json b/internal/exec/runtime/testdata/conformance/assign_chain_state_do_and_exit.expected.json index 77a93482b2..2555ca16f5 100644 --- a/internal/exec/runtime/testdata/conformance/assign_chain_state_do_and_exit.expected.json +++ b/internal/exec/runtime/testdata/conformance/assign_chain_state_do_and_exit.expected.json @@ -6,8 +6,8 @@ "finalState": "settled", "stateVisits": ["go", "settled"], "slots": { - "s.reading": {"type": "Real", "value": 3.5}, - "s.peak": {"type": "Real", "value": 9.5} + "s.reading": {"type": "Rational", "value": "3.5"}, + "s.peak": {"type": "Rational", "value": "9.5"} } } ] diff --git a/internal/exec/runtime/testdata/conformance/assign_chain_state_entry_guard_reads.expected.json b/internal/exec/runtime/testdata/conformance/assign_chain_state_entry_guard_reads.expected.json index 62c0cafb48..74c5fdb483 100644 --- a/internal/exec/runtime/testdata/conformance/assign_chain_state_entry_guard_reads.expected.json +++ b/internal/exec/runtime/testdata/conformance/assign_chain_state_entry_guard_reads.expected.json @@ -5,7 +5,7 @@ { "finalState": "tripped", "stateVisits": ["go", "tripped"], - "slots": {"s.reading": {"type": "Real", "value": 4.5}} + "slots": {"s.reading": {"type": "Rational", "value": "4.5"}} } ] } diff --git a/internal/exec/runtime/testdata/conformance/assign_chain_through_port.expected.json b/internal/exec/runtime/testdata/conformance/assign_chain_through_port.expected.json index 8cf29ee947..4eafb3c8e3 100644 --- a/internal/exec/runtime/testdata/conformance/assign_chain_through_port.expected.json +++ b/internal/exec/runtime/testdata/conformance/assign_chain_through_port.expected.json @@ -5,7 +5,7 @@ { "finalState": "driving", "stateVisits": ["driving"], - "slots": {"link.level": {"type": "Real", "value": 1.5}} + "slots": {"link.level": {"type": "Rational", "value": "1.5"}} } ] } diff --git a/internal/exec/runtime/testdata/conformance/assign_chain_transition_effect.expected.json b/internal/exec/runtime/testdata/conformance/assign_chain_transition_effect.expected.json index 1b4e97fd62..c1b025ea00 100644 --- a/internal/exec/runtime/testdata/conformance/assign_chain_transition_effect.expected.json +++ b/internal/exec/runtime/testdata/conformance/assign_chain_transition_effect.expected.json @@ -5,7 +5,7 @@ { "finalState": "settled", "stateVisits": ["waiting", "settled"], - "slots": {"s.reading": {"type": "Real", "value": 2.5}} + "slots": {"s.reading": {"type": "Rational", "value": "2.5"}} } ] } diff --git a/internal/exec/runtime/testdata/conformance/assign_write_conforms_to_target_type.expected.json b/internal/exec/runtime/testdata/conformance/assign_write_conforms_to_target_type.expected.json index 53e92af6c4..3bd76fc78e 100644 --- a/internal/exec/runtime/testdata/conformance/assign_write_conforms_to_target_type.expected.json +++ b/internal/exec/runtime/testdata/conformance/assign_write_conforms_to_target_type.expected.json @@ -8,7 +8,7 @@ "slots": { "lead": {"type": "Instance"}, "reading": {"type": "Integer", "value": 4}, - "ratio": {"type": "Real", "value": 0.5} + "ratio": {"type": "Rational", "value": "0.5"} }, "identical": [ ["lead", "scout"] diff --git a/internal/exec/runtime/testdata/conformance/calc_cast_composed_operands.expected.json b/internal/exec/runtime/testdata/conformance/calc_cast_composed_operands.expected.json index fd54f899b9..eb7f602c28 100644 --- a/internal/exec/runtime/testdata/conformance/calc_cast_composed_operands.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_cast_composed_operands.expected.json @@ -7,7 +7,7 @@ "fractionalDecidedFirst": {"type": "Sequence", "elements": []}, "integralSubtractedLast": {"type": "Sequence", "elements": []}, "integralSubtractedFirst": {"type": "Sequence", "elements": []}, - "wholeRealKeptLast": {"type": "Real", "value": 5.0}, - "wholeRealKeptFirst": {"type": "Real", "value": 5.0} + "wholeRealKeptLast": {"type": "Rational", "value": "5.0"}, + "wholeRealKeptFirst": {"type": "Rational", "value": "5.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/calc_cast_sequence_elementwise.expected.json b/internal/exec/runtime/testdata/conformance/calc_cast_sequence_elementwise.expected.json index 7faa714b0a..278bb5dffc 100644 --- a/internal/exec/runtime/testdata/conformance/calc_cast_sequence_elementwise.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_cast_sequence_elementwise.expected.json @@ -9,9 +9,9 @@ ]}, "rationalsOfMixed": {"type": "Sequence", "elements": [ {"type": "Integer", "value": 1}, - {"type": "Real", "value": 2.5}, + {"type": "Rational", "value": "2.5"}, {"type": "Integer", "value": 3}, - {"type": "Real", "value": 4.0} + {"type": "Rational", "value": "4.0"} ]}, "noneConform": {"type": "Sequence", "elements": []}, "emptyStaysEmpty": {"type": "Sequence", "elements": []} diff --git a/internal/exec/runtime/testdata/conformance/calc_library_aggregation_identity.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_aggregation_identity.expected.json index f2586e59fc..c28cdb6979 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_aggregation_identity.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_aggregation_identity.expected.json @@ -4,9 +4,9 @@ "evaluate": "test::c", "outputs": { "total": {"type": "Integer", "value": 6}, - "realTotal": {"type": "Real", "value": 3.5}, + "realTotal": {"type": "Rational", "value": "3.5"}, "emptyTotal": {"type": "Integer", "value": 0}, - "emptyRealTotal": {"type": "Real", "value": 0.0}, + "emptyRealTotal": {"type": "Rational", "value": "0.0"}, "product": {"type": "Integer", "value": 24}, "emptyProduct": {"type": "Integer", "value": 1}, "unqualifiedTotal": {"type": "Integer", "value": 9} diff --git a/internal/exec/runtime/testdata/conformance/calc_library_conversions.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_conversions.expected.json index 9690dca832..2fe3713c4e 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_conversions.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_conversions.expected.json @@ -5,7 +5,7 @@ "outputs": { "realFromString": {"type": "Real", "value": 1.5}, "realFromExponent": {"type": "Real", "value": -2500.0}, - "rationalFromString": {"type": "Real", "value": 0.25}, + "rationalFromString": {"type": "Rational", "value": "0.25"}, "integerFromString": {"type": "Integer", "value": -7}, "naturalFromString": {"type": "Integer", "value": 42}, "booleanFromString": {"type": "Boolean", "value": true}, @@ -19,12 +19,13 @@ "stringFromNull": {"type": "String", "value": "null"}, "stringFromLiteral": {"type": "String", "value": "Color::red"}, "roundTrip": {"type": "Boolean", "value": true}, + "realTenthIsNotTenth": {"type": "Boolean", "value": false}, "naturalFromInteger": {"type": "Integer", "value": 9}, "integerFromReal": {"type": "Integer", "value": 2}, "integerFromNegativeReal": {"type": "Integer", "value": -2}, "integerFromRational": {"type": "Integer", "value": 0}, - "rationalFromReal": {"type": "Real", "value": 0.75}, - "rationalFromInteger": {"type": "Real", "value": 2.0}, + "rationalFromReal": {"type": "Rational", "value": "0.75"}, + "rationalFromInteger": {"type": "Rational", "value": "2.0"}, "rationalFromIntegerIsReal": {"type": "Boolean", "value": true}, "realPart": {"type": "Real", "value": 2.5}, "imaginaryPart": {"type": "Real", "value": 0.0}, diff --git a/internal/exec/runtime/testdata/conformance/calc_library_conversions.sysml b/internal/exec/runtime/testdata/conformance/calc_library_conversions.sysml index 791a4379a9..276695105c 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_conversions.sysml +++ b/internal/exec/runtime/testdata/conformance/calc_library_conversions.sysml @@ -21,7 +21,8 @@ package test { out stringFromAnything = BaseFunctions::ToString(3); out stringFromNull = BaseFunctions::ToString(null); out stringFromLiteral = BaseFunctions::ToString(Color::red); - out roundTrip = RealFunctions::ToReal(RealFunctions::ToString(0.1)) == 0.1; + out roundTrip = RealFunctions::ToReal(RealFunctions::ToString(RealFunctions::ToReal("0.1"))) == RealFunctions::ToReal("0.1"); + out realTenthIsNotTenth = RealFunctions::ToReal("0.1") == 0.1; out naturalFromInteger = IntegerFunctions::ToNatural(9); out integerFromReal = RealFunctions::ToInteger(2.7); out integerFromNegativeReal = RealFunctions::ToInteger(-2.7); diff --git a/internal/exec/runtime/testdata/conformance/calc_library_operator_call_forms.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_operator_call_forms.expected.json index 5cf794d678..92ccecbedb 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_operator_call_forms.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_operator_call_forms.expected.json @@ -9,9 +9,9 @@ "realSum": {"type": "Real", "value": 3.0}, "realNegated": {"type": "Real", "value": -5.0}, "realProduct": {"type": "Real", "value": 1.2089258196146292e24}, - "rationalSum": {"type": "Integer", "value": 3}, + "rationalSum": {"type": "Rational", "value": "3.0"}, "product": {"type": "Integer", "value": 12}, - "quotient": {"type": "Real", "value": 0.25}, + "quotient": {"type": "Rational", "value": "0.25"}, "naturalQuotient": {"type": "Integer", "value": 2}, "remainder": {"type": "Integer", "value": 1}, "power": {"type": "Integer", "value": 1024}, @@ -46,7 +46,7 @@ {"type": "Integer", "value": 3} ]}, "sameElements": {"type": "Boolean", "value": true}, - "largest": {"type": "Real", "value": 7.5}, + "largest": {"type": "Rational", "value": "7.5"}, "smallest": {"type": "String", "value": "apple"} } } diff --git a/internal/exec/runtime/testdata/conformance/calc_library_overload_by_argument_type.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_overload_by_argument_type.expected.json index 0661d9c212..5b8c054de2 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_overload_by_argument_type.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_overload_by_argument_type.expected.json @@ -4,9 +4,9 @@ "instantiate": "test::Probe", "slots": { "absInteger": {"type": "Integer", "value": 2}, - "absReal": {"type": "Real", "value": 2.5}, + "absReal": {"type": "Rational", "value": "2.5"}, "absComplex": {"type": "Real", "value": 5.0}, "zeroComplex": {"type": "Boolean", "value": true}, - "maxMixed": {"type": "Real", "value": 3.0} + "maxMixed": {"type": "Rational", "value": "3.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/calc_library_rational_denominator_beyond_int64.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_rational_denominator_beyond_int64.expected.json index a53e421283..8083d3028d 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_rational_denominator_beyond_int64.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_rational_denominator_beyond_int64.expected.json @@ -1,5 +1,5 @@ { "libraries": true, "type": "calc", - "result": {"type": "Integer", "value": "73786976294838206464"} + "result": {"type": "Integer", "value": "10000000000000000000000"} } diff --git a/internal/exec/runtime/testdata/conformance/calc_library_rational_denominator_beyond_int64.sysml b/internal/exec/runtime/testdata/conformance/calc_library_rational_denominator_beyond_int64.sysml index 775c01c425..4b4672a2ba 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_rational_denominator_beyond_int64.sysml +++ b/internal/exec/runtime/testdata/conformance/calc_library_rational_denominator_beyond_int64.sysml @@ -1,9 +1,9 @@ -// The binary64 nearest 0.0001 has denominator 2^66, an Integer beyond int64, -// so denom answers it exactly rather than a truncated or overflowing value. +// The Rational 10^-22 has denominator 10^22, an Integer beyond int64, so denom +// answers it exactly rather than a truncated or overflowing value. package test { private import ScalarValues::*; calc def tooFine { - return : Integer = RationalFunctions::denom(0.0001); + return : Integer = RationalFunctions::denom(0.0000000000000000000001); } } diff --git a/internal/exec/runtime/testdata/conformance/calc_library_rational_terms.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_rational_terms.expected.json index 1f06e35977..5be54ff1f5 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_rational_terms.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_rational_terms.expected.json @@ -3,14 +3,15 @@ "type": "calcUsage", "evaluate": "test::c", "outputs": { - "third": {"type": "Real", "value": 0.3333333333333333}, + "third": {"type": "Rational", "value": "1/3"}, "thirdIsTheQuotient": {"type": "Boolean", "value": true}, - "sixQuarters": {"type": "Real", "value": 1.5}, - "negative": {"type": "Real", "value": -0.75}, - "negativeDenominator": {"type": "Real", "value": -0.75}, - "zero": {"type": "Real", "value": 0.0}, - "whole": {"type": "Real", "value": 2.0}, + "sixQuarters": {"type": "Rational", "value": "1.5"}, + "negative": {"type": "Rational", "value": "-0.75"}, + "negativeDenominator": {"type": "Rational", "value": "-0.75"}, + "zero": {"type": "Rational", "value": "0.0"}, + "whole": {"type": "Rational", "value": "2.0"}, "tenth": {"type": "Boolean", "value": true}, + "binaryTenthIsNotTenth": {"type": "Boolean", "value": false}, "numerOfThreeQuarters": {"type": "Integer", "value": 3}, "denomOfThreeQuarters": {"type": "Integer", "value": 4}, "numerOfNegative": {"type": "Integer", "value": -3}, @@ -23,10 +24,10 @@ "denomOfInteger": {"type": "Integer", "value": 1}, "numerOfWholeRational": {"type": "Integer", "value": 2}, "denomOfWholeRational": {"type": "Integer", "value": 1}, - "numerOfTenth": {"type": "Integer", "value": 3602879701896397}, - "denomOfTenth": {"type": "Integer", "value": 36028797018963968}, - "numerOfThird": {"type": "Integer", "value": 6004799503160661}, - "denomOfThird": {"type": "Integer", "value": 18014398509481984}, + "numerOfTenth": {"type": "Integer", "value": 1}, + "denomOfTenth": {"type": "Integer", "value": 10}, + "numerOfThird": {"type": "Integer", "value": 1}, + "denomOfThird": {"type": "Integer", "value": 3}, "tenthRoundTrips": {"type": "Boolean", "value": true}, "thirdRoundTrips": {"type": "Boolean", "value": true}, "negativeRoundTrips": {"type": "Boolean", "value": true}, diff --git a/internal/exec/runtime/testdata/conformance/calc_library_rational_terms.sysml b/internal/exec/runtime/testdata/conformance/calc_library_rational_terms.sysml index b664cfe11c..5a622c0c5d 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_rational_terms.sysml +++ b/internal/exec/runtime/testdata/conformance/calc_library_rational_terms.sysml @@ -1,7 +1,7 @@ -// A Rational is a binary64 here (docs/project/exact-rational-evaluation.md): -// rat(n, d) is the quotient `/` computes, and numer/denom read the exact -// numerator and denominator of that binary64, so rat(numer(x), denom(x)) is x -// while numer(0.1) is the 16-digit numerator of the double nearest 0.1. +// A Rational is exact (KerML 1.0 §9.3.2.2.8, docs/project/exact-rational-evaluation.md): +// rat(n, d) is the quotient `/` computes, numer/denom read its lowest terms, so +// rat(numer(x), denom(x)) is x, numer(0.1) is 1, and the double nearest 0.1 is +// another Rational than 0.1. package test { private import ScalarValues::*; @@ -13,7 +13,8 @@ package test { out negativeDenominator = RationalFunctions::rat(3, -4); out zero = RationalFunctions::rat(0, 1); out whole = RationalFunctions::rat(4, 2); - out tenth = RationalFunctions::rat(3602879701896397, 36028797018963968) == 0.1; + out tenth = RationalFunctions::rat(1, 10) == 0.1; + out binaryTenthIsNotTenth = RationalFunctions::rat(3602879701896397, 36028797018963968) == 0.1; out numerOfThreeQuarters = RationalFunctions::numer(0.75); out denomOfThreeQuarters = RationalFunctions::denom(0.75); out numerOfNegative = RationalFunctions::numer(-0.75); diff --git a/internal/exec/runtime/testdata/conformance/calc_library_vector_custom_specialization_element.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_vector_custom_specialization_element.expected.json index edbdc60fcc..94334f6f13 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_vector_custom_specialization_element.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_vector_custom_specialization_element.expected.json @@ -1,5 +1,5 @@ { "libraries": true, "type": "calc", - "error": "type mismatch: cannot write ⟨1, 2.5, 3⟩ (vector) to a feature typed by IntegerVector: it declares elements : Integer, got element 2.5 (a Real)" + "error": "type mismatch: cannot write ⟨1, 2.5, 3⟩ (vector) to a feature typed by IntegerVector: it declares elements : Integer, got element 2.5 (a Rational)" } diff --git a/internal/exec/runtime/testdata/conformance/calc_library_vector_quantity_num_type.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_vector_quantity_num_type.expected.json index 2b5cc3dde1..1d79e88e7d 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_vector_quantity_num_type.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_vector_quantity_num_type.expected.json @@ -1,5 +1,5 @@ { "libraries": true, "type": "calc", - "error": "type mismatch: cannot write ⟨1.5, 2.5⟩ [m] (vector quantity) to a feature typed by Counts: it declares num : Integer, got element 1.5 (a Real)" + "error": "type mismatch: cannot write ⟨1.5, 2.5⟩ [m] (vector quantity) to a feature typed by Counts: it declares num : Integer, got element 1.5 (a Rational)" } diff --git a/internal/exec/runtime/testdata/conformance/calc_library_vector_specialization_fixed_element_type.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_vector_specialization_fixed_element_type.expected.json index 0008262add..a7800243a7 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_vector_specialization_fixed_element_type.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_vector_specialization_fixed_element_type.expected.json @@ -1,5 +1,5 @@ { "libraries": true, "type": "calc", - "error": "feature value v.elements: type mismatch: cannot write 1.5 (a Real) to a feature typed by Integer" + "error": "feature value v.elements: type mismatch: cannot write 1.5 (a Rational) to a feature typed by Integer" } diff --git a/internal/exec/runtime/testdata/conformance/calc_library_vector_sum.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_vector_sum.expected.json index 5997210909..7c7a726c11 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_vector_sum.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_vector_sum.expected.json @@ -2,8 +2,8 @@ "libraries": true, "type": "calc", "result": {"type": "Vector", "elements": [ - {"type": "Real", "value": 5.0}, - {"type": "Real", "value": 7.0}, - {"type": "Real", "value": 9.0} + {"type": "Rational", "value": "5.0"}, + {"type": "Rational", "value": "7.0"}, + {"type": "Rational", "value": "9.0"} ]} } diff --git a/internal/exec/runtime/testdata/conformance/calc_library_vector_sum_flat_sequence.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_vector_sum_flat_sequence.expected.json index 9d53173cbf..4eb330018e 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_vector_sum_flat_sequence.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_vector_sum_flat_sequence.expected.json @@ -1,5 +1,5 @@ { "libraries": true, "type": "calc", - "error": "type mismatch: function VectorFunctions::sum requires a collection of vectors (coll: VectorValue[*]), element 1 is a Real" + "error": "type mismatch: function VectorFunctions::sum requires a collection of vectors (coll: VectorValue[*]), element 1 is a Rational" } diff --git a/internal/exec/runtime/testdata/conformance/calc_loop_until_body.expected.json b/internal/exec/runtime/testdata/conformance/calc_loop_until_body.expected.json index ea2c866111..1b24399740 100644 --- a/internal/exec/runtime/testdata/conformance/calc_loop_until_body.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_loop_until_body.expected.json @@ -7,7 +7,7 @@ } ], "result": { - "type": "Real", - "value": 0.5625 + "type": "Rational", + "value": "0.5625" } } diff --git a/internal/exec/runtime/testdata/conformance/calc_natural_quotient.expected.json b/internal/exec/runtime/testdata/conformance/calc_natural_quotient.expected.json index 972a994c2b..80dade3e18 100644 --- a/internal/exec/runtime/testdata/conformance/calc_natural_quotient.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_natural_quotient.expected.json @@ -5,5 +5,5 @@ {"type": "Integer", "value": 5}, {"type": "Integer", "value": 2} ], - "result": {"type": "Real", "value": 2.5} + "result": {"type": "Rational", "value": "2.5"} } diff --git a/internal/exec/runtime/testdata/conformance/calc_return_parameter.expected.json b/internal/exec/runtime/testdata/conformance/calc_return_parameter.expected.json index 720f37f7e3..c247e86900 100644 --- a/internal/exec/runtime/testdata/conformance/calc_return_parameter.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_return_parameter.expected.json @@ -1,5 +1,5 @@ { "type": "calc", "inputs": [], - "result": {"type": "Real", "value": 0.25} + "result": {"type": "Rational", "value": "0.25"} } diff --git a/internal/exec/runtime/testdata/conformance/calc_rk4_lunar_descent.expected.json b/internal/exec/runtime/testdata/conformance/calc_rk4_lunar_descent.expected.json index 3ee3ca6c74..94e5bd38b5 100644 --- a/internal/exec/runtime/testdata/conformance/calc_rk4_lunar_descent.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_rk4_lunar_descent.expected.json @@ -2,5 +2,5 @@ "type": "calc", "evaluate": "test::Propagate", "inputs": [{"type": "Integer", "value": 3}], - "result": {"type": "Real", "value": 15001.719185373526} + "result": {"type": "Rational", "value": "244225428175771200528215026849/16279829342085518803125000"} } diff --git a/internal/exec/runtime/testdata/conformance/calc_sample_pair_arithmetic.expected.json b/internal/exec/runtime/testdata/conformance/calc_sample_pair_arithmetic.expected.json index a34a668686..9e6ca5220b 100644 --- a/internal/exec/runtime/testdata/conformance/calc_sample_pair_arithmetic.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_sample_pair_arithmetic.expected.json @@ -3,8 +3,8 @@ "type": "calcUsage", "evaluate": "test::c", "outputs": { - "difference": {"type": "Real", "value": 2.0}, - "product": {"type": "Real", "value": 8.0}, - "interpolated": {"type": "Real", "value": 5.0} + "difference": {"type": "Rational", "value": "2.0"}, + "product": {"type": "Rational", "value": "8.0"}, + "interpolated": {"type": "Rational", "value": "5.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/calc_two_valued_member_refused_scalar.expected.json b/internal/exec/runtime/testdata/conformance/calc_two_valued_member_refused_scalar.expected.json index 4eeb42e3da..a09ca166d4 100644 --- a/internal/exec/runtime/testdata/conformance/calc_two_valued_member_refused_scalar.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_two_valued_member_refused_scalar.expected.json @@ -2,5 +2,5 @@ "libraries": true, "type": "calcUsage", "evaluate": "test::c", - "error": "type mismatch: operator '+' is not defined for a sequence and a Real" + "error": "type mismatch: operator '+' is not defined for a sequence and a Rational" } diff --git a/internal/exec/runtime/testdata/conformance/calc_usage_instance_slots.expected.json b/internal/exec/runtime/testdata/conformance/calc_usage_instance_slots.expected.json index e43f5dfb20..6bf2f102c7 100644 --- a/internal/exec/runtime/testdata/conformance/calc_usage_instance_slots.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_usage_instance_slots.expected.json @@ -4,6 +4,6 @@ "slots": { "base": {"type": "Integer", "value": 8}, "total": {"type": "Integer", "value": 10}, - "margin": {"type": "Real", "value": 5.0} + "margin": {"type": "Rational", "value": "5.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/calc_usage_outputs_one_binding.expected.json b/internal/exec/runtime/testdata/conformance/calc_usage_outputs_one_binding.expected.json index e91303ead3..682e605a4f 100644 --- a/internal/exec/runtime/testdata/conformance/calc_usage_outputs_one_binding.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_usage_outputs_one_binding.expected.json @@ -1,5 +1,5 @@ { "type": "calc", "evaluate": "test::Interleaved", - "result": {"type": "Real", "value": 1010.0} + "result": {"type": "Rational", "value": "1010.0"} } diff --git a/internal/exec/runtime/testdata/conformance/instance_constraint_binding.expected.json b/internal/exec/runtime/testdata/conformance/instance_constraint_binding.expected.json index e64ef5e31a..ba4904ebaa 100644 --- a/internal/exec/runtime/testdata/conformance/instance_constraint_binding.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_constraint_binding.expected.json @@ -2,7 +2,7 @@ "type": "instance", "instantiate": "test::Vehicle", "slots": { - "gross": {"type": "Real", "value": 1800.0} + "gross": {"type": "Rational", "value": "1800.0"} }, "constraints": { "withinLimit": true, diff --git a/internal/exec/runtime/testdata/conformance/instance_derived_slots.expected.json b/internal/exec/runtime/testdata/conformance/instance_derived_slots.expected.json index a5e002dcc5..0488e3ac2e 100644 --- a/internal/exec/runtime/testdata/conformance/instance_derived_slots.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_derived_slots.expected.json @@ -2,9 +2,9 @@ "type": "instance", "instantiate": "test::Vehicle", "slots": { - "mass": {"type": "Real", "value": 1500.0}, - "folded": {"type": "Real", "value": 12.0}, - "doubled": {"type": "Real", "value": 3000.0}, - "total": {"type": "Real", "value": 1770.0} + "mass": {"type": "Rational", "value": "1500.0"}, + "folded": {"type": "Rational", "value": "12.0"}, + "doubled": {"type": "Rational", "value": "3000.0"}, + "total": {"type": "Rational", "value": "1770.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/instance_inherited_constraint.expected.json b/internal/exec/runtime/testdata/conformance/instance_inherited_constraint.expected.json index 5df7f12845..e454791f3e 100644 --- a/internal/exec/runtime/testdata/conformance/instance_inherited_constraint.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_inherited_constraint.expected.json @@ -2,7 +2,7 @@ "type": "instance", "instantiate": "test::Vehicle", "slots": { - "mass": {"type": "Real", "value": 1500.0} + "mass": {"type": "Rational", "value": "1500.0"} }, "constraints": { "headroom": true, diff --git a/internal/exec/runtime/testdata/conformance/instance_nested_usage_body.expected.json b/internal/exec/runtime/testdata/conformance/instance_nested_usage_body.expected.json index 23a2418ddf..aeb28ab30b 100644 --- a/internal/exec/runtime/testdata/conformance/instance_nested_usage_body.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_nested_usage_body.expected.json @@ -2,7 +2,7 @@ "type": "instance", "instantiate": "test::Car", "slots": { - "enginePower": {"type": "Real", "value": 250.0}, - "seatCount": {"type": "Real", "value": 4.0} + "enginePower": {"type": "Rational", "value": "250.0"}, + "seatCount": {"type": "Rational", "value": "4.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/instance_quantity_calculations.expected.json b/internal/exec/runtime/testdata/conformance/instance_quantity_calculations.expected.json index 604ba7c68a..7b8dfdd645 100644 --- a/internal/exec/runtime/testdata/conformance/instance_quantity_calculations.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_quantity_calculations.expected.json @@ -19,8 +19,8 @@ "label": {"type": "String", "value": "3 [m]"}, "metres": {"type": "Real", "value": 3.0}, "count": {"type": "Integer", "value": 3}, - "ratio": {"type": "Real", "value": 2.5}, + "ratio": {"type": "Rational", "value": "2.5"}, "stretched": {"type": "Quantity", "value": 7.5, "unit": "m"}, - "grown": {"type": "Real", "value": 4.0} + "grown": {"type": "Rational", "value": "4.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/instance_quantity_coherent_units.expected.json b/internal/exec/runtime/testdata/conformance/instance_quantity_coherent_units.expected.json index d439c1bfbc..0974897bb5 100644 --- a/internal/exec/runtime/testdata/conformance/instance_quantity_coherent_units.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_quantity_coherent_units.expected.json @@ -10,7 +10,7 @@ "perLightMass": {"type": "Quantity", "value": 5.0, "unit": "SI::'m⋅s⁻²'"}, "work": {"type": "Quantity", "value": 2.0, "unit": "SI::J"}, "torque": {"type": "Quantity", "value": 6, "unit": "SI::'N⋅m'"}, - "ratio": {"type": "Real", "value": 1.0}, + "ratio": {"type": "Rational", "value": "1"}, "halfTorque": {"type": "Quantity", "value": 3, "unit": "SI::'N⋅m'"}, "unlisted": {"type": "Quantity", "value": 6, "unit": "K*kg"}, "swept": {"type": "Quantity", "value": 6, "unit": "m*rad"}, diff --git a/internal/exec/runtime/testdata/conformance/instance_redefined_attribute_default.expected.json b/internal/exec/runtime/testdata/conformance/instance_redefined_attribute_default.expected.json index 46ba0881cf..cada571e30 100644 --- a/internal/exec/runtime/testdata/conformance/instance_redefined_attribute_default.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_redefined_attribute_default.expected.json @@ -2,8 +2,8 @@ "type": "instance", "instantiate": "test::truck", "slots": { - "grossMass": {"type": "Real", "value": 1000.0}, - "mass": {"type": "Real", "value": 1000.0}, - "payload": {"type": "Real", "value": 400.0} + "grossMass": {"type": "Rational", "value": "1000.0"}, + "mass": {"type": "Rational", "value": "1000.0"}, + "payload": {"type": "Rational", "value": "400.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/instance_tensor_quantity.expected.json b/internal/exec/runtime/testdata/conformance/instance_tensor_quantity.expected.json index 64b532876f..f1504dbb08 100644 --- a/internal/exec/runtime/testdata/conformance/instance_tensor_quantity.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_tensor_quantity.expected.json @@ -36,7 +36,7 @@ "order": {"type": "Integer", "value": 2}, "flattenedSize": {"type": "Integer", "value": 4}, "num": {"type": "Sequence", "elements": [ - {"type": "Real", "value": 1.0}, {"type": "Real", "value": 2.0}, {"type": "Real", "value": 3.0}, {"type": "Real", "value": 4.0} + {"type": "Rational", "value": "1.0"}, {"type": "Rational", "value": "2.0"}, {"type": "Rational", "value": "3.0"}, {"type": "Rational", "value": "4.0"} ]}, "unbound": {"type": "Boolean", "value": false}, "isBound": {"type": "Boolean", "value": true}, diff --git a/internal/exec/runtime/testdata/conformance/instance_tensor_rank_three.expected.json b/internal/exec/runtime/testdata/conformance/instance_tensor_rank_three.expected.json index e08c430429..a8545e0180 100644 --- a/internal/exec/runtime/testdata/conformance/instance_tensor_rank_three.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_tensor_rank_three.expected.json @@ -224,36 +224,36 @@ "type": "Sequence", "elements": [ { - "type": "Real", - "value": 1.0 + "type": "Rational", + "value": "1.0" }, { - "type": "Real", - "value": 2.0 + "type": "Rational", + "value": "2.0" }, { - "type": "Real", - "value": 3.0 + "type": "Rational", + "value": "3.0" }, { - "type": "Real", - "value": 4.0 + "type": "Rational", + "value": "4.0" }, { - "type": "Real", - "value": 5.0 + "type": "Rational", + "value": "5.0" }, { - "type": "Real", - "value": 6.0 + "type": "Rational", + "value": "6.0" }, { - "type": "Real", - "value": 7.0 + "type": "Rational", + "value": "7.0" }, { - "type": "Real", - "value": 8.0 + "type": "Rational", + "value": "8.0" } ] }, diff --git a/internal/exec/runtime/testdata/conformance/instance_tensor_rank_three_failures.expected.json b/internal/exec/runtime/testdata/conformance/instance_tensor_rank_three_failures.expected.json index 078808ec2a..1d7896fad3 100644 --- a/internal/exec/runtime/testdata/conformance/instance_tensor_rank_three_failures.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_tensor_rank_three_failures.expected.json @@ -81,7 +81,7 @@ "error": "index out of range: BaseFunctions::'#': index 2 is 4, dimension 2 has 1..3" }, "nonIntegerIndex": { - "error": "type mismatch: BaseFunctions::'#' requires an Integer index, got a Real" + "error": "type mismatch: BaseFunctions::'#' requires an Integer index, got a Rational" }, "short": { "error": "multiplicity violation: function TensorCalculations::'[': 7 elements for a reference of dimensions [2, 2, 2]: the declaration `in elements: Number[1..n]` has `n = mRef.flattenedSize` = 8, and Array binds mRefs to one reference per component" diff --git a/internal/exec/runtime/testdata/conformance/instance_unnamed_redefinition.expected.json b/internal/exec/runtime/testdata/conformance/instance_unnamed_redefinition.expected.json index 124cdd0cc8..2e4d265076 100644 --- a/internal/exec/runtime/testdata/conformance/instance_unnamed_redefinition.expected.json +++ b/internal/exec/runtime/testdata/conformance/instance_unnamed_redefinition.expected.json @@ -2,6 +2,6 @@ "type": "instance", "instantiate": "test::Car", "slots": { - "enginePower": {"type": "Real", "value": 250.0} + "enginePower": {"type": "Rational", "value": "250.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/multiplicity_default_composite.expected.json b/internal/exec/runtime/testdata/conformance/multiplicity_default_composite.expected.json index eb3fbbc4d5..a834767243 100644 --- a/internal/exec/runtime/testdata/conformance/multiplicity_default_composite.expected.json +++ b/internal/exec/runtime/testdata/conformance/multiplicity_default_composite.expected.json @@ -3,6 +3,6 @@ "instantiate": "test::bay", "slots": { "stowed": {"type": "Sequence", "elements": [{"type": "Instance"}, {"type": "Instance"}]}, - "volumes": {"type": "Sequence", "elements": [{"type": "Real", "value": 2.0}, {"type": "Real", "value": 3.5}]} + "volumes": {"type": "Sequence", "elements": [{"type": "Rational", "value": "2.0"}, {"type": "Rational", "value": "3.5"}]} } } diff --git a/internal/exec/runtime/testdata/conformance/multiplicity_default_redefinition.expected.json b/internal/exec/runtime/testdata/conformance/multiplicity_default_redefinition.expected.json index 9e89b6cb1a..0ffd73f627 100644 --- a/internal/exec/runtime/testdata/conformance/multiplicity_default_redefinition.expected.json +++ b/internal/exec/runtime/testdata/conformance/multiplicity_default_redefinition.expected.json @@ -2,8 +2,8 @@ "type": "instance", "instantiate": "test::derived", "slots": { - "bare": {"type": "Sequence", "elements": [{"type": "Real", "value": 7.0}, {"type": "Real", "value": 8.0}, {"type": "Real", "value": 9.0}]}, - "kept": {"type": "Sequence", "elements": [{"type": "Real", "value": 1.0}, {"type": "Real", "value": 2.0}]}, - "valued": {"type": "Sequence", "elements": [{"type": "Real", "value": 1.0}, {"type": "Real", "value": 2.0}]} + "bare": {"type": "Sequence", "elements": [{"type": "Rational", "value": "7.0"}, {"type": "Rational", "value": "8.0"}, {"type": "Rational", "value": "9.0"}]}, + "kept": {"type": "Sequence", "elements": [{"type": "Rational", "value": "1.0"}, {"type": "Rational", "value": "2.0"}]}, + "valued": {"type": "Sequence", "elements": [{"type": "Rational", "value": "1.0"}, {"type": "Rational", "value": "2.0"}]} } } diff --git a/internal/exec/runtime/testdata/conformance/nested_redefinition_chain.expected.json b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain.expected.json index 67222c2591..b43cc89160 100644 --- a/internal/exec/runtime/testdata/conformance/nested_redefinition_chain.expected.json +++ b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain.expected.json @@ -2,9 +2,9 @@ "type": "instance", "instantiate": "test::top", "slots": { - "mid.scale": {"type": "Real", "value": 4.0}, - "mid.leaf.value": {"type": "Real", "value": 99.0}, - "mid.leaf.other": {"type": "Real", "value": 5.0} + "mid.scale": {"type": "Rational", "value": "4.0"}, + "mid.leaf.value": {"type": "Rational", "value": "99.0"}, + "mid.leaf.other": {"type": "Rational", "value": "5.0"} }, "constraints": { "readsLeaf": true, diff --git a/internal/exec/runtime/testdata/conformance/nested_redefinition_chain_equiv.expected.json b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain_equiv.expected.json index af297b6912..42d2a2f6e2 100644 --- a/internal/exec/runtime/testdata/conformance/nested_redefinition_chain_equiv.expected.json +++ b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain_equiv.expected.json @@ -2,12 +2,12 @@ "type": "instance", "instantiate": "test::World", "slots": { - "shorthand.mid.leaf.value": {"type": "Real", "value": 99.0}, - "shorthand.mid.scale": {"type": "Real", "value": 4.0}, - "nestedBody.mid.leaf.value": {"type": "Real", "value": 99.0}, - "nestedBody.mid.scale": {"type": "Real", "value": 4.0}, - "outerRead.mid.leaf.value": {"type": "Real", "value": 14.0}, - "outerRead.mid.scale": {"type": "Real", "value": 7.0} + "shorthand.mid.leaf.value": {"type": "Rational", "value": "99.0"}, + "shorthand.mid.scale": {"type": "Rational", "value": "4.0"}, + "nestedBody.mid.leaf.value": {"type": "Rational", "value": "99.0"}, + "nestedBody.mid.scale": {"type": "Rational", "value": "4.0"}, + "outerRead.mid.leaf.value": {"type": "Rational", "value": "14.0"}, + "outerRead.mid.scale": {"type": "Rational", "value": "7.0"} }, "constraints": { "wheelsRedefined": true diff --git a/internal/exec/runtime/testdata/conformance/nested_redefinition_precedence.expected.json b/internal/exec/runtime/testdata/conformance/nested_redefinition_precedence.expected.json index f539d1a963..7ae16fffae 100644 --- a/internal/exec/runtime/testdata/conformance/nested_redefinition_precedence.expected.json +++ b/internal/exec/runtime/testdata/conformance/nested_redefinition_precedence.expected.json @@ -2,7 +2,7 @@ "type": "instance", "instantiate": "test::top", "slots": { - "mid.scale": {"type": "Real", "value": 9.0}, - "mid.leaf.value": {"type": "Real", "value": 99.0} + "mid.scale": {"type": "Rational", "value": "9.0"}, + "mid.leaf.value": {"type": "Rational", "value": "99.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/send_new_redefined_payload_feature.expected.json b/internal/exec/runtime/testdata/conformance/send_new_redefined_payload_feature.expected.json index 88077a6d96..b4be2c3a77 100644 --- a/internal/exec/runtime/testdata/conformance/send_new_redefined_payload_feature.expected.json +++ b/internal/exec/runtime/testdata/conformance/send_new_redefined_payload_feature.expected.json @@ -12,8 +12,8 @@ "finalState": "gotLabelled", "stateVisits": ["standingBy", "gotPositional", "gotLabelled"], "slots": { - "positional": {"type": "Real", "value": 46.0}, - "labelled": {"type": "Real", "value": 5.0} + "positional": {"type": "Rational", "value": "46.0"}, + "labelled": {"type": "Rational", "value": "5.0"} } } ] diff --git a/internal/exec/runtime/testdata/conformance/send_new_through_alias.expected.json b/internal/exec/runtime/testdata/conformance/send_new_through_alias.expected.json index 707a6e4296..85e6892bb2 100644 --- a/internal/exec/runtime/testdata/conformance/send_new_through_alias.expected.json +++ b/internal/exec/runtime/testdata/conformance/send_new_through_alias.expected.json @@ -12,7 +12,7 @@ "finalState": "confirmed", "stateVisits": ["standingBy", "logged", "confirmed"], "slots": { - "frames": {"type": "Real", "value": 7.0} + "frames": {"type": "Rational", "value": "7.0"} } } ] diff --git a/internal/exec/runtime/testdata/conformance/send_via_owner_nested_port_path.expected.json b/internal/exec/runtime/testdata/conformance/send_via_owner_nested_port_path.expected.json index a0cb287e58..9e540b3270 100644 --- a/internal/exec/runtime/testdata/conformance/send_via_owner_nested_port_path.expected.json +++ b/internal/exec/runtime/testdata/conformance/send_via_owner_nested_port_path.expected.json @@ -12,7 +12,7 @@ "finalState": "logged", "stateVisits": ["standingBy", "logged"], "slots": { - "frames": {"type": "Real", "value": 5.0} + "frames": {"type": "Rational", "value": "5.0"} } } ] diff --git a/internal/exec/runtime/testdata/conformance/send_via_owner_port_from_nested_part.expected.json b/internal/exec/runtime/testdata/conformance/send_via_owner_port_from_nested_part.expected.json index 10795db6d8..a2517d7f60 100644 --- a/internal/exec/runtime/testdata/conformance/send_via_owner_port_from_nested_part.expected.json +++ b/internal/exec/runtime/testdata/conformance/send_via_owner_port_from_nested_part.expected.json @@ -12,7 +12,7 @@ "finalState": "logged", "stateVisits": ["standingBy", "logged"], "slots": { - "frames": {"type": "Real", "value": 3.0} + "frames": {"type": "Rational", "value": "3.0"} } } ] diff --git a/internal/exec/runtime/testdata/conformance/snapshot_succession_portion_reads.expected.json b/internal/exec/runtime/testdata/conformance/snapshot_succession_portion_reads.expected.json index 1e9d755457..e1d570f6e3 100644 --- a/internal/exec/runtime/testdata/conformance/snapshot_succession_portion_reads.expected.json +++ b/internal/exec/runtime/testdata/conformance/snapshot_succession_portion_reads.expected.json @@ -2,8 +2,8 @@ "type": "instance", "instantiate": "test::craft", "slots": { - "mass": {"type": "Real", "value": 120.0}, - "postSeparation.mass": {"type": "Real", "value": 118.0}, - "postFlyby.mass": {"type": "Real", "value": 96.0} + "mass": {"type": "Rational", "value": "120.0"}, + "postSeparation.mass": {"type": "Rational", "value": "118.0"}, + "postFlyby.mass": {"type": "Rational", "value": "96.0"} } } diff --git a/internal/exec/runtime/testdata/conformance/state_space_discrete_difference.expected.json b/internal/exec/runtime/testdata/conformance/state_space_discrete_difference.expected.json index 5e8344b75a..810e06e674 100644 --- a/internal/exec/runtime/testdata/conformance/state_space_discrete_difference.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_space_discrete_difference.expected.json @@ -4,11 +4,11 @@ "evaluate": "test::account", "outputs": { "stateSpace": {"type": "Vector", "elements": [ - {"type": "Real", "value": 1628.894626777441}, - {"type": "Real", "value": 10.0} + {"type": "Rational", "value": "1628.89462677744140625"}, + {"type": "Rational", "value": "10.0"} ]}, "output": {"type": "Vector", "elements": [ - {"type": "Real", "value": 1628.894626777441} + {"type": "Rational", "value": "1628.89462677744140625"} ]}, "time": {"type": "Quantity", "value": 10.0, "unit": "s"} } diff --git a/internal/exec/runtime/testdata/conformance/state_target_transition_guard.expected.json b/internal/exec/runtime/testdata/conformance/state_target_transition_guard.expected.json index 573aa33458..d17903e906 100644 --- a/internal/exec/runtime/testdata/conformance/state_target_transition_guard.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_target_transition_guard.expected.json @@ -4,7 +4,7 @@ "finalState": "decelerating", "stateVisits": ["coasting", "decelerating"], "outputs": { - "alt": {"type": "Real", "value": 10.0}, + "alt": {"type": "Rational", "value": "10.0"}, "fired": {"type": "Integer", "value": 1} } } diff --git a/internal/exec/runtime/that_test.go b/internal/exec/runtime/that_test.go index 753badba86..dd79634e39 100644 --- a/internal/exec/runtime/that_test.go +++ b/internal/exec/runtime/that_test.go @@ -9,10 +9,10 @@ import ( // realValue reports the real a feature value holds. func realValue(t *testing.T, v Value) float64 { t.Helper() - if v.Kind != ValConst || v.Const.Kind != semantics.ValReal { - t.Fatalf("value is %v, want a real", v) + if v.Kind != ValConst || (v.Const.Kind != semantics.ValReal && v.Const.Kind != semantics.ValRational) { + t.Fatalf("value is %v, want a Real or a Rational", v) } - return v.Const.Real + return v.Const.AsReal() } // A usage's `that` names the object featuring the usage's values, so a chain diff --git a/internal/exec/runtime/trace.go b/internal/exec/runtime/trace.go index d4093b21bd..187e5cb204 100644 --- a/internal/exec/runtime/trace.go +++ b/internal/exec/runtime/trace.go @@ -665,6 +665,8 @@ func formatConst(c semantics.Value) string { return c.FormatInt() case semantics.ValReal: return semantics.FormatReal(c.Real) + case semantics.ValRational: + return c.FormatRational() case semantics.ValBool: return strconv.FormatBool(c.Bool) case semantics.ValInfinity: diff --git a/internal/exec/runtime/value_equality.go b/internal/exec/runtime/value_equality.go index 618b16a506..68e110901d 100644 --- a/internal/exec/runtime/value_equality.go +++ b/internal/exec/runtime/value_equality.go @@ -43,13 +43,18 @@ func (ctx *Context) valueKey(v Value) valueKey { switch v.Kind { case ValConst: switch v.Const.Kind { - case semantics.ValInt, semantics.ValReal: + case semantics.ValInt, semantics.ValRational, semantics.ValReal: if n, ok := v.Const.WholeNumber(); ok { key.intVal = n } else if n, ok := wholeBeyondInt64(v.Const); ok { key.bigVal = n.FormatInt() - } else { + } else if v.Const.Kind != semantics.ValRational { key.realVal = v.Const.Real + } else if f := v.Const.AsReal(); semantics.CompareExactReal(v.Const, f) == 0 { + // A Rational a binary64 holds exactly equals that Real and shares its key. + key.realVal = f + } else { + key.bigVal = v.Const.FormatRational() } case semantics.ValBool: key.boolVal = v.Const.Bool diff --git a/internal/exec/runtime/vector_functions.go b/internal/exec/runtime/vector_functions.go index 81c54300ce..8f59198646 100644 --- a/internal/exec/runtime/vector_functions.go +++ b/internal/exec/runtime/vector_functions.go @@ -114,6 +114,59 @@ func realElements(name, param string, val Value) ([]float64, error) { return out, nil } +// cartesian applies a CartesianVectorValue operation, whose library declaration +// types its scalars and vector elements Real, to its arguments held as Reals. +func cartesian(apply libraryApply) libraryApply { + return func(name string, ctx *Context, args []Value) (Value, error) { + held := make([]Value, len(args)) + for i, arg := range args { + var err error + if held[i], err = realHeld(arg); err != nil { + return Value{}, functionError(name, err) + } + } + return apply(name, ctx, held) + } +} + +// realHeld is a scalar, sequence or unitless vector with each exact number as its +// nearest Real, refused where no finite Real is. +func realHeld(v Value) (Value, error) { + switch v.Kind { + case ValConst: + if !v.Const.IsExact() { + return v, nil + } + real, err := semantics.RealOf(v.Const) + if err != nil { + return Value{}, err + } + return Value{Kind: ValConst, Const: real}, nil + case ValSequence: + elements := elementsOf(v) + held := make([]Value, len(elements)) + for i := range elements { + var err error + if held[i], err = realHeld(elements[i]); err != nil { + return Value{}, err + } + } + return sequenceOf(held), nil + case ValVector: + vec := v.Vector() + held := make([]semantics.Value, len(vec.Elements)) + for i, elem := range vec.Elements { + h, err := realHeld(Value{Kind: ValConst, Const: elem}) + if err != nil { + return Value{}, err + } + held[i] = h.Const + } + return Value{Kind: ValVector, ref: &Vector{Elements: held, Object: vec.Object}}, nil + } + return v, nil +} + // vectorValue builds a vector from its elements, charging them against the run's // element budget; an element outside the range of its kind is reported. func (ctx *Context) vectorValue(elements []semantics.Value) (Value, error) { @@ -258,22 +311,29 @@ func checkedNumeric(v semantics.Value) (semantics.Value, error) { return semantics.RealResult(v.Real) } -// isNumeric reports whether a value is an Integer or a Real. +// isNumeric reports whether a value is an Integer, a Rational or a Real. func isNumeric(v semantics.Value) bool { - return v.Kind == semantics.ValInt || v.Kind == semantics.ValReal + return v.IsNumeric() } // elementArith applies an arithmetic operator to two numeric elements: two // Integers exactly, refused only beyond maxBits; a Real result outside its range // is reported rather than returned infinite. func elementArith(name string, op ast.OperatorKind, a, b semantics.Value, maxBits int64) (semantics.Value, error) { - if a.Kind == semantics.ValInt && b.Kind == semantics.ValInt { + if a.Kind == semantics.ValInt && b.Kind == semantics.ValInt && op != ast.OpDiv { result, err := semantics.IntArith(op, a, b, maxBits) if err != nil { return semantics.Value{}, fmt.Errorf("function %s: %w", name, integerSizeHint(err)) } return result, nil } + if a.IsExact() && b.IsExact() && op != ast.OpPow { + result, err := semantics.RatArith(op, a, b, maxBits) + if err != nil { + return semantics.Value{}, fmt.Errorf("function %s: %w", name, integerSizeHint(err)) + } + return result, nil + } if a.IsNumeric() && b.IsNumeric() { res, ok := semantics.RealArith(op, toReal(a), toReal(b)) if !ok { @@ -402,8 +462,11 @@ func (ctx *Context) combineVectors(name string, op ast.OperatorKind, v, w vector // zeroLike is the zero of a numeric value's kind, which negation subtracts from. func zeroLike(v semantics.Value) semantics.Value { - if v.Kind == semantics.ValInt { + switch v.Kind { + case semantics.ValInt: return semantics.Value{Kind: semantics.ValInt} + case semantics.ValRational: + return semantics.RatOf(semantics.IntValue(0)) } return semantics.Value{Kind: semantics.ValReal} } diff --git a/internal/exec/runtime/write_conformance.go b/internal/exec/runtime/write_conformance.go index 607fe91246..947a0e89e1 100644 --- a/internal/exec/runtime/write_conformance.go +++ b/internal/exec/runtime/write_conformance.go @@ -231,12 +231,61 @@ func (ctx *Context) checkWriteType(scope *symbols.Scope, what func() string, dec } // holdForDeclared shapes an admitted value as the declared type holds it: quantities in -// its preferred unit, scalars an enumeration admits as the enumerated value they equal. +// its preferred unit, a Rational a Real feature holds as its nearest binary64, scalars +// an enumeration admits as the enumerated value they equal. func (ctx *Context) holdForDeclared(value *Value, declared *symbols.Symbol) error { ctx.spellForDeclared(value, declared) + if err := ctx.holdAsReal(value, declared); err != nil { + return err + } return ctx.holdAsEnumerated(value, declared) } +// holdAsReal rounds each exact Rational a feature typed by Real holds to the nearest +// binary64 once; an Integer keeps its kind, and a magnitude no Real holds is refused. +func (ctx *Context) holdAsReal(value *Value, declared *symbols.Symbol) error { + if declared == nil || ctx.model.semantics == nil { + return nil + } + switch value.Kind { + case ValConst: + if value.Const.Kind != semantics.ValRational || ctx.model.semantics.PrimTypeOf(declared) != semantics.PrimReal { + return nil + } + real, err := semantics.RealOf(value.Const) + if err != nil { + return fmt.Errorf("%s as a Real: %w", FormatValue(*value), err) + } + *value = Value{Kind: ValConst, Const: real} + case ValSequence, ValSet: + elements := elementsOf(*value) + var held []Value + for i := range elements { + if elements[i].Kind != ValConst || elements[i].Const.Kind != semantics.ValRational { + continue + } + if held == nil { + if ctx.model.semantics.PrimTypeOf(declared) != semantics.PrimReal { + return nil + } + held = append([]Value(nil), elements...) + } + if err := ctx.holdAsReal(&held[i], declared); err != nil { + return err + } + } + if held == nil { + return nil + } + if value.Kind == ValSequence { + *value = sequenceOf(held) + return nil + } + *value = ctx.setOf(held) + } + return nil +} + // holdAsEnumerated stores a scalar admitted by an enumeration-typed feature as the // enumerated value it equals, so what the feature holds is of the enumeration. func (ctx *Context) holdAsEnumerated(value *Value, declared *symbols.Symbol) error { diff --git a/internal/exec/smt/encode.go b/internal/exec/smt/encode.go index 5565b666bc..683cbee064 100644 --- a/internal/exec/smt/encode.go +++ b/internal/exec/smt/encode.go @@ -362,7 +362,7 @@ func (e *Encoding) collectFlows(node ast.Node) error { continue } p := &solve.Var{Name: "pending(" + target.Name + ")", Sort: target.Sort, Symbol: target.Symbol, - Dimension: target.Dimension, Unit: target.Unit} + Dimension: target.Dimension, Unit: target.Unit, Binary64: target.Binary64} e.pending[target.Name] = p e.features[p.Name] = p e.flagged[p.Name] = true @@ -1318,7 +1318,7 @@ func (e *Encoding) begin(x *nodeEffect, node ast.Node, where string) error { } e.fresh++ v := &solve.Var{Name: fmt.Sprintf("%s@%s#%d", name, where, e.fresh), Sort: p.v.Sort, - Symbol: p.v.Symbol, Dimension: p.v.Dimension, Unit: p.v.Unit} + Symbol: p.v.Symbol, Dimension: p.v.Dimension, Unit: p.v.Unit, Binary64: p.v.Binary64} e.declare(v) e.assert(eq(solve.VarTerm(v), value), "start of "+name) x.env.values[name] = solve.VarTerm(v) @@ -1570,7 +1570,7 @@ func (e *Encoding) write(x *nodeEffect, path *solve.Term, target *solve.Var, val } e.fresh++ v := &solve.Var{Name: fmt.Sprintf("%s@%s#%d", target.Name, where, e.fresh), Sort: target.Sort, - Symbol: target.Symbol, Dimension: target.Dimension, Unit: target.Unit} + Symbol: target.Symbol, Dimension: target.Dimension, Unit: target.Unit, Binary64: target.Binary64} e.declare(v) e.assert(eq(solve.VarTerm(v), value), "write to "+target.Name) x.env.write(target.Name, solve.VarTerm(v)) @@ -1588,7 +1588,8 @@ func (e *Encoding) merge(cond *solve.Term, then, otherwise *env, where string) * } e.fresh++ v := &solve.Var{Name: fmt.Sprintf("%s@%s#%d", name, where, e.fresh), Sort: a.Sort, - Symbol: e.features[name].Symbol, Dimension: e.features[name].Dimension, Unit: e.features[name].Unit} + Symbol: e.features[name].Symbol, Dimension: e.features[name].Dimension, Unit: e.features[name].Unit, + Binary64: e.features[name].Binary64} e.declare(v) e.assert(eq(solve.VarTerm(v), ite(cond, a, b)), "merge of "+name) merged.values[name] = solve.VarTerm(v) diff --git a/internal/exec/smt/state.go b/internal/exec/smt/state.go index d20bf2290e..64bdeeab3d 100644 --- a/internal/exec/smt/state.go +++ b/internal/exec/smt/state.go @@ -268,6 +268,7 @@ func (s *State) value(base *solve.Var) *solve.Var { Sort: base.Sort, Symbol: base.Symbol, Dimension: base.Dimension, + Binary64: base.Binary64, Unit: base.Unit, File: base.File, Span: base.Span, @@ -332,7 +333,7 @@ func (s *State) vars(features []*solve.Var, flagged map[string]bool) []*solve.Va } func intVar(name string) *solve.Var { return &solve.Var{Name: name, Sort: solve.Int} } -func realVar(name string) *solve.Var { return &solve.Var{Name: name, Sort: solve.Real} } +func realVar(name string) *solve.Var { return &solve.Var{Name: name, Sort: solve.Real, Binary64: true} } func boolVar(name string) *solve.Var { return &solve.Var{Name: name, Sort: solve.Bool} } func sortedVar(name string, sort solve.Sort) *solve.Var { return &solve.Var{Name: name, Sort: sort} diff --git a/internal/exec/solve/agreement_test.go b/internal/exec/solve/agreement_test.go index 04ed4b5296..16e7d0044b 100644 --- a/internal/exec/solve/agreement_test.go +++ b/internal/exec/solve/agreement_test.go @@ -4,8 +4,8 @@ import ( "context" "errors" "fmt" + "math/big" "os" - "strconv" "strings" "testing" @@ -120,8 +120,8 @@ func TestSolvedIntegerDivisionAgreesWithEvaluator(t *testing.T) { } } -// Beyond 2^53 the encoding divides the exact Int terms and the evaluator rounds -// the exact ratio once, so the solved quotient still matches the evaluated one. +// Beyond 2^53 the encoding divides the exact Int terms and the evaluator's +// quotient is the exact Rational, so the solved quotient matches the evaluated one. func TestSolvedQuotientAgreesBeyondFloatExactRange(t *testing.T) { solver := requireSolver(t) // 2^53+1 over 3 divides exactly; rounding the dividend to float64 first @@ -145,7 +145,7 @@ func TestSolvedQuotientAgreesBeyondFloatExactRange(t *testing.T) { } // The exact-quotient condition the evaluator satisfies is satisfiable for - // the solver too, and the pre-rounded-operand answer is refused by both. + // the solver too, and a rounded-operand answer is refused by both. for _, tc := range []struct { quotient string want Status @@ -166,19 +166,19 @@ func TestSolvedQuotientAgreesBeyondFloatExactRange(t *testing.T) { } } -// sameReal compares two rendered numbers by value, so the solver's `0.0` matches -// the negative zero the evaluator renders for `0 / -3`. +// sameReal compares two rendered numbers by exact value, so the solver's `0.0` +// matches the evaluator's `0.0` and `-7/2` its `-3.5`. func sameReal(t *testing.T, got, want string) bool { t.Helper() - gotF, err := strconv.ParseFloat(got, 64) - if err != nil { - t.Fatalf("parse solved value %q: %v", got, err) + gotR, ok := new(big.Rat).SetString(got) + if !ok { + t.Fatalf("parse solved value %q", got) } - wantF, err := strconv.ParseFloat(want, 64) - if err != nil { - t.Fatalf("parse evaluated value %q: %v", want, err) + wantR, ok := new(big.Rat).SetString(want) + if !ok { + t.Fatalf("parse evaluated value %q", want) } - return gotF == wantF + return gotR.Cmp(wantR) == 0 } // Where a whole answer differs from the evaluator's — a truncated or Euclidean @@ -304,22 +304,22 @@ func evaluatedDivision(t *testing.T, a, b int) (quotient, remainder string) { if err != nil { t.Fatalf("instantiate eval::P: %v", err) } - return featureReal(t, ctx, inst, "q"), featureInteger(t, ctx, inst, "r") + return featureRational(t, ctx, inst, "q"), featureInteger(t, ctx, inst, "r") } -// featureReal is the Real a feature value holds, rendered as the runtime -// renders one. -func featureReal(t *testing.T, ctx *runtime.Context, inst *runtime.Instance, name string) string { +// featureRational is the exact Rational a feature value holds, rendered as the +// runtime renders one. +func featureRational(t *testing.T, ctx *runtime.Context, inst *runtime.Instance, name string) string { t.Helper() fv, err := inst.GetFeatureValue(ctx, name) if err != nil { t.Fatalf("read feature value %s: %v", name, err) } val := fv.HeldValue() - if val.Kind != runtime.ValConst || val.Const.Kind != semantics.ValReal { - t.Fatalf("feature value %s holds %v, want a Real", name, val) + if val.Kind != runtime.ValConst || val.Const.Kind != semantics.ValRational { + t.Fatalf("feature value %s holds %v, want a Rational", name, val) } - return semantics.FormatReal(val.Const.Real) + return val.Const.FormatRational() } // featureInteger is the integer a feature value holds, materialized as any reader diff --git a/internal/exec/solve/pin.go b/internal/exec/solve/pin.go index 576181ee41..f2be278839 100644 --- a/internal/exec/solve/pin.go +++ b/internal/exec/solve/pin.go @@ -498,6 +498,12 @@ func (t *translator) pinConst(p Pin, v *Var, text string) (*Term, error) { msgValuesMeasuredIn+v.Dimension+", and a bare number states no unit") } return RealTerm(new(big.Rat).SetInt(c.BigInt())), nil + case c.Kind == semantics.ValRational && v.Sort.Kind == SortReal: + if v.Dimension != "" { + return nil, t.pinRefusal(p, v, text, + msgValuesMeasuredIn+v.Dimension+", and a bare number states no unit") + } + return RealTerm(c.Rat()), nil case c.Kind == semantics.ValReal && v.Sort.Kind == SortReal: if v.Dimension != "" { return nil, t.pinRefusal(p, v, text, @@ -638,6 +644,8 @@ func constText(c semantics.Value) string { switch c.Kind { case semantics.ValInt: return c.FormatInt() + case semantics.ValRational: + return c.FormatRational() case semantics.ValReal: return strconv.FormatFloat(c.Real, 'g', -1, 64) case semantics.ValBool: @@ -654,6 +662,8 @@ func ratOfConst(c semantics.Value) (*big.Rat, bool) { switch c.Kind { case semantics.ValInt: return new(big.Rat).SetInt(c.BigInt()), true + case semantics.ValRational: + return c.Rat(), true case semantics.ValReal: return ratOfFloat(c.Real) } diff --git a/internal/exec/solve/query.go b/internal/exec/solve/query.go index 9da7471762..cef6fe2af3 100644 --- a/internal/exec/solve/query.go +++ b/internal/exec/solve/query.go @@ -73,6 +73,10 @@ type Var struct { // Sort is the sort of its values. Sort Sort + // Binary64 marks a variable whose feature is declared Real, whose values the + // evaluator holds as binary64; Integer and Rational values are exact. + Binary64 bool + // Symbol is the feature declaration it stands for. Symbol *symbols.Symbol diff --git a/internal/exec/solve/reference.go b/internal/exec/solve/reference.go index b32aeb81a5..828cabfc83 100644 --- a/internal/exec/solve/reference.go +++ b/internal/exec/solve/reference.go @@ -317,7 +317,8 @@ func (t *translator) declare(name string, sort Sort, sym *symbols.Symbol, dimens if v, ok := t.vars[name]; ok { return v } - v := &Var{Name: name, Sort: sort, Symbol: sym, Dimension: dimension} + v := &Var{Name: name, Sort: sort, Symbol: sym, Dimension: dimension, + Binary64: sort.Kind == SortReal && t.model.PrimTypeOf(sym) == semantics.PrimReal} if sym != nil { v.File = sym.DocName v.Span = sym.DeclSpan diff --git a/internal/exec/solve/replay.go b/internal/exec/solve/replay.go index f333a49191..b2a39d3e0b 100644 --- a/internal/exec/solve/replay.go +++ b/internal/exec/solve/replay.go @@ -1,6 +1,7 @@ package solve import ( + "errors" "fmt" "math" "math/big" @@ -10,8 +11,8 @@ import ( ) // Rounded reports whether the query asserts or optimizes a value the evaluator -// computes in float64 — a real operation, a widened integer, or a real literal -// with no exact float64 — whose rounding the exact-real encoding does not model. +// computes in binary64 — arithmetic a Real takes part in — whose rounding the +// exact-real encoding does not model. Integer and Rational arithmetic is exact. func (q *Query) Rounded() bool { for _, a := range q.Assertions { if roundedTerm(a.Term) { @@ -40,22 +41,14 @@ func (c *Core) Rounded() bool { return false } -// roundedTerm reports whether the term computes a value float64 may round: real -// arithmetic, a widened integer (rounded beyond 2^53), or a real literal whose -// decimal has no exact float64. +// roundedTerm reports whether the term computes a value binary64 may round: +// arithmetic a Real takes part in, which also rounds its exact operands once. func roundedTerm(t *Term) bool { switch t.Op { - case OpReal: - _, exact := t.Real.Float64() - if !exact { - return true - } - case OpAdd, OpSub, OpMul, OpDiv, OpNeg: - if t.Sort.Kind == SortReal { + case OpAdd, OpSub, OpMul, OpDiv: + if t.Sort.Kind == SortReal && t.Binary64() { return true } - case OpToReal: - return true } for _, arg := range t.Args { if roundedTerm(arg) { @@ -66,14 +59,21 @@ func roundedTerm(t *Term) bool { } // replayValue is one value a replayed term yields, in the evaluator's own -// representations: int64, float64, bool, or a string naming a literal or a -// datatype value. +// representations: an exact Integer or Rational, a binary64 Real, a bool, or a +// string naming a literal or a datatype value. type replayValue struct { kind SortKind b bool - i semantics.Value - f float64 - s string + // i is an Integer, or with exact a Rational of the Real sort. + i semantics.Value + exact bool + f float64 + s string +} + +// exactReal is a Real-sorted value the evaluator holds exactly, as a Rational. +func exactReal(v semantics.Value) replayValue { + return replayValue{kind: SortReal, i: v, exact: true} } // replayWitness re-runs the query's assertions through the evaluator's own @@ -108,7 +108,7 @@ func (q *Query) Confirm(values map[string]ModelValue) (bool, string) { if value.Kind == SortDatatype && !contains(v.Sort.Values, value.Text) { return false, fmt.Sprintf("the witness gives %s %s, not a value of %s", v.Name, value.Text, v.Sort.Name) } - val, err := evaluatorValue(value) + val, err := evaluatorValue(value, v.Binary64) if err != nil { return false, fmt.Sprintf("the evaluator cannot hold the value the solver chose for %s: %v", v.Name, err) } @@ -133,9 +133,9 @@ func (q *Query) Confirm(values map[string]ModelValue) (bool, string) { } // evaluatorValue holds one model value in the evaluator's representation, -// reporting a value the evaluator cannot hold — a Real with no finite float64 — -// as an error. -func evaluatorValue(value ModelValue) (replayValue, error) { +// exactly unless binary64, reporting a value the evaluator cannot hold — a +// Real with no finite float64 — as an error. +func evaluatorValue(value ModelValue, binary64 bool) (replayValue, error) { switch value.Kind { case SortBool: return replayValue{kind: SortBool, b: value.Bool}, nil @@ -147,6 +147,9 @@ func evaluatorValue(value ModelValue) (replayValue, error) { } return replayValue{kind: SortInt, i: semantics.BigIntValue(new(big.Int).Set(value.Number.Num()))}, nil case SortReal: + if !binary64 { + return exactReal(semantics.RatValue(value.Number)), nil + } // The evaluator holds the nearest float64, which is where a witness // only the exact encoding can hold is caught by the replay. f, _ := value.Number.Float64() @@ -159,8 +162,8 @@ func evaluatorValue(value ModelValue) (replayValue, error) { } // replayTerm evaluates a term as the runtime evaluator computes it: exact -// Integer and float64 arithmetic, integers rounded to float64 where a real -// meets them. An evaluation the evaluator would report — a zero divisor, an +// Integer and Rational arithmetic, and binary64 arithmetic where a Real takes +// part, its exact operands rounded once to float64. An evaluation the evaluator would report — a zero divisor, an // Integer past the size budget — is an error, never a verdict. func replayTerm(t *Term, env map[string]replayValue) (replayValue, error) { switch t.Op { @@ -169,10 +172,8 @@ func replayTerm(t *Term, env map[string]replayValue) (replayValue, error) { case OpInt: return replayValue{kind: SortInt, i: semantics.BigIntValue(t.IntBig())}, nil case OpReal: - // The evaluator parses a real literal to the nearest float64, which - // Float64 on the exact rational also is. - f, _ := t.Real.Float64() - return replayValue{kind: SortReal, f: f}, nil + // A decimal literal is an exact Rational. + return exactReal(semantics.RatValue(t.Real)), nil case OpString: return replayValue{kind: SortString, s: t.Str}, nil case OpValue: @@ -223,6 +224,9 @@ func replayTerm(t *Term, env map[string]replayValue) (replayValue, error) { if val.kind == SortInt { return replayValue{kind: SortInt, i: semantics.IntNeg(val.i)}, nil } + if val.exact { + return exactReal(semantics.RatNeg(val.i)), nil + } if val.kind == SortReal { return replayValue{kind: SortReal, f: -val.f}, nil } @@ -243,9 +247,9 @@ func replayTerm(t *Term, env map[string]replayValue) (replayValue, error) { if val.kind != SortInt { return replayValue{}, fmt.Errorf("widening a non-integer") } - // The nearest float64 is the widening the evaluator applies where a - // real meets an integer, which rounds beyond 2^53. - return replayValue{kind: SortReal, f: val.i.AsReal()}, nil + // An Integer is a Rational exactly; arithmetic a Real takes part in + // rounds it then. + return exactReal(val.i), nil } return replayValue{}, fmt.Errorf("the replay does not define this term") } @@ -279,8 +283,9 @@ func replayJunction(t *Term, env map[string]replayValue) (replayValue, error) { return replayValue{kind: SortBool, b: !deciding}, nil } -// replayEquality compares as the evaluator compares: numbers through float64, -// which is what `valueEqual` delegates to, and everything else by identity. +// replayEquality compares as the evaluator compares: numbers by their exact +// values, a binary64 Real by the exact value it holds, and everything else by +// identity. func replayEquality(t *Term, env map[string]replayValue) (replayValue, error) { left, err := replayTerm(t.Args[0], env) if err != nil { @@ -293,7 +298,7 @@ func replayEquality(t *Term, env map[string]replayValue) (replayValue, error) { var eq bool switch { case left.numeric() && right.numeric(): - eq = left.asReal() == right.asReal() + eq = compareNumbers(left, right) == 0 case left.kind == SortBool && right.kind == SortBool: eq = left.b == right.b case left.kind == right.kind: @@ -307,8 +312,8 @@ func replayEquality(t *Term, env map[string]replayValue) (replayValue, error) { return replayValue{kind: SortBool, b: eq}, nil } -// replayComparison orders as the evaluator orders: two integers exactly, a -// mixed pair through float64. +// replayComparison orders as the evaluator orders: by exact value, a binary64 +// Real by the exact value it holds. func replayComparison(t *Term, env map[string]replayValue) (replayValue, error) { left, err := replayTerm(t.Args[0], env) if err != nil { @@ -335,44 +340,14 @@ func replayComparison(t *Term, env map[string]replayValue) (replayValue, error) if !left.numeric() || !right.numeric() { return replayValue{}, fmt.Errorf("ordering non-numbers") } - var res bool - if left.kind == SortInt && right.kind == SortInt { - order := semantics.CompareInt(left.i, right.i) - switch t.Op { - case OpLt: - res = order < 0 - case OpLe: - res = order <= 0 - case OpGt: - res = order > 0 - case OpGe: - res = order >= 0 - } - return replayValue{kind: SortBool, b: res}, nil - } - lf, rf := left.asReal(), right.asReal() - switch t.Op { - case OpLt: - res = lf < rf - case OpLe: - res = lf <= rf - case OpGt: - res = lf > rf - case OpGe: - res = lf >= rf - } + res, _ := semantics.OrderSatisfies(map[Op]ast.OperatorKind{OpLt: ast.OpLt, OpLe: ast.OpLe, OpGt: ast.OpGt, OpGe: ast.OpGe}[t.Op], compareNumbers(left, right)) return replayValue{kind: SortBool, b: res}, nil } -// replayArithmetic computes as the evaluator computes: int64 with a reported -// overflow, an integer quotient as the exact ratio rounded once, and float64 -// everywhere a real takes part, with a non-finite result reported. +// replayArithmetic computes as the evaluator computes: Integers and Rationals +// exactly within the size budget, an Integer quotient as the exact Rational, and +// binary64 where a Real takes part, with a non-finite result reported. func replayArithmetic(t *Term, env map[string]replayValue) (replayValue, error) { - // A whole-number quotient is the exact ratio rounded once to float64, not - // float64 division of widened operands. - if t.Op == OpDiv && t.IntRatio { - return replayRatio(t, env) - } left, err := replayTerm(t.Args[0], env) if err != nil { return replayValue{}, err @@ -385,14 +360,17 @@ func replayArithmetic(t *Term, env map[string]replayValue) (replayValue, error) return replayValue{}, fmt.Errorf("arithmetic on non-numbers") } astOp := map[Op]ast.OperatorKind{OpAdd: ast.OpAdd, OpSub: ast.OpSub, OpMul: ast.OpMul, OpDiv: ast.OpDiv}[t.Op] - if left.kind == SortInt && right.kind == SortInt { - if t.Op == OpDiv { - q, ok := semantics.IntQuotient(left.i, right.i) - if !ok { - return replayValue{}, fmt.Errorf("division by zero") - } - return replayValue{kind: SortReal, f: q}, nil + if left.isExact() && right.isExact() && (t.Op == OpDiv || left.kind != SortInt || right.kind != SortInt) { + res, err := semantics.RatArith(astOp, left.i, right.i, semantics.DefaultMaxIntegerBits) + if errors.Is(err, semantics.ErrDivisionByZero) { + return replayValue{}, fmt.Errorf("division by zero") + } + if err != nil { + return replayValue{}, err } + return exactReal(res), nil + } + if left.kind == SortInt && right.kind == SortInt { res, err := semantics.IntArith(astOp, left.i, right.i, semantics.DefaultMaxIntegerBits) if err != nil { return replayValue{}, err @@ -431,56 +409,34 @@ func replayIntDiv(t *Term, env map[string]replayValue) (replayValue, error) { return replayValue{kind: SortInt, i: semantics.BigIntValue(q)}, nil } -// replayRatio computes a whole-number quotient as the evaluator does: the exact -// ratio of its integer operands, rounded once to the nearest float64. -func replayRatio(t *Term, env map[string]replayValue) (replayValue, error) { - left, err := exactRat(t.Args[0], env) - if err != nil { - return replayValue{}, err - } - right, err := exactRat(t.Args[1], env) - if err != nil { - return replayValue{}, err - } - if right.Sign() == 0 { - return replayValue{}, fmt.Errorf("division by zero") - } - f, _ := new(big.Rat).Quo(left, right).Float64() - if math.IsInf(f, 0) || math.IsNaN(f) { - return replayValue{}, fmt.Errorf("the quotient is not a finite Real") - } - return replayValue{kind: SortReal, f: f}, nil -} - -// exactRat reads a ratio operand exactly, stripping the encoding's Int-to-Real -// widening so no float64 rounding precedes the quotient's own. -func exactRat(t *Term, env map[string]replayValue) (*big.Rat, error) { - if t.Op == OpToReal { - t = t.Args[0] - } - if t.Op == OpReal { - return t.Real, nil - } - val, err := replayTerm(t, env) - if err != nil { - return nil, err - } - if val.kind == SortInt { - return new(big.Rat).SetInt(val.i.BigInt()), nil - } - if val.kind == SortReal { - return new(big.Rat).SetFloat64(val.f), nil - } - return nil, fmt.Errorf("a quotient operand yielded no number") -} - // numeric reports whether the value is an integer or a real. func (v replayValue) numeric() bool { return v.kind == SortInt || v.kind == SortReal } -// asReal is the value as the evaluator widens it where a real meets it. +// isExact reports whether the value is an Integer or a Rational. +func (v replayValue) isExact() bool { return v.kind == SortInt || v.exact } + +// asReal is the value as the evaluator rounds it where a Real meets it. func (v replayValue) asReal() float64 { - if v.kind == SortInt { + if v.isExact() { return v.i.AsReal() } return v.f } + +// compareNumbers orders two numbers by exact value, a binary64 by the exact +// value it holds, as the evaluator's mixed comparison does. +func compareNumbers(a, b replayValue) int { + switch { + case a.isExact() && b.isExact(): + return semantics.CompareRat(a.i, b.i) + case a.isExact(): + return semantics.CompareExactReal(a.i, b.f) + case b.isExact(): + return -semantics.CompareExactReal(b.i, a.f) + case a.f < b.f: + return -1 + case a.f > b.f: + return 1 + } + return 0 +} diff --git a/internal/exec/solve/replay_test.go b/internal/exec/solve/replay_test.go index fbf45bf16e..cffb298594 100644 --- a/internal/exec/solve/replay_test.go +++ b/internal/exec/solve/replay_test.go @@ -7,9 +7,9 @@ import ( "testing" ) -// The half-ulp quotient (2^53 + 1) / 2 is not representable in float64: the -// evaluator answers the nearest double, the exact encoding holds the exact -// ratio. The replay confirms the witness under the evaluator's arithmetic. +// The half-ulp quotient (2^53 + 1) / 2 is not representable in float64, and +// both the evaluator and the encoding hold it as the exact Rational. The replay +// confirms the witness under the evaluator's arithmetic. func TestSolvedHalfUlpQuotientAgreesWithEvaluator(t *testing.T) { solver := requireSolver(t) wantQ, wantR := evaluatedDivision(t, 9007199254740993, 2) @@ -30,10 +30,8 @@ func TestSolvedHalfUlpQuotientAgreesWithEvaluator(t *testing.T) { values["test::C::q"], values["test::C::r"], wantQ, wantR) } - // Both spellings of the quotient satisfy the evaluator, which rounds each - // to the same float64; only the exact ratio satisfies the exact encoding. - // The exact-real unsat is not presented as an evaluator verdict: the query - // is marked rounded, which the REPL reports as undecided, not unsat. + // Only the exact ratio satisfies the evaluator and the encoding alike, so + // the query is not marked rounded: its unsat is the evaluator's verdict. for _, tc := range []struct { quotient string want Status @@ -50,8 +48,8 @@ func TestSolvedHalfUlpQuotientAgreesWithEvaluator(t *testing.T) { assert constraint { a / 2 == %s } } }`, tc.quotient), "test::C") - if !cond.Rounded() { - t.Errorf("a half-ulp quotient query is not marked rounded") + if cond.Rounded() { + t.Errorf("an exact Integer quotient query is marked rounded") } solved(t, solver, cond, tc.want) } @@ -65,13 +63,12 @@ func TestSolvedWitnessRejectedByEvaluatorIsUndecided(t *testing.T) { // Exactly x = 1/10 solves this, but float64 0.1 * 3.0 is not float64 0.3. "product": `in x : Real; assert constraint { x * 3.0 == 0.3 }`, - // Exactly 1/10 + 2/10 == 3/10, but float64 0.1 + 0.2 is not float64 0.3. + // Exactly 1/10 + 2/10 == 3/10, but Reals hold binary64 values, and no + // float64 is one tenth. "sum": `in x : Real; in y : Real; assert constraint { x == 0.1 } assert constraint { y == 0.2 } assert constraint { x + y == 0.3 }`, - // No variables at all: the empty witness is still replayed. - "constants": `assert constraint { 0.1 + 0.2 == 0.3 }`, } for name, body := range cases { t.Run(name, func(t *testing.T) { @@ -105,6 +102,35 @@ func TestSolvedWitnessConfirmedByEvaluatorStaysSat(t *testing.T) { solved(t, solver, q, StatusSat) } +// Rational arithmetic is exact in the evaluator (KerML 9.3.2.2.8), so the exact +// encoding's witness is confirmed and the query makes no rounded claim. +func TestSolvedExactRationalWitnessStaysSat(t *testing.T) { + solver := requireSolver(t) + cases := map[string]string{ + "sum": `in x : Rational; in y : Rational; + assert constraint { x == 0.1 } + assert constraint { y == 0.2 } + assert constraint { x + y == 0.3 }`, + "product": `in x : Rational; + assert constraint { x * 3.0 == 0.3 }`, + // No variables at all: the empty witness is still replayed. + "constants": `assert constraint { 0.1 + 0.2 == 0.3 }`, + } + for name, body := range cases { + t.Run(name, func(t *testing.T) { + q := constraintQuery(t, fmt.Sprintf(` + package test { + private import ScalarValues::Rational; + constraint def C { %s } + }`, body), "test::C") + if q.Rounded() { + t.Error("an exact Rational query is marked rounded") + } + solved(t, solver, q, StatusSat) + }) + } +} + // Rounded marks exactly the queries whose conditions the evaluator computes in // float64; a query over integers alone makes no rounded claim. func TestRoundedMarksFloatComputingQueries(t *testing.T) { @@ -130,10 +156,17 @@ func TestRoundedMarksFloatComputingQueries(t *testing.T) { if exact.Rounded() { t.Error("an integer-only query is marked rounded") } + x := &Var{Name: "x", Sort: Real, Binary64: true} objective := &Query{Objectives: []Objective{{ - Term: Binary(OpAdd, Real, RealTerm(big.NewRat(1, 10)), RealTerm(big.NewRat(1, 5))), + Term: Binary(OpAdd, Real, VarTerm(x), RealTerm(big.NewRat(1, 5))), }}} if !objective.Rounded() { t.Error("a query optimizing a real sum is not marked rounded") } + rational := &Query{Objectives: []Objective{{ + Term: Binary(OpAdd, Real, RealTerm(big.NewRat(1, 10)), RealTerm(big.NewRat(1, 5))), + }}} + if rational.Rounded() { + t.Error("a query optimizing an exact Rational sum is marked rounded") + } } diff --git a/internal/exec/solve/rounded_census_test.go b/internal/exec/solve/rounded_census_test.go index 6c8ee35634..1ef6673a21 100644 --- a/internal/exec/solve/rounded_census_test.go +++ b/internal/exec/solve/rounded_census_test.go @@ -15,7 +15,6 @@ package solve import ( "context" - "math/big" "os" "path/filepath" "sort" @@ -275,26 +274,15 @@ func exactQuery(q *Query) bool { } // exactCensusTerm mirrors roundedTerm, but proves exactness instead of assuming -// rounding: a rounding-capable node is exact only when it is constant and every -// intermediate value is exactly representable in float64. +// rounding: Integer and Rational arithmetic is exact in the evaluator, and +// arithmetic a Real variable takes part in is exact only when every value it +// computes is a float64, which no free variable makes provable. func exactCensusTerm(t *Term) bool { switch t.Op { - case OpReal: - if _, exact := t.Real.Float64(); !exact { - return false - } - case OpAdd, OpSub, OpMul, OpDiv, OpNeg: - if t.Sort.Kind == SortReal { - if _, ok := exactFold(t); !ok { - return false - } - return true - } - case OpToReal: - if _, ok := exactFold(t); !ok { + case OpAdd, OpSub, OpMul, OpDiv: + if t.Sort.Kind == SortReal && readsReal(t) { return false } - return true } for _, arg := range t.Args { if !exactCensusTerm(arg) { @@ -304,65 +292,20 @@ func exactCensusTerm(t *Term) bool { return true } -// exactFold folds a constant term exactly, reporting ok only when the evaluator -// computing it in float64 provably yields the same value: every intermediate a -// float64 rounds is exactly representable. -func exactFold(t *Term) (*big.Rat, bool) { +// readsReal reports whether arithmetic reaches a variable declared Real, whose +// values the evaluator holds in binary64. +func readsReal(t *Term) bool { switch t.Op { - case OpInt: - return new(big.Rat).SetInt(t.IntBig()), true - case OpReal: - if _, exact := t.Real.Float64(); !exact { - return nil, false - } - return t.Real, true - case OpToReal: - v, ok := exactFold(t.Args[0]) - if !ok { - return nil, false - } - // float64(int64) is exact only within ±2^53. - if _, exact := v.Float64(); !exact { - return nil, false - } - return v, true - case OpNeg: - v, ok := exactFold(t.Args[0]) - if !ok { - return nil, false - } - return new(big.Rat).Neg(v), true - case OpAdd, OpSub, OpMul, OpDiv: - left, ok := exactFold(t.Args[0]) - if !ok { - return nil, false - } - right, ok := exactFold(t.Args[1]) - if !ok { - return nil, false - } - var v *big.Rat - switch t.Op { - case OpAdd: - v = new(big.Rat).Add(left, right) - case OpSub: - v = new(big.Rat).Sub(left, right) - case OpMul: - v = new(big.Rat).Mul(left, right) - case OpDiv: - if right.Sign() == 0 { - return nil, false + case OpVar: + return t.Var.Binary64 + case OpAdd, OpSub, OpMul, OpDiv, OpNeg, OpIte: + for _, arg := range t.Args { + if readsReal(arg) { + return true } - v = new(big.Rat).Quo(left, right) } - if t.Sort.Kind == SortReal { - if _, exact := v.Float64(); !exact { - return nil, false - } - } - return v, true } - return nil, false + return false } // queryTriggers names what trips the Rounded marker in a query, deduplicated. @@ -385,53 +328,24 @@ func queryTriggers(q *Query) []string { return out } -// termTriggers classifies each node that trips roundedTerm: what rounds, and -// whether the node is constant (recoverable when exact) or over free values. +// termTriggers classifies each node that trips roundedTerm: binary64 +// arithmetic, over free values since a Real variable takes part. func termTriggers(t *Term, add func(string)) { switch t.Op { - case OpReal: - if _, exact := t.Real.Float64(); !exact { - add("inexact real literal") - } - case OpAdd, OpSub, OpMul, OpDiv, OpNeg: - if t.Sort.Kind == SortReal { - if _, ok := exactFold(t); ok { - add("constant real arithmetic (exact)") - } else if constantTerm(t) { - add("constant real arithmetic (inexact)") - } else if t.Op == OpDiv { + case OpAdd, OpSub, OpMul, OpDiv: + if t.Sort.Kind == SortReal && readsReal(t) { + if t.Op == OpDiv { add("division over free values") } else { add("real arithmetic over free values") } } - case OpToReal: - if _, ok := exactFold(t); ok { - add("integer widening (constant, exact)") - } else if constantTerm(t) { - add("integer widening (constant, beyond 2^53)") - } else { - add("integer widening over free values") - } } for _, arg := range t.Args { termTriggers(arg, add) } } -// constantTerm reports whether no variable takes part in the term. -func constantTerm(t *Term) bool { - if t.Op == OpVar { - return false - } - for _, arg := range t.Args { - if !constantTerm(arg) { - return false - } - } - return true -} - // report logs the census: per-corpus counts, then every marked query with what // trips it and what the solver answered. func report(t *testing.T, summaries map[string]*censusSummary) { diff --git a/internal/exec/solve/rounding.go b/internal/exec/solve/rounding.go index 47847647c2..3d1da78fa6 100644 --- a/internal/exec/solve/rounding.go +++ b/internal/exec/solve/rounding.go @@ -8,20 +8,19 @@ import ( // RoundingSound returns a new query over the same variables and sorts plus one // fresh Real variable per rounding site, whose models over-approximate every -// evaluation the runtime evaluator's float64 arithmetic performs, as replay.go -// models it. An unsatisfiable rounding-sound query therefore proves the exact +// evaluation the runtime evaluator's binary64 arithmetic performs, as replay.go +// models it; Integer and Rational arithmetic is exact and needs none. An unsatisfiable rounding-sound query therefore proves the exact // query's unsat holds under the evaluator's arithmetic too; the query itself is // never replayed — only its verdict is used. // -// A rounding site is a real-sorted `+`, `-`, `*` or `/` (a whole-number ratio -// counting as one site over its integer operands), a widened integer, or a real -// literal, which is replaced by the exact rational of the float64 the evaluator -// parses. Negation is exact and is not a site. Identical rewritten subterms -// share one site. +// A rounding site is a `+`, `-`, `*` or `/` a Real takes part in, or an exact +// operand of one, which the evaluator rounds once to binary64; an exact literal +// operand is replaced by the exact rational of its nearest float64. Negation is +// exact and is not a site. Identical rewritten subterms share one site. // // Each site variable r is bounded by the doubles the exact value v lies // between: for every double z the axioms state `(v <= z) => r <= z` and -// `(v >= z) => r >= z`, where z is every Real-sorted variable, the value 0 and +// `(v >= z) => r >= z`, where z is every Real variable, the value 0 and // every rewritten real literal (all always doubles), and every other site r' // whose guard is the condition under which the evaluator computes it. No guard // bounds a site by itself. @@ -149,8 +148,7 @@ type roundingDouble struct { site *roundingSite } -// doubles is the set Z the site's bounds are taken over: every Real-sorted -// variable, the value 0, every rewritten real literal, and every site guarded +// doubles is the set Z the site's bounds are taken over: every Real variable, the value 0, every rewritten real literal, and every site guarded // by its evaluation condition, each once. func (r *roundingRewrite) doubles(q *Query) []roundingDouble { var out []roundingDouble @@ -165,7 +163,7 @@ func (r *roundingRewrite) doubles(q *Query) []roundingDouble { } add(RealTerm(new(big.Rat)), nil, nil) for _, v := range q.Vars { - if v.Sort.Kind == SortReal { + if v.Binary64 { add(VarTerm(v), nil, nil) } } @@ -201,23 +199,18 @@ type roundingRewrite struct { } // rewrite returns the term's rounding-sound form under evaluation condition e: -// sites replaced by their variables, literals by their float64 values. A nil e +// sites replaced by their variables, exact literals binary64 arithmetic meets by +// their float64 values. A nil e // is the unconstrained path of the first condition, and BoolTerm(false) the // path of an assertion the evaluator does not check in order. func (r *roundingRewrite) rewrite(t, e *Term) *Term { switch t.Op { case OpReal: - return r.literal(t) - case OpDiv: - if t.IntRatio { - return r.ratio(t, e) - } - case OpToReal: - arg := r.rewrite(t.Args[0], e) - if arg.Op == OpInt { - return r.literal(RealTerm(new(big.Rat).SetInt(arg.IntBig()))) + // A literal binary64 holds exactly is a double the sites are bounded by. + if _, exact := t.Real.Float64(); exact { + r.literals = append(r.literals, t) } - return r.site(&Term{Op: OpToReal, Sort: Real, Args: []*Term{arg}}, e) + return t case OpNeg: return Unary(OpNeg, t.Sort, r.rewrite(t.Args[0], e)) case OpAnd, OpOr: @@ -245,14 +238,34 @@ func (r *roundingRewrite) rewrite(t, e *Term) *Term { otherwise := r.rewrite(t.Args[2], and(e, Not(cond))) return Ite(cond, then, otherwise) } - if (t.Op == OpAdd || t.Op == OpSub || t.Op == OpMul || t.Op == OpDiv) && t.Sort.Kind == SortReal { - return r.site(&Term{Op: t.Op, Sort: Real, Args: r.args(t, e)}, e) + if (t.Op == OpAdd || t.Op == OpSub || t.Op == OpMul || t.Op == OpDiv) && t.Sort.Kind == SortReal && t.Binary64() { + args := make([]*Term, len(t.Args)) + for i, arg := range t.Args { + args[i] = r.double(arg, e) + } + return r.site(&Term{Op: t.Op, Sort: Real, Args: args}, e) } return r.rebuild(t, e) } -// literal is a real literal as the evaluator parses it: the exact rational of -// its float64, which is what replay computes. A literal float64 cannot hold is +// double is an operand of binary64 arithmetic as the evaluator holds it: a +// binary64 value as it is, an exact literal as its nearest float64, and any +// other exact value rounded once at a site of its own. +func (r *roundingRewrite) double(t, e *Term) *Term { + if t.Binary64() { + return r.rewrite(t, e) + } + switch { + case t.Op == OpReal: + return r.literal(t) + case t.Op == OpToReal && t.Args[0].Op == OpInt: + return r.literal(RealTerm(new(big.Rat).SetInt(t.Args[0].IntBig()))) + } + return r.site(r.rewrite(t, e), e) +} + +// literal is an exact literal as binary64 arithmetic rounds it: the exact +// rational of its nearest float64, which is what replay computes. A literal float64 cannot hold is // kept exact, since the evaluator could not evaluate it anyway. func (r *roundingRewrite) literal(t *Term) *Term { f, _ := t.Real.Float64() @@ -264,22 +277,6 @@ func (r *roundingRewrite) literal(t *Term) *Term { return lit } -// ratio is a whole-number quotient's one site: the exact ratio of its integer -// operands, whose widenings are not separate sites. -func (r *roundingRewrite) ratio(t, e *Term) *Term { - left := t.Args[0] - right := t.Args[1] - if left.Op == OpToReal { - left = left.Args[0] - } - if right.Op == OpToReal { - right = right.Args[0] - } - a := r.rewrite(left, e) - b := r.rewrite(right, e) - return r.site(&Term{Op: OpDiv, Sort: Real, IntRatio: true, Args: []*Term{ToReal(a), ToReal(b)}}, e) -} - // args rewrites a term's operands under the same evaluation condition. func (r *roundingRewrite) args(t, e *Term) []*Term { args := make([]*Term, len(t.Args)) diff --git a/internal/exec/solve/rounding_test.go b/internal/exec/solve/rounding_test.go index 9dcc5fc13b..a4513a2625 100644 --- a/internal/exec/solve/rounding_test.go +++ b/internal/exec/solve/rounding_test.go @@ -8,11 +8,12 @@ import ( "testing" ) -// realVars are named Real-sorted variables for building queries by hand. +// realVars are named variables declared Real, held in binary64, for building +// queries by hand. func realVars(names ...string) []*Var { vars := make([]*Var, len(names)) for i, name := range names { - vars[i] = &Var{Name: name, Sort: Real} + vars[i] = &Var{Name: name, Sort: Real, Binary64: true} } return vars } @@ -86,54 +87,61 @@ func TestRoundingSoundSharedSites(t *testing.T) { } } -// TestRoundingSoundRatioIsOneSite: a whole-number quotient is the exact ratio -// rounded once, so its integer operands' widenings are not separate sites. -func TestRoundingSoundRatioIsOneSite(t *testing.T) { +// TestRoundingSoundRatioIsExact: a whole-number quotient is the exact Rational, +// so it is no site at all. +func TestRoundingSoundRatioIsExact(t *testing.T) { a := &Var{Name: "a", Sort: Int} b := &Var{Name: "b", Sort: Int} q := &Query{Kind: "constraint", Element: "C", Vars: []*Var{a, b}, Assertions: []Assertion{ {Term: Binary(OpGt, Bool, RatioDiv(VarTerm(a), VarTerm(b)), RealTerm(big.NewRat(1, 1))), From: Provenance{Role: RoleRequired}}, }} sound := q.RoundingSound() - if n := len(siteVars(sound)); n != 1 { - t.Fatalf("declared %d site variables, want the quotient's one: %v", n, sound.Vars) + if n := len(siteVars(sound)); n != 0 { + t.Fatalf("declared %d site variables, want none: %v", n, sound.Vars) } - if got := writeTerm(sound.Assertions[0].Term); got != "(> |rounded!!0| 1.0)" { + if got := writeTerm(sound.Assertions[0].Term); got != "(> (/ (to_real a) (to_real b)) 1.0)" { t.Errorf("rewritten assertion is %s", got) } } // TestRoundingSoundNegationIsExact: negation is not a site — the float64 -// negation is exact — while a widened integer is one, rounding beyond 2^53. +// negation is exact — while an Integer operand of Real arithmetic is one, +// rounding beyond 2^53, as is the sum. func TestRoundingSoundNegationIsExact(t *testing.T) { vars := realVars("x") i := &Var{Name: "i", Sort: Int} q := &Query{Kind: "constraint", Element: "C", Vars: append(vars, i), Assertions: []Assertion{ - {Term: Binary(OpGt, Bool, Unary(OpNeg, Real, VarTerm(vars[0])), ToReal(VarTerm(i))), From: Provenance{Role: RoleRequired}}, + {Term: Binary(OpGt, Bool, Binary(OpAdd, Real, Unary(OpNeg, Real, VarTerm(vars[0])), ToReal(VarTerm(i))), RealTerm(new(big.Rat))), From: Provenance{Role: RoleRequired}}, }} sound := q.RoundingSound() sites := siteVars(sound) - if len(sites) != 1 { - t.Fatalf("declared %d site variables, want only the widened integer's", len(sites)) + if len(sites) != 2 { + t.Fatalf("declared %d site variables, want the widened integer's and the sum's", len(sites)) } - if got := writeTerm(sound.Assertions[0].Term); got != "(> (- x) |rounded!!0|)" { + if got := writeTerm(sound.Assertions[0].Term); got != "(> |rounded!!1| 0.0)" { t.Errorf("rewritten assertion is %s", got) } } -// TestRoundingSoundLiteralIsTheFloat64: a real literal is the exact rational of -// the float64 the evaluator parses — 0.1 is not the real 1/10. +// TestRoundingSoundLiteralIsTheFloat64: an exact literal binary64 arithmetic +// meets is the exact rational of its nearest float64 — 0.1 + x rounds 1/10. func TestRoundingSoundLiteralIsTheFloat64(t *testing.T) { vars := realVars("x") tenth, _ := new(big.Rat).SetString("0.1") q := &Query{Kind: "constraint", Element: "C", Vars: vars, Assertions: []Assertion{ - {Term: Binary(OpGt, Bool, VarTerm(vars[0]), RealTerm(tenth)), From: Provenance{Role: RoleRequired}}, + {Term: Binary(OpGt, Bool, Binary(OpAdd, Real, VarTerm(vars[0]), RealTerm(tenth)), RealTerm(new(big.Rat))), From: Provenance{Role: RoleRequired}}, }} sound := q.RoundingSound() - comparison := sound.Assertions[0].Term - lit := comparison.Args[1] - if lit.Op != OpReal { - t.Fatalf("rewritten literal is %s", writeTerm(comparison)) + var lit *Term + for _, a := range axioms(sound) { + walkTerms(a.Term, func(t *Term) { + if t.Op == OpAdd && t.Args[1].Op == OpReal { + lit = t.Args[1] + } + }) + } + if lit == nil { + t.Fatalf("no rewritten sum in:\n%s", Script(sound)) } f, _ := tenth.Float64() exact := new(big.Rat).SetFloat64(f) @@ -147,8 +155,8 @@ func TestRoundingSoundLiteralIsTheFloat64(t *testing.T) { // the assertion order, and the operands deciding an `and`, `or`, `implies` or // `ite` within a term. func TestRoundingSoundEvaluationGuards(t *testing.T) { - x := &Var{Name: "x", Sort: Real} - y := &Var{Name: "y", Sort: Real} + x := &Var{Name: "x", Sort: Real, Binary64: true} + y := &Var{Name: "y", Sort: Real, Binary64: true} b := &Var{Name: "b", Sort: Bool} site0 := Binary(OpAdd, Real, VarTerm(x), RealTerm(big.NewRat(1, 1))) site1 := Binary(OpMul, Real, VarTerm(y), RealTerm(big.NewRat(2, 1))) @@ -346,8 +354,8 @@ func TestRoundingSoundDivergenceNotProved(t *testing.T) { // evaluation condition is unconditional, and no term in the rewritten query // carries a nil argument. func TestRoundingSoundSharedSiteOnTheOpenPath(t *testing.T) { - x := &Var{Name: "x", Sort: Real} - y := &Var{Name: "y", Sort: Real} + x := &Var{Name: "x", Sort: Real, Binary64: true} + y := &Var{Name: "y", Sort: Real, Binary64: true} sum := Binary(OpAdd, Real, VarTerm(x), VarTerm(y)) rw := &roundingRewrite{sites: map[string]*roundingSite{}} term := rw.rewrite(Or( @@ -407,3 +415,11 @@ func TestRoundingSoundSharedTautologyProves(t *testing.T) { t.Fatalf("result is %s proved=%v, want unsat proved", result.Status, result.RoundingProved) } } + +// walkTerms visits a term and every term beneath it. +func walkTerms(t *Term, visit func(*Term)) { + visit(t) + for _, arg := range t.Args { + walkTerms(arg, visit) + } +} diff --git a/internal/exec/solve/term.go b/internal/exec/solve/term.go index b0b5bc5967..a614fc1e81 100644 --- a/internal/exec/solve/term.go +++ b/internal/exec/solve/term.go @@ -108,10 +108,27 @@ type Term struct { Str string // IntRatio marks an OpDiv over widened integers: a whole-number quotient, - // which the evaluator computes as the exact ratio rounded once to float64. + // which the evaluator computes as the exact Rational ratio. IntRatio bool } +// Binary64 reports whether the evaluator computes the term in binary64: a Real +// variable, or arithmetic one takes part in. Integer and Rational arithmetic, +// literals included, is exact. +func (t *Term) Binary64() bool { + switch t.Op { + case OpVar: + return t.Var.Binary64 + case OpAdd, OpSub, OpMul, OpDiv, OpNeg, OpIte: + for _, arg := range t.Args { + if arg.Binary64() { + return true + } + } + } + return false +} + // Literal reports whether the term is a literal value, which is what makes a // product or a quotient linear. func (t *Term) Literal() bool { diff --git a/internal/exec/solve/translate.go b/internal/exec/solve/translate.go index b02a6ca7c0..bd8c4ecc20 100644 --- a/internal/exec/solve/translate.go +++ b/internal/exec/solve/translate.go @@ -868,6 +868,8 @@ func (t *translator) folded(n *ast.OperatorExpr) (*Term, bool) { switch val.Kind { case semantics.ValInt: return BigIntTerm(val.BigInt()), true + case semantics.ValRational: + return RealTerm(val.Rat()), true case semantics.ValReal: rat := new(big.Rat).SetFloat64(val.Real) if rat == nil { diff --git a/internal/frontend/core/core.go b/internal/frontend/core/core.go index befc285d76..6a757832a8 100644 --- a/internal/frontend/core/core.go +++ b/internal/frontend/core/core.go @@ -417,6 +417,14 @@ func attributeValue(value *symbolfacts.Value) *JValue { switch value.Const.Kind { case semantics.ValInt: return &JValue{IntValue: ptr(I64(value.Const.Int))} + case semantics.ValRational: + if f, exact := value.Const.BinaryExact(); exact { + return &JValue{RealValue: ptr(F64(f))} + } + return &JValue{RationalValue: &JRational{ + Numerator: value.Const.RatNumer().FormatInt(), + Denominator: value.Const.RatDenom().FormatInt(), + }} case semantics.ValReal: return &JValue{RealValue: ptr(F64(value.Const.Real))} case semantics.ValBool: diff --git a/internal/frontend/core/json.go b/internal/frontend/core/json.go index caa9322196..03c01976ed 100644 --- a/internal/frontend/core/json.go +++ b/internal/frontend/core/json.go @@ -186,6 +186,14 @@ type JValue struct { StringValue *string `json:"stringValue,omitempty"` Null *string `json:"null,omitempty"` Infinity *bool `json:"infinity,omitempty"` + // RationalValue is an exact Rational binary64 does not hold exactly. + RationalValue *JRational `json:"rationalValue,omitempty"` +} + +// JRational is an exact Rational in lowest terms, its denominator positive. +type JRational struct { + Numerator string `json:"numerator,omitempty"` + Denominator string `json:"denominator,omitempty"` } func oneofArms(message string, arms map[string]bool) error { diff --git a/internal/frontend/engine/json.go b/internal/frontend/engine/json.go index 1faa2f43e6..9d553e3a64 100644 --- a/internal/frontend/engine/json.go +++ b/internal/frontend/engine/json.go @@ -118,6 +118,13 @@ type JValue struct { TensorQuantity *JTensorQuantity `json:"tensorQuantity,omitempty"` Metaobject *JMetaobject `json:"metaobject,omitempty"` Undetermined *JUndetermined `json:"undetermined,omitempty"` + RationalValue *JRational `json:"rationalValue,omitempty"` +} + +// JRational is the Rational message: an exact rational in lowest terms. +type JRational struct { + Numerator string `json:"numerator,omitempty"` + Denominator string `json:"denominator,omitempty"` } // UnmarshalJSON rejects a value setting two arms of the oneof, as protojson @@ -131,8 +138,9 @@ func (v *JValue) UnmarshalJSON(data []byte) error { *v = JValue(a) return oneofArms("value", map[string]bool{ "intValue": v.IntValue != nil, "bigIntValue": v.BigIntValue != nil, - "realValue": v.RealValue != nil, - "boolValue": v.BoolValue != nil, "stringValue": v.StringValue != nil, + "rationalValue": v.RationalValue != nil, + "realValue": v.RealValue != nil, + "boolValue": v.BoolValue != nil, "stringValue": v.StringValue != nil, "instanceId": v.InstanceId != nil, "sequence": v.Sequence != nil, "null": v.Null != nil, "quantity": v.Quantity != nil, "enumLiteral": v.EnumLiteral != nil, "unset": v.Unset != nil, @@ -176,6 +184,8 @@ type JQuantity struct { Unit string `json:"unit,omitempty"` UnitTerm *JUnitTerm `json:"unitTerm,omitempty"` BigIntMagnitude *string `json:"bigIntMagnitude,omitempty"` + + RationalMagnitude *JRational `json:"rationalMagnitude,omitempty"` } // UnmarshalJSON rejects a quantity setting both magnitude arms of its oneof. @@ -188,7 +198,7 @@ func (q *JQuantity) UnmarshalJSON(data []byte) error { *q = JQuantity(a) return oneofArms("quantity", map[string]bool{ "intMagnitude": q.IntMagnitude != nil, "realMagnitude": q.RealMagnitude != nil, - "bigIntMagnitude": q.BigIntMagnitude != nil, + "bigIntMagnitude": q.BigIntMagnitude != nil, "rationalMagnitude": q.RationalMagnitude != nil, }) } diff --git a/internal/frontend/engine/values.go b/internal/frontend/engine/values.go index 827b17cfe8..dfbbd0a3ec 100644 --- a/internal/frontend/engine/values.go +++ b/internal/frontend/engine/values.go @@ -62,6 +62,11 @@ func valueToProto(rt *runtime.Context, val runtime.Value, idx *symbols.Index) *J return &JValue{IntValue: i64(n)} } return &JValue{BigIntValue: text(val.Const.FormatInt())} + case semantics.ValRational: + if f, exact := val.Const.BinaryExact(); exact { + return &JValue{RealValue: f64(f)} + } + return &JValue{RationalValue: rationalToProto(val.Const)} case semantics.ValReal: return &JValue{RealValue: f64(val.Const.Real)} case semantics.ValBool: @@ -345,6 +350,12 @@ func quantityToProto(q *runtime.Quantity) *JQuantity { } else { pq.BigIntMagnitude = text(q.Num.FormatInt()) } + case semantics.ValRational: + if f, exact := q.Num.BinaryExact(); exact { + pq.RealMagnitude = f64(f) + } else { + pq.RationalMagnitude = rationalToProto(q.Num) + } case semantics.ValReal: pq.RealMagnitude = f64(q.Num.Real) default: @@ -400,6 +411,8 @@ var ( errBigIntegerNotDecimal = errors.New("big Integer is not decimal") + errRationalNotCanonical = errors.New("rational is not canonical: not in lowest terms, or exactly a double") + errVectorQuantityEmpty = errors.New("vector quantity has no components") errMeasurementRefNeedsIndex = errors.New("a measurement reference needs the model's symbols to be converted from the wire") @@ -487,11 +500,32 @@ func protoToRuntimeValue(rt *runtime.Context, pv *JValue, idx *symbols.Index, se return runtime.Value{}, err } return runtime.Value{Kind: runtime.ValConst, Const: num}, nil + case pv.RationalValue != nil: + num, err := protoToRational(pv.RationalValue) + if err != nil { + return runtime.Value{}, err + } + return runtime.Value{Kind: runtime.ValConst, Const: num}, nil default: return protoToScalar(pv), nil } } +// rationalToProto marshals an exact Rational that is no Integer. +func rationalToProto(v semantics.Value) *JRational { + return &JRational{Numerator: v.RatNumer().FormatInt(), Denominator: v.RatDenom().FormatInt()} +} + +// protoToRational reads a rationalValue or rationalMagnitude, refusing one not +// in lowest terms or one a double holds exactly, which crosses as realValue. +func protoToRational(pr *JRational) (semantics.Value, error) { + v, ok := semantics.CanonicalRational(pr.Numerator, pr.Denominator) + if !ok { + return semantics.Value{}, fmt.Errorf("%w: %q/%q", errRationalNotCanonical, pr.Numerator, pr.Denominator) + } + return v, nil +} + // protoToBigInteger reads the decimal a bigIntValue or bigIntMagnitude // carries; one within int64 reads as the int64 Integer it equals. func protoToBigInteger(digits string) (semantics.Value, error) { @@ -688,6 +722,8 @@ func protoToNumber(pv *JValue) (semantics.Value, error) { return semantics.IntValue(int64(*pv.IntValue)), nil case pv.BigIntValue != nil: return protoToBigInteger(*pv.BigIntValue) + case pv.RationalValue != nil: + return protoToRational(pv.RationalValue) case pv.RealValue != nil: return semantics.Value{Kind: semantics.ValReal, Real: float64(*pv.RealValue)}, nil } @@ -745,6 +781,11 @@ func protoToQuantity(pq *JQuantity, idx *symbols.Index, sem *semantics.Model) (r if err != nil { return runtime.Value{}, fmt.Errorf("quantity in %q: %w", pq.Unit, err) } + case pq.RationalMagnitude != nil: + num, err = protoToRational(pq.RationalMagnitude) + if err != nil { + return runtime.Value{}, fmt.Errorf("quantity in %q: %w", pq.Unit, err) + } case pq.RealMagnitude != nil: num = semantics.Value{Kind: semantics.ValReal, Real: float64(*pq.RealMagnitude)} default: @@ -1248,6 +1289,12 @@ func protoToScalar(pv *JValue) runtime.Value { return runtime.Value{Kind: runtime.ValNull} } return runtime.Value{Kind: runtime.ValConst, Const: num} + case pv.RationalValue != nil: + num, err := protoToRational(pv.RationalValue) + if err != nil { + return runtime.Value{Kind: runtime.ValNull} + } + return runtime.Value{Kind: runtime.ValConst, Const: num} case pv.RealValue != nil: return runtime.Value{Kind: runtime.ValConst, Const: semantics.Value{Kind: semantics.ValReal, Real: float64(*pv.RealValue)}} case pv.BoolValue != nil: diff --git a/internal/frontend/grpc/capability_response.go b/internal/frontend/grpc/capability_response.go index 8d039c1bc9..2a54163329 100644 --- a/internal/frontend/grpc/capability_response.go +++ b/internal/frontend/grpc/capability_response.go @@ -89,7 +89,8 @@ func (s *Service) filterValueCapabilities(value *pb.Value) { if value == nil { return } - if !s.capabilities.has(CapabilityBigIntValues) && protoconv.ValueHoldsBigInt(value) { + if !s.capabilities.has(CapabilityBigIntValues) && protoconv.ValueHoldsBigInt(value) || + !s.capabilities.has(CapabilityRationalValues) && protoconv.ValueHoldsRational(value) { value.Kind = protoconv.UnsupportedShown(displayValue(value)) return } @@ -248,6 +249,10 @@ func displayQuantity(pq *pb.Quantity) runtime.Value { if big, err := protoconv.ProtoToBigInteger(m.BigIntMagnitude); err == nil { num = big } + case *pb.Quantity_RationalMagnitude: + if rational, err := protoconv.ProtoToRational(m.RationalMagnitude); err == nil { + num = rational + } case *pb.Quantity_RealMagnitude: num = semantics.Value{Kind: semantics.ValReal, Real: m.RealMagnitude} } @@ -266,7 +271,7 @@ func describeUnitTerm(term *pb.UnitTerm) string { } var parts []string if term.GetScaleNum() != term.GetScaleDen() { - parts = append(parts, fmt.Sprintf("%g/%g", term.GetScaleNum(), term.GetScaleDen())) + parts = append(parts, semantics.FormatScaleTerm(term.GetScaleNum())+"/"+semantics.FormatScaleTerm(term.GetScaleDen())) } for _, factor := range term.GetFactors() { if factor.GetExponent() == 1 { diff --git a/internal/frontend/grpc/convert_big_integer_test.go b/internal/frontend/grpc/convert_big_integer_test.go index e9969d2ed5..09fffafb55 100644 --- a/internal/frontend/grpc/convert_big_integer_test.go +++ b/internal/frontend/grpc/convert_big_integer_test.go @@ -162,8 +162,8 @@ package W { // rather than reading it as null. An Integer within int64 is unaffected. func TestBigIntCapability(t *testing.T) { ctx := context.Background() - if all := Capabilities(); all[len(all)-1] != CapabilityBigIntValues { - t.Errorf("capabilities %v do not end with %q, the newest", all, CapabilityBigIntValues) + if all := Capabilities(); all[len(all)-2] != CapabilityBigIntValues || all[len(all)-1] != CapabilityRationalValues { + t.Errorf("capabilities %v do not end with %q then %q, the newest", all, CapabilityBigIntValues, CapabilityRationalValues) } withheld := mustNewServiceWithout(t, CapabilityBigIntValues) diff --git a/internal/frontend/grpc/describe_test.go b/internal/frontend/grpc/describe_test.go index 1259c4e1b3..4a00cd894f 100644 --- a/internal/frontend/grpc/describe_test.go +++ b/internal/frontend/grpc/describe_test.go @@ -5,6 +5,7 @@ import ( "strconv" pb "github.com/Open-MBEE/OpenSysML/api/proto" + "github.com/Open-MBEE/OpenSysML/internal/frontend/protoconv" ) // describeValue reports the set oneof arm of a pb.Value and its payload. @@ -45,6 +46,10 @@ func describeQuantity(q *pb.Quantity) string { magnitude = strconv.FormatInt(m.IntMagnitude, 10) case *pb.Quantity_RealMagnitude: magnitude = strconv.FormatFloat(m.RealMagnitude, 'g', -1, 64) + case *pb.Quantity_RationalMagnitude: + if r, err := protoconv.ProtoToRational(m.RationalMagnitude); err == nil { + magnitude = r.FormatRational() + } } return fmt.Sprintf("%s [%s] = %s", magnitude, q.GetUnit(), describeUnitTerm(q.GetUnitTerm())) } diff --git a/internal/frontend/grpc/docquery.go b/internal/frontend/grpc/docquery.go index 88576de7cf..f89c12d6f9 100644 --- a/internal/frontend/grpc/docquery.go +++ b/internal/frontend/grpc/docquery.go @@ -59,6 +59,11 @@ func (s *Service) RunDocumentQuery(ctx context.Context, req *pb.RunDocumentQuery return nil, err } } + if slices.ContainsFunc(req.Bindings, bindingHoldsRational) { + if err := s.requireCapability(CapabilityRationalValues); err != nil { + return nil, err + } + } bindings, err := documentBindings(qctx.Index, qctx.Model, held, req.Bindings) if err != nil { return nil, err @@ -73,9 +78,24 @@ func (s *Service) RunDocumentQuery(ctx context.Context, req *pb.RunDocumentQuery return nil, statusErrorf(connect.CodeUnimplemented, "capability %q is unavailable: query %s answers an Integer beyond int64", CapabilityBigIntValues, req.QueryId) } + if !s.capabilities.has(CapabilityRationalValues) && slices.ContainsFunc(response.Rows, rowHoldsRational) { + return nil, statusErrorf(connect.CodeUnimplemented, + "capability %q is unavailable: query %s answers an exact Rational", CapabilityRationalValues, req.QueryId) + } return response, nil } +func bindingHoldsRational(binding *pb.DocumentQueryBinding) bool { + return slices.ContainsFunc(binding.GetValues(), protoconv.DocumentValueHoldsRational) +} + +func rowHoldsRational(row *pb.DocumentQueryRow) bool { + return protoconv.DocumentValueHoldsRational(row.GetElement()) || + slices.ContainsFunc(row.GetCells(), func(cell *pb.DocumentQueryCell) bool { + return slices.ContainsFunc(cell.GetValues(), protoconv.DocumentValueHoldsRational) + }) +} + func bindingHoldsBigInt(binding *pb.DocumentQueryBinding) bool { return slices.ContainsFunc(binding.GetValues(), protoconv.DocumentValueHoldsBigInt) } @@ -224,6 +244,12 @@ func boundValue(idx *symbols.Index, sem *semantics.Model, held *heldObjects, par return queryexec.Value{}, statusErrorf(connect.CodeInvalidArgument, "binding %s: %v", parameter, err) } return queryexec.IntegerOf(integer), nil + case *pb.DocumentValue_RationalValue: + rational, err := protoconv.ProtoToRational(kind.RationalValue) + if err != nil { + return queryexec.Value{}, statusErrorf(connect.CodeInvalidArgument, "binding %s: %v", parameter, err) + } + return queryexec.RationalOf(rational), nil case *pb.DocumentValue_RealValue: return queryexec.RealValue(kind.RealValue), nil case *pb.DocumentValue_BoolValue: @@ -317,6 +343,12 @@ func documentValue(idx *symbols.Index, value queryexec.Value) *pb.DocumentValue return &pb.DocumentValue{Kind: &pb.DocumentValue_IntValue{IntValue: n}} } return &pb.DocumentValue{Kind: &pb.DocumentValue_BigIntValue{BigIntValue: integer.FormatInt()}} + case queryexec.ValueRational: + rational, _ := value.Rational() + if f, exact := rational.BinaryExact(); exact { + return &pb.DocumentValue{Kind: &pb.DocumentValue_RealValue{RealValue: f}} + } + return &pb.DocumentValue{Kind: &pb.DocumentValue_RationalValue{RationalValue: protoconv.RationalToProto(rational)}} case queryexec.ValueReal: realVal, _ := value.Real() return &pb.DocumentValue{Kind: &pb.DocumentValue_RealValue{RealValue: realVal}} diff --git a/internal/frontend/grpc/quantity_test.go b/internal/frontend/grpc/quantity_test.go index 5a6110063f..39454bc39a 100644 --- a/internal/frontend/grpc/quantity_test.go +++ b/internal/frontend/grpc/quantity_test.go @@ -93,6 +93,7 @@ func TestQuantityCrossesTheWire(t *testing.T) { expr string unit string real float64 + rational string intVal int64 isInt bool reduction string @@ -102,15 +103,16 @@ func TestQuantityCrossesTheWire(t *testing.T) { {expr: "5.0 [SI::kg]", unit: "SI::kg", real: 5.0, reduction: "1000/1·SI::gram", wantScaled: true}, {expr: "3 [SI::m]", unit: "SI::m", intVal: 3, isInt: true, reduction: "SI::metre"}, {expr: "10.0 [SI::m] / 2.0 [SI::s]", unit: "SI::'m/s'", real: 5.0, reduction: "SI::metre·SI::second^-1"}, - {expr: "5.4 [SI::km/SI::h]", unit: "SI::km/SI::h", real: 5.4, reduction: "5/18·SI::metre·SI::second^-1", wantScaled: true}, + {expr: "5.4 [SI::km/SI::h]", unit: "SI::km/SI::h", rational: "27/5", reduction: "5/18·SI::metre·SI::second^-1", wantScaled: true}, // Grouping the notation needs survives, so the text reads back as the unit written. {expr: "3.0 [SI::m/(SI::s*SI::kg)]", unit: "SI::m/(SI::s*SI::kg)", real: 3.0, reduction: "1/1000·SI::gram^-1·SI::metre·SI::second^-1", wantScaled: true}, {expr: "4.0 [(SI::m*SI::s)**2]", unit: "(SI::m*SI::s)**2", real: 4.0, reduction: "SI::metre^2·SI::second^2"}, {expr: "8.0 [(SI::m**2)**3]", unit: "(SI::m**2)**3", real: 8.0, reduction: "SI::metre^6"}, // A point on a measurement scale carries the scale by name and as its one // factor: a point reduces to no unit, so the scale itself is the reduction. - {expr: "26.85 [SI::'°C_abs']", unit: "'°C_abs'", real: 26.85, reduction: "SI::degree celsius (absolute temperature scale)"}, - {expr: "26.85 [SI::'°C_abs'] + 10.0 [SI::'°C']", unit: "'°C_abs'", real: 36.85, reduction: "SI::degree celsius (absolute temperature scale)"}, + // A decimal no double holds crosses exactly, as a Rational. + {expr: "26.85 [SI::'°C_abs']", unit: "'°C_abs'", rational: "537/20", reduction: "SI::degree celsius (absolute temperature scale)"}, + {expr: "26.85 [SI::'°C_abs'] + 10.0 [SI::'°C']", unit: "'°C_abs'", rational: "737/20", reduction: "SI::degree celsius (absolute temperature scale)"}, {expr: "30.0 [SI::'°C_abs'] - 20.0 [SI::'°C_abs']", unit: "'°C'", real: 10.0, reduction: "SI::kelvin"}, {expr: "5.0 [Time::UTC] + 3.0 [SI::s]", unit: "UTC", real: 8.0, reduction: "Time::Coordinated Universal Time"}, } @@ -125,6 +127,11 @@ func TestQuantityCrossesTheWire(t *testing.T) { if got.GetIntMagnitude() != tc.intVal { t.Errorf("int_magnitude = %d, want %d", got.GetIntMagnitude(), tc.intVal) } + } else if tc.rational != "" { + r := got.GetRationalMagnitude() + if r.GetNumerator()+"/"+r.GetDenominator() != tc.rational { + t.Errorf("rational_magnitude = %v, want %s", r, tc.rational) + } } else if got.GetRealMagnitude() != tc.real { t.Errorf("real_magnitude = %v, want %v", got.GetRealMagnitude(), tc.real) } @@ -660,8 +667,8 @@ package Imperial { // Two packages declaring one short name for the same unit is an ambiguity the // reduction cannot settle: the text stays opaque rather than picked at random. got = describeQuantity(evaluate("Q::Area", fathom, inFull("Nautical::fathom", fathom.GetUnitTerm()))) - if got != "4 [Nautical::fathom*fathom] = 3.34450944/1·SI::metre^2" { - t.Errorf("ambiguous fathom * Nautical::fathom over the wire = %s, want 4 [Nautical::fathom*fathom] = 3.34450944/1·SI::metre^2", got) + if got != "4 [Nautical::fathom*fathom] = 1306449/390625·SI::metre^2" { + t.Errorf("ambiguous fathom * Nautical::fathom over the wire = %s, want 4 [Nautical::fathom*fathom] = 1306449/390625·SI::metre^2", got) } // One short name written twice may name two units, each read where the // reduction puts it: `cable*cable` over both cables is Nautical::cable times @@ -701,7 +708,7 @@ package Imperial { {"speed times seconds", "Q::Dist", []*pb.Quantity{unnamed(speed.GetUnitTerm()), second}, "2 [SI::metre] = SI::metre"}, {"speed times a metre", "Q::Area", []*pb.Quantity{unnamed(speed.GetUnitTerm()), metre}, "4 [SI::'m²⋅s⁻¹'] = SI::metre^2·SI::second^-1"}, {"kilometres times a metre", "Q::Area", []*pb.Quantity{unnamed(byHand.GetUnitTerm()), metre}, "4 ['1000·metre'*m] = 1000/1·SI::metre^2"}, - {"kilometres alone", "Q::Area", []*pb.Quantity{unnamed(byHand.GetUnitTerm()), unnamed(byHand.GetUnitTerm())}, "4 ['1000·metre'**2] = 1e+06/1·SI::metre^2"}, + {"kilometres alone", "Q::Area", []*pb.Quantity{unnamed(byHand.GetUnitTerm()), unnamed(byHand.GetUnitTerm())}, "4 ['1000·metre'**2] = 1000000/1·SI::metre^2"}, } { if got := describeQuantity(evaluate(tc.calc, tc.args...)); got != tc.want { t.Errorf("%s, sent without unit text, = %s, want %s", tc.name, got, tc.want) @@ -710,8 +717,8 @@ package Imperial { // A scaled ratio sent nameless names no dimension-one unit: it scales what it // multiplies and cancels to a number, as an unnamed ratio does locally. hundredth := unnamed(&pb.UnitTerm{ScaleNum: 1, ScaleDen: 100}) - if got := describeQuantity(evaluate("Q::Area", hundredth, metre)); got != "4 ['1/100'*m] = 1/100·SI::metre" { - t.Errorf("a nameless hundredth * m = %s, want 4 ['1/100'*m] = 1/100·SI::metre", got) + if got := describeQuantity(evaluate("Q::Area", hundredth, metre)); got != "4 ['0.01'*m] = 1/100·SI::metre" { + t.Errorf("a nameless hundredth * m = %s, want 4 ['0.01'*m] = 1/100·SI::metre", got) } resp, err := srv.EvaluateCalc(context.Background(), &pb.EvaluateCalcRequest{ ModelHash: hash, SymbolId: "Q::Area", @@ -811,9 +818,9 @@ package Imperial { { "a short name two units bear", sent("fathom", fathomTerm), nauticalFathom, - "6 [Nautical::fathom*fathom] = 3.34450944/1·SI::metre^2", - "6 [fathom] = 3.34450944/1.8288·SI::metre", - "1 [Nautical::fathom] = 3.34450944/1.8288·SI::metre", + "6 [Nautical::fathom*fathom] = 1306449/390625·SI::metre^2", + "6 [fathom] = 1143/625·SI::metre", + "1 [Nautical::fathom] = 1143/625·SI::metre", }, { "a short name spelling a keyword", diff --git a/internal/frontend/grpc/service.go b/internal/frontend/grpc/service.go index a05ebe2216..48dd769867 100644 --- a/internal/frontend/grpc/service.go +++ b/internal/frontend/grpc/service.go @@ -209,6 +209,12 @@ const CapabilityInfinityValue = "infinity_value" // without it reads the arm sent to it as null, so a client must not send one. const CapabilityBigIntValues = "big_int_values" +// CapabilityRationalValues names the capability of carrying an exact Rational +// that is no Integer as Value.rational_value, Quantity.rational_magnitude and +// DocumentValue.rational_value, rather than as an unsupported null. A service +// without it reads the arm sent to it as null, so a client must not send one. +const CapabilityRationalValues = "rational_values" + // CapabilityDiagnosticCodes names the capability of populating Diagnostic.code, // so an empty code is a finding none was assigned rather than an older service. const CapabilityDiagnosticCodes = "diagnostic_codes" @@ -285,6 +291,7 @@ var capabilities = []string{ CapabilityActionBodyStatementAuthoring, CapabilityMigrate, CapabilityBigIntValues, + CapabilityRationalValues, } type capabilityAvailability struct { @@ -526,7 +533,12 @@ func (s *Service) requireValueCapabilities(pv *pb.Value) error { } } if protoconv.ValueCarriesBigInt(pv) { - return s.requireCapability(CapabilityBigIntValues) + if err := s.requireCapability(CapabilityBigIntValues); err != nil { + return err + } + } + if protoconv.ValueCarriesRational(pv) { + return s.requireCapability(CapabilityRationalValues) } return nil } diff --git a/internal/frontend/grpc/typefacts_test.go b/internal/frontend/grpc/typefacts_test.go index 5f58e5ea60..0c0be2c165 100644 --- a/internal/frontend/grpc/typefacts_test.go +++ b/internal/frontend/grpc/typefacts_test.go @@ -104,7 +104,8 @@ package Demo { } `) cases := []struct{ fqn, prim string }{ - {"Demo::Vehicle::mass", "Real"}, + // A LiteralRational's value is classified in Rational (KerML 8.4.4.9.2). + {"Demo::Vehicle::mass", "Rational"}, {"Demo::Vehicle::doors", "Integer"}, {"Demo::Vehicle::electric", "Boolean"}, {"Demo::Vehicle::name", "String"}, diff --git a/internal/frontend/protoconv/convert.go b/internal/frontend/protoconv/convert.go index 99182118f0..65e4691d10 100644 --- a/internal/frontend/protoconv/convert.go +++ b/internal/frontend/protoconv/convert.go @@ -59,6 +59,11 @@ func ValueToProtoIn(rt *runtime.Context, val runtime.Value, idx *symbols.Index) return &pb.Value{Kind: &pb.Value_IntValue{IntValue: n}} } return &pb.Value{Kind: &pb.Value_BigIntValue{BigIntValue: val.Const.FormatInt()}} + case semantics.ValRational: + if f, exact := val.Const.BinaryExact(); exact { + return &pb.Value{Kind: &pb.Value_RealValue{RealValue: f}} + } + return &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: RationalToProto(val.Const)}} case semantics.ValReal: return &pb.Value{Kind: &pb.Value_RealValue{RealValue: val.Const.Real}} case semantics.ValBool: @@ -337,6 +342,12 @@ func QuantityToProto(q *runtime.Quantity) *pb.Quantity { } else { pq.Magnitude = &pb.Quantity_BigIntMagnitude{BigIntMagnitude: q.Num.FormatInt()} } + case semantics.ValRational: + if f, exact := q.Num.BinaryExact(); exact { + pq.Magnitude = &pb.Quantity_RealMagnitude{RealMagnitude: f} + } else { + pq.Magnitude = &pb.Quantity_RationalMagnitude{RationalMagnitude: RationalToProto(q.Num)} + } case semantics.ValReal: pq.Magnitude = &pb.Quantity_RealMagnitude{RealMagnitude: q.Num.Real} default: @@ -345,6 +356,21 @@ func QuantityToProto(q *runtime.Quantity) *pb.Quantity { return pq } +// RationalToProto marshals an exact Rational no binary64 holds exactly. +func RationalToProto(v semantics.Value) *pb.Rational { + return &pb.Rational{Numerator: v.RatNumer().FormatInt(), Denominator: v.RatDenom().FormatInt()} +} + +// ProtoToRational reads a Rational, refusing one not in lowest terms or one a +// double holds exactly, which crosses as real_value. +func ProtoToRational(pr *pb.Rational) (semantics.Value, error) { + v, ok := semantics.CanonicalRational(pr.GetNumerator(), pr.GetDenominator()) + if !ok { + return semantics.Value{}, fmt.Errorf("%w: %q/%q", ErrRationalNotCanonical, pr.GetNumerator(), pr.GetDenominator()) + } + return v, nil +} + // UnitTermToProto marshals a unit's reduction to base units, naming each base // unit by qualified name: a symbol pointer means nothing to another process. func UnitTermToProto(term semantics.UnitTerm) *pb.UnitTerm { @@ -417,6 +443,10 @@ var ( // that is not an optionally signed run of decimal digits. ErrBigIntegerNotDecimal = errors.New("big Integer is not decimal") + // ErrRationalNotCanonical reports a rational_value or rational_magnitude + // that is not in lowest terms, or that a double holds exactly. + ErrRationalNotCanonical = errors.New("rational is not canonical: not in lowest terms, or exactly a double") + // ErrVectorQuantityEmpty reports a vector quantity of no components, whose // num is Number[1..*]. ErrVectorQuantityEmpty = errors.New("vector quantity has no components") @@ -580,6 +610,57 @@ func quantityHoldsBigInt(q *pb.Quantity) bool { return ok } +// ValueCarriesRational reports whether a value, or any value nested in it, holds +// an exact Rational: the arms the rational_values capability governs. +func ValueCarriesRational(pv *pb.Value) bool { + return valueCarries(pv, ValueHoldsRational) +} + +// ValueHoldsRational reports whether a value is itself an exact Rational or +// carries one as a vector component or a quantity magnitude, its own or a +// component's; a collection's elements are left to the caller. +func ValueHoldsRational(pv *pb.Value) bool { + switch k := pv.GetKind().(type) { + case *pb.Value_RationalValue: + return true + case *pb.Value_Vector: + return slices.ContainsFunc(k.Vector.GetComponents(), isRationalValue) + case *pb.Value_Quantity: + return quantityHoldsRational(k.Quantity) + case *pb.Value_VectorQuantity: + return slices.ContainsFunc(k.VectorQuantity.GetComponents(), quantityHoldsRational) + case *pb.Value_TensorQuantity: + return slices.ContainsFunc(k.TensorQuantity.GetComponents(), quantityHoldsRational) + case *pb.Value_EnumLiteral: + return k.EnumLiteral.GetValue() != nil && ValueCarriesRational(k.EnumLiteral.GetValue()) + } + return false +} + +// DocumentValueHoldsRational reports whether a document value is an exact +// Rational, a quantity whose magnitude is one, or an event row whose time is. +func DocumentValueHoldsRational(dv *pb.DocumentValue) bool { + switch k := dv.GetKind().(type) { + case *pb.DocumentValue_RationalValue: + return true + case *pb.DocumentValue_Quantity: + return quantityHoldsRational(k.Quantity) + case *pb.DocumentValue_Event: + return DocumentValueHoldsRational(k.Event.GetTime()) + } + return false +} + +func isRationalValue(pv *pb.Value) bool { + _, ok := pv.GetKind().(*pb.Value_RationalValue) + return ok +} + +func quantityHoldsRational(q *pb.Quantity) bool { + _, ok := q.GetMagnitude().(*pb.Quantity_RationalMagnitude) + return ok +} + // ValueCarriesComplex reports whether a value, or any value nested in it, is a // Complex: the kind the complex_values capability governs. func ValueCarriesComplex(pv *pb.Value) bool { @@ -693,6 +774,12 @@ func ProtoToRuntimeValue(rt *runtime.Context, pv *pb.Value, idx *symbols.Index, return runtime.Value{}, err } return runtime.Value{Kind: runtime.ValConst, Const: num}, nil + case *pb.Value_RationalValue: + num, err := ProtoToRational(k.RationalValue) + if err != nil { + return runtime.Value{}, err + } + return runtime.Value{Kind: runtime.ValConst, Const: num}, nil default: return ProtoToScalar(pv), nil } @@ -869,6 +956,8 @@ func protoToNumber(pv *pb.Value) (semantics.Value, error) { return semantics.IntValue(k.IntValue), nil case *pb.Value_BigIntValue: return ProtoToBigInteger(k.BigIntValue) + case *pb.Value_RationalValue: + return ProtoToRational(k.RationalValue) case *pb.Value_RealValue: return semantics.Value{Kind: semantics.ValReal, Real: k.RealValue}, nil } @@ -926,6 +1015,11 @@ func ProtoToQuantity(pq *pb.Quantity, idx *symbols.Index, sem *semantics.Model) if err != nil { return runtime.Value{}, fmt.Errorf("quantity in %q: %w", pq.GetUnit(), err) } + case *pb.Quantity_RationalMagnitude: + num, err = ProtoToRational(m.RationalMagnitude) + if err != nil { + return runtime.Value{}, fmt.Errorf("quantity in %q: %w", pq.GetUnit(), err) + } case *pb.Quantity_RealMagnitude: num = semantics.Value{Kind: semantics.ValReal, Real: m.RealMagnitude} default: @@ -1436,6 +1530,12 @@ func ProtoToScalar(pv *pb.Value) runtime.Value { return runtime.Value{Kind: runtime.ValNull} } return runtime.Value{Kind: runtime.ValConst, Const: num} + case *pb.Value_RationalValue: + num, err := ProtoToRational(k.RationalValue) + if err != nil { + return runtime.Value{Kind: runtime.ValNull} + } + return runtime.Value{Kind: runtime.ValConst, Const: num} case *pb.Value_RealValue: return runtime.Value{Kind: runtime.ValConst, Const: semantics.Value{Kind: semantics.ValReal, Real: k.RealValue}} case *pb.Value_BoolValue: diff --git a/internal/frontend/repl/check_test.go b/internal/frontend/repl/check_test.go index a66ba932ff..30783597ab 100644 --- a/internal/frontend/repl/check_test.go +++ b/internal/frontend/repl/check_test.go @@ -94,11 +94,12 @@ func TestCheckLeavesRoundedUnsatUndecided(t *testing.T) { requireSolver(t) s := checkSession(t, ` package Check { - private import ScalarValues::Integer; + private import ScalarValues::Real; constraint def HalfUlp { - in a : Integer; - assert constraint { a == 9007199254740993 } - assert constraint { a / 2 == 4503599627370496.0 } + in a : Real; + in b : Real; + in c : Real; + assert constraint { (a + b) + c != a + (b + c) } } }`) got := run(t, s, "%check HalfUlp") @@ -110,6 +111,28 @@ func TestCheckLeavesRoundedUnsatUndecided(t *testing.T) { } } +// An Integer quotient is an exact Rational, so an exact-real unsat about it is +// the evaluator's own verdict. +func TestCheckReportsExactQuotientUnsat(t *testing.T) { + requireSolver(t) + s := checkSession(t, ` + package Check { + private import ScalarValues::Integer; + constraint def HalfUlp { + in a : Integer; + assert constraint { a == 9007199254740993 } + assert constraint { a / 2 == 4503599627370496.0 } + } + }`) + got := run(t, s, "%check HalfUlp") + wants(t, got, "✗ Constraint HalfUlp is unsatisfiable") + rejects(t, got, "rounds these conditions in floating point") + + if reports := s.CheckSolve("HalfUlp"); reports[0].Status != SolveUnsat { + t.Errorf("status is %s, want unsat", reports[0].Status) + } +} + func TestCheckSolvesARequirement(t *testing.T) { requireSolver(t) s := checkSession(t, checkModel) diff --git a/internal/frontend/repl/compile_test.go b/internal/frontend/repl/compile_test.go index b978b02f58..880afe0cb9 100644 --- a/internal/frontend/repl/compile_test.go +++ b/internal/frontend/repl/compile_test.go @@ -36,6 +36,9 @@ var compiledCases = []compiledCase{ {"Neg", []string{"5"}}, {"Neg", []string{"-9223372036854775808"}}, {"Logic", []string{"false", "true", "0"}}, {"Logic", []string{"false", "false", "0"}}, {"Logic", []string{"true", "true", "0"}}, {"Logic", []string{"true", "true", "5"}}, + {"ExactRational", []string{"6", "0.1"}}, {"ExactRational", []string{"5", "0.0"}}, {"ExactRational", []string{"7", "0.09"}}, + {"ExactRational", []string{"-6", "-1e308"}}, {"ExactRational", []string{"6", "0.1000000000000001"}}, + {"ExactWide", []string{"3"}}, {"ExactWide", []string{"-3"}}, {"ExactWide", []string{"0"}}, {"Compare", []string{"2.0", "2"}}, {"Compare", []string{"2.5", "2"}}, {"Compare", []string{"0.1", "1"}}, {"Sign", []string{"-9"}}, {"Sign", []string{"0"}}, {"Sign", []string{"3"}}, {"FirstAbove", []string{"50"}}, {"FirstAbove", []string{"0"}}, @@ -553,6 +556,10 @@ func TestCompileRefusesWhatItCannotCompile(t *testing.T) { {"Narrowed", "a Real bound to x, which is Integer"}, {"RecordParam", "type Refused::Point is not Integer, Real or Boolean"}, {"EnumParam", "type Refused::Color is not Integer, Real or Boolean"}, + {"RationalParam", "type ScalarValues::Rational is not Integer, Real or Boolean"}, + {"ExactProduct", "exact Rational arithmetic '*' over a value binary64 does not hold exactly"}, + {"ExactPower", "'**' of an exact Rational by an Integer exponent"}, + {"ExactCompare", "'<' of an exact Rational binary64 does not hold exactly"}, {"Extent", "operator 'all'"}, {"RealIntIdentity", "'===' between Real and Integer"}, {"SelectNonBoolean", "select whose body yields Integer, not a Boolean"}, diff --git a/internal/frontend/repl/docquery.go b/internal/frontend/repl/docquery.go index 6b43ea82ed..714cba0842 100644 --- a/internal/frontend/repl/docquery.go +++ b/internal/frontend/repl/docquery.go @@ -428,6 +428,9 @@ func formatQueryValue(value queryexec.Value) string { if integer, ok := value.Integer(); ok { return strconv.FormatInt(integer, 10) } + if rational, ok := value.Rational(); ok { + return semantics.FormatConst(rational) + } if realVal, ok := value.Real(); ok { return semantics.FormatReal(realVal) } diff --git a/internal/frontend/repl/evalin_test.go b/internal/frontend/repl/evalin_test.go index b8d3fd5d48..73ed65e320 100644 --- a/internal/frontend/repl/evalin_test.go +++ b/internal/frontend/repl/evalin_test.go @@ -184,7 +184,7 @@ func TestEvalInDeclarationScopeCompoundsOverValuelessFeaturesAreUndetermined(t * } // A chain over the four wheels holds four values, which no scalar operator takes. got := run(t, s, "%eval in car : wheels.radius * 2.0") - wants(t, got, "type mismatch", "an undetermined Real sequence and a Real") + wants(t, got, "type mismatch", "an undetermined Real sequence and a Rational") rejects(t, got, "no value for feature", "= "+runtime.UndeterminedText) for _, expr := range []string{"car::unsetMass + 1.0", "car::doubled"} { got := run(t, s, "%eval "+expr) diff --git a/internal/frontend/repl/explain_test.go b/internal/frontend/repl/explain_test.go index 86dc518ec1..c0fd5e091c 100644 --- a/internal/frontend/repl/explain_test.go +++ b/internal/frontend/repl/explain_test.go @@ -42,11 +42,12 @@ func TestExplainLeavesRoundedUnsatUndecided(t *testing.T) { requireSolver(t) s := checkSession(t, ` package Explain { - private import ScalarValues::Integer; + private import ScalarValues::Real; constraint def HalfUlp { - in a : Integer; - assert constraint { a == 9007199254740993 } - assert constraint { a / 2 == 4503599627370496.0 } + in a : Real; + in b : Real; + in c : Real; + assert constraint { (a + b) + c != a + (b + c) } } }`) got := run(t, s, "%explain HalfUlp") diff --git a/internal/frontend/repl/testdata/compile_calcs.sysml b/internal/frontend/repl/testdata/compile_calcs.sysml index f0bc1da197..947b9ef7b9 100644 --- a/internal/frontend/repl/testdata/compile_calcs.sysml +++ b/internal/frontend/repl/testdata/compile_calcs.sysml @@ -63,6 +63,14 @@ package Compiled { return : Boolean = (p and 10 / n > 1) or (q implies 10 % n == 0) or not p; } + // Exact Rationals compile where binary64 gives the interpreter's exact answer. + calc def ExactRational { + in a : Integer; + in x : Real; + return : Boolean = 0.1 + 0.2 == 0.3 and 1 / 3 * 3 == 1 and a / 3 >= 2 and a * 1.0 + a == 2 * a and x < 0.1; + } + calc def ExactWide { in a : Integer; return : Real = a * 2.0 + 0.5; } + calc def Compare { in a : Real; in b : Integer; @@ -143,7 +151,8 @@ package Compiled { calc def Big { in a : Integer; - return : Real = a * 1.0e300 * 1.0e10; + attribute big : Real = 1.0e300; + return : Real = a * big * 1.0e10; } // Both operands can fail; the left one must be reported, as the interpreter does. @@ -541,6 +550,11 @@ package Refused { enum def Color { red; green; } calc def Extent { return : Natural = size(all Color); } calc def RecordParam { in p : Point; return : Real = p.x + p.y; } + // Compiled code holds Rationals as binary64, so exact Rationals it cannot round once are refused. + calc def RationalParam { in x : Rational; return : Real = x; } + calc def ExactProduct { in a : Integer; return : Real = a * 0.1; } + calc def ExactPower { in a : Integer; return : Real = (a * 0.5) ** 2; } + calc def ExactCompare { in a : Integer; return : Boolean = a / 3 < 0.1; } calc def EnumParam { in c : Color; return : Boolean = c == Color::red; } calc def RealIntIdentity { in x : Real; in y : Integer; return : Boolean = x === y; } calc def SelectNonBoolean { in x : Integer[0..*]; return : Integer[0..*] = x->select {in v; v}; } diff --git a/internal/frontend/symbolfacts/facts.go b/internal/frontend/symbolfacts/facts.go index bb014105cd..6f58b6ab63 100644 --- a/internal/frontend/symbolfacts/facts.go +++ b/internal/frontend/symbolfacts/facts.go @@ -355,6 +355,8 @@ func primOfValue(sem *semantics.Model, value ast.Node) (string, bool) { switch val.Kind { case semantics.ValInt: return semantics.PrimInteger.String(), true + case semantics.ValRational: + return semantics.PrimRational.String(), true case semantics.ValReal: return semantics.PrimReal.String(), true case semantics.ValBool: diff --git a/internal/ir/docplan/compiler.go b/internal/ir/docplan/compiler.go index 0ddf6436e0..444a411abc 100644 --- a/internal/ir/docplan/compiler.go +++ b/internal/ir/docplan/compiler.go @@ -2038,11 +2038,11 @@ func (c *compiler) bindingValue( } return BindingValue{kind: BindingInteger, integer: integer, origin: origin}, nil case *ast.LiteralReal: - realVal, err := strconv.ParseFloat(expression.Value, 64) + rational, err := semantics.ParseRational(expression.Value, semantics.DefaultMaxIntegerBits) if err != nil { return BindingValue{}, c.unsupportedBinding(content, member, entry, parameter) } - return BindingValue{kind: BindingReal, real: realVal, origin: origin}, nil + return rationalBinding(rational, origin), nil case *ast.LiteralBool: return BindingValue{kind: BindingBoolean, boolean: expression.Value, origin: origin}, nil case *ast.OperatorExpr: @@ -2076,11 +2076,11 @@ func (c *compiler) signedBinding( } return BindingValue{kind: BindingInteger, integer: integer, origin: origin}, nil case *ast.LiteralReal: - realVal, err := strconv.ParseFloat(sign+operand.Value, 64) + rational, err := semantics.ParseRational(sign+operand.Value, semantics.DefaultMaxIntegerBits) if err != nil { return BindingValue{}, c.unsupportedBinding(content, member, entry, parameter) } - return BindingValue{kind: BindingReal, real: realVal, origin: origin}, nil + return rationalBinding(rational, origin), nil default: return BindingValue{}, c.unsupportedBinding(content, member, entry, parameter) } @@ -2216,6 +2216,14 @@ func (c *compiler) valueConforms(value BindingValue, expected string) bool { return false } +// rationalBinding binds a decimal literal's exact value, an Integer when it is one. +func rationalBinding(value semantics.Value, origin symbols.Origin) BindingValue { + if value.Kind == semantics.ValInt { + return BindingValue{kind: BindingInteger, integer: value, origin: origin} + } + return BindingValue{kind: BindingRational, integer: value, origin: origin} +} + func scalarBindingType(value BindingValue) (semantics.PrimType, bool) { switch value.Kind() { case BindingBoolean: @@ -2224,6 +2232,8 @@ func scalarBindingType(value BindingValue) (semantics.PrimType, bool) { return semantics.PrimString, true case BindingInteger: return semantics.PrimInteger, true + case BindingRational: + return semantics.PrimRational, true case BindingReal: return semantics.PrimReal, true default: diff --git a/internal/ir/docplan/compiler_test.go b/internal/ir/docplan/compiler_test.go index cabe56da7a..64c4afec7b 100644 --- a/internal/ir/docplan/compiler_test.go +++ b/internal/ir/docplan/compiler_test.go @@ -653,9 +653,9 @@ func TestCompileAcceptsSignedNumericBindings(t *testing.T) { if !bindings[1].Values()[0].Origin().Located() { t.Fatalf("offset origin = %+v", bindings[1].Values()[0].Origin()) } - factor, ok := bindings[2].Values()[0].Real() - if !ok || factor != 2.5 { - t.Fatalf("factor = %g %v", factor, ok) + factor, ok := bindings[2].Values()[0].Rational() + if !ok || factor.FormatRational() != "2.5" { + t.Fatalf("factor = %s %v", factor.FormatRational(), ok) } } diff --git a/internal/ir/docplan/plan.go b/internal/ir/docplan/plan.go index be77ebfd72..631697857d 100644 --- a/internal/ir/docplan/plan.go +++ b/internal/ir/docplan/plan.go @@ -168,7 +168,9 @@ const ( BindingString BindingKind = "string" BindingInteger BindingKind = "integer" BindingReal BindingKind = "real" - BindingBoolean BindingKind = "boolean" + // BindingRational is an exact decimal literal, `0.1`. + BindingRational BindingKind = "rational" + BindingBoolean BindingKind = "boolean" ) // BindingValue is one statically planned value for a query parameter. @@ -208,6 +210,11 @@ func (v BindingValue) IntegerConst() (semantics.Value, bool) { return v.integer, v.kind == BindingInteger } +// Rational returns the bound exact Rational when the value is a rational. +func (v BindingValue) Rational() (semantics.Value, bool) { + return v.integer, v.kind == BindingRational +} + // Real returns the bound real when the value is a real. func (v BindingValue) Real() (float64, bool) { return v.real, v.kind == BindingReal } diff --git a/internal/semantic/semantics/annotations.go b/internal/semantic/semantics/annotations.go index 20120ede76..8229aa4536 100644 --- a/internal/semantic/semantics/annotations.go +++ b/internal/semantic/semantics/annotations.go @@ -909,12 +909,37 @@ func (m *Model) constantFeatureValues(member *symbols.Symbol, seen map[*symbols. } values = append(values, m.declaredValue(member.OwnerScope, element)) } - return values, true, nil + return m.heldAsDeclared(member, values), true, nil } if _, empty := usage.Value.(*ast.NullExpr); empty { return nil, true, nil } - return []symbols.FilterValue{m.declaredValue(member.OwnerScope, usage.Value)}, true, nil + return m.heldAsDeclared(member, []symbols.FilterValue{m.declaredValue(member.OwnerScope, usage.Value)}), true, nil +} + +// heldAsDeclared holds each exact Rational a Real-typed feature declares as its nearest +// binary64; one no binary64 holds stays undetermined, for evaluation to refuse. +func (m *Model) heldAsDeclared(member *symbols.Symbol, values []symbols.FilterValue) []symbols.FilterValue { + isReal := false + for _, typ := range m.FeatureTypes(member) { + isReal = isReal || m.PrimTypeOf(typ) == PrimReal + } + if !isReal { + return values + } + for i, value := range values { + if value.Kind != symbols.FilterValueRational { + continue + } + number, _ := FilterNumber(value) + held, err := RealOf(number) + if err != nil { + values[i] = symbols.FilterValue{} + continue + } + values[i] = symbols.FilterValue{Kind: symbols.FilterValueReal, Real: held.Real} + } + return values } // declaredValue is annotationValue for a feature's own value, where a reference diff --git a/internal/semantic/semantics/coherent_quantity.go b/internal/semantic/semantics/coherent_quantity.go index 81be96b08f..731bff5731 100644 --- a/internal/semantic/semantics/coherent_quantity.go +++ b/internal/semantic/semantics/coherent_quantity.go @@ -40,8 +40,13 @@ func (m *Model) CoherentQuantity(q Quantity, declared *symbols.Symbol) (Quantity } num := q.Num if !q.Unit.Term.Same(coherent.Term) { - var err error - if num, err = RealResult(ConvertMagnitude(q.Num.AsReal(), q.Unit.Term.Scale, coherent.Term.Scale)); err != nil { + exact, ok, err := ExactConversion(q.Num, q.Unit.Term.Scale, coherent.Term.Scale, DefaultMaxIntegerBits) + if err != nil { + return Quantity{}, err + } + if ok { + num = exact + } else if num, err = RealResult(ConvertMagnitude(q.Num.AsReal(), q.Unit.Term.Scale, coherent.Term.Scale)); err != nil { return Quantity{}, err } } diff --git a/internal/semantic/semantics/eval.go b/internal/semantic/semantics/eval.go index 33f88be38d..d075d3fa9c 100644 --- a/internal/semantic/semantics/eval.go +++ b/internal/semantic/semantics/eval.go @@ -93,6 +93,7 @@ const ( ValReal ValBool ValInfinity // the `*` bound / unbounded value + ValRational // an exact Rational, held in lowest terms ) // StringTypeName names the scalar type of a string value, as a sent signal's type. @@ -106,6 +107,8 @@ func ScalarTypeName(kind ValueKind) string { return "Integer" case ValReal: return "Real" + case ValRational: + return "Rational" case ValBool: return "Boolean" } @@ -113,13 +116,18 @@ func ScalarTypeName(kind ValueKind) string { } // SentScalarTypes names the scalar types a send of node's value may send, as -// the runtime names them: one when node's scalar type is known, Integer and Real -// for a number of unknown kind, and none when node's value is not a scalar. +// the runtime names them: one when node's scalar type is known (a Rational also +// as a Real), Integer and Real for a number of unknown kind, and none when +// node's value is not a scalar. func (m *Model) SentScalarTypes(scope *symbols.Scope, node ast.Node) []string { if _, ok := node.(*ast.LiteralString); ok { return []string{StringTypeName} } if value, ok := m.Eval(node); ok { + // A Rational is a Real (ScalarValues::Rational specializes Real). + if value.Kind == ValRational { + return []string{ScalarTypeName(ValRational), ScalarTypeName(ValReal)} + } if name := ScalarTypeName(value.Kind); name != "" { return []string{name} } @@ -151,16 +159,23 @@ func (m *Model) SentScalarTypes(scope *symbols.Scope, node ast.Node) []string { // as an immutable big.Int, with Int zero, so each Integer has one // representation. Read an Integer through Int64, BigInt or the Int functions // rather than through Int, which is meaningful only when IsBigInt is false. +// +// A Rational is exact (rational.go): its numerator in Int and positive +// denominator in den when both fit int64, an immutable big.Rat otherwise. A +// Real is an IEEE 754 binary64. type Value struct { Kind ValueKind Bool bool + den uint32 Int int64 Real float64 - big *big.Int + ext *big.Rat } -// IsNumeric reports whether the value is an integer or a real. -func (v Value) IsNumeric() bool { return v.Kind == ValInt || v.Kind == ValReal } +// IsNumeric reports whether the value is an Integer, a Rational or a Real. +func (v Value) IsNumeric() bool { + return v.Kind == ValInt || v.Kind == ValReal || v.Kind == ValRational +} // WholeNumber returns the value as an int64 when it is a whole number within // that range: an integer, or a finite real with no fractional part (4 / 2 is 2.0). @@ -168,6 +183,11 @@ func (v Value) WholeNumber() (int64, bool) { switch v.Kind { case ValInt: return v.Int64() + case ValRational: + if !v.RatIsWhole() { + return 0, false + } + return v.RatNumer().Int64() case ValReal: // MaxInt64 has no float64; 2^63 is the next value up and is out of range. if v.Real != math.Trunc(v.Real) || v.Real >= -float64(math.MinInt64) || v.Real < math.MinInt64 { @@ -210,12 +230,15 @@ func UnboundedOrder(l, r Value) (order int, ok bool) { } } -// AsReal returns the value as a float64 (int and real only). An Integer rounds -// to the nearest float64, ties to even, and one beyond the float64 range to the -// infinity of its sign. +// AsReal returns the number as a float64. An Integer or a Rational rounds to +// the nearest float64, ties to even, and one beyond the float64 range to the +// infinity of its sign: the one conversion mixed Rational/Real arithmetic makes. func (v Value) AsReal() float64 { - if v.Kind == ValInt { + switch v.Kind { + case ValInt: return intToFloat(v) + case ValRational: + return ratToFloat(v) } return v.Real } @@ -392,11 +415,11 @@ func evalConst(n ast.Node, maxBits int64) (Value, bool) { case *ast.LiteralInteger: return ParseInteger(e.Value) case *ast.LiteralReal: - f, err := ParseReal(e.Value) + v, err := ParseRational(e.Value, maxBits) if err != nil { return Value{}, false } - return Value{Kind: ValReal, Real: f}, true + return v, true case *ast.LiteralBool: return Value{Kind: ValBool, Bool: e.Value}, true case *ast.LiteralInfinity: @@ -443,6 +466,9 @@ func EvalUnary(op ast.OperatorKind, v Value) (Value, bool) { if v.Kind == ValInt { return IntNeg(v), true } + if v.Kind == ValRational { + return RatNeg(v), true + } if v.Kind == ValReal { return Value{Kind: ValReal, Real: -v.Real}, true } @@ -529,10 +555,12 @@ func evalEquality(op ast.OperatorKind, l, r Value) (Value, bool) { eq = order == 0 case l.Kind == ValInt && r.Kind == ValInt: eq = CompareInt(l, r) == 0 - case l.Kind == ValInt && r.Kind == ValReal: - eq = !math.IsNaN(r.Real) && CompareIntReal(l, r.Real) == 0 - case l.Kind == ValReal && r.Kind == ValInt: - eq = !math.IsNaN(l.Real) && CompareIntReal(r, l.Real) == 0 + case l.IsExact() && r.IsExact(): + eq = CompareRat(l, r) == 0 + case l.IsExact() && r.Kind == ValReal: + eq = !math.IsNaN(r.Real) && CompareExactReal(l, r.Real) == 0 + case l.Kind == ValReal && r.IsExact(): + eq = !math.IsNaN(l.Real) && CompareExactReal(r, l.Real) == 0 case l.IsNumeric() && r.IsNumeric(): eq = l.AsReal() == r.AsReal() default: @@ -563,12 +591,16 @@ func evalComparison(op ast.OperatorKind, l, r Value) (Value, bool) { res, _ := OrderSatisfies(op, CompareInt(l, r)) return Value{Kind: ValBool, Bool: res}, true } - if l.Kind == ValInt && r.Kind == ValReal && !math.IsNaN(r.Real) { - res, _ := OrderSatisfies(op, CompareIntReal(l, r.Real)) + if l.IsExact() && r.IsExact() { + res, _ := OrderSatisfies(op, CompareRat(l, r)) + return Value{Kind: ValBool, Bool: res}, true + } + if l.IsExact() && r.Kind == ValReal && !math.IsNaN(r.Real) { + res, _ := OrderSatisfies(op, CompareExactReal(l, r.Real)) return Value{Kind: ValBool, Bool: res}, true } - if l.Kind == ValReal && r.Kind == ValInt && !math.IsNaN(l.Real) { - res, _ := OrderSatisfies(op, -CompareIntReal(r, l.Real)) + if l.Kind == ValReal && r.IsExact() && !math.IsNaN(l.Real) { + res, _ := OrderSatisfies(op, -CompareExactReal(r, l.Real)) return Value{Kind: ValBool, Bool: res}, true } lf, rf := l.AsReal(), r.AsReal() @@ -614,18 +646,19 @@ func evalArithmetic(op ast.OperatorKind, l, r Value, maxBits int64) (Value, bool } return v, true } - // Integer arithmetic when both operands are integers (except division, - // which may be fractional — keep it real to avoid silent truncation). - if l.Kind == ValInt && r.Kind == ValInt { - if op == ast.OpDiv { - q, ok := IntQuotient(l, r) - if !ok || math.IsInf(q, 0) { - return Value{}, false - } - return Value{Kind: ValReal, Real: q}, true - } + // Integer arithmetic stays Integer except division, which IntegerFunctions + // declares Rational; exact operands give an exact Rational; a Real operand + // makes the operation binary64. + if l.Kind == ValInt && r.Kind == ValInt && op != ast.OpDiv { return evalIntArith(op, l, r, maxBits) } + if l.IsExact() && r.IsExact() { + res, err := RatArith(op, l, r, maxBits) + if err != nil { + return Value{}, false + } + return res, true + } return evalRealArith(op, l.AsReal(), r.AsReal()) } @@ -678,8 +711,10 @@ func RealArith(op ast.OperatorKind, a, b float64) (float64, bool) { // constant folder and the runtime share, so a folded and an evaluated // exponentiation agree. Integer operands with a non-negative exponent give an // Integer, as IntegerFunctions::'**' declares, refused with ErrIntegerSizeLimit -// when it would need more than maxBits; every other numeric combination gives a -// Real, as RealFunctions::'**' does. A Real result that is not finite is an +// when it would need more than maxBits; an exact base with any other whole +// exponent gives the exact Rational RationalFunctions::'**' declares, refused +// with ErrRationalSizeLimit; every other numeric combination gives a Real, as +// RealFunctions::'**' does. A Real result that is not finite is an // error rather than a NaN or an infinity: the folder declines on it, the // runtime reports it. func Pow(l, r Value, maxBits int64) (Value, error) { @@ -690,6 +725,10 @@ func Pow(l, r Value, maxBits int64) (Value, error) { if l.Kind == ValInt && r.Kind == ValInt && r.IntSign() >= 0 { return IntPow(l, r, maxBits) } + // RationalFunctions::'**' takes an Integer exponent; a Rational one is RealFunctions'. + if l.IsExact() && r.Kind == ValInt { + return RatPow(l, r, maxBits) + } base, exp := l.AsReal(), r.AsReal() switch { diff --git a/internal/semantic/semantics/eval_test.go b/internal/semantic/semantics/eval_test.go index c36b803d19..bca7811e36 100644 --- a/internal/semantic/semantics/eval_test.go +++ b/internal/semantic/semantics/eval_test.go @@ -71,8 +71,8 @@ func TestParseReal(t *testing.T) { t.Errorf("ParseReal(%q) = (%v, %v), want %v", text, got, err, ErrArithmeticOverflow) } } - if v, ok := evalExpr(t, "1e-400"); ok { - t.Errorf("1e-400 folded to %+v, want not evaluable", v) + if v, ok := evalExpr(t, "1e-400"); !ok || v.Kind != ValRational || v.FormatRational() != "1e-400" { + t.Errorf("1e-400 folded to %+v ok=%v, want the exact Rational", v, ok) } } @@ -90,19 +90,20 @@ func TestParseRealRejectsNonDecimalNotation(t *testing.T) { } } +// An Integer quotient is the exact Rational IntegerFunctions::'/' declares. func TestEvalRealArithmetic(t *testing.T) { v, ok := evalExpr(t, "10 / 4") - if !ok || v.Kind != ValReal || v.Real != 2.5 { - t.Fatalf("10/4 = %+v ok=%v, want real 2.5", v, ok) + if !ok || v.Kind != ValRational || v.FormatRational() != "2.5" { + t.Fatalf("10/4 = %+v ok=%v, want the Rational 5/2", v, ok) } } -// A quotient of operands beyond 2^53 folds as the exact ratio rounded once, -// the same answer the runtime computes. +// A quotient of operands beyond 2^53 folds exactly, a whole one staying a +// Rational, the same answer the runtime computes. func TestEvalFoldsExactQuotientBeyondFloatRange(t *testing.T) { v, ok := evalExpr(t, "9007199254740993 / 3") - if !ok || v.Kind != ValReal || v.Real != 3002399751580331 { - t.Fatalf("9007199254740993/3 = %+v ok=%v, want real 3002399751580331", v, ok) + if !ok || v.Kind != ValRational || v.FormatRational() != "3002399751580331.0" { + t.Fatalf("9007199254740993/3 = %+v ok=%v, want the Rational 3002399751580331", v, ok) } } diff --git a/internal/semantic/semantics/filter.go b/internal/semantic/semantics/filter.go index 6927e84e85..cb2fd25786 100644 --- a/internal/semantic/semantics/filter.go +++ b/internal/semantic/semantics/filter.go @@ -1272,6 +1272,8 @@ func constValue(v Value) symbols.FilterValue { return symbols.FilterValue{Kind: symbols.FilterValueInt, Int: v.Int} case ValReal: return symbols.FilterValue{Kind: symbols.FilterValueReal, Real: v.Real} + case ValRational: + return symbols.FilterValue{Kind: symbols.FilterValueRational, Rat: v.Rat()} case ValBool: return symbols.FilterValue{Kind: symbols.FilterValueBool, Bool: v.Bool} default: @@ -1303,8 +1305,8 @@ func asQuantity(v symbols.FilterValue) (*Quantity, bool) { if q, ok := QuantityOf(v); ok { return q, true } - if n, ok := numericValue(v); ok { - return &Quantity{Num: Value{Kind: ValReal, Real: n}, Unit: UnitOne()}, true + if n, ok := FilterNumber(v); ok { + return &Quantity{Num: n, Unit: UnitOne()}, true } return nil, false } @@ -1318,12 +1320,28 @@ func emptyValue() symbols.FilterValue { return symbols.FilterValue{Kind: symbols.FilterValueEmpty} } -// exactFilterOrder orders two Integers, or an Integer and a non-NaN Real, +// FilterNumber is the number a FilterValueInt, FilterValueRational or +// FilterValueReal holds. +func FilterNumber(v symbols.FilterValue) (Value, bool) { + switch v.Kind { + case symbols.FilterValueInt: + return FilterInteger(v), true + case symbols.FilterValueRational: + return RatValue(new(big.Rat).Set(v.Rat)), true + case symbols.FilterValueReal: + return Value{Kind: ValReal, Real: v.Real}, true + } + return Value{}, false +} + +// exactFilterOrder orders two exact numbers, or an Integer and a non-NaN Real, // without rounding either: -1, 0 or 1, and whether it applies. func exactFilterOrder(left, right symbols.FilterValue) (int, bool) { + l, lok := FilterNumber(left) + r, rok := FilterNumber(right) switch { - case left.Kind == symbols.FilterValueInt && right.Kind == symbols.FilterValueInt: - return CompareInt(FilterInteger(left), FilterInteger(right)), true + case lok && rok && l.IsExact() && r.IsExact(): + return CompareRat(l, r), true case left.Kind == symbols.FilterValueInt && right.Kind == symbols.FilterValueReal && !math.IsNaN(right.Real): return CompareIntReal(FilterInteger(left), right.Real), true case left.Kind == symbols.FilterValueReal && right.Kind == symbols.FilterValueInt && !math.IsNaN(left.Real): @@ -1336,10 +1354,9 @@ func exactFilterOrder(left, right symbols.FilterValue) (int, bool) { // number at all. func numericValue(v symbols.FilterValue) (float64, bool) { switch v.Kind { - case symbols.FilterValueInt: - return FilterInteger(v).AsReal(), true - case symbols.FilterValueReal: - return v.Real, true + case symbols.FilterValueInt, symbols.FilterValueRational, symbols.FilterValueReal: + n, _ := FilterNumber(v) + return n.AsReal(), true default: return 0, false } @@ -1354,6 +1371,8 @@ func describeValueKind(k symbols.FilterValueKind) string { return "an integer" case symbols.FilterValueReal: return "a real" + case symbols.FilterValueRational: + return "a rational" case symbols.FilterValueString: return "a string" case symbols.FilterValueRef: diff --git a/internal/semantic/semantics/integer.go b/internal/semantic/semantics/integer.go index abcd68635d..a85071bc5c 100644 --- a/internal/semantic/semantics/integer.go +++ b/internal/semantic/semantics/integer.go @@ -38,7 +38,7 @@ func BigIntValue(b *big.Int) Value { if b.IsInt64() { return Value{Kind: ValInt, Int: b.Int64()} } - return Value{Kind: ValInt, big: b} + return Value{Kind: ValInt, ext: new(big.Rat).SetInt(b)} } // ParseInteger reads decimal Integer notation of any magnitude. @@ -74,11 +74,11 @@ func isIntegerNotation(text string) bool { } // IsBigInt reports whether v is an Integer outside the int64 range. -func (v Value) IsBigInt() bool { return v.Kind == ValInt && v.big != nil } +func (v Value) IsBigInt() bool { return v.Kind == ValInt && v.ext != nil } // Int64 returns v as an int64 when it is an Integer within that range. func (v Value) Int64() (int64, bool) { - if v.Kind != ValInt || v.big != nil { + if v.Kind != ValInt || v.ext != nil { return 0, false } return v.Int, true @@ -86,28 +86,33 @@ func (v Value) Int64() (int64, bool) { // BigInt returns the Integer v as a fresh big.Int the caller may modify. func (v Value) BigInt() *big.Int { - if v.big != nil { - return new(big.Int).Set(v.big) + if v.ext != nil { + return new(big.Int).Set(v.ext.Num()) } return big.NewInt(v.Int) } // BigIntView is the Integer v's big.Int when it is beyond int64, nil otherwise; // the Value shares it, so the caller must not modify it. -func (v Value) BigIntView() *big.Int { return v.big } +func (v Value) BigIntView() *big.Int { + if v.ext == nil { + return nil + } + return v.ext.Num() +} // bigView is the Integer v as a big.Int that must not be modified. func (v Value) bigView() *big.Int { - if v.big != nil { - return v.big + if v.ext != nil { + return v.ext.Num() } return big.NewInt(v.Int) } // IntSign is -1, 0 or 1 by the sign of the Integer v. func (v Value) IntSign() int { - if v.big != nil { - return v.big.Sign() + if v.ext != nil { + return v.ext.Num().Sign() } switch { case v.Int < 0: @@ -120,8 +125,8 @@ func (v Value) IntSign() int { // IntBitLen is the number of bits of the Integer v's magnitude. func (v Value) IntBitLen() int64 { - if v.big != nil { - return int64(v.big.BitLen()) + if v.ext != nil { + return int64(v.ext.Num().BitLen()) } if v.Int == math.MinInt64 { return 64 @@ -135,8 +140,8 @@ func (v Value) IntBitLen() int64 { // FormatInt renders the Integer v in full decimal notation. func (v Value) FormatInt() string { - if v.big != nil { - return v.big.String() + if v.ext != nil { + return v.ext.Num().String() } return strconv.FormatInt(v.Int, 10) } @@ -147,12 +152,15 @@ func (v Value) Equal(w Value) bool { if v.Kind == ValInt && w.Kind == ValInt { return CompareInt(v, w) == 0 } + if v.Kind == ValRational && w.Kind == ValRational { + return CompareRat(v, w) == 0 + } return v == w } // CompareInt orders two Integers exactly: -1, 0 or 1. func CompareInt(a, b Value) int { - if a.big == nil && b.big == nil { + if a.ext == nil && b.ext == nil { switch { case a.Int < b.Int: return -1 @@ -173,7 +181,7 @@ func CompareIntReal(a Value, r float64) int { case math.IsInf(r, -1): return 1 } - if a.big == nil && a.Int >= -1<<53 && a.Int <= 1<<53 { + if a.ext == nil && a.Int >= -1<<53 && a.Int <= 1<<53 { f := float64(a.Int) switch { case f < r: @@ -189,10 +197,10 @@ func CompareIntReal(a Value, r float64) int { // intToFloat is the Integer v rounded to the nearest float64, ties to even; an // Integer beyond the float64 range rounds to the infinity of its sign. func intToFloat(v Value) float64 { - if v.big == nil { + if v.ext == nil { return float64(v.Int) } - f, _ := new(big.Float).SetInt(v.big).Float64() + f, _ := new(big.Float).SetInt(v.ext.Num()).Float64() return f } @@ -227,7 +235,7 @@ func sizedInt(b *big.Int, maxBits int64) (Value, error) { // computed there; any other is computed exactly, refused only when it would // exceed maxBits. func IntArith(op ast.OperatorKind, a, b Value, maxBits int64) (Value, error) { - if a.big == nil && b.big == nil { + if a.ext == nil && b.ext == nil { if res, ok := int64Arith(op, a.Int, b.Int); ok { return IntValue(res), nil } @@ -273,7 +281,7 @@ func int64Arith(op ast.OperatorKind, a, b int64) (int64, bool) { // IntNeg is the negation of the Integer v. func IntNeg(v Value) Value { - if v.big == nil && v.Int != math.MinInt64 { + if v.ext == nil && v.Int != math.MinInt64 { return IntValue(-v.Int) } return BigIntValue(new(big.Int).Neg(v.bigView())) @@ -293,7 +301,7 @@ func IntRem(a, b Value) (Value, bool) { if b.IntSign() == 0 { return Value{}, false } - if a.big == nil && b.big == nil { + if a.ext == nil && b.ext == nil { return IntValue(a.Int % b.Int), true } return BigIntValue(new(big.Int).Rem(a.bigView(), b.bigView())), true @@ -304,7 +312,7 @@ func IntDivTrunc(a, b Value) (Value, bool) { if b.IntSign() == 0 { return Value{}, false } - if a.big == nil && b.big == nil && (a.Int != math.MinInt64 || b.Int != -1) { + if a.ext == nil && b.ext == nil && (a.Int != math.MinInt64 || b.Int != -1) { return IntValue(a.Int / b.Int), true } return BigIntValue(new(big.Int).Quo(a.bigView(), b.bigView())), true @@ -320,7 +328,7 @@ func IntQuotient(a, b Value) (float64, bool) { return 0, false } var r *big.Rat - if a.big == nil && b.big == nil { + if a.ext == nil && b.ext == nil { r = new(big.Rat).SetFrac64(a.Int, b.Int) } else { r = new(big.Rat).SetFrac(a.bigView(), b.bigView()) @@ -333,7 +341,7 @@ func IntQuotient(a, b Value) (float64, bool) { // refused with ErrIntegerSizeLimit before it is computed when the result would // need more than maxBits. func IntPow(a, n Value, maxBits int64) (Value, error) { - if a.big == nil && n.big == nil { + if a.ext == nil && n.ext == nil { if res, ok := intPow(a.Int, n.Int); ok { return IntValue(res), nil } @@ -341,9 +349,9 @@ func IntPow(a, n Value, maxBits int64) (Value, error) { switch { case n.IntSign() == 0: return IntValue(1), nil - case a.big == nil && (a.Int == 0 || a.Int == 1): + case a.ext == nil && (a.Int == 0 || a.Int == 1): return a, nil - case a.big == nil && a.Int == -1: + case a.ext == nil && a.Int == -1: if n.bigView().Bit(0) == 0 { return IntValue(1), nil } diff --git a/internal/semantic/semantics/layout.go b/internal/semantic/semantics/layout.go index 327504fda5..2f5c1e8097 100644 --- a/internal/semantic/semantics/layout.go +++ b/internal/semantic/semantics/layout.go @@ -782,10 +782,9 @@ func (m *Model) readReal(site *LayoutSite, b MetadataBinding, into *float64) boo func (m *Model) readRealValue(site *LayoutSite, scope *symbols.Scope, value ast.Node, feature string, into *float64) bool { v := m.annotationValue(scope, value) switch v.Kind { - case symbols.FilterValueReal: - *into = v.Real - case symbols.FilterValueInt: - *into = FilterInteger(v).AsReal() + case symbols.FilterValueReal, symbols.FilterValueRational, symbols.FilterValueInt: + n, _ := FilterNumber(v) + *into = n.AsReal() default: site.Problems = append(site.Problems, LayoutProblem{Node: value, Message: fmt.Sprintf("%s of %s is not a constant number", feature, simpleName(site.TypeFQN))}) diff --git a/internal/semantic/semantics/pow_test.go b/internal/semantic/semantics/pow_test.go index c6fc68adce..a38ea1733d 100644 --- a/internal/semantic/semantics/pow_test.go +++ b/internal/semantic/semantics/pow_test.go @@ -29,7 +29,7 @@ func TestPowValues(t *testing.T) { {"zero exponent is one", intVal(7), intVal(0), intVal(1)}, {"negative integer base, even exponent", intVal(-3), intVal(2), intVal(9)}, {"negative integer base, odd exponent", intVal(-3), intVal(3), intVal(-27)}, - {"negative exponent gives a Real", intVal(2), intVal(-1), realVal(0.5)}, + {"negative exponent gives a Rational", intVal(2), intVal(-1), ratVal(1, 2)}, {"real base and exponent", realVal(2), realVal(0.5), realVal(math.Sqrt2)}, {"mixed operands widen to Real", intVal(9), realVal(0.5), realVal(3)}, {"negative base with whole real exponent", realVal(-2), realVal(3), realVal(-8)}, @@ -86,7 +86,7 @@ func TestFoldExponentiation(t *testing.T) { "2 ^ 3": intVal(8), "2.0 ** 0.5": realVal(math.Sqrt2), "9 ** 0.5": realVal(3), - "2 ** -2": realVal(0.25), + "2 ** -2": ratVal(1, 4), "9223372036854775807 ** 2": bigVal("85070591730234615847396907784232501249"), } for src, want := range folded { @@ -107,3 +107,11 @@ func TestFoldExponentiation(t *testing.T) { } } } + +func ratVal(num, den int64) Value { + v, ok := FracValue(num, den) + if !ok { + panic("zero denominator") + } + return v +} diff --git a/internal/semantic/semantics/quantity.go b/internal/semantic/semantics/quantity.go index 019c555198..48365f1762 100644 --- a/internal/semantic/semantics/quantity.go +++ b/internal/semantic/semantics/quantity.go @@ -114,6 +114,8 @@ func FormatConst(c Value) string { return c.FormatInt() case ValReal: return FormatReal(c.Real) + case ValRational: + return c.FormatRational() case ValBool: return strconv.FormatBool(c.Bool) case ValInfinity: diff --git a/internal/semantic/semantics/quantity_eval.go b/internal/semantic/semantics/quantity_eval.go index 7361f4a148..678d429ef9 100644 --- a/internal/semantic/semantics/quantity_eval.go +++ b/internal/semantic/semantics/quantity_eval.go @@ -166,16 +166,13 @@ func QuantityBinary(op ast.OperatorKind, left, right Quantity) (Quantity, error) } // AddQuantities is a sum or difference in the left operand's unit; Integer -// magnitudes in one unit stay Integer, a conversion makes a Real. +// magnitudes in one unit stay Integer, an exact magnitude converts between +// exact scales exactly, and any other conversion makes a Real. func AddQuantities(op ast.OperatorKind, left, right Quantity, maxBits int64) (Quantity, error) { - converted, err := right.ConvertTo(left.Unit) + rhs, err := convertedMagnitude(right, left.Unit, maxBits) if err != nil { return Quantity{}, err } - rhs := Value{Kind: ValReal, Real: converted} - if right.Num.Kind == ValInt && left.Unit.Term.Scale == right.Unit.Term.Scale { - rhs = right.Num - } num, err := MagnitudeArith(op, left.Num, rhs, maxBits) if err != nil { return Quantity{}, err @@ -184,19 +181,49 @@ func AddQuantities(op ast.OperatorKind, left, right Quantity, maxBits int64) (Qu } // ConvertQuantity expresses a quantity in a commensurable unit, keeping an -// integral magnitude where the two units share one scale: `3 [km]` in `m` is `3000.0 [m]`. +// integral magnitude where the two units share one scale, and an exact one +// exact between exact scales: `3 [km]` in `m` is the Rational `3000.0 [m]`. func ConvertQuantity(q Quantity, unit Unit) (Quantity, error) { - converted, err := q.ConvertTo(unit) + num, err := convertedMagnitude(q, unit, DefaultMaxIntegerBits) if err != nil { return Quantity{}, err } - num := Value{Kind: ValReal, Real: converted} - if q.Num.Kind == ValInt && q.Unit.Term.Scale == unit.Term.Scale { - num = q.Num - } return InUnit(num, unit) } +// convertedMagnitude is q's magnitude expressed in unit: unchanged in the same +// scale, exact for an exact magnitude between exact scales, else a Real. +func convertedMagnitude(q Quantity, unit Unit, maxBits int64) (Value, error) { + converted, err := q.ConvertTo(unit) + if err != nil { + return Value{}, err + } + if q.Num.IsExact() { + if q.Unit.Term.Scale == unit.Term.Scale { + return q.Num, nil + } + if num, ok, err := ExactConversion(q.Num, q.Unit.Term.Scale, unit.Term.Scale, maxBits); ok || err != nil { + return num, err + } + } + return Value{Kind: ValReal, Real: converted}, nil +} + +// ExactConversion is the exact magnitude given over the scale from expressed +// over the scale to, a Rational; ok=false when either scale is not exact. +func ExactConversion(magnitude Value, from, to Scale, maxBits int64) (Value, bool, error) { + f, fok := from.Exact() + t, tok := to.Exact() + if !fok || !tok || !magnitude.IsExact() { + return Value{}, false, nil + } + num, err := RatArith(ast.OpMul, magnitude, RatValue(f.Quo(f, t)), maxBits) + if err != nil { + return Value{}, false, err + } + return num, true, nil +} + // ScaleQuantities is a product or quotient whose unit is the product or // quotient of the operands' units: `10 [m] / 2 [s]` is `5.0 [m/s]`. func ScaleQuantities(op ast.OperatorKind, left, right Quantity, maxBits int64) (Quantity, error) { @@ -258,7 +285,7 @@ func CompareMagnitudes(left, right Quantity) (int, error) { // exactMagnitudes expresses two commensurable Integer magnitudes over their base // units as rationals, which is exact only while the scale ratios are whole. func exactMagnitudes(left, right Quantity) (*big.Rat, *big.Rat, bool) { - if left.Num.Kind != ValInt || right.Num.Kind != ValInt || !left.Unit.Term.Commensurable(right.Unit.Term) { + if !left.Num.IsExact() || !right.Num.IsExact() || !left.Unit.Term.Commensurable(right.Unit.Term) { return nil, nil, false } l, lok := exactMagnitude(left.Num, left.Unit.Term.Scale) @@ -267,15 +294,11 @@ func exactMagnitudes(left, right Quantity) (*big.Rat, *big.Rat, bool) { } func exactMagnitude(magnitude Value, scale Scale) (*big.Rat, bool) { - if scale.IsZero() || !isWhole(scale.Num) || !isWhole(scale.Den) { - return nil, false - } - num, den := new(big.Rat).SetFloat64(scale.Num), new(big.Rat).SetFloat64(scale.Den) - if num == nil || den == nil { + s, ok := scale.Exact() + if !ok { return nil, false } - m := new(big.Rat).SetInt(magnitude.BigInt()) - return m.Mul(m, num.Quo(num, den)), true + return s.Mul(s, magnitude.Rat()), true } // adoptZeroUnit gives a zero naming no unit the unit of the other operand. @@ -356,8 +379,13 @@ func InUnit(num Value, unit Unit) (Quantity, error) { // keeping its kind unless a scale factor is left to apply. func dimensionlessQuantity(num Value, term UnitTerm) (Quantity, error) { if term.Scale != UnitScale(1) { - var err error - if num, err = RealResult(ConvertMagnitude(num.AsReal(), term.Scale, UnitScale(1))); err != nil { + exact, ok, err := ExactConversion(num, term.Scale, UnitScale(1), DefaultMaxIntegerBits) + if err != nil { + return Quantity{}, err + } + if ok { + num = exact + } else if num, err = RealResult(ConvertMagnitude(num.AsReal(), term.Scale, UnitScale(1))); err != nil { return Quantity{}, err } } @@ -367,16 +395,12 @@ func dimensionlessQuantity(num Value, term UnitTerm) (Quantity, error) { // MagnitudeArith combines two magnitudes as the bare operator does: Integer // operands keep an Integer result except under `/`, within maxBits. func MagnitudeArith(op ast.OperatorKind, left, right Value, maxBits int64) (Value, error) { - if left.Kind == ValInt && right.Kind == ValInt { - if op == ast.OpDiv { - q, ok := IntQuotient(left, right) - if !ok { - return Value{}, ErrDivisionByZero - } - return RealResult(q) - } + if left.Kind == ValInt && right.Kind == ValInt && op != ast.OpDiv { return IntArith(op, left, right, maxBits) } + if left.IsExact() && right.IsExact() { + return RatArith(op, left, right, maxBits) + } res, ok := RealArith(op, left.AsReal(), right.AsReal()) if !ok { return Value{}, ErrDivisionByZero @@ -386,6 +410,16 @@ func MagnitudeArith(op ast.OperatorKind, left, right Value, maxBits int64) (Valu // RealResult wraps a computed Real, reporting a NaN or an infinity instead of // carrying it. +// RealOf is the exact number v held as a Real: its nearest binary64, refused +// where that is not finite or rounds a nonzero v to zero. +func RealOf(v Value) (Value, error) { + x := v.AsReal() + if x == 0 && v.Kind == ValRational && v.RatSign() != 0 { + return Value{}, fmt.Errorf("%w: a nonzero Rational below the least Real", ErrArithmeticOverflow) + } + return RealResult(x) +} + func RealResult(x float64) (Value, error) { switch { case math.IsNaN(x): diff --git a/internal/semantic/semantics/rational.go b/internal/semantic/semantics/rational.go new file mode 100644 index 0000000000..7aea0d3d8a --- /dev/null +++ b/internal/semantic/semantics/rational.go @@ -0,0 +1,635 @@ +package semantics + +import ( + "errors" + "fmt" + "math" + "math/big" + "math/bits" + "strconv" + "strings" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// ErrRationalSizeLimit reports a Rational result whose numerator and +// denominator together would need more bits than the evaluation's numeric size +// budget allows. A Rational is never rounded to fit. +var ErrRationalSizeLimit = errors.New("rational size limit exceeded") + +// A Rational (KerML 1.0 §9.3.2.2.8) is held exactly, in lowest terms with a +// positive denominator. One whose terms fit int64 is held in Int and den, with +// rat nil; any other is held as an immutable big.Rat, so each Rational has one +// representation. A whole Rational stays a Rational: `6 / 3` is the Rational 2. + +// RatValue is the Rational r, normalized. The Value takes r over: the caller +// must not modify it afterwards. +func RatValue(r *big.Rat) Value { + if r.Num().IsInt64() && r.Num().Int64() != math.MinInt64 && r.Denom().IsUint64() && r.Denom().Uint64() <= math.MaxUint32 { + return Value{Kind: ValRational, Int: r.Num().Int64(), den: uint32(r.Denom().Uint64())} + } + return Value{Kind: ValRational, ext: r} +} + +// FracValue is the Rational num/den in lowest terms; ok=false for a zero den. +func FracValue(num, den int64) (Value, bool) { + if den == 0 { + return Value{}, false + } + if v, ok := smallRat(num, den); ok { + return v, true + } + return RatValue(new(big.Rat).SetFrac64(num, den)), true +} + +// smallRat normalizes num/den within int64; ok=false when that would overflow. +func smallRat(num, den int64) (Value, bool) { + if num == math.MinInt64 || den == math.MinInt64 { + return Value{}, false + } + if den < 0 { + num, den = -num, -den + } + if g := gcd64(abs64(num), den); g > 1 { + num, den = num/g, den/g + } + if den > math.MaxUint32 { + return Value{}, false + } + return Value{Kind: ValRational, Int: num, den: uint32(den)}, true +} + +func abs64(x int64) int64 { + if x < 0 { + return -x + } + return x +} + +func gcd64(a, b int64) int64 { + for b != 0 { + a, b = b, a%b + } + if a == 0 { + return 1 + } + return a +} + +// IsRational reports whether v is an exact Rational (not an Integer). +func (v Value) IsRational() bool { return v.Kind == ValRational } + +// IsExact reports whether v is an exact number: an Integer or a Rational. +func (v Value) IsExact() bool { return v.Kind == ValInt || v.Kind == ValRational } + +// ratView is the exact number v (Integer or Rational) as a big.Rat that must +// not be modified. +func (v Value) ratView() *big.Rat { + switch { + case v.ext != nil: + return v.ext + case v.Kind == ValRational: + return new(big.Rat).SetFrac64(v.Int, int64(v.den)) + } + return new(big.Rat).SetInt64(v.Int) +} + +// Rat returns the exact number v (Integer or Rational) as a fresh big.Rat the +// caller may modify. +func (v Value) Rat() *big.Rat { return new(big.Rat).Set(v.ratView()) } + +// small reports v's terms when v is an Integer within int64 or a Rational held +// in int64 terms. +func (v Value) small() (num, den int64, ok bool) { + switch { + case v.Kind == ValRational && v.ext == nil: + return v.Int, int64(v.den), true + case v.Kind == ValInt && v.ext == nil && v.Int != math.MinInt64: + return v.Int, 1, true + } + return 0, 0, false +} + +// RatNumer is the numerator, in lowest terms, of the exact number v. +func (v Value) RatNumer() Value { + if num, _, ok := v.small(); ok { + return IntValue(num) + } + if v.Kind == ValInt { + return v + } + return BigIntValue(new(big.Int).Set(v.ext.Num())) +} + +// RatDenom is the positive denominator, in lowest terms, of the exact number v. +func (v Value) RatDenom() Value { + if _, den, ok := v.small(); ok { + return IntValue(den) + } + if v.Kind == ValInt { + return IntValue(1) + } + return BigIntValue(new(big.Int).Set(v.ext.Denom())) +} + +// RatSign is -1, 0 or 1 by the sign of the exact number v. +func (v Value) RatSign() int { + if v.Kind == ValInt { + return v.IntSign() + } + if v.ext != nil { + return v.ext.Sign() + } + switch { + case v.Int < 0: + return -1 + case v.Int > 0: + return 1 + } + return 0 +} + +// RatBitLen is the size of the exact number v: the bits of its numerator's +// magnitude plus the bits of its denominator. +func (v Value) RatBitLen() int64 { + if num, den, ok := v.small(); ok { + return int64(bits.Len64(uint64(abs64(num))) + bits.Len64(uint64(den))) + } + r := v.ratView() + return int64(r.Num().BitLen() + r.Denom().BitLen()) +} + +// RatIsWhole reports whether the exact number v has no fractional part. +func (v Value) RatIsWhole() bool { + if v.Kind == ValInt { + return true + } + if v.ext != nil { + return v.ext.IsInt() + } + return v.den == 1 +} + +// ratToFloat is the exact number v rounded to the nearest float64, ties to +// even; one beyond the float64 range rounds to the infinity of its sign. +func ratToFloat(v Value) float64 { + if num, den, ok := v.small(); ok && abs64(num) <= 1<<53 && den <= 1<<53 { + return float64(num) / float64(den) + } + f, _ := v.ratView().Float64() + return f +} + +// RationalOfReal is the Rational a finite Real exactly is. +func RationalOfReal(r float64) (Value, bool) { + if math.IsNaN(r) || math.IsInf(r, 0) { + return Value{}, false + } + if r == math.Trunc(r) && math.Abs(r) < 1<<53 { + return Value{Kind: ValRational, Int: int64(r), den: 1}, true + } + return RatValue(new(big.Rat).SetFloat64(r)), true +} + +// RationalSizeExceeded reports a Rational result of bits numerator-plus- +// denominator bits beyond the maxBits an evaluation allows. +func RationalSizeExceeded(bits, maxBits int64) error { + return fmt.Errorf("%w: the result needs at least %d bits of numerator and denominator, beyond the %d-bit size budget", ErrRationalSizeLimit, bits, maxBits) +} + +// sizedRat is the Rational r, refused when its terms exceed maxBits. +func sizedRat(r *big.Rat, maxBits int64) (Value, error) { + if bits := int64(r.Num().BitLen() + r.Denom().BitLen()); bits > maxBits { + return Value{}, RationalSizeExceeded(bits, maxBits) + } + return RatValue(r), nil +} + +// RatOf is the exact number v (Integer or Rational) as a Rational. +func RatOf(v Value) Value { + if v.Kind == ValRational { + return v + } + if v.ext == nil && v.Int != math.MinInt64 { + return Value{Kind: ValRational, Int: v.Int, den: 1} + } + if v.ext == nil { + return RatValue(new(big.Rat).SetInt64(v.Int)) + } + return Value{Kind: ValRational, ext: v.ext} +} + +// RatArith is exact addition, subtraction, multiplication, division and +// truncated remainder over Integers and Rationals, as RationalFunctions +// declares them: the result is a Rational, whole or not. A zero divisor is +// ErrDivisionByZero; a result beyond maxBits is ErrRationalSizeLimit. +func RatArith(op ast.OperatorKind, a, b Value, maxBits int64) (Value, error) { + if (op == ast.OpDiv || op == ast.OpMod) && b.RatSign() == 0 { + return Value{}, ErrDivisionByZero + } + if an, ad, ok := a.small(); ok { + if bn, bd, ok := b.small(); ok { + if res, ok := smallRatArith(op, an, ad, bn, bd); ok { + if bits := res.RatBitLen(); bits > maxBits { + return Value{}, RationalSizeExceeded(bits, maxBits) + } + return res, nil + } + } + } + x, y := a.ratView(), b.ratView() + switch op { + case ast.OpAdd: + return sizedRat(new(big.Rat).Add(x, y), maxBits) + case ast.OpSub: + return sizedRat(new(big.Rat).Sub(x, y), maxBits) + case ast.OpMul: + return sizedRat(new(big.Rat).Mul(x, y), maxBits) + case ast.OpDiv: + return sizedRat(new(big.Rat).Quo(x, y), maxBits) + case ast.OpMod: + q := new(big.Rat).Quo(x, y) + t := new(big.Int).Quo(q.Num(), q.Denom()) + r := new(big.Rat).Sub(x, new(big.Rat).Mul(y, new(big.Rat).SetInt(t))) + return sizedRat(r, maxBits) + } + return Value{}, fmt.Errorf("%w: '%s' is no Rational arithmetic operator", ErrArithmeticDomain, op) +} + +// smallRatArith is RatArith within int64 terms; ok=false when an intermediate +// would overflow, so the caller computes it over math/big. +func smallRatArith(op ast.OperatorKind, an, ad, bn, bd int64) (Value, bool) { + switch op { + case ast.OpAdd, ast.OpSub: + if op == ast.OpSub { + if bn == math.MinInt64 { + return Value{}, false + } + bn = -bn + } + g := gcd64(ad, bd) + l, ok1 := mulInt(an, bd/g) + r, ok2 := mulInt(bn, ad/g) + d, ok3 := mulInt(ad, bd/g) + if !ok1 || !ok2 || !ok3 { + return Value{}, false + } + n, ok := int64Arith(ast.OpAdd, l, r) + if !ok { + return Value{}, false + } + return smallRat(n, d) + case ast.OpMul: + g1, g2 := gcd64(abs64(an), bd), gcd64(abs64(bn), ad) + n, ok1 := mulInt(an/g1, bn/g2) + d, ok2 := mulInt(ad/g2, bd/g1) + if !ok1 || !ok2 { + return Value{}, false + } + return smallRat(n, d) + case ast.OpDiv: + if bn == math.MinInt64 { + return Value{}, false + } + if bn < 0 { + an, bn = -an, -bn + } + return smallRatArith(ast.OpMul, an, ad, bd, bn) + } + return Value{}, false +} + +// CompareExactReal orders the exact number v against the non-NaN Real r +// exactly, neither rounded to the other. +func CompareExactReal(v Value, r float64) int { + if v.Kind == ValInt { + return CompareIntReal(v, r) + } + switch { + case math.IsInf(r, 1): + return -1 + case math.IsInf(r, -1): + return 1 + } + x, _ := RationalOfReal(r) + return CompareRat(v, x) +} + +// RatNeg is the negation of the exact number v, of v's kind. +func RatNeg(v Value) Value { + if v.Kind == ValInt { + return IntNeg(v) + } + if v.ext == nil { + return Value{Kind: ValRational, Int: -v.Int, den: v.den} + } + return RatValue(new(big.Rat).Neg(v.ext)) +} + +// RatAbs is the magnitude of the exact number v, of v's kind. +func RatAbs(v Value) Value { + if v.RatSign() < 0 { + return RatNeg(v) + } + return v +} + +// CompareRat orders two exact numbers (Integers or Rationals) exactly. +func CompareRat(a, b Value) int { + if a.Kind == ValInt && b.Kind == ValInt { + return CompareInt(a, b) + } + if an, ad, ok := a.small(); ok { + if bn, bd, ok := b.small(); ok { + l, ok1 := mulInt(an, bd) + r, ok2 := mulInt(bn, ad) + if ok1 && ok2 { + switch { + case l < r: + return -1 + case l > r: + return 1 + } + return 0 + } + } + } + return a.ratView().Cmp(b.ratView()) +} + +// RatFloor is the greatest Integer not above the exact number v. +func RatFloor(v Value) Value { + if v.Kind == ValInt { + return v + } + if v.ext == nil { + d := int64(v.den) + q := v.Int / d + if v.Int%d != 0 && v.Int < 0 { + q-- + } + return IntValue(q) + } + q, _ := new(big.Int).DivMod(v.ext.Num(), v.ext.Denom(), new(big.Int)) + return BigIntValue(q) +} + +// RatRound is the Integer nearest the exact number v, a half rounding away +// from zero as RealFunctions::round does over a Real. +func RatRound(v Value) Value { + if v.Kind == ValInt { + return v + } + half := Value{Kind: ValRational, Int: 1, den: 2} + shifted, err := RatArith(ast.OpAdd, RatAbs(v), half, math.MaxInt64) + if err != nil { + return Value{} + } + n := RatFloor(shifted) + if v.RatSign() < 0 { + return IntNeg(n) + } + return n +} + +// RatTrunc is the exact number v rounded toward zero, as an Integer. +func RatTrunc(v Value) Value { + if v.RatSign() < 0 { + return IntNeg(RatFloor(RatNeg(v))) + } + return RatFloor(v) +} + +// RatPow is base**n for an exact base and an Integer exponent, the Rational +// RationalFunctions::'**' declares. A negative exponent of zero is +// ErrArithmeticDomain; a result beyond maxBits is ErrRationalSizeLimit, +// refused before it is computed. +func RatPow(base, n Value, maxBits int64) (Value, error) { + sign := n.IntSign() + if sign == 0 { + return Value{Kind: ValRational, Int: 1, den: 1}, nil + } + if base.RatSign() == 0 { + if sign < 0 { + return Value{}, fmt.Errorf("%w: 0 ** %s is undefined (negative exponent)", ErrArithmeticDomain, n.FormatInt()) + } + return RatOf(base), nil + } + r := base.ratView() + num, den := r.Num(), r.Denom() + if num.CmpAbs(den) == 0 && den.Cmp(big.NewInt(1)) == 0 { + if num.Sign() > 0 || n.bigView().Bit(0) == 0 { + return Value{Kind: ValRational, Int: 1, den: 1}, nil + } + return Value{Kind: ValRational, Int: -1, den: 1}, nil + } + e := new(big.Int).Abs(n.bigView()) + // |num| and den are coprime, so the power's terms need at least + // (bitlen-1)*e+1 bits each, summed over the terms that are not 1. + lower := new(big.Int) + for _, t := range []*big.Int{num, den} { + if t.CmpAbs(big.NewInt(1)) != 0 { + term := new(big.Int).Mul(big.NewInt(int64(t.BitLen()-1)), e) + lower.Add(lower, term.Add(term, big.NewInt(1))) + } + } + if !lower.IsInt64() || lower.Int64() > maxBits { + b := int64(math.MaxInt64) + if lower.IsInt64() { + b = lower.Int64() + } + return Value{}, RationalSizeExceeded(b, maxBits) + } + pn := new(big.Int).Exp(num, e, nil) + pd := new(big.Int).Exp(den, e, nil) + if sign < 0 { + pn, pd = pd, pn + } + return sizedRat(new(big.Rat).SetFrac(pn, pd), maxBits) +} + +// ParseRational reads decimal notation — an optional sign, digits with an +// optional fraction and an optional decimal exponent — as the exact Rational +// it denotes: `0.1` is 1/10, `1.5e3` is 1500. Other notation is +// ErrRealNotation; a value whose terms exceed maxBits is ErrRationalSizeLimit. +func ParseRational(text string, maxBits int64) (Value, error) { + if !isRealNotation(text) { + return Value{}, ErrRealNotation + } + mantissa, exponent := text, "" + if i := strings.IndexAny(text, "eE"); i >= 0 { + mantissa, exponent = text[:i], text[i+1:] + } + neg := false + switch { + case strings.HasPrefix(mantissa, "-"): + neg, mantissa = true, mantissa[1:] + case strings.HasPrefix(mantissa, "+"): + mantissa = mantissa[1:] + } + whole, frac, _ := strings.Cut(mantissa, ".") + digits := strings.TrimLeft(whole+frac, "0") + if digits == "" { + return Value{Kind: ValRational, Int: 0, den: 1}, nil + } + scale := -int64(len(frac)) + if exponent != "" { + e, err := strconv.ParseInt(exponent, 10, 64) + if err != nil || e > math.MaxInt64/4 || e < math.MinInt64/4 { + return Value{}, RationalSizeExceeded(math.MaxInt64, maxBits) + } + scale += e + } + trimmed := strings.TrimRight(digits, "0") + scale += int64(len(digits) - len(trimmed)) + digits = trimmed + // 10^k needs more than 3.32*k bits; the significand needs 3.32 per digit, less one. + if lower := int64(float64(len(digits)-1)*math.Log2(10)) + int64(float64(abs64(scale))*math.Log2(10)); lower > maxBits { + return Value{}, RationalSizeExceeded(lower, maxBits) + } + if scale >= -18 && scale <= 0 && len(digits) <= 18 { + n, _ := strconv.ParseInt(digits, 10, 64) + if neg { + n = -n + } + v, _ := FracValue(n, pow10(-scale)) + return v, nil + } + n, _ := new(big.Int).SetString(digits, 10) + if neg { + n.Neg(n) + } + p := new(big.Int).Exp(big.NewInt(10), big.NewInt(abs64(scale)), nil) + r := new(big.Rat) + if scale >= 0 { + r.SetInt(n.Mul(n, p)) + } else { + r.SetFrac(n, p) + } + return sizedRat(r, maxBits) +} + +func pow10(k int64) int64 { + p := int64(1) + for ; k > 0; k-- { + p *= 10 + } + return p +} + +// ParseRationalText reads a Rational as ToRational does: decimal notation, or +// the `numer/denom` form a non-terminating Rational prints as, each term an +// optionally signed decimal Integer and the denominator nonzero. +func ParseRationalText(text string, maxBits int64) (Value, error) { + if n, d, ok := strings.Cut(text, "/"); ok { + num, okN := ParseInteger(n) + den, okD := ParseInteger(d) + if !okN || !okD { + return Value{}, ErrRealNotation + } + if den.IntSign() == 0 { + return Value{}, ErrDivisionByZero + } + return RatArith(ast.OpDiv, num, den, maxBits) + } + return ParseRational(text, maxBits) +} + +// FormatRational renders the Rational v exactly. One whose denominator has no +// prime factor but 2 and 5 is a terminating decimal and prints as one, as a +// Real does (`0.1`, `2.0`, `1.5e-05`); any other prints as `numer/denom` +// (`1/3`), since no finite decimal is it. +func (v Value) FormatRational() string { + num, den := v.RatNumer(), v.RatDenom() + d := den.BigInt() + twos := d.TrailingZeroBits() + d.Rsh(d, twos) + fives := uint(0) + five, ten := big.NewInt(5), big.NewInt(10) + for m := new(big.Int); ; fives++ { + q, r := new(big.Int).QuoRem(d, five, m) + if r.Sign() != 0 { + break + } + d = q + } + if d.Cmp(big.NewInt(1)) != 0 { + return num.FormatInt() + "/" + den.FormatInt() + } + k := max(twos, fives) + scaled := num.BigInt() + scaled.Mul(scaled, new(big.Int).Exp(ten, big.NewInt(int64(k)), nil)) + scaled.Quo(scaled, den.bigView()) + return formatDecimal(scaled, int64(k)) +} + +// formatDecimal renders n·10^-k in FormatReal's layout: positional between +// 1e-4 and 1e21, exponent notation outside, with a ".0" on a whole value. +func formatDecimal(n *big.Int, k int64) string { + sign := "" + if n.Sign() < 0 { + sign = "-" + } + digits := new(big.Int).Abs(n).String() + if digits == "0" { + return "0.0" + } + trimmed := strings.TrimRight(digits, "0") + k -= int64(len(digits) - len(trimmed)) + digits = trimmed + exp := int64(len(digits)) - k - 1 + if exp < -4 || exp >= 21 { + mantissa := digits[:1] + if len(digits) > 1 { + mantissa += "." + digits[1:] + } + esign := "+" + if exp < 0 { + esign, exp = "-", -exp + } + e := strconv.FormatInt(exp, 10) + if len(e) < 2 { + e = "0" + e + } + return sign + mantissa + "e" + esign + e + } + switch { + case k <= 0: + return sign + digits + strings.Repeat("0", int(-k)) + ".0" + case int64(len(digits)) > k: + cut := int64(len(digits)) - k + return sign + digits[:cut] + "." + digits[cut:] + } + return sign + "0." + strings.Repeat("0", int(k-int64(len(digits)))) + digits +} + +// BinaryExact is the binary64 a Rational is exactly, when one is: 0.5 and +// 1800.0 are, 0.1 and 1 / 3 are not. +func (v Value) BinaryExact() (float64, bool) { + if v.Kind != ValRational { + return 0, false + } + f, exact := v.Rat().Float64() + return f, exact && !math.IsInf(f, 0) +} + +// CanonicalRational reads the Rational a wire encoding spells as two decimals: +// a numerator over a positive denominator sharing no factor with it, and no +// binary64 holds exactly, which crosses as a double instead; so no Rational has +// two spellings. +func CanonicalRational(numerator, denominator string) (Value, bool) { + num, okNum := ParseInteger(numerator) + den, okDen := ParseInteger(denominator) + if !okNum || !okDen || CompareInt(den, IntValue(1)) < 0 { + return Value{}, false + } + n, d := num.BigInt(), den.BigInt() + if new(big.Int).GCD(nil, nil, new(big.Int).Abs(n), d).Cmp(big.NewInt(1)) != 0 { + return Value{}, false + } + v := RatValue(new(big.Rat).SetFrac(n, d)) + if _, exact := v.BinaryExact(); exact { + return Value{}, false + } + return v, true +} diff --git a/internal/semantic/semantics/units.go b/internal/semantic/semantics/units.go index 2f02312616..780804985f 100644 --- a/internal/semantic/semantics/units.go +++ b/internal/semantic/semantics/units.go @@ -4,8 +4,10 @@ import ( "errors" "fmt" "math" + "math/big" "slices" "sort" + "strconv" "strings" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -62,20 +64,70 @@ func UnitScale(n float64) Scale { return Scale{Num: n, Den: 1} } // factor is. func (s Scale) IsZero() bool { return s.Num == 0 || s.Den == 0 } -// Times returns the product of two ratios. +// maxExactScaleTerm bounds the terms of a ratio held exactly: every whole +// float64 up to 2^53 is the Integer it reads as. +const maxExactScaleTerm = 1 << 53 + +// Exact is the ratio as a Rational, when both its terms are whole and within +// maxExactScaleTerm; a ratio past that bound was rounded and is not exact. +func (s Scale) Exact() (*big.Rat, bool) { + if s.IsZero() || !exactScaleTerm(s.Num) || !exactScaleTerm(s.Den) { + return nil, false + } + return new(big.Rat).SetFrac64(int64(s.Num), int64(s.Den)), true +} + +func exactScaleTerm(f float64) bool { return isWhole(f) && math.Abs(f) <= maxExactScaleTerm } + +// ScaleOfRat is the Rational r as a ratio, exact when its lowest terms fit +// maxExactScaleTerm and otherwise the nearest float64 terms. +func ScaleOfRat(r *big.Rat) Scale { + n, d := r.Num(), r.Denom() + if n.IsInt64() && d.IsInt64() && exactScaleTerm(float64(n.Int64())) && exactScaleTerm(float64(d.Int64())) { + return Scale{Num: float64(n.Int64()), Den: float64(d.Int64())} + } + nf, _ := new(big.Float).SetInt(n).Float64() + df, _ := new(big.Float).SetInt(d).Float64() + return Scale{Num: nf, Den: df} +} + +// Times returns the product of two ratios, exact where both are. func (s Scale) Times(other Scale) Scale { + if a, ok := s.Exact(); ok { + if b, ok := other.Exact(); ok { + return ScaleOfRat(a.Mul(a, b)) + } + } return reduceScale(Scale{Num: s.Num * other.Num, Den: s.Den * other.Den}) } -// DividedBy returns the quotient of two ratios. +// DividedBy returns the quotient of two ratios, exact where both are. func (s Scale) DividedBy(other Scale) Scale { + if a, ok := s.Exact(); ok { + if b, ok := other.Exact(); ok { + return ScaleOfRat(a.Quo(a, b)) + } + } return reduceScale(Scale{Num: s.Num * other.Den, Den: s.Den * other.Num}) } -// Pow raises the ratio to an exponent. A negative exponent inverts the ratio -// rather than raising it to a negative power, which would evaluate it: `h^-1` -// stays 1/3600 instead of becoming 0.0002777777777777778. +// maxExactScalePower bounds the whole exponent a ratio is raised to exactly. +const maxExactScalePower = 64 + +// Pow raises the ratio to an exponent, exactly for an exact ratio and a whole +// exponent. A negative exponent inverts the ratio rather than raising it to a +// negative power, which would evaluate it: `h^-1` stays 1/3600 instead of +// becoming 0.0002777777777777778. func (s Scale) Pow(exp float64) Scale { + if r, ok := s.Exact(); ok && isWhole(exp) && math.Abs(exp) <= maxExactScalePower { + e := big.NewInt(int64(math.Abs(exp))) + num := new(big.Int).Exp(r.Num(), e, nil) + den := new(big.Int).Exp(r.Denom(), e, nil) + if exp < 0 { + num, den = den, num + } + return ScaleOfRat(new(big.Rat).SetFrac(num, den)) + } if exp < 0 { return reduceScale(Scale{Num: math.Pow(s.Den, -exp), Den: math.Pow(s.Num, -exp)}) } @@ -84,10 +136,22 @@ func (s Scale) Pow(exp float64) Scale { // String renders the ratio, as a ratio when it is not whole. func (s Scale) String() string { + if r, ok := s.Exact(); ok && !r.IsInt() { + return RatValue(r).FormatRational() + } if s.Den == 1 { - return fmt.Sprintf("%g", s.Num) + return FormatScaleTerm(s.Num) + } + return FormatScaleTerm(s.Num) + "/" + FormatScaleTerm(s.Den) +} + +// FormatScaleTerm renders a scale's numerator or denominator, a whole one +// binary64 holds exactly in full. +func FormatScaleTerm(f float64) string { + if f == math.Trunc(f) && math.Abs(f) <= maxExactScaleTerm { + return strconv.FormatFloat(f, 'f', -1, 64) } - return fmt.Sprintf("%g/%g", s.Num, s.Den) + return fmt.Sprintf("%g", f) } // reduceScale cancels a whole common divisor, keeping composed factors small, @@ -162,7 +226,15 @@ func (t UnitTerm) Commensurable(other UnitTerm) bool { // Same reports whether two terms are one reduction: commensurable at equal scale. func (t UnitTerm) Same(other UnitTerm) bool { - return t.Commensurable(other) && t.Scale.Num*other.Scale.Den == other.Scale.Num*t.Scale.Den + if !t.Commensurable(other) { + return false + } + if a, ok := t.Scale.Exact(); ok { + if b, ok := other.Scale.Exact(); ok { + return a.Cmp(b) == 0 + } + } + return t.Scale.Num*other.Scale.Den == other.Scale.Num*t.Scale.Den } // DimensionKey identifies the base units the term is over, exponents included @@ -567,6 +639,9 @@ func (m *Model) numericMember(sym *symbols.Symbol, name string) (Scale, bool) { if op, ok := expr.(*ast.OperatorExpr); ok && op.Operator == ast.OpDiv && len(op.Operands) == 2 { num, numOK := m.Eval(op.Operands[0]) den, denOK := m.Eval(op.Operands[1]) + if numOK && denOK && num.IsExact() && den.IsExact() && den.RatSign() != 0 { + return ScaleOfRat(new(big.Rat).Quo(num.Rat(), den.Rat())), true + } if numOK && denOK && num.IsNumeric() && den.IsNumeric() && den.AsReal() != 0 { return reduceScale(Scale{Num: num.AsReal(), Den: den.AsReal()}), true } @@ -575,6 +650,9 @@ func (m *Model) numericMember(sym *symbols.Symbol, name string) (Scale, bool) { if !ok || !val.IsNumeric() { return Scale{}, false } + if val.IsExact() { + return ScaleOfRat(val.Rat()), true + } return UnitScale(val.AsReal()), true } diff --git a/internal/semantic/symbols/filter.go b/internal/semantic/symbols/filter.go index 65388f9ae0..d0f6d567aa 100644 --- a/internal/semantic/symbols/filter.go +++ b/internal/semantic/symbols/filter.go @@ -143,6 +143,8 @@ const ( // FilterValueQuantity is a magnitude in a measurement unit (`5 [kg]`), // carried in Quantity. FilterValueQuantity + // FilterValueRational is an exact Rational, carried in Rat. + FilterValueRational ) // FilterValue is a constant a filter predicate yields or compares: a literal, or @@ -151,9 +153,11 @@ type FilterValue struct { Kind FilterValueKind Bool bool // Int is a FilterValueInt within int64; BigInt holds one beyond it, with Int zero. - Int int64 - BigInt *big.Int - Real float64 + Int int64 + BigInt *big.Int + Real float64 + // Rat is a FilterValueRational in lowest terms; it is not modified. + Rat *big.Rat Str string RefFQN string Quantity QuantityValue diff --git a/internal/translate/codegen/compile.go b/internal/translate/codegen/compile.go index f055cbe2a8..ede5fc50b4 100644 --- a/internal/translate/codegen/compile.go +++ b/internal/translate/codegen/compile.go @@ -100,6 +100,8 @@ type funcCompiler struct { // result is the return binding once a declaration or a return has fixed it. result binding temps int + // exactTemps is how the interpreter holds each hoisted temporary's value. + exactTemps map[string]exactness } // resultWhere names the result in a multiplicity diagnostic. @@ -436,7 +438,7 @@ func scalarType(fqn string) (Type, Range, bool) { return TypeInt, RangeNatural, true case "ScalarValues::Positive": return TypeInt, RangePositive, true - case "ScalarValues::Real", "ScalarValues::Rational": + case "ScalarValues::Real": return TypeReal, RangeAny, true case "ScalarValues::Boolean": return TypeBool, RangeAny, true @@ -755,7 +757,11 @@ func (fc *funcCompiler) compileExpr(n ast.Node) (Expr, error) { if err != nil { return nil, fc.unsupported(fmt.Sprintf("literal %s is outside the Real range", n.Value)) } - return RealLit{Value: v}, nil + exact, err := semantics.ParseRational(n.Value, semantics.DefaultMaxIntegerBits) + if err != nil { + return nil, fc.unsupported(fmt.Sprintf("literal %s: %v", n.Value, err)) + } + return RealLit{Value: v, Rat: exact.Rat()}, nil case *ast.LiteralBool: return BoolLit{Value: n.Value}, nil case *ast.FeatureReference: @@ -881,13 +887,23 @@ func (fc *funcCompiler) compileOperator(n *ast.OperatorExpr) (Expr, error) { return nil, fc.unsupported("`**` of an Integer by a non-literal Integer exponent (write the exponent as a literal, or make the base Real)") } } - return wrap(Binary{Op: n.Operator, L: l, R: r, T: t}), nil + var x Expr = Binary{Op: n.Operator, L: l, R: r, T: t} + if t == TypeReal { + if x, err = fc.rationalArithmetic(x.(Binary)); err != nil { + return nil, err + } + } + return wrap(x), nil case ast.OpLt, ast.OpLe, ast.OpGt, ast.OpGe: l, r, _, wrap, err := fc.numericOperands(n) if err != nil { return nil, err } - return wrap(Binary{Op: n.Operator, L: l, R: r, T: TypeBool}), nil + x, err := fc.rationalComparison(Binary{Op: n.Operator, L: l, R: r, T: TypeBool}) + if err != nil { + return nil, err + } + return wrap(x), nil case ast.OpEq, ast.OpNeq, ast.OpEqEqEq, ast.OpNeqEqEq: return fc.compileEquality(n) case ast.OpAnd, ast.OpConditionalAnd, ast.OpOr, ast.OpConditionalOr, ast.OpXor, ast.OpImplies: @@ -927,6 +943,9 @@ func (fc *funcCompiler) compileOperator(n *ast.OperatorExpr) (Expr, error) { if x.Type() == TypeBool { return nil, fc.unsupported(fmt.Sprintf("'%s' over a Boolean", n.Operator)) } + if n.Operator == ast.OpNeg && x.Type() == TypeReal { + return fc.rationalNeg(x), nil + } return Unary{Op: n.Operator, X: x, T: x.Type()}, nil case ast.OpNot: if len(n.Operands) != 1 { @@ -983,6 +1002,10 @@ func (fc *funcCompiler) hoist(v Expr, lets []Let) (Expr, []Let) { fc.temps++ // A NUL cannot occur in a source name, so the temporary shadows nothing. name := fmt.Sprintf("\x00%d", fc.temps) + if fc.exactTemps == nil { + fc.exactTemps = map[string]exactness{} + } + fc.exactTemps[name] = fc.classify(v, false) return Var{Name: name, T: v.Type()}, append(lets, Let{Name: name, Value: v}) } @@ -1164,6 +1187,13 @@ func (fc *funcCompiler) finishLibCall(fqn string, params []string, args []Arg) ( if why != "" { return nil, fc.unsupported(why) } + if op == LibIsZero || op == LibIsUnit { + for _, a := range args { + if fc.exactness(a.Value) == exactRational { + return nil, fc.unsupported(fmt.Sprintf("%s of an exact Rational binary64 does not hold exactly (compiled code holds Rationals as binary64)", fqn)) + } + } + } for i, a := range args { if args[i].Value, err = fc.coerce(a.Value, op.Operands()[a.Param], fmt.Sprintf("argument for %s of %s", params[a.Param], fqn)); err != nil { return nil, err diff --git a/internal/translate/codegen/compile_fn.go b/internal/translate/codegen/compile_fn.go index 840e618972..40bf200ab7 100644 --- a/internal/translate/codegen/compile_fn.go +++ b/internal/translate/codegen/compile_fn.go @@ -453,6 +453,9 @@ func (fc *funcCompiler) compileSample(n *ast.InvocationExpr) (Sample, error) { if dom, err = fc.toMany(dom, dom.Type().Seq(), "argument for parameter \"domainValues\""); err != nil { return Sample{}, err } + if err := fc.exactOperands("Sample", dom); err != nil { + return Sample{}, err + } x := fc.temp(dom.Type().Elem()) at, err := fc.applyToOne(fargs[0], Var{Name: x.Name, T: x.Type}) if err != nil { diff --git a/internal/translate/codegen/compile_seq.go b/internal/translate/codegen/compile_seq.go index 41661beabd..bd48b2ab6e 100644 --- a/internal/translate/codegen/compile_seq.go +++ b/internal/translate/codegen/compile_seq.go @@ -94,6 +94,9 @@ func (fc *funcCompiler) pairOperand(v, other Expr, left bool, fail string, bare desc := fixed if desc == "" { desc = article(other.Type().Elem()) + if other.Type().Elem() == TypeReal && fc.exactness(other) != binary64 { + desc = "a Rational" + } } if left && other.Type().Many() { return ToOne{X: v, Fail: fail, Bare: bare, Other: other, OtherOne: desc} @@ -247,7 +250,7 @@ func (fc *funcCompiler) compileEquality(n *ast.OperatorExpr) (Expr, error) { l, _ = fc.coerce(l, t, "") r, _ = fc.coerce(r, t, "") } - return Binary{Op: op, L: l, R: r, T: TypeBool}, nil + return fc.rationalComparison(Binary{Op: op, L: l, R: r, T: TypeBool}) } fc.c.collections = true le, re := lt.Elem(), rt.Elem() @@ -271,6 +274,11 @@ func (fc *funcCompiler) compileEquality(n *ast.OperatorExpr) (Expr, error) { if r, err = fc.toMany(r, re.Seq(), "the right operand of '"+n.Operator.String()+"'"); err != nil { return nil, err } + if fc.exactness(l) == exactRational || fc.exactness(r) == exactRational { + if err := fc.exactOperands(fmt.Sprintf("'%s'", n.Operator), l, r); err != nil { + return nil, err + } + } return SeqEq{L: l, R: r, Neq: neq, Ident: ident}, nil } @@ -363,6 +371,11 @@ func (fc *funcCompiler) seqCall(op SeqOp, realAgg bool, args []Expr) (Expr, erro return nil, err } } + if !realAgg { + if err := fc.exactOperands(op.Name(), args...); err != nil { + return nil, err + } + } t, err := fc.seqResult(op, elem) if err != nil { return nil, err @@ -460,6 +473,9 @@ func (fc *funcCompiler) compileBodyOp(op SeqOp, operand ast.Node, body ast.Node) default: return nil, fc.unsupported(op.Name() + " with a body") } + if err := fc.exactOperands(op.Name(), seq); err != nil { + return nil, err + } return Fold{Op: op, Seq: seq, Body: lambda, T: t}, nil } diff --git a/internal/translate/codegen/emit_c.go b/internal/translate/codegen/emit_c.go index f72c3b93dc..cebdc871d2 100644 --- a/internal/translate/codegen/emit_c.go +++ b/internal/translate/codegen/emit_c.go @@ -124,6 +124,34 @@ static inline int sysml_cmp_ir(sysml_int a, sysml_real r) { return r > t ? -1 : (r < t ? 1 : 0); } +/* An Integer in exact Rational arithmetic, computed in binary64 only over values binary64 holds. */ +static inline sysml_real sysml_to_real_exact(sysml_int a) { + if (__builtin_expect(a > (1LL << 53) || a < -(1LL << 53), 0)) sysml_fail("unsupported: exact Rational arithmetic over an Integer beyond 2^53, which binary64 does not hold exactly"); + return (sysml_real)a; +} + +/* r with an exact Rational's unsigned zero. */ +static inline sysml_real sysml_unsigned_zero(sysml_real r) { return r == 0 ? 0.0 : r; } + +/* The exact whole number r, which binary64 holds below 2^53. */ +static inline sysml_real sysml_whole(sysml_real r) { + if (__builtin_expect(r >= 9007199254740992.0 || r <= -9007199254740992.0, 0)) sysml_fail("unsupported: exact Rational arithmetic reaching 2^53, beyond which binary64 does not hold a whole number exactly"); + return sysml_unsigned_zero(r); +} + +/* Orders the exact quotient a/b, b nonzero, against the whole number c. */ +static inline int sysml_cmp_q(sysml_int a, sysml_int b, __int128 c) { + __int128 n = a, d = b; + if (d < 0) { n = -n; d = -d; } + __int128 m = c * d; + return n < m ? -1 : (n > m ? 1 : 0); +} + +static inline sysml_int sysml_nonzero(sysml_int b) { + if (__builtin_expect(b == 0, 0)) sysml_fail("division by zero"); + return b; +} + static inline sysml_real sysml_finite(sysml_real r) { if (__builtin_expect(!isfinite(r), 0)) sysml_fail("arithmetic overflow: result is not a finite Real"); return r; @@ -402,7 +430,7 @@ static sysml_real sysml_parse_real(const char *s, const char *name) { fprintf(stderr, "argument %s: arithmetic overflow: %s is outside the Real range\n", name, s); exit(1); } - return v; + return v == 0 ? 0.0 : v; } static sysml_bool sysml_parse_bool(const char *s, const char *name) { @@ -754,6 +782,9 @@ func (e *cEmitter) expr(x Expr) string { if x.X.Type().Many() { return "sysml_widen(" + e.expr(x.X) + ")" } + if x.Exact { + return "sysml_to_real_exact(" + e.expr(x.X) + ")" + } return "(sysml_real)" + e.expr(x.X) case Unary: return e.unary(x) @@ -829,6 +860,9 @@ func (e *cEmitter) unary(x Unary) string { if x.T == TypeInt { return "sysml_neg(" + operand + ")" } + if x.Exact { + return "(0.0 - " + operand + ")" + } return "(-" + operand + ")" } e.err = fmt.Errorf("codegen: C emitter has no unary case for %s", x.Op) @@ -836,6 +870,18 @@ func (e *cEmitter) unary(x Unary) string { } func (e *cEmitter) binary(x Binary) string { + if q, ok := exactQuotient(x.L); ok && isComparison(x.Op) { + return e.sequenced([]Expr{q.L, q.R}, func(v []string) string { + return fmt.Sprintf("({ sysml_nonzero(%s); %s; })", v[1], e.sequenced([]Expr{wholeOperand(x.R)}, func(c []string) string { + return fmt.Sprintf("(sysml_cmp_q(%s, %s, (__int128)%s) %s 0)", v[0], v[1], c[0], cOperator(x.Op)) + })) + }) + } + if q, ok := exactQuotient(x.R); ok && isComparison(x.Op) { + return e.sequenced([]Expr{wholeOperand(x.L), q.L, q.R}, func(v []string) string { + return fmt.Sprintf("(-sysml_cmp_q(%s, sysml_nonzero(%s), (__int128)%s) %s 0)", v[1], v[2], v[0], cOperator(x.Op)) + }) + } if i, ok := widenedInt(x.L); ok && isComparison(x.Op) && !isWidenedInt(x.R) { return e.sequenced([]Expr{i, x.R}, func(v []string) string { return fmt.Sprintf("(sysml_cmp_ir(%s, %s) %s 0)", v[0], v[1], cOperator(x.Op)) @@ -861,21 +907,26 @@ func (e *cEmitter) binary(x Binary) string { func (e *cEmitter) strict(x Binary, l, r string) string { operands := x.L.Type() switch x.Op { - case ast.OpAdd, ast.OpSub, ast.OpMul: + case ast.OpAdd, ast.OpSub: + if operands == TypeInt { + return fmt.Sprintf("sysml_%s(%s, %s)", map[ast.OperatorKind]string{ast.OpAdd: "add", ast.OpSub: "sub"}[x.Op], l, r) + } + return cUnsignedZero(x, fmt.Sprintf("sysml_finite(%s %s %s)", l, cOperator(x.Op), r)) + case ast.OpMul: if operands == TypeInt { - return fmt.Sprintf("sysml_%s(%s, %s)", map[ast.OperatorKind]string{ast.OpAdd: "add", ast.OpSub: "sub", ast.OpMul: "mul"}[x.Op], l, r) + return fmt.Sprintf("sysml_mul(%s, %s)", l, r) } - return fmt.Sprintf("sysml_finite(%s %s %s)", l, cOperator(x.Op), r) + return cUnsignedZero(x, fmt.Sprintf("sysml_finite(%s * %s)", l, r)) case ast.OpDiv: if operands == TypeInt { return fmt.Sprintf("sysml_quot(%s, %s)", l, r) } - return fmt.Sprintf("sysml_rdiv(%s, %s)", l, r) + return cUnsignedZero(x, fmt.Sprintf("sysml_rdiv(%s, %s)", l, r)) case ast.OpMod: if operands == TypeInt { return fmt.Sprintf("sysml_mod(%s, %s)", l, r) } - return fmt.Sprintf("sysml_rmod(%s, %s)", l, r) + return cUnsignedZero(x, fmt.Sprintf("sysml_rmod(%s, %s)", l, r)) case ast.OpPow: return e.pow(x, l, r) case ast.OpLt, ast.OpLe, ast.OpGt, ast.OpGe, ast.OpEq, ast.OpNeq: @@ -887,6 +938,17 @@ func (e *cEmitter) strict(x Binary, l, r string) string { return "0" } +// cUnsignedZero gives an exact Rational operation's zero result no sign. +func cUnsignedZero(x Binary, r string) string { + if x.Whole && x.Guard { + return "sysml_whole(" + r + ")" + } + if x.Exact { + return "sysml_unsigned_zero(" + r + ")" + } + return r +} + // pow follows semantics.Pow: Integer ** Integer is an Integer, anything else a // Real. The compiler has already widened the operands of a Real power. func (e *cEmitter) pow(x Binary, l, r string) string { diff --git a/internal/translate/codegen/emit_go.go b/internal/translate/codegen/emit_go.go index 40489dab21..b76126912e 100644 --- a/internal/translate/codegen/emit_go.go +++ b/internal/translate/codegen/emit_go.go @@ -216,6 +216,45 @@ func sysmlCmpIR(a sysmlInt, r float64) int { // sysmlToReal is the binary64 nearest a, ties to even; beyond the binary64 // range it is an infinity, which the arithmetic it feeds reports. +// sysmlToRealExact is the Integer a in exact Rational arithmetic, which +// compiled code computes in binary64 only over values binary64 holds. +func sysmlToRealExact(a sysmlInt) float64 { + if a.big != nil || a.small > 1<<53 || a.small < -(1<<53) { + sysmlFail("unsupported: exact Rational arithmetic over an Integer beyond 2^53, which binary64 does not hold exactly") + } + return float64(a.small) +} + +// sysmlWhole is the exact whole number r, which binary64 holds below 2^53. +func sysmlWhole(r float64) float64 { + if r >= 1<<53 || r <= -(1<<53) { + sysmlFail("unsupported: exact Rational arithmetic reaching 2^53, beyond which binary64 does not hold a whole number exactly") + } + return sysmlUnsignedZero(r) +} + +// sysmlRatQuot is the exact Integer quotient a/b. +func sysmlRatQuot(a, b sysmlInt) *big.Rat { + if b.sign() == 0 { + sysmlFail("division by zero") + } + return new(big.Rat).SetFrac(a.toBig(), b.toBig()) +} + +// sysmlCmpQR orders the exact quotient q against the finite Real r. +func sysmlCmpQR(q *big.Rat, r float64) int { return q.Cmp(new(big.Rat).SetFloat64(r)) } + +// sysmlCmpQI orders the exact quotient q against the Integer i. +func sysmlCmpQI(q *big.Rat, i sysmlInt) int { return q.Cmp(new(big.Rat).SetInt(i.toBig())) } + +// sysmlUnsignedZero is r with an exact Rational's unsigned zero. +func sysmlUnsignedZero(r float64) float64 { + if r == 0 { + return 0 + } + return r +} + func sysmlToReal(a sysmlInt) float64 { if a.big == nil { return float64(a.small) @@ -442,7 +481,7 @@ func sysmlParseReal(s, name string) float64 { fmt.Fprintf(os.Stderr, "argument %s: arithmetic overflow: %s is outside the Real range\n", name, s) os.Exit(1) } - return v + return sysmlUnsignedZero(v) } // sysmlRealNotation reports whether s is decimal Real notation: an optional @@ -724,6 +763,9 @@ func (e *goEmitter) expr(x Expr) string { if x.X.Type().Many() { return "sysmlWiden(" + e.expr(x.X) + ")" } + if x.Exact { + return "sysmlToRealExact(" + e.expr(x.X) + ")" + } return "sysmlToReal(" + e.expr(x.X) + ")" case Unary: operand := e.expr(x.X) @@ -736,6 +778,9 @@ func (e *goEmitter) expr(x Expr) string { if x.T == TypeInt { return "sysmlNeg(" + operand + ")" } + if x.Exact { + return "(0 - " + operand + ")" + } return "(-" + operand + ")" } case Binary: @@ -783,21 +828,26 @@ func (e *goEmitter) binary(x Binary) string { l, r := e.expr(x.L), e.expr(x.R) ints := x.L.Type() == TypeInt switch x.Op { - case ast.OpAdd, ast.OpSub, ast.OpMul: + case ast.OpAdd, ast.OpSub: + if ints { + return fmt.Sprintf("sysml%s(%s, %s)", map[ast.OperatorKind]string{ast.OpAdd: "Add", ast.OpSub: "Sub"}[x.Op], l, r) + } + return goUnsignedZero(x, fmt.Sprintf("sysmlFinite(%s %s %s)", l, cOperator(x.Op), r)) + case ast.OpMul: if ints { - return fmt.Sprintf("sysml%s(%s, %s)", map[ast.OperatorKind]string{ast.OpAdd: "Add", ast.OpSub: "Sub", ast.OpMul: "Mul"}[x.Op], l, r) + return fmt.Sprintf("sysmlMul(%s, %s)", l, r) } - return fmt.Sprintf("sysmlFinite(%s %s %s)", l, cOperator(x.Op), r) + return goUnsignedZero(x, fmt.Sprintf("sysmlFinite(%s * %s)", l, r)) case ast.OpDiv: if ints { return fmt.Sprintf("sysmlQuot(%s, %s)", l, r) } - return fmt.Sprintf("sysmlRDiv(%s, %s)", l, r) + return goUnsignedZero(x, fmt.Sprintf("sysmlRDiv(%s, %s)", l, r)) case ast.OpMod: if ints { return fmt.Sprintf("sysmlMod(%s, %s)", l, r) } - return fmt.Sprintf("sysmlRMod(%s, %s)", l, r) + return goUnsignedZero(x, fmt.Sprintf("sysmlRMod(%s, %s)", l, r)) case ast.OpPow: if x.T == TypeInt { return fmt.Sprintf("sysmlIPow(%s, %s)", l, r) @@ -821,12 +871,35 @@ func (e *goEmitter) binary(x Binary) string { return "0" } +// goUnsignedZero gives an exact Rational operation's zero result no sign. +func goUnsignedZero(x Binary, r string) string { + if x.Whole && x.Guard { + return "sysmlWhole(" + r + ")" + } + if x.Exact { + return "sysmlUnsignedZero(" + r + ")" + } + return r +} + // mixedComparison orders an Integer against a Real exactly, as the // interpreter does, rather than comparing the Integer's binary64 rounding. func (e *goEmitter) mixedComparison(x Binary) (string, bool) { if !isComparison(x.Op) { return "", false } + if q, ok := exactQuotient(x.L); ok { + if i, ok := widenedInt(x.R); ok { + return fmt.Sprintf("(sysmlCmpQI(sysmlRatQuot(%s, %s), %s) %s 0)", e.expr(q.L), e.expr(q.R), e.expr(i), cOperator(x.Op)), true + } + return fmt.Sprintf("(sysmlCmpQR(sysmlRatQuot(%s, %s), %s) %s 0)", e.expr(q.L), e.expr(q.R), e.expr(x.R), cOperator(x.Op)), true + } + if q, ok := exactQuotient(x.R); ok { + if i, ok := widenedInt(x.L); ok { + return fmt.Sprintf("func() bool { l := %s; return -sysmlCmpQI(sysmlRatQuot(%s, %s), l) %s 0 }()", e.expr(i), e.expr(q.L), e.expr(q.R), cOperator(x.Op)), true + } + return fmt.Sprintf("func() bool { l := %s; return -sysmlCmpQR(sysmlRatQuot(%s, %s), l) %s 0 }()", e.expr(x.L), e.expr(q.L), e.expr(q.R), cOperator(x.Op)), true + } if i, ok := widenedInt(x.L); ok && !isWidenedInt(x.R) { return fmt.Sprintf("(sysmlCmpIR(%s, %s) %s 0)", e.expr(i), e.expr(x.R), cOperator(x.Op)), true } diff --git a/internal/translate/codegen/integer.go b/internal/translate/codegen/integer.go index 60a47f5bc8..7a9b539c79 100644 --- a/internal/translate/codegen/integer.go +++ b/internal/translate/codegen/integer.go @@ -44,6 +44,20 @@ func widenedInt(x Expr) (Expr, bool) { return nil, false } +// exactQuotient is x as an Integer quotient compared exactly, if it is one. +func exactQuotient(x Expr) (Binary, bool) { + q, ok := x.(Binary) + return q, ok && q.Exact && q.Op == ast.OpDiv && q.L.Type() == TypeInt +} + +// wholeOperand is the whole number x as an Integer where it widens one. +func wholeOperand(x Expr) Expr { + if i, ok := widenedInt(x); ok { + return i + } + return x +} + func isWidenedInt(x Expr) bool { _, ok := widenedInt(x) return ok diff --git a/internal/translate/codegen/ir.go b/internal/translate/codegen/ir.go index 68f2fc63ee..5bb732f05a 100644 --- a/internal/translate/codegen/ir.go +++ b/internal/translate/codegen/ir.go @@ -150,7 +150,11 @@ type IntLit struct { Value int64 Big *big.Int } -type RealLit struct{ Value float64 } +type RealLit struct { + Value float64 + // Rat is the exact Rational a decimal literal denotes; nil for a Real. + Rat *big.Rat +} type BoolLit struct{ Value bool } // Var reads a parameter or a body-local variable. @@ -165,6 +169,14 @@ type Binary struct { Op ast.OperatorKind L, R Expr T Type + // Exact is a Rational operation over operands binary64 holds exactly, + // computed as one correctly rounded binary64 operation; an Integer + // quotient is then compared exactly against a whole number. + Exact bool + // Whole is an Exact sum, difference or product of whole numbers. + Whole bool + // Guard requires a Whole result below 2^53, where binary64 holds it exactly. + Guard bool } // Unary applies `-`, `+` or `not`. @@ -172,6 +184,8 @@ type Unary struct { Op ast.OperatorKind X Expr T Type + // Exact negates an exact Rational, whose zero is unsigned. + Exact bool } // Cond is `if c ? a else b`, both branches of type T. @@ -202,7 +216,11 @@ type LibCall struct { } // ToReal widens an Integer to a Real; over a collection, every element. -type ToReal struct{ X Expr } +type ToReal struct { + X Expr + // Exact requires the Integer to be one binary64 holds exactly. + Exact bool +} // NullLit is `null`, the empty value of collection type T (or TypeNull). type NullLit struct{ T Type } diff --git a/internal/translate/codegen/rational.go b/internal/translate/codegen/rational.go new file mode 100644 index 0000000000..f8fc5d84b6 --- /dev/null +++ b/internal/translate/codegen/rational.go @@ -0,0 +1,398 @@ +package codegen + +import ( + "fmt" + "math" + "math/big" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// exactness is how the interpreter holds a numeric expression's value. +type exactness int + +const ( + // binary64 is a Real, held as binary64. + binary64 exactness = iota + // exactWhole is an exact whole number of magnitude at most 2^53. + exactWhole + // exactDouble is an exact Integer or Rational that binary64 holds exactly. + exactDouble + // exactRational is an exact Integer or Rational binary64 may not hold. + exactRational +) + +const maxExactDouble = 1 << 53 + +// exactness classifies x. Compiled code holds every Rational as binary64, so +// it compiles exact arithmetic only where one rounding gives the exact result. +func (fc *funcCompiler) exactness(x Expr) exactness { return fc.classify(x, true) } + +// classify is exactness; reached is whether exactWiden can still guard x's +// Integer leaves and whole results, which a temporary or element has lost. +func (fc *funcCompiler) classify(x Expr, reached bool) exactness { + switch x := x.(type) { + case IntLit: + if x.Big == nil && x.Value >= -maxExactDouble && x.Value <= maxExactDouble { + return exactWhole + } + return exactRational + case RealLit: + if x.Rat == nil { + return binary64 + } + if f, exact := x.Rat.Float64(); exact { + if x.Rat.IsInt() && math.Abs(f) <= maxExactDouble { + return exactWhole + } + return exactDouble + } + return exactRational + case Var: + if e, ok := fc.exactTemps[x.Name]; ok { + return e + } + case ToReal: + if !reached && !x.Exact && x.X.Type().Elem() == TypeInt { + return exactRational + } + return fc.classify(x.X, reached) + case Unary: + return fc.classify(x.X, reached) + case Binary: + if x.T.Elem() == TypeReal && isArithmetic(x.Op) { + if fc.classify(x.L, reached) == binary64 || fc.classify(x.R, reached) == binary64 { + return binary64 + } + if x.Whole && (reached || x.Guard) { + return exactWhole + } + return exactRational + } + case Cond: + return join(fc.classify(x.Then, reached), fc.classify(x.Else, reached)) + case LibCall: + switch x.Op { + case LibAbsReal, LibMaxReal, LibMinReal: + e := fc.classify(x.Args[0].Value, reached) + for _, a := range x.Args[1:] { + e = join(e, fc.classify(a.Value, reached)) + } + return e + } + case ToMany: + return fc.classify(x.X, reached) + case ToOne: + return fc.classify(x.X, reached) + case Checked: + return fc.classify(x.X, reached) + case Framed: + return fc.classify(x.X, reached) + case Let: + return fc.classify(x.In, reached) + case Coalesce: + return join(fc.classify(x.L, false), fc.classify(x.R, false)) + case SeqLit: + if len(x.Elems) == 0 { + return binary64 + } + e := fc.classify(x.Elems[0], false) + for _, el := range x.Elems[1:] { + e = join(e, fc.classify(el, false)) + } + return e + case Index: + return fc.classify(x.Seq, false) + } + if x.Type().Elem() == TypeInt { + return exactWhole + } + return binary64 +} + +// join is the class of a value that is either of class a or of class b: a Real +// or an exact number binary64 holds is held exactly, though not known whole. +func join(a, b exactness) exactness { + if (a == binary64) != (b == binary64) && max(a, b) != exactRational { + return exactDouble + } + return max(a, b) +} + +// heldExactly is whether binary64 holds every value of class e exactly. +func heldExactly(e exactness) bool { return e == exactWhole || e == exactDouble } + +func isArithmetic(op ast.OperatorKind) bool { + switch op { + case ast.OpAdd, ast.OpSub, ast.OpMul, ast.OpDiv, ast.OpMod, ast.OpPow: + return true + } + return false +} + +// constRat is the exact value of a constant numeric expression. +func constRat(x Expr) (*big.Rat, bool) { + switch x := x.(type) { + case RealLit: + return x.Rat, x.Rat != nil + case IntLit: + if x.Big != nil { + return new(big.Rat).SetInt(x.Big), true + } + return new(big.Rat).SetInt64(x.Value), true + case ToReal: + if !x.X.Type().Many() { + return constRat(x.X) + } + } + return nil, false +} + +// exactLit is the literal of the exact Rational r, if binary64 has a finite +// nonzero-for-nonzero value for it. +func exactLit(r *big.Rat) (RealLit, bool) { + f, _ := r.Float64() + if math.IsInf(f, 0) || (f == 0 && r.Sign() != 0) { + return RealLit{}, false + } + return RealLit{Value: f + 0, Rat: r}, true +} + +// rationalArithmetic checks the Real-typed arithmetic b. With a Real operand +// it is binary64 arithmetic; over exact operands only constants (folded) and a +// single operation over values binary64 holds exactly (one correct rounding) +// agree with the interpreter's exact Rational result. +func (fc *funcCompiler) rationalArithmetic(b Binary) (Expr, error) { + l, r := fc.exactness(b.L), fc.exactness(b.R) + if l == binary64 || r == binary64 { + return b, nil + } + if b.Op == ast.OpPow { + if !isWidenedInt(b.R) { + return b, nil + } + if a, okA := constRat(b.L); okA { + if n, okN := b.R.(ToReal).X.(IntLit); okN && n.Big == nil { + if v, err := semantics.RatPow(semantics.RatValue(a), semantics.IntValue(n.Value), semantics.DefaultMaxIntegerBits); err == nil { + if lit, ok := exactLit(v.Rat()); ok { + return lit, nil + } + } + } + } + return nil, fc.unsupported("'**' of an exact Rational by an Integer exponent (compiled code holds Rationals as binary64)") + } + if a, okA := constRat(b.L); okA { + if c, okC := constRat(b.R); okC && b.Op != ast.OpMod { + if v, err := semantics.RatArith(b.Op, semantics.RatValue(a), semantics.RatValue(c), semantics.DefaultMaxIntegerBits); err == nil { + if lit, ok := exactLit(v.Rat()); ok { + return lit, nil + } + } + } + } + if b.Op == ast.OpDiv && b.L.Type() == TypeInt { + return b, nil + } + if heldExactly(l) && heldExactly(r) { + if b.Op == ast.OpMul && (scalesExactly(b.L) || scalesExactly(b.R)) { + // Scaling by 2^k, k >= 0, commutes with rounding, so neither operand needs a guard. + } else { + b.L, b.R = exactWiden(b.L), exactWiden(b.R) + } + b.Exact = true + b.Whole = l == exactWhole && r == exactWhole && (b.Op == ast.OpAdd || b.Op == ast.OpSub || b.Op == ast.OpMul) + return b, nil + } + return nil, fc.unsupported(fmt.Sprintf("exact Rational arithmetic '%s' over a value binary64 does not hold exactly (compiled code holds Rationals as binary64)", b.Op)) +} + +// scalesExactly is whether x is a constant 2^k, k >= 0. +func scalesExactly(x Expr) bool { + r, ok := constRat(x) + if !ok || !r.IsInt() || r.Sign() <= 0 { + return false + } + n := r.Num() + return n.BitLen()-1 == int(n.TrailingZeroBits()) +} + +// exactWiden guards x's Integer leaves and whole results, so that binary64 +// holds them exactly or the program fails. +func exactWiden(x Expr) Expr { + switch w := x.(type) { + case ToReal: + if w.X.Type() == TypeInt { + w.Exact = true + } + return w + case Binary: + if w.Whole { + w.Guard = true + } + return w + case Unary: + if w.Exact { + w.X = exactWiden(w.X) + } + return w + case Cond: + w.Then, w.Else = exactWiden(w.Then), exactWiden(w.Else) + return w + case ToOne: + w.X = exactWiden(w.X) + return w + case Checked: + w.X = exactWiden(w.X) + return w + case Framed: + w.X = exactWiden(w.X) + return w + case Let: + w.In = exactWiden(w.In) + return w + case LibCall: + switch w.Op { + case LibAbsReal, LibMaxReal, LibMinReal: + args := make([]Arg, len(w.Args)) + for i, a := range w.Args { + args[i] = Arg{Param: a.Param, Value: exactWiden(a.Value)} + } + w.Args = args + } + return w + } + return x +} + +// rationalComparison checks the comparison or equality b over Real operands. +// A Real against a Rational literal binary64 does not hold compares exactly +// through the literal's nearest binary64 and the side the Rational lies on. +func (fc *funcCompiler) rationalComparison(b Binary) (Expr, error) { + if b.L.Type() != TypeReal || b.R.Type() != TypeReal { + return b, nil + } + l, r := fc.exactness(b.L), fc.exactness(b.R) + if l != exactRational && r != exactRational { + if l != binary64 { + b.L = exactWiden(b.L) + } + if r != binary64 { + b.R = exactWiden(b.R) + } + return b, nil + } + if q, ok := intQuotient(b.L); ok && r == exactWhole { + b.L, b.R = q, exactWiden(b.R) + return b, nil + } + if q, ok := intQuotient(b.R); ok && l == exactWhole { + b.L, b.R = exactWiden(b.L), q + return b, nil + } + if a, okA := constRat(b.L); okA { + if c, okC := constRat(b.R); okC { + return BoolLit{Value: compares(b.Op, a.Cmp(c))}, nil + } + } + if c, ok := constRat(b.R); ok && l == binary64 { + return tieBroken(b.Op, b.L, c), nil + } + if c, ok := constRat(b.L); ok && r == binary64 { + return tieBroken(flipped(b.Op), b.R, c), nil + } + return nil, fc.unsupported(fmt.Sprintf("'%s' of an exact Rational binary64 does not hold exactly (compiled code holds Rationals as binary64)", b.Op)) +} + +// intQuotient is the Integer quotient x marked to be compared exactly. +func intQuotient(x Expr) (Binary, bool) { + q, ok := x.(Binary) + if !ok || q.Op != ast.OpDiv || q.L.Type() != TypeInt { + return Binary{}, false + } + q.Exact = true + return q, true +} + +func compares(op ast.OperatorKind, c int) bool { + switch op { + case ast.OpLt: + return c < 0 + case ast.OpLe: + return c <= 0 + case ast.OpGt: + return c > 0 + case ast.OpGe: + return c >= 0 + case ast.OpEq: + return c == 0 + } + return c != 0 +} + +func flipped(op ast.OperatorKind) ast.OperatorKind { + switch op { + case ast.OpLt: + return ast.OpGt + case ast.OpLe: + return ast.OpGe + case ast.OpGt: + return ast.OpLt + case ast.OpGe: + return ast.OpLe + } + return op +} + +// tieBroken is `x op r` for a finite binary64 x and a Rational r binary64 does +// not hold: no binary64 lies strictly between r and its nearest d, so x orders +// against r as against d except at x == d, where it falls on r's side of d. +func tieBroken(op ast.OperatorKind, x Expr, r *big.Rat) Expr { + d, _ := r.Float64() + if math.IsInf(d, 0) { + d = math.Copysign(math.MaxFloat64, d) + } + above := r.Cmp(new(big.Rat).SetFloat64(d)) > 0 + switch op { + case ast.OpLt, ast.OpLe: + if above { + op = ast.OpLe + } else { + op = ast.OpLt + } + case ast.OpGt, ast.OpGe: + if above { + op = ast.OpGt + } else { + op = ast.OpGe + } + case ast.OpEq: + op, d = ast.OpLt, -math.MaxFloat64 + default: + op, d = ast.OpGe, -math.MaxFloat64 + } + return Binary{Op: op, L: x, R: RealLit{Value: d}, T: TypeBool} +} + +// rationalNeg is `-x` over Reals; an exact zero negates to itself. +func (fc *funcCompiler) rationalNeg(x Expr) Expr { + if fc.exactness(x) == binary64 { + return Unary{Op: ast.OpNeg, X: x, T: TypeReal} + } + if lit, ok := x.(RealLit); ok && lit.Rat != nil { + return RealLit{Value: 0 - lit.Value, Rat: new(big.Rat).Neg(lit.Rat)} + } + return Unary{Op: ast.OpNeg, X: x, T: TypeReal, Exact: true} +} + +// exactOperands refuses the collection operation what over Real elements the +// interpreter holds exactly. +func (fc *funcCompiler) exactOperands(what string, operands ...Expr) error { + for _, x := range operands { + if x.Type().Elem() == TypeReal && fc.exactness(x) != binary64 { + return fc.unsupported(fmt.Sprintf("%s over exact Rationals (compiled code holds Rationals as binary64)", what)) + } + } + return nil +} diff --git a/internal/workspace/libs/stdlib.snapshot b/internal/workspace/libs/stdlib.snapshot index 845d74b9ff880557092049da08997aaf1e008072..a0f537a13b1e671635ce689b7d37ea3d9c553f58 100644 GIT binary patch delta 4996 zcma)A4Nz3q6@H6==(2!{i$w%fBpwtO`~gK1-!5WI>JP?g)s70jNTU=Ls+QPh%tLt* z+fh+5H>09bjW)F^{;n}H;=id9SB;`JM)1Fh5-?TjjIBNQ-1l~uRWoJg4sySHe(pKn zIrlu9o9>vJag|OEsMvCStGP(g6qBa-X-X?i@z<09O$pSL)|wKeDQz^Rt)`eYC0J7| zni8TZp_&q=DeW|+y{2@~lyFVys3}%WiO`fzn({MEiPV(Nni8ccT{IA6pVfYD(R{WK26U$I#m{TyuUC75PaQ>AsiOa@leKE`(M zwdxGkG#YM)NwJSEe9(WEqw|dM=?5aFyvYihU>3{X46Q@eJEbc#-*kxcr5Wr<18iiO zRbXb}W8tZ{ElHA>%PhYNrm=HlAduw_g9Ij@fw$?;m)JN3gF}4{*S`ai&UxcNHj(OG z6=`Pi*fs_RefZ6JTf{?O0P&hLhS!82oQyJ;(XV>5H%jzu1E@8Dh9LJPDN^7W5{ z|5Hdtc@Vi^s^!qdfnzcZNBFVS5Vc=J%u}cLUJ^UENAO6kxcN3IrN%-OTX0yXSaEHk zVGoHtAKQVNB$bKQ8Mix{Fe&$n%-ibhj_$5{r78%a(&*!do;Y|{B|f)h8}~tAC=ZY% z4VCFvZV(Ip0@7LU8_Otkjf(S1jCn7GXbj!JPcL_TN7m!L;QCjh}bf>(#J(ZNX+K&^;tZaa^M$lnvUJ_wa?!SpD zeOT;an8sDA|LiUZMvV|zM9>sxY&8tWCxQP|XUY6m_N^2hj|45Vu*M>m?{vztJJna|7dTZ_A(lpb&MU+5V+|_wIK7@;g}Y zY-np)(fV>F->)|R>Lk7n+mXyePY90<2w&{?T0%bBxRmv<&dFi(Z*Az{kkq==`NxVJ zY{v=cN}enAy~%z~&33_k4~wM-atDr`(302qGTgv)G8qi_Pz+}xs`;=p!s}}P0~5#) zwS$6Fkju?C2HaghyGs2KY%_V7qNHmO)$x*1N!5dXJBbcRQXDJgVYvDTraP~lh9M%y z-DfK@X1+*&l^q- zJy_;yoV6-lFkR%@Tfxh?;Dg)TIgtFG`R%le9GKF14mz`%^KdG(uIOT`x2_s})ML6g z8N9KfA9>|!pYB6rE3-Wo9nQ?cN}&z2{Q)PL{EN^nxVTo-6!mYP7OPWUOCf z>-$Nch;DIhGH?&ET~BtfqyqRQr%Mw5R|5O07NRMh{uLd?HF8huyf9{;mc2{80@S}|K{^b*P^Z|rVjcjNXS1g)tj z8D3s)D9TzlKz}{zwn82q$liDl<9tuN9`|35IYGO83~=wd&*qgy+F9~H^q1z-8qqa# zYRajq=^k>Ic+rVy;=~*kTGVY;nUv<#RQdFA$)iI&eV!ICd76fRM2k=Uj=m(^w0b{y zj>~Ju{+$S?r!Gr2>G4-@@NlX}(~dwoj~eFhA%(_fZ8oB!Ha81bB^d`y?=1MVJ!+!M z>%}WA|2StNU642{zAly-&BfhGsj^zgyU0ZkhNSlR@r*W?WcrMMH)D^saOe(x&Mde^*0}{=1su2D)k8S< z=(eDCdUz8ZoOw3uRe{ORCvQ4EgJU$hxLJvWCFo0X%$LhoFsl&Tv4hF+AERJ~OBZUFg6rHVyqds#sE#}Iko zO%dM&@-J8R?tM5(oy${__{m7mtI@e)uLmpqVpx3Q@6F-!F8w!!-Nh$BfV=5sVeb~2rFCxk}< z#n82*_)XPyCVo@d9zb+EuFG4{gLPu0RTo69g+eQP_yA|4m1$SHiSQPGg>NNVaS}Y91;(Qh%C-$&OHr~O9QW3F4I&i(Hu(u zchhhM0-Y6&utf|a3rBPn;mHlsb7Kr5b?Zh=6_6h9_u06A!Fuxd%pl<}O(RIIm9$&c zn|4Jq%SYieMk7e$S$xLuEo;N-MnW{pd;)P6x(vTDajocyUSQHUD`dXw9=8`KGy|># nT)lAh#+8Vx4=x+7BwWe3`r_(`t3R#*xKePXIun`)))f5@Zi)%w delta 4797 zcmai24OEm>8lD-2?*n8Qi(efWL?B*J2U7$ynE{nmP=u^Kx~bvSax~4bOb@oiR9~Jp zMf`zxO>N7pwiOjkn_W%O)zwN($-mTN3Z;@MLO)QJx$pPQcfa|8cITYSIrIMC_df4) z@6ge9Mc+ZIYD&PGzprQzolIhSi5Vo;O=8_879g=ei3t)DB^D&HV2OoDtcS!xC1#XZ zn8ZvH>nX8&BzCXF!X*|Vu}FzUNi14oF%s(~vEC9hODtAmaT2pgEM8)LB$gntz7k86 zSdzq&C3c_0Qg$*3C=_?^IeYHX^B0EWwAB!fQ%=J~V@0NJU79|oq-bw@o#g@i^#;tr zWyKJJCF5a%XgwcQTM(ujhRYu!dC?vS#*&Q?D4uNlOY^Uh?>&g6&F}$+7sJoOukW{g zK8_XPwmlGm&+mbWcziqv_~b|cv>$t0z+BI_d99G(S}+0ZdegX1goSyRR@ijP zr`3il9)Y6Bn?vL>apvg40@I)_brn_l``&rvR5tf>ksk;qAt7T-O2F0OT#TPihA}2y zzaq6Sx8t}Huo`z*b*s|!T!*DIz>F37q*&{#u*`={Y@Gs&f_1GnzV8!rsHN%W!R*>F z6^0tbl)9*2<}b|~jceTp3-%(3f|R=TKNjb)~|x;8Z;VL5i}IZ;_xkhXe@gfxui*4 zc5~;O9f#_&vCkpuBjG0;x2~oG7WKm<|6pM(%p3LcR9yD4bRJ|JX3tSd!N_s*tUtbSn)pC zO_P`Zdx4>6m#Txgn`aqIL%Z@SqYZVN)0HZWdp%WaXxR=D)ReoMA=9|;v6xN@%QG+lkJZtHR&AlV))mF1=QtAPEGnDyyxoA# zjSz`NV<9@qnWbCr23uuhmln9C9D;kush!m$Uc2FX%Z>x_*6S z?F!Q=#_DEmo%Z9WkoOaDB9j*@Zzp?{v1jJBXYs^#$RZTV^>-nfupbw-7esO^*)eI) z3scKKc41>Fb?|T7AyM`IJ^7J`zFPWQ-%)P+`bwgK;~zkv@TWj_`+V>Q51O#N9)fz% z5VqR5OItle;R`#!B2Kz^edsr_gY6pGP7hVMcIxSO&WFs>XS?D1=1vNH_U)&K2cJEF zrbSSI{VqWgrjG{^ixxt6Ycg2)cYAb#`<7YvIil{a(+V`= zaTjr*DpqN;(2h^Lz#94W5Z&4|y|0ewmUJ27_ULubeL0}bGmUB_E_d^eJ5q)?EM|b% z-*_uWn?9pa|Dv+j)SSlpVGEo63sS{C1!V|>#ZRHyVAYJ7%l7r zQaVlSu^UF>8@nLTM?+|Ek>-S6fE~5J9L~MtCKtNo+*}~;681T(4R<|zatL>p{BYr{ z^Q2}uF|;CGw>nKN$H zzYp>ZCN;KukMXs3Y%GI&vFIuUpuK@GQho*keT{U~&n}F#DvM8o1#g{!7_=TB(D33( zOxIKcm_zR>({k?Hiq+sh&7>;#gAQUenAu9eL^b&4G-Pu0MTJ5%4 zJ{S5%oL2Cl%9Up8;%l+7iAIKegKM@yzc3%IuFzNO9Wxp2^B_62UWs|_%FNTTo1N%% z(&V{;v2Mv8tTxQ^oV5x^RqpOu^p8GJ_0h@AlA)@i#^Q;DrX$pyD;Xolzh6^_r6ZxQ z^8Sc8mm%X_mD_j$+O-|(-v3eh9NwRdZ3xA9={sPq^B=>525}I53O;MLd%c9@JYpk} z__d^~-tzj>l>gn}yrcz&*FDg3tvm{cx(RX|!C{o+o6+60=d#(_$*^=8{3Z z9)9jJ@(j^v{n1T1A!w?CAha$fs62fV5<+BV<0R=}Hk&cA*VR#vW^2qw&q!r%u4gRS znY-b3XT{jeS93~|)_Ay(dz|~MW5RTAMU#o!%HvK5>~bc2jecivL>>v_7SpBKI7s)` zH2vo24KCByyR$KFB}~P0-$6R2mqIFjb{68qkz-#?ESvP8=P*rvtg-Y9I-$4D48E0B zQi{&m@|z)_u3hEyXv(tXuRUB zjJ&L1m+RFNv0?Ew$S`rKdfI-{Pj-w?t=xazh669qu#C8;D&#`&i`uX_grOa4Yk&G< zsfJUBFu68j>hwI1M14xRU2`>8RqU?SrimTGBSDtfx8gFrB2L_WWa7)BDj9bj93p?1 zcuiBqC4y%EHDZ>`U9q#kT=O+iT8FZ6*Y|f$WtD9EKOOHxX;{@rmjvf5K0!1MaWyn^ zMt!BiPT32J*9f-Bo`Z6`sbago8}@GkAxy2v&PiB#Sr7>aplv>p`+z1&%x!oCUz>>a zN_K91{e$qME0B!kUwIYcAH!{9ysGqg%|>D0+FhU9Fm4v~rrsxn z@k8RT3Qm;?Ca&oM!(7F0e(q$B{7Ol~_56B7{T0a2Qc2$y|1&OWg?z8Xd2PVY33-#0 z4fNBzJIzhRmr9SgyUHjvKY}$%?^}*~MmD(|YgcZ%LfOrz_lbyc^NGdjPB*I)$}{;+ zD3@vzO0#J7>&GugT0S|Dds8jI7aRWPX2t2va7Z2IhqTdCC$jnQfs$&&cunk3U!vhE z`ziw{Uqjzl9Q80=FkWgSsBpRenVVI(aXrVq&q*Y!|4q<#Pe0HSqV>m>L*PPA>jvyI zmwuC2ZxB~<@DD79EH#GKD>(+_yOJ!o=j`Ui!aR6AktgDWoL*j^_$yQE{eBdpLVz( zTSk+4#R~dk8krJ~ZziU6*DjM(`C%SSGEsh-)U^|Fs_@ZB vcjA)N4xp7vD~(n^TK#Dapk<|%PHP~o`)LiLHJDZgts%5BU8x;e$DIEKoe)}D diff --git a/tests/grpc/conformance_test.go b/tests/grpc/conformance_test.go index ad6a8ac945..9ce0e31dba 100644 --- a/tests/grpc/conformance_test.go +++ b/tests/grpc/conformance_test.go @@ -16,6 +16,7 @@ import ( pb "github.com/Open-MBEE/OpenSysML/api/proto" "github.com/Open-MBEE/OpenSysML/internal/exec/analysis" "github.com/Open-MBEE/OpenSysML/internal/frontend/grpc" + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/tests/fixtures" "github.com/Open-MBEE/OpenSysML/tests/testutil/gobuild" ) @@ -843,6 +844,10 @@ func describeQuantity(q *pb.Quantity) string { magnitude = strconv.FormatInt(m.IntMagnitude, 10) case *pb.Quantity_RealMagnitude: magnitude = strconv.FormatFloat(m.RealMagnitude, 'g', -1, 64) + case *pb.Quantity_RationalMagnitude: + if r, ok := semantics.CanonicalRational(m.RationalMagnitude.GetNumerator(), m.RationalMagnitude.GetDenominator()); ok { + magnitude = r.FormatRational() + } } return fmt.Sprintf("%s [%s] = %s", magnitude, q.GetUnit(), unitTermText(q.GetUnitTerm())) } diff --git a/tests/semantics/quantity_eval_test.go b/tests/semantics/quantity_eval_test.go index 1d510a796e..84d71c0495 100644 --- a/tests/semantics/quantity_eval_test.go +++ b/tests/semantics/quantity_eval_test.go @@ -140,7 +140,7 @@ func TestQuantityExpressionsFold(t *testing.T) { {"affirmed", symbols.FilterValueQuantity, "3 [kg]"}, {"chosen", symbols.FilterValueQuantity, "1 [kg]"}, {"quotient", symbols.FilterValueQuantity, "3.0 [m/s]"}, - {"ratio", symbols.FilterValueReal, "2.0"}, + {"ratio", symbols.FilterValueRational, "2.0"}, {"heavier", symbols.FilterValueBool, "true"}, } for _, tc := range cases { @@ -156,6 +156,8 @@ func TestQuantityExpressionsFold(t *testing.T) { got = q.String() case symbols.FilterValueReal: got = semantics.FormatReal(value.Real) + case symbols.FilterValueRational: + got = semantics.RatValue(value.Rat).FormatRational() case symbols.FilterValueBool: if value.Bool { got = "true" @@ -222,8 +224,8 @@ func TestCompareQuantitiesConvertsCommensurableUnits(t *testing.T) { // TestCompareMagnitudesKeepsLargeIntegersExact: Integer magnitudes above 2^53, // which one float64 cannot tell apart, stay ordered in one unit and across a -// whole scale ratio; a Real operand or a fractional scale still compares as -// float64, so 1 m and 100 cm remain equal. +// whole scale ratio. A decimal literal is an exact Rational, so 2^53+1 kg is +// above 9007199254740992.0 kg, and 1 m and 100 cm remain equal. func TestCompareMagnitudesKeepsLargeIntegersExact(t *testing.T) { m, idx := quantityFixture(t) q := func(expr string) semantics.Quantity { return fold(t, m, idx, expr) } @@ -237,7 +239,7 @@ func TestCompareMagnitudesKeepsLargeIntegersExact(t *testing.T) { {"9007199254740993 [kg]", "9007199254740992000 [g]", 1}, {"9007199254740992000 [g]", "9007199254740993 [kg]", -1}, {"9007199254740992000 [g]", "9007199254740992 [kg]", 0}, - {"9007199254740993 [kg]", "9007199254740992.0 [kg]", 0}, + {"9007199254740993 [kg]", "9007199254740992.0 [kg]", 1}, {"1 [m]", "100 [cm]", 0}, {"1 [m]", "101 [cm]", -1}, } From 203b3c6b0df6ce930b5dfd4b202f4040a44c97e1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:28:19 +0000 Subject: [PATCH 02/26] feat(client): carry exact Rationals in the Python and Node clients Co-Authored-By: jason.han --- client/node/src/core/capabilities.ts | 2 + client/node/src/core/connection.ts | 5 + client/node/src/core/document.ts | 40 +++- client/node/src/core/index.ts | 5 + client/node/src/core/typed.ts | 14 +- client/node/src/core/values.ts | 248 +++++++++++++++++++++--- client/node/test/rational.test.ts | 207 ++++++++++++++++++++ client/python/opensysml/__init__.py | 3 +- client/python/opensysml/capabilities.py | 4 + client/python/opensysml/connection.py | 33 +++- client/python/opensysml/document.py | 26 ++- client/python/opensysml/values.py | 108 +++++++++-- client/python/tests/test_rational.py | 158 +++++++++++++++ 13 files changed, 790 insertions(+), 63 deletions(-) create mode 100644 client/node/test/rational.test.ts create mode 100644 client/python/tests/test_rational.py diff --git a/client/node/src/core/capabilities.ts b/client/node/src/core/capabilities.ts index 1ebcc978fb..691a102c87 100644 --- a/client/node/src/core/capabilities.ts +++ b/client/node/src/core/capabilities.ts @@ -113,6 +113,8 @@ export const CAPABILITY_ENGINES = "engines"; export const CAPABILITY_UNDETERMINED_VALUE = "undetermined_value"; /** An Integer beyond int64 as `Value.bigIntValue`, `Quantity.bigIntMagnitude` and `DocumentValue.bigIntValue`; a service without it reads one sent to it as null. */ export const CAPABILITY_BIG_INT_VALUES = "big_int_values"; +/** An exact Rational no double holds as `Value.rationalValue`, `Quantity.rationalMagnitude` and `DocumentValue.rationalValue`; a service without it reads one sent to it as null. */ +export const CAPABILITY_RATIONAL_VALUES = "rational_values"; /** * Orders capability names by code unit, the order the service reports them in. diff --git a/client/node/src/core/connection.ts b/client/node/src/core/connection.ts index b8cde52ad3..b34c22306b 100644 --- a/client/node/src/core/connection.ts +++ b/client/node/src/core/connection.ts @@ -12,6 +12,7 @@ import { create } from "@bufbuild/protobuf"; import { CAPABILITY_APPLY_EDITS, CAPABILITY_BIG_INT_VALUES, + CAPABILITY_RATIONAL_VALUES, CAPABILITY_CONVERT, CAPABILITY_DOCUMENT_QUERY, CAPABILITY_ENGINES, @@ -101,6 +102,7 @@ import { } from "./query.js"; import { bindingHoldsBigInt, + bindingHoldsRational, buildBindings, documentResult, type BindingValues, @@ -568,6 +570,9 @@ export class Connection { if (wire.some(bindingHoldsBigInt)) { requireCapability(this.info, CAPABILITY_BIG_INT_VALUES, upgradeRemedy(CAPABILITY_BIG_INT_VALUES)); } + if (wire.some(bindingHoldsRational)) { + requireCapability(this.info, CAPABILITY_RATIONAL_VALUES, upgradeRemedy(CAPABILITY_RATIONAL_VALUES)); + } const response = await callRpc( this.rpc.runDocumentQuery( create(RunDocumentQueryRequestSchema, { diff --git a/client/node/src/core/document.ts b/client/node/src/core/document.ts index ad8dbdf207..47ece82669 100644 --- a/client/node/src/core/document.ts +++ b/client/node/src/core/document.ts @@ -19,7 +19,16 @@ import { UnitTermSchema, } from "../generated/sysml_pb.js"; import { DocumentQueryError, UnsupportedValueError } from "./errors.js"; -import { decodeBigInteger, fitsInt64, formatValue, type SysMLValue } from "./values.js"; +import { + decodeBigInteger, + decodeRational, + encodeQuantityMagnitude, + encodeRational, + fitsInt64, + formatValue, + rationalAsDouble, + type SysMLValue, +} from "./values.js"; /** Whether a wire binding sends an Integer beyond int64, which needs `big_int_values`. */ export function bindingHoldsBigInt(binding: DocumentQueryBinding): boolean { @@ -30,6 +39,15 @@ export function bindingHoldsBigInt(binding: DocumentQueryBinding): boolean { ); } +/** Whether a wire binding sends an exact Rational no double holds, which needs `rational_values`. */ +export function bindingHoldsRational(binding: DocumentQueryBinding): boolean { + return binding.values.some( + (value) => + value.kind.case === "rationalValue" || + (value.kind.case === "quantity" && value.kind.value.magnitude.case === "rationalMagnitude"), + ); +} + /** A model element, named by qualified name. */ export class ElementRef { /** Qualified name of the element. */ @@ -238,6 +256,7 @@ export type DocumentValue = | number | boolean | { kind: "infinity" } + | Extract | ({ kind: "quantity" } & Extract); /** What `bindings` accepts for one parameter: one value or several. */ @@ -369,6 +388,12 @@ function boundValue(parameter: string, value: DocumentValue): PbDocumentValue { `answered by queries, not bound to them`, ); } + if (typeof value === "object" && "kind" in value && value.kind === "rational") { + const double = rationalAsDouble(value); + return double === undefined + ? create(DocumentValueSchema, { kind: { case: "rationalValue", value: encodeRational(value) } }) + : create(DocumentValueSchema, { kind: { case: "realValue", value: double } }); + } if (typeof value === "object" && "kind" in value && value.kind === "quantity") { return create(DocumentValueSchema, { kind: { case: "quantity", value: boundQuantity(value) }, @@ -385,12 +410,7 @@ function boundQuantity( ): ReturnType> { const magnitude = value.magnitude; return create(QuantitySchema, { - magnitude: - magnitude.kind === "real" - ? { case: "realMagnitude", value: magnitude.value } - : fitsInt64(magnitude.value) - ? { case: "intMagnitude", value: magnitude.value } - : { case: "bigIntMagnitude", value: magnitude.value.toString() }, + magnitude: encodeQuantityMagnitude(magnitude), unit: value.unit, ...(value.unitTerm === undefined ? {} @@ -500,6 +520,8 @@ function valueOf(value: PbDocumentValue | undefined): DocumentValue { return decodeBigInteger(kind.value); case "realValue": return kind.value; + case "rationalValue": + return { kind: "rational" as const, ...decodeRational(kind.value) }; case "boolValue": return kind.value; case "infinity": @@ -515,7 +537,9 @@ function valueOf(value: PbDocumentValue | undefined): DocumentValue { ? { kind: "int", value: decodeBigInteger(magnitude.value) } : magnitude.case === "realMagnitude" ? { kind: "real", value: magnitude.value } - : { kind: "real", value: 0 }, + : magnitude.case === "rationalMagnitude" + ? { kind: "rational", ...decodeRational(magnitude.value) } + : { kind: "real", value: 0 }, unit: kind.value.unit, }; return decoded; diff --git a/client/node/src/core/index.ts b/client/node/src/core/index.ts index 8c2cef3f7e..eca605fb6e 100644 --- a/client/node/src/core/index.ts +++ b/client/node/src/core/index.ts @@ -126,6 +126,7 @@ export { CAPABILITY_CASE_EVALUATIONS, CAPABILITY_COMPLEX_VALUES, CAPABILITY_BIG_INT_VALUES, + CAPABILITY_RATIONAL_VALUES, CAPABILITY_CONVERT, CAPABILITY_MIGRATE, CAPABILITY_DIAGNOSTIC_CODES, @@ -261,7 +262,10 @@ export { decodeVerdict, decodeStanding, encodeValue, + formatRational, formatValue, + rational, + rationalToNumber, requireInput, toValue, valuesEqual, @@ -275,6 +279,7 @@ export type { MeasurementRefValue, MetaobjectValue, QuantityValue, + RationalValue, SysMLValue, SysMLVerdict, ValueInput, diff --git a/client/node/src/core/typed.ts b/client/node/src/core/typed.ts index ad1a7b51ee..91b9bc19a7 100644 --- a/client/node/src/core/typed.ts +++ b/client/node/src/core/typed.ts @@ -12,11 +12,12 @@ import { TypeMismatchError, } from "./errors.js"; import type { Instance } from "./model.js"; -import type { - ComplexValue, - EnumValue, - QuantityValue, - SysMLValue, +import { + rationalToNumber, + type ComplexValue, + type EnumValue, + type QuantityValue, + type SysMLValue, } from "./values.js"; /** A quantity feature value: a magnitude and the unit it is expressed in. */ @@ -182,6 +183,9 @@ export function asReal(featureName: string, value: SysMLValue): number { if (value.kind === "int") { return Number(value.value); } + if (value.kind === "rational") { + return rationalToNumber(value); + } throw mismatch(featureName, "float", value); } diff --git a/client/node/src/core/values.ts b/client/node/src/core/values.ts index 7c4a90ed80..2ea29113f1 100644 --- a/client/node/src/core/values.ts +++ b/client/node/src/core/values.ts @@ -10,6 +10,7 @@ import type { MeasurementRef, Metaobject as MetaobjectMessage, Quantity, + Rational, TensorQuantity, UnitTerm, Value, @@ -27,6 +28,7 @@ import { MeasurementRefSchema, MetaobjectSchema, QuantitySchema, + RationalSchema, TensorQuantitySchema, UnitFactorSchema, UnitTermSchema, @@ -43,6 +45,7 @@ import { CAPABILITY_INFINITY_VALUE, CAPABILITY_MEASUREMENT_REFS, CAPABILITY_METAOBJECT_VALUES, + CAPABILITY_RATIONAL_VALUES, CAPABILITY_SET_VALUES, CAPABILITY_STRUCTURED_VALUES, CAPABILITY_TENSOR_VALUES, @@ -52,8 +55,17 @@ import { } from "./capabilities.js"; import { MalformedValueError, UnsupportedValueError, type FailureCause } from "./errors.js"; -/** A quantity's magnitude: an integer or a real, never both. */ -export type Magnitude = { kind: "int"; value: bigint } | { kind: "real"; value: number }; +/** An exact Rational in lowest terms over a positive denominator. */ +export interface RationalValue { + numerator: bigint; + denominator: bigint; +} + +/** A quantity's magnitude: an Integer, an exact Rational or a Real, kept apart. */ +export type Magnitude = + | { kind: "int"; value: bigint } + | ({ kind: "rational" } & RationalValue) + | { kind: "real"; value: number }; /** One unit raised to an exponent, as the service factorises a derived unit. */ export interface UnitFactor { @@ -172,6 +184,7 @@ export interface UndeterminedValue { */ export type SysMLValue = | { kind: "int"; value: bigint } + | ({ kind: "rational" } & RationalValue) | { kind: "real"; value: number } | { kind: "complex"; value: ComplexValue } | { kind: "boolean"; value: boolean } @@ -323,6 +336,8 @@ export function decodeValue(value: Value | undefined): SysMLValue { return { kind: "int", value: kind.value }; case "bigIntValue": return { kind: "int", value: decodeBigInteger(kind.value) }; + case "rationalValue": + return { kind: "rational", ...decodeRational(kind.value) }; case "realValue": return { kind: "real", value: kind.value }; case "complex": @@ -387,6 +402,7 @@ export function decodeValue(value: Value | undefined): SysMLValue { export function encodeValue(value: SysMLValue): Value { switch (value.kind) { case "int": + case "rational": return encodeMagnitude(value); case "real": return create(ValueSchema, { kind: { case: "realValue", value: value.value } }); @@ -575,6 +591,8 @@ export function formatValue(value: SysMLValue): string { switch (value.kind) { case "int": return value.value.toString(); + case "rational": + return formatRational(value); case "real": return formatReal(value.value); case "complex": @@ -627,7 +645,38 @@ function formatReal(value: number): string { } function formatMagnitude(magnitude: Magnitude): string { - return magnitude.kind === "int" ? magnitude.value.toString() : formatReal(magnitude.value); + switch (magnitude.kind) { + case "int": + return magnitude.value.toString(); + case "rational": + return formatRational(magnitude); + case "real": + return formatReal(magnitude.value); + } +} + +/** A terminating Rational as its decimal, `0.1`; any other as `numerator/denominator`, `1/3`. */ +export function formatRational(value: RationalValue): string { + let twos = 0; + let fives = 0; + let rest = value.denominator; + while (rest % 2n === 0n) { + rest /= 2n; + twos++; + } + while (rest % 5n === 0n) { + rest /= 5n; + fives++; + } + if (rest !== 1n) { + return `${value.numerator.toString()}/${value.denominator.toString()}`; + } + const places = Math.max(twos, fives, 1); + const scaled = (value.numerator * 10n ** BigInt(places)) / value.denominator; + const negative = scaled < 0n; + const digits = (negative ? -scaled : scaled).toString().padStart(places + 1, "0"); + const point = digits.length - places; + return `${negative ? "-" : ""}${digits.slice(0, point)}.${digits.slice(point)}`; } /** `Tensor(2, 2, 2)[1.0, …, 8.0][Pa]` when every component shares a unit; else each with its own. */ @@ -660,6 +709,9 @@ function decodeQuantity(quantity: Quantity): QuantityValue { case "realMagnitude": magnitude = { kind: "real", value: quantity.magnitude.value }; break; + case "rationalMagnitude": + magnitude = { kind: "rational", ...decodeRational(quantity.magnitude.value) }; + break; default: throw new MalformedValueError(`a quantity in [${quantity.unit}] has no magnitude`); } @@ -673,12 +725,7 @@ function decodeQuantity(quantity: Quantity): QuantityValue { function encodeQuantity(quantity: QuantityValue): Quantity { return create(QuantitySchema, { - magnitude: - quantity.magnitude.kind !== "int" - ? { case: "realMagnitude", value: quantity.magnitude.value } - : fitsInt64(quantity.magnitude.value) - ? { case: "intMagnitude", value: quantity.magnitude.value } - : { case: "bigIntMagnitude", value: quantity.magnitude.value.toString() }, + magnitude: encodeQuantityMagnitude(quantity.magnitude), unit: quantity.unit, ...(quantity.unitTerm === undefined ? {} : { unitTerm: encodeUnitTerm(quantity.unitTerm) }), }); @@ -756,10 +803,34 @@ function formatUnitTerm(term: UnitFactorization): string { return parts.length === 0 ? "1" : parts.join("·"); } +/** A quantity magnitude as the wire's `Quantity.magnitude` oneof writes it. */ +export function encodeQuantityMagnitude(magnitude: Magnitude): Quantity["magnitude"] { + switch (magnitude.kind) { + case "real": + return { case: "realMagnitude", value: magnitude.value }; + case "rational": { + const double = rationalAsDouble(magnitude); + return double === undefined + ? { case: "rationalMagnitude", value: encodeRational(magnitude) } + : { case: "realMagnitude", value: double }; + } + case "int": + return fitsInt64(magnitude.value) + ? { case: "intMagnitude", value: magnitude.value } + : { case: "bigIntMagnitude", value: magnitude.value.toString() }; + } +} + function encodeMagnitude(magnitude: Magnitude): Value { if (magnitude.kind === "real") { return create(ValueSchema, { kind: { case: "realValue", value: magnitude.value } }); } + if (magnitude.kind === "rational") { + const double = rationalAsDouble(magnitude); + return double === undefined + ? create(ValueSchema, { kind: { case: "rationalValue", value: encodeRational(magnitude) } }) + : create(ValueSchema, { kind: { case: "realValue", value: double } }); + } return fitsInt64(magnitude.value) ? create(ValueSchema, { kind: { case: "intValue", value: magnitude.value } }) : create(ValueSchema, { kind: { case: "bigIntValue", value: magnitude.value.toString() } }); @@ -781,6 +852,107 @@ export function decodeBigInteger(text: string): bigint { return BigInt(text); } +/** The `Rational` message of an exact Rational, numerator and denominator in full. */ +export function encodeRational(value: RationalValue): Rational { + return create(RationalSchema, { + numerator: value.numerator.toString(), + denominator: value.denominator.toString(), + }); +} + +/** + * Reads a `rational_value` or `rational_magnitude`. + * + * @throws {MalformedValueError} for one not in lowest terms over a positive + * denominator, or one a double holds, which travels as `real_value`. + */ +export function decodeRational(rational: Rational): RationalValue { + const numerator = decodeBigInteger(rational.numerator); + const denominator = decodeBigInteger(rational.denominator); + const text = `${rational.numerator}/${rational.denominator}`; + if (denominator <= 0n || gcd(numerator, denominator) !== 1n) { + throw new MalformedValueError(`a Rational ${text} is not in lowest terms over a positive denominator`); + } + if (rationalAsDouble({ numerator, denominator }) !== undefined) { + throw new MalformedValueError(`a Rational ${text} is a double, which travels as real_value`); + } + return { numerator, denominator }; +} + +/** The exact Rational `numerator/denominator`, reduced; throws a RangeError over a zero denominator. */ +export function rational(numerator: bigint, denominator = 1n): { kind: "rational" } & RationalValue { + if (denominator === 0n) { + throw new RangeError("a Rational's denominator is not zero"); + } + const sign = denominator < 0n ? -1n : 1n; + const divisor = gcd(numerator, denominator); + return { kind: "rational", numerator: (sign * numerator) / divisor, denominator: (sign * denominator) / divisor }; +} + +function gcd(a: bigint, b: bigint): bigint { + a = a < 0n ? -a : a; + b = b < 0n ? -b : b; + while (b !== 0n) { + [a, b] = [b, a % b]; + } + return a; +} + +function bitLength(n: bigint): number { + return n === 0n ? 0 : (n < 0n ? -n : n).toString(2).length; +} + +/** The double that holds a reduced Rational exactly, or undefined where none does. */ +export function rationalAsDouble(value: RationalValue): number | undefined { + const { numerator, denominator } = value; + if ((denominator & (denominator - 1n)) !== 0n) { + return undefined; + } + const shift = bitLength(denominator) - 1; + let odd = numerator; + let zeros = 0; + while (odd !== 0n && odd % 2n === 0n) { + odd /= 2n; + zeros++; + } + if (bitLength(odd) > 53 || zeros - shift < -1074 || bitLength(numerator) - shift > 1024) { + return undefined; + } + return scaleByPowerOfTwo(Number(odd), zeros - shift); +} + +/** The double nearest the Rational: how a Real reads one. */ +export function rationalToNumber(value: RationalValue): number { + const exact = rationalAsDouble(value); + if (exact !== undefined) { + return exact; + } + const negative = value.numerator < 0n; + const magnitude = negative ? -value.numerator : value.numerator; + // A quotient of at least 55 bits, its last bit sticky, rounds once to 53. + const shift = 55 - (bitLength(magnitude) - bitLength(value.denominator)); + const num = shift >= 0 ? magnitude << BigInt(shift) : magnitude; + const den = shift >= 0 ? value.denominator : value.denominator << BigInt(-shift); + let quotient = num / den; + if (num % den !== 0n) { + quotient |= 1n; + } + const result = scaleByPowerOfTwo(Number(quotient), -shift); + return negative ? -result : result; +} + +function scaleByPowerOfTwo(x: number, exponent: number): number { + while (exponent > 1000) { + x *= 2 ** 1000; + exponent -= 1000; + } + while (exponent < -1000) { + x *= 2 ** -1000; + exponent += 1000; + } + return x * 2 ** exponent; +} + /** The flattened size the dimensions demand, refusing a dimension that is not positive. */ function checkShape(what: string, dimensions: bigint[], elementCount: number): void { let size = 1n; @@ -837,6 +1009,7 @@ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { } switch (a.kind) { case "int": + case "rational": case "real": case "complex": return numbersEqual(a, b); @@ -950,17 +1123,35 @@ function numbersEqual(a: NumberValue, b: SysMLValue): boolean { } b = { kind: "real", value: b.value.real }; } - if (a.kind === "int") { - if (b.kind === "int") return a.value === b.value; - return b.kind === "real" && realIsInt(b.value, a.value); + if (a.kind === "real" && b.kind === "real") { + return a.value === b.value; } - if (b.kind === "int") return realIsInt(a.value, b.value); - return b.kind === "real" && a.value === b.value; + const x = exactOf(a); + const y = exactOf(b); + return x !== undefined && y !== undefined && x.numerator * y.denominator === y.numerator * x.denominator; } -// Whether r is exactly the integer n, never rounding n. -function realIsInt(r: number, n: bigint): boolean { - return Number.isInteger(r) && BigInt(r) === n; +// The exact number a value holds, a finite Real's double included, never rounding. +function exactOf(value: SysMLValue | Magnitude): RationalValue | undefined { + switch (value.kind) { + case "int": + return { numerator: value.value, denominator: 1n }; + case "rational": + return value; + case "real": + return Number.isFinite(value.value) ? doubleAsRational(value.value) : undefined; + default: + return undefined; + } +} + +function doubleAsRational(x: number): RationalValue { + let denominator = 1n; + while (!Number.isInteger(x)) { + x *= 2; + denominator *= 2n; + } + return { numerator: BigInt(x), denominator }; } function magnitudesEqual(a: Magnitude[], b: Magnitude[]): boolean { @@ -991,23 +1182,22 @@ function quantitiesEqual(a: QuantityValue, b: QuantityValue): boolean { ) { return false; } - if ( - a.magnitude.kind === "int" && - b.magnitude.kind === "int" && - wholeScale(a.unitTerm) && - wholeScale(b.unitTerm) - ) { + const x = a.magnitude.kind === "real" ? undefined : exactOf(a.magnitude); + const y = b.magnitude.kind === "real" ? undefined : exactOf(b.magnitude); + if (x !== undefined && y !== undefined && wholeScale(a.unitTerm) && wholeScale(b.unitTerm)) { // m₁·n₁/d₁ = m₂·n₂/d₂ exactly, cross-multiplied in bigint. return ( - a.magnitude.value * BigInt(a.unitTerm.scaleNum) * BigInt(b.unitTerm.scaleDen) === - b.magnitude.value * BigInt(b.unitTerm.scaleNum) * BigInt(a.unitTerm.scaleDen) + x.numerator * y.denominator * BigInt(a.unitTerm.scaleNum) * BigInt(b.unitTerm.scaleDen) === + y.numerator * x.denominator * BigInt(b.unitTerm.scaleNum) * BigInt(a.unitTerm.scaleDen) ); } return baseMagnitude(a.magnitude, a.unitTerm) === baseMagnitude(b.magnitude, b.unitTerm); } function baseMagnitude(magnitude: Magnitude, term: UnitFactorization): number { - return (Number(magnitude.value) * term.scaleNum) / term.scaleDen; + const value = + magnitude.kind === "rational" ? rationalToNumber(magnitude) : Number(magnitude.value); + return (value * term.scaleNum) / term.scaleDen; } // The reduction as base unit → exponent, repeated base units summed and @@ -1094,6 +1284,8 @@ function decodeVector(vector: Vector): Magnitude[] { return { kind: "int", value: decodeBigInteger(component.kind.value) }; case "realValue": return { kind: "real", value: component.kind.value }; + case "rationalValue": + return { kind: "rational", ...decodeRational(component.kind.value) }; default: throw new MalformedValueError( `a vector component is ${component.kind.case ?? "empty"}, not a number`, @@ -1287,9 +1479,13 @@ export function requireInput(value: SysMLValue, info: ServerInfo): void { if (magnitudes.some((magnitude) => magnitude.kind === "int" && !fitsInt64(magnitude.value))) { require(CAPABILITY_BIG_INT_VALUES); } + if (magnitudes.some((magnitude) => magnitude.kind === "rational" && rationalAsDouble(magnitude) === undefined)) { + require(CAPABILITY_RATIONAL_VALUES); + } }; switch (value.kind) { case "int": + case "rational": requireMagnitudes([value]); return; case "complex": diff --git a/client/node/test/rational.test.ts b/client/node/test/rational.test.ts new file mode 100644 index 0000000000..03f8844675 --- /dev/null +++ b/client/node/test/rational.test.ts @@ -0,0 +1,207 @@ +// Exact Rationals: the rational arms, the double a Rational falls back to, and +// the rational_values capability that gates sending one. + +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; +import { + QuantitySchema, + RationalSchema, + ValueSchema, + VectorSchema, +} from "../src/generated/sysml_pb.js"; +import { + CAPABILITY_RATIONAL_VALUES, + MalformedValueError, + MissingCapabilityError, + ServerInfo, + buildBindings, + decodeValue, + encodeValue, + formatRational, + formatValue, + rational, + rationalToNumber, + toValue, + valuesEqual, + type SysMLValue, +} from "../src/node/index.js"; +import { fakeConnection } from "./support/fake.js"; + +const third = rational(1n, 3n); +const tenth = rational(1n, 10n); + +function roundTrip(value: SysMLValue): SysMLValue { + return decodeValue(fromBinary(ValueSchema, toBinary(ValueSchema, encodeValue(value)))); +} + +test("a Rational no double holds travels as rational_value and reads back exactly", () => { + for (const value of [third, tenth, rational(-7n, 3n), rational(1n, 3n ** 200n), rational(2n ** 60n + 1n)]) { + const wire = encodeValue(value); + assert.equal(wire.kind.case, "rationalValue"); + assert.deepEqual(roundTrip(value), value); + } +}); + +test("a Rational a double holds travels as real_value", () => { + for (const [value, double] of [ + [rational(1n, 2n), 0.5], + [rational(-3n, 4n), -0.75], + [rational(5n), 5], + [rational(1n, 2n ** 1074n), 2 ** -1074], + [rational(2n ** 1023n), 2 ** 1023], + ] as const) { + assert.deepEqual(encodeValue(value).kind, { case: "realValue", value: double }); + } + assert.equal(encodeValue(rational(1n, 2n ** 1075n)).kind.case, "rationalValue"); + assert.equal(encodeValue(rational(2n ** 1024n)).kind.case, "rationalValue"); + assert.equal(encodeValue(rational(2n ** 53n + 1n)).kind.case, "rationalValue"); +}); + +test("rational reduces over a positive denominator and refuses a zero one", () => { + assert.deepEqual(rational(6n, -4n), { kind: "rational", numerator: -3n, denominator: 2n }); + assert.throws(() => rational(1n, 0n), RangeError); +}); + +test("a rational_value not in canonical form is malformed", () => { + for (const [numerator, denominator] of [ + ["2", "6"], + ["1", "-3"], + ["1", "0"], + ["1", "2"], + ["x", "3"], + ]) { + const wire = create(ValueSchema, { + kind: { case: "rationalValue", value: create(RationalSchema, { numerator, denominator }) }, + }); + assert.throws(() => decodeValue(wire), MalformedValueError); + } +}); + +test("a terminating Rational prints as its decimal, any other as numerator/denominator", () => { + assert.equal(formatValue(tenth), "0.1"); + assert.equal(formatValue(rational(-1n, 8n)), "-0.125"); + assert.equal(formatValue(rational(5n)), "5.0"); + assert.equal(formatValue(third), "1/3"); + assert.equal(formatRational(rational(-7n, 3n)), "-7/3"); +}); + +test("a Rational equals the number it is, Integer and Real alike, never rounding", () => { + assert.ok(valuesEqual(rational(4n, 2n), { kind: "int", value: 2n })); + assert.ok(valuesEqual(rational(1n, 2n), { kind: "real", value: 0.5 })); + assert.ok(!valuesEqual(tenth, { kind: "real", value: 0.1 })); + assert.ok(!valuesEqual(third, rational(1n, 4n))); + assert.ok(valuesEqual(third, rational(2n, 6n))); + assert.ok(!valuesEqual(third, { kind: "real", value: Infinity })); +}); + +test("a Rational read as a Real is the double nearest it", () => { + assert.equal(rationalToNumber(third), 1 / 3); + assert.equal(rationalToNumber(tenth), 0.1); + assert.equal(rationalToNumber(rational(-2n, 3n)), -2 / 3); + assert.equal(rationalToNumber(rational(10n ** 400n, 3n * 10n ** 399n)), 10 / 3); + assert.equal(rationalToNumber(rational(2n ** 1100n, 3n)), Infinity); +}); + +test("a quantity, a vector and a document value keep a Rational exactly", () => { + const quantity: SysMLValue = { kind: "quantity", magnitude: third, unit: "kg" }; + const wire = encodeValue(quantity); + assert.equal(wire.kind.case, "quantity"); + assert.equal(wire.kind.value.magnitude.case, "rationalMagnitude"); + assert.deepEqual(roundTrip(quantity), quantity); + const half: SysMLValue = { kind: "quantity", magnitude: rational(1n, 2n), unit: "kg" }; + const halfWire = encodeValue(half); + assert.equal(halfWire.kind.case === "quantity" ? halfWire.kind.value.magnitude.case : "", "realMagnitude"); + + const vector: SysMLValue = { kind: "vector", components: [third, { kind: "int", value: 2n }] }; + assert.deepEqual(roundTrip(vector), vector); + assert.throws( + () => + decodeValue( + create(ValueSchema, { + kind: { + case: "vector", + value: create(VectorSchema, { + components: [ + create(ValueSchema, { + kind: { case: "rationalValue", value: create(RationalSchema, { numerator: "4", denominator: "6" }) }, + }), + ], + }), + }, + }), + ), + MalformedValueError, + ); + + const [bound] = buildBindings({ x: third }); + assert.equal(bound.values[0].kind.case, "rationalValue"); + const [boundHalf] = buildBindings({ x: rational(1n, 2n) }); + assert.deepEqual(boundHalf.values[0].kind, { case: "realValue", value: 0.5 }); + const [boundQuantity] = buildBindings({ m: { kind: "quantity", magnitude: tenth, unit: "kg" } }); + const magnitude = boundQuantity.values[0].kind; + assert.equal(magnitude.case === "quantity" ? magnitude.value.magnitude.case : "", "rationalMagnitude"); +}); + +test("a Rational no double holds is refused before the call by a service without rational_values", () => { + const older = new ServerInfo({ + version: "v", + capabilities: ["structured_values", "tensor_values", "set_values", "big_int_values"], + answered: true, + origin: "test", + }); + const unitTerm = { scaleNum: 1, scaleDen: 1, factors: [{ unitId: "SI::kg", exponent: 1 }] }; + for (const input of [ + third, + [1n, third], + { kind: "set", elements: [third] }, + { kind: "quantity", magnitude: third, unit: "" }, + { kind: "vector", components: [third] }, + { kind: "vectorQuantity", components: [{ magnitude: third, unit: "", unitTerm }] }, + { kind: "tensorQuantity", dimensions: [1n], components: [{ magnitude: third, unit: "", unitTerm }] }, + ] as const) { + assert.throws( + () => toValue(input as never, older), + (error: unknown) => { + assert.ok(error instanceof MissingCapabilityError); + assert.equal(error.capability, CAPABILITY_RATIONAL_VALUES); + return true; + }, + ); + } + assert.equal(toValue(rational(1n, 2n), older).kind.case, "realValue"); + const current = new ServerInfo({ + version: "v", + capabilities: [CAPABILITY_RATIONAL_VALUES], + answered: true, + origin: "test", + }); + assert.equal(toValue(third, current).kind.case, "rationalValue"); +}); + +test("a document query bound to a Rational no double holds needs rational_values", async () => { + const conn = await fakeConnection(["document_query"]); + for (const bound of [third, { kind: "quantity", magnitude: third, unit: "kg" }] as const) { + await assert.rejects( + () => conn.runDocumentQuery("hash", "Q::q", { x: bound }), + (error: unknown) => { + assert.ok(error instanceof MissingCapabilityError); + assert.equal(error.capability, CAPABILITY_RATIONAL_VALUES); + return true; + }, + ); + } +}); + +test("a quantity magnitude reads rational_magnitude", () => { + const wire = create(ValueSchema, { + kind: { + case: "quantity", + value: create(QuantitySchema, { + magnitude: { case: "rationalMagnitude", value: create(RationalSchema, { numerator: "-1", denominator: "3" }) }, + unit: "m", + }), + }, + }); + assert.deepEqual(decodeValue(wire), { kind: "quantity", magnitude: rational(-1n, 3n), unit: "m" }); +}); diff --git a/client/python/opensysml/__init__.py b/client/python/opensysml/__init__.py index 39fe2fe17f..d6c6005f8b 100644 --- a/client/python/opensysml/__init__.py +++ b/client/python/opensysml/__init__.py @@ -26,6 +26,7 @@ CAPABILITY_CONSTRAINT_BODY_AUTHORING, CAPABILITY_STATE_ACTION_AUTHORING, CAPABILITY_BIG_INT_VALUES, + CAPABILITY_RATIONAL_VALUES, MissingCapabilityError, ServerInfo, ) @@ -99,7 +100,7 @@ "InstanceTypeError", "InvalidRequestError", "ManifestSignatureError", "MissingCapabilityError", "CAPABILITY_CONSTRAINT_BODY_AUTHORING", "CAPABILITY_STATE_ACTION_AUTHORING", - "CAPABILITY_BIG_INT_VALUES", + "CAPABILITY_BIG_INT_VALUES", "CAPABILITY_RATIONAL_VALUES", "ModelError", "ModelFileNotFoundError", "ModelNotFoundError", "ServiceError", "ServiceTimeoutError", "StaleServiceError", "SymbolNotFoundError", diff --git a/client/python/opensysml/capabilities.py b/client/python/opensysml/capabilities.py index 7b72833909..33abf06804 100644 --- a/client/python/opensysml/capabilities.py +++ b/client/python/opensysml/capabilities.py @@ -251,6 +251,10 @@ #: ``DocumentValue.big_int_value``; a service without it reads one sent to it as null. CAPABILITY_BIG_INT_VALUES = "big_int_values" +#: An exact Rational no double holds as ``Value.rational_value``, ``Quantity.rational_magnitude`` +#: and ``DocumentValue.rational_value``; a service without it reads one sent to it as null. +CAPABILITY_RATIONAL_VALUES = "rational_values" + @dataclass(frozen=True) class ServerInfo: diff --git a/client/python/opensysml/connection.py b/client/python/opensysml/connection.py index b670dece80..b2639a6cf8 100644 --- a/client/python/opensysml/connection.py +++ b/client/python/opensysml/connection.py @@ -1,6 +1,7 @@ """Connection class for communicating with sysml-grpc service.""" import atexit +from fractions import Fraction import grpc import os import queue @@ -44,6 +45,7 @@ CAPABILITY_FUNCTION_VALUES, CAPABILITY_IMPLICIT_PARAMETERS, CAPABILITY_BIG_INT_VALUES, + CAPABILITY_RATIONAL_VALUES, CAPABILITY_INFINITY_VALUE, CAPABILITY_MEASUREMENT_REFS, CAPABILITY_METAOBJECT_VALUES, @@ -76,7 +78,7 @@ path_is_v1, ) from opensysml.diagnostic import Diagnostic -from opensysml.document import binding_holds_big_int, build_bindings, result_of as document_result +from opensysml.document import binding_holds_big_int, binding_holds_rational, build_bindings, result_of as document_result from opensysml.edit import error_for_failure, failure_name, referrers_of, result_of from opensysml.enumeration import EnumLiteral from opensysml.exploration import Exploration, Outcome @@ -113,6 +115,8 @@ _Infinity, integer_to_pb, pb_holds_big_int, + pb_holds_rational, + rational_value_to_pb, value_to_python, ) @@ -1767,6 +1771,12 @@ def run_document_query(self, model_hash, query_id, bindings=None): CAPABILITY_BIG_INT_VALUES, upgrade_remedy(CAPABILITY_BIG_INT_VALUES), ) + if any(binding_holds_rational(binding) for binding in request.bindings): + require( + self.server_info(), + CAPABILITY_RATIONAL_VALUES, + upgrade_remedy(CAPABILITY_RATIONAL_VALUES), + ) with translate_rpc_errors( not_found=SymbolNotFoundError, unimplemented=self._capability_refusal((CAPABILITY_DOCUMENT_QUERY,)), @@ -2934,6 +2944,17 @@ def _require_big_int_values(self, value): ) return value + def _require_exact_values(self, value): + """Refuse to send a big Integer or exact Rational a service without its capability would read as null.""" + self._require_big_int_values(value) + if pb_holds_rational(value): + require( + self.server_info(), + CAPABILITY_RATIONAL_VALUES, + upgrade_remedy(CAPABILITY_RATIONAL_VALUES), + ) + return value + def _require_schedule(self, schedule): """Refuse to send a schedule a service without ``schedule`` would run under the default.""" for capability in self._schedule_capabilities(schedule): @@ -3005,6 +3026,8 @@ def _python_to_value(self, py_value): return sysml_pb2.Value(instance_id=py_value.id) elif isinstance(py_value, int): return self._require_big_int_values(integer_to_pb(py_value)) + elif isinstance(py_value, Fraction): + return self._require_exact_values(rational_value_to_pb(py_value)) elif isinstance(py_value, float): return sysml_pb2.Value(real_value=py_value) elif isinstance(py_value, complex): @@ -3019,7 +3042,7 @@ def _python_to_value(self, py_value): elif isinstance(py_value, Instance): return sysml_pb2.Value(instance_id=py_value.id) elif isinstance(py_value, Quantity): - return self._require_big_int_values(sysml_pb2.Value(quantity=py_value.to_pb())) + return self._require_exact_values(sysml_pb2.Value(quantity=py_value.to_pb())) elif isinstance(py_value, _Infinity): self._require_infinity_value() return sysml_pb2.Value(infinity=True) @@ -3037,16 +3060,16 @@ def _python_to_value(self, py_value): return sysml_pb2.Value(array=py_value.to_pb(self._python_to_value)) elif isinstance(py_value, Vector): self._require_structured_values() - return self._require_big_int_values(sysml_pb2.Value(vector=py_value.to_pb())) + return self._require_exact_values(sysml_pb2.Value(vector=py_value.to_pb())) elif isinstance(py_value, VectorQuantity): self._require_structured_values() - return self._require_big_int_values(sysml_pb2.Value(vector_quantity=py_value.to_pb())) + return self._require_exact_values(sysml_pb2.Value(vector_quantity=py_value.to_pb())) elif isinstance(py_value, (SetValue, set, frozenset)): self._require_set_values() return sysml_pb2.Value(set=SetValue(py_value).to_pb(self._python_to_value)) elif isinstance(py_value, TensorQuantity): self._require_tensor_values() - return self._require_big_int_values(sysml_pb2.Value(tensor_quantity=py_value.to_pb())) + return self._require_exact_values(sysml_pb2.Value(tensor_quantity=py_value.to_pb())) elif isinstance(py_value, EnumLiteral): literal = sysml_pb2.EnumLiteral( literal_id=py_value.literal_id, diff --git a/client/python/opensysml/document.py b/client/python/opensysml/document.py index 229127362b..589cd99e19 100644 --- a/client/python/opensysml/document.py +++ b/client/python/opensysml/document.py @@ -7,7 +7,7 @@ Query that :mod:`opensysml.query` builds. A binding value is a plain Python value (``str``, ``int``, ``float``, -``bool``), a :class:`~opensysml.values.Quantity`, an :class:`ElementRef` +``bool``), an exact Rational as a :class:`fractions.Fraction`, a :class:`~opensysml.values.Quantity`, an :class:`ElementRef` naming a model element by qualified name, or an :class:`ObjectRef` naming an object the service holds for the model since ``instantiate`` — by id or by path (``"car.wheels[2]"``). Answered cells decode back to the same kinds, plus @@ -18,6 +18,7 @@ """ from dataclasses import dataclass +from fractions import Fraction from typing import Optional, Sequence, Union from opensysml.errors import OpenSysMLError, UnsupportedValueError @@ -27,8 +28,11 @@ Quantity, _Infinity, fits_int64, + holds_exactly_as_double, integer_from_decimal, integer_to_decimal, + rational_from_pb, + rational_to_pb, ) @@ -258,6 +262,20 @@ def binding_holds_big_int(binding: "sysml_pb2.DocumentQueryBinding") -> bool: return any(_document_holds_big_int(value) for value in binding.values) +def binding_holds_rational(binding: "sysml_pb2.DocumentQueryBinding") -> bool: + """Whether a wire binding sends an exact Rational no double holds, which needs ``rational_values``.""" + return any(_document_holds_rational(value) for value in binding.values) + + +def _document_holds_rational(value: "sysml_pb2.DocumentValue") -> bool: + kind = value.WhichOneof("kind") + if kind == "rational_value": + return True + if kind == "quantity": + return value.quantity.WhichOneof("magnitude") == "rational_magnitude" + return False + + def _document_holds_big_int(value: "sysml_pb2.DocumentValue") -> bool: kind = value.WhichOneof("kind") if kind == "big_int_value": @@ -317,6 +335,10 @@ def _bound_value(parameter, value): return sysml_pb2.DocumentValue(int_value=value) if isinstance(value, float): return sysml_pb2.DocumentValue(real_value=value) + if isinstance(value, Fraction): + if holds_exactly_as_double(value): + return sysml_pb2.DocumentValue(real_value=float(value)) + return sysml_pb2.DocumentValue(rational_value=rational_to_pb(value)) if isinstance(value, Quantity): return sysml_pb2.DocumentValue(quantity=_bound_quantity(parameter, value)) if isinstance(value, DocumentVerdict): @@ -402,6 +424,8 @@ def _value_of(value): return integer_from_decimal(value.big_int_value) if kind == "real_value": return value.real_value + if kind == "rational_value": + return rational_from_pb(value.rational_value) if kind == "bool_value": return value.bool_value if kind == "infinity": diff --git a/client/python/opensysml/values.py b/client/python/opensysml/values.py index 606ac02e52..2ea8426270 100644 --- a/client/python/opensysml/values.py +++ b/client/python/opensysml/values.py @@ -9,8 +9,8 @@ from opensysml.errors import FeatureValueError, OpenSysMLError, UnsupportedValueError from opensysml.proto import sysml_pb2 -#: What a quantity's magnitude can be: the service keeps Integer and Real apart. -Magnitude = Union[int, float] +#: What a quantity's magnitude can be: the service keeps Integer, Rational and Real apart. +Magnitude = Union[int, Fraction, float] _INT64_MIN = -(1 << 63) _INT64_MAX = (1 << 63) - 1 @@ -78,6 +78,71 @@ def integer_from_decimal(text: str) -> int: return -value if text.startswith("-") else value +def pb_holds_rational(value: "sysml_pb2.Value") -> bool: + """Whether a wire value is an exact Rational no double holds, or carries one as a magnitude. + + A collection's elements are values of their own, which it leaves to the caller. + """ + kind = value.WhichOneof("kind") + if kind == "rational_value": + return True + if kind == "quantity": + return _quantity_holds_rational(value.quantity) + if kind == "vector": + return any(component.WhichOneof("kind") == "rational_value" for component in value.vector.components) + if kind == "vector_quantity": + return any(_quantity_holds_rational(q) for q in value.vector_quantity.components) + if kind == "tensor_quantity": + return any(_quantity_holds_rational(q) for q in value.tensor_quantity.components) + return False + + +def _quantity_holds_rational(quantity: "sysml_pb2.Quantity") -> bool: + return quantity.WhichOneof("magnitude") == "rational_magnitude" + + +def holds_exactly_as_double(value: Fraction) -> bool: + """Whether a double holds the Rational exactly, so that it travels as ``real_value``.""" + try: + f = float(value) + except OverflowError: + return False + return math.isfinite(f) and Fraction(f) == value + + +def rational_to_pb(value: Fraction) -> "sysml_pb2.Rational": + """The ``Rational`` message of an exact Rational, numerator and denominator in full.""" + return sysml_pb2.Rational( + numerator=integer_to_decimal(value.numerator), + denominator=integer_to_decimal(value.denominator), + ) + + +def rational_from_pb(pb_rational: "sysml_pb2.Rational") -> Fraction: + """The exact Rational a ``Rational`` message spells. + + Raises: + UnsupportedValueError: If it is not in lowest terms over a positive + denominator, or a double holds it, which travels as ``real_value``. + """ + numerator = integer_from_decimal(pb_rational.numerator) + denominator = integer_from_decimal(pb_rational.denominator) + text = f"{pb_rational.numerator}/{pb_rational.denominator}" + if denominator <= 0 or math.gcd(numerator, denominator) != 1: + raise UnsupportedValueError(f"Rational {text} is not in lowest terms over a positive denominator") + value = Fraction(numerator, denominator) + if holds_exactly_as_double(value): + raise UnsupportedValueError(f"Rational {text} is a double, which travels as real_value") + return value + + +def rational_value_to_pb(value: Fraction) -> "sysml_pb2.Value": + """Encode a Rational as ``real_value`` where a double holds it, ``rational_value`` otherwise.""" + if holds_exactly_as_double(value): + return sysml_pb2.Value(real_value=float(value)) + return sysml_pb2.Value(rational_value=rational_to_pb(value)) + + def integer_to_pb(value: int) -> "sysml_pb2.Value": """Encode an Integer as ``int_value`` within int64, ``big_int_value`` beyond it.""" if fits_int64(value): @@ -282,6 +347,8 @@ def from_pb(cls, pb_quantity) -> "Quantity": magnitude = integer_from_decimal(pb_quantity.big_int_magnitude) elif which == 'real_magnitude': magnitude = pb_quantity.real_magnitude + elif which == 'rational_magnitude': + magnitude = rational_from_pb(pb_quantity.rational_magnitude) else: raise UnsupportedValueError( f"quantity in [{pb_quantity.unit}] carries no magnitude" @@ -305,9 +372,9 @@ def to_pb(self) -> "sysml_pb2.Quantity": decides commensurability over the reduction and rejects a unit sent without one. """ - if isinstance(self.magnitude, bool) or not isinstance(self.magnitude, (int, float)): + if isinstance(self.magnitude, bool) or not isinstance(self.magnitude, (int, Fraction, float)): raise UnsupportedValueError( - f"quantity magnitude {self.magnitude!r} is neither an Integer nor a Real" + f"quantity magnitude {self.magnitude!r} is neither an Integer nor a Real nor a Rational" ) if not self.unit.reduced: raise UnsupportedValueError( @@ -320,8 +387,10 @@ def to_pb(self) -> "sysml_pb2.Quantity": pb_quantity.big_int_magnitude = integer_to_decimal(self.magnitude) elif isinstance(self.magnitude, int): pb_quantity.int_magnitude = self.magnitude + elif isinstance(self.magnitude, Fraction) and not holds_exactly_as_double(self.magnitude): + pb_quantity.rational_magnitude.CopyFrom(rational_to_pb(self.magnitude)) else: - pb_quantity.real_magnitude = self.magnitude + pb_quantity.real_magnitude = float(self.magnitude) return pb_quantity def base_magnitude(self) -> float: @@ -364,8 +433,8 @@ def __eq__(self, other: object) -> bool: return self.base_magnitude() == other.base_magnitude() def _exact_base_magnitude(self) -> Optional[Fraction]: - """The base magnitude as a Fraction while it can be exact: an int over a whole scale.""" - if isinstance(self.magnitude, bool) or not isinstance(self.magnitude, int): + """The base magnitude as a Fraction while it can be exact: an Integer or Rational over a whole scale.""" + if isinstance(self.magnitude, bool) or not isinstance(self.magnitude, (int, Fraction)): return None num, den = self.unit.scale_num, self.unit.scale_den if not (float(num).is_integer() and float(den).is_integer()): @@ -432,7 +501,7 @@ def __mul__(self, other: Magnitude) -> "Quantity": client does not compose: the runtime does, so `x * y` over quantities belongs in the model. """ - if isinstance(other, bool) or not isinstance(other, (int, float)): + if isinstance(other, bool) or not isinstance(other, (int, Fraction, float)): return NotImplemented return Quantity(self.magnitude * other, self.unit) @@ -440,7 +509,7 @@ def __mul__(self, other: Magnitude) -> "Quantity": def __truediv__(self, other: Magnitude) -> "Quantity": """Divide the magnitude by a number, keeping the unit.""" - if isinstance(other, bool) or not isinstance(other, (int, float)): + if isinstance(other, bool) or not isinstance(other, (int, Fraction, float)): return NotImplemented return Quantity(self.magnitude / other, self.unit) @@ -637,16 +706,16 @@ def __str__(self) -> str: def _is_number(value: object) -> bool: - """Whether a value is an Integer or a Real as the wire keeps them apart: a bool is neither.""" - return isinstance(value, (int, float)) and not isinstance(value, bool) + """Whether a value is an Integer, a Rational or a Real as the wire keeps them apart: a bool is none.""" + return isinstance(value, (int, Fraction, float)) and not isinstance(value, bool) def _number_to_pb(value: Magnitude) -> "sysml_pb2.Value": - return ( - integer_to_pb(value) - if isinstance(value, int) - else sysml_pb2.Value(real_value=value) - ) + if isinstance(value, int): + return integer_to_pb(value) + if isinstance(value, Fraction): + return rational_value_to_pb(value) + return sysml_pb2.Value(real_value=value) def _format_number(value: Magnitude) -> str: @@ -810,6 +879,8 @@ def from_pb(cls, pb_vector) -> "Vector": components.append(integer_from_decimal(pb_component.big_int_value)) elif kind == "real_value": components.append(pb_component.real_value) + elif kind == "rational_value": + components.append(rational_from_pb(pb_component.rational_value)) else: raise UnsupportedValueError( f"malformed vector: component is {kind or 'empty'}, not a number" @@ -1192,7 +1263,8 @@ def value_to_python(pb_value, resolve_instance=None): :class:`InstanceRef` holding the id. Returns: - int, float, complex, bool, str, list, None, :data:`UNSET`, + int, :class:`fractions.Fraction` for an exact Rational no double + holds, float, complex, bool, str, list, None, :data:`UNSET`, :data:`INFINITY`, an :class:`Undetermined`, a :class:`Quantity`, a :class:`MeasurementRef`, a :class:`Function`, a :class:`Metaobject`, an :class:`Array`, a :class:`Vector`, a @@ -1213,6 +1285,8 @@ def value_to_python(pb_value, resolve_instance=None): return integer_from_decimal(pb_value.big_int_value) if kind == 'real_value': return pb_value.real_value + if kind == 'rational_value': + return rational_from_pb(pb_value.rational_value) if kind == 'complex': return complex(pb_value.complex.real, pb_value.complex.imaginary) if kind == 'bool_value': diff --git a/client/python/tests/test_rational.py b/client/python/tests/test_rational.py new file mode 100644 index 0000000000..d907d065d1 --- /dev/null +++ b/client/python/tests/test_rational.py @@ -0,0 +1,158 @@ +"""KerML Rationals are exact: one no double holds crosses the wire in full. + +The service sends a Rational a double holds exactly, as ``0.5``, as +``real_value`` and any other, as ``1/3`` or ``0.1``, as the numerator and +denominator of ``rational_value``; the client decodes the latter to a +``fractions.Fraction`` and encodes a ``Fraction`` the same way. +""" + +from fractions import Fraction +from unittest.mock import Mock, patch + +import pytest + +from opensysml.capabilities import ( + CAPABILITY_DOCUMENT_QUERY, + CAPABILITY_RATIONAL_VALUES, + CAPABILITY_SET_VALUES, + CAPABILITY_STRUCTURED_VALUES, + CAPABILITY_TENSOR_VALUES, + CAPABILITY_VERIFICATION, + MissingCapabilityError, +) +from opensysml.connection import Connection +from opensysml.document import build_bindings, _value_of +from opensysml.errors import UnsupportedValueError +from opensysml.proto import sysml_pb2 +from opensysml.values import ( + Quantity, + TensorQuantity, + Unit, + UnitFactor, + Vector, + VectorQuantity, + holds_exactly_as_double, + rational_from_pb, + rational_value_to_pb, + value_to_python, +) + +KG = Unit(text="kg", factors=(UnitFactor("SI::kg", 1),), reduction_given=True) + + +@pytest.mark.parametrize("value", [Fraction(1, 3), Fraction(1, 10), Fraction(-7, 3), Fraction(1, 3 ** 200)]) +def test_a_rational_no_double_holds_round_trips_exactly(value): + pb = rational_value_to_pb(value) + assert pb.WhichOneof("kind") == "rational_value" + assert pb.rational_value.numerator == str(value.numerator) + assert pb.rational_value.denominator == str(value.denominator) + assert value_to_python(sysml_pb2.Value.FromString(pb.SerializeToString())) == value + + +@pytest.mark.parametrize("value", [Fraction(1, 2), Fraction(-3, 4), Fraction(5), Fraction(0)]) +def test_a_rational_a_double_holds_travels_as_real_value(value): + pb = rational_value_to_pb(value) + assert pb.WhichOneof("kind") == "real_value" + assert pb.real_value == float(value) + + +def test_a_rational_beyond_the_double_range_is_not_a_double(): + assert not holds_exactly_as_double(Fraction(10 ** 400)) + assert rational_value_to_pb(Fraction(10 ** 400)).WhichOneof("kind") == "rational_value" + + +@pytest.mark.parametrize( + "numerator, denominator, reason", + [ + ("2", "6", "lowest terms"), + ("1", "-3", "positive denominator"), + ("1", "0", "positive denominator"), + ("1", "2", "travels as real_value"), + ("1.5", "3", "not decimal"), + ], +) +def test_a_noncanonical_rational_is_refused(numerator, denominator, reason): + with pytest.raises(UnsupportedValueError, match=reason): + rational_from_pb(sysml_pb2.Rational(numerator=numerator, denominator=denominator)) + + +def test_a_rational_quantity_magnitude_round_trips(): + pb = Quantity(Fraction(1, 3), KG).to_pb() + assert pb.WhichOneof("magnitude") == "rational_magnitude" + assert Quantity.from_pb(pb).magnitude == Fraction(1, 3) + assert Quantity(Fraction(1, 2), KG).to_pb().real_magnitude == 0.5 + + +def test_rational_quantities_compare_exactly(): + gram = Unit(text="g", factors=(UnitFactor("SI::kg", 1),), scale_num=1, scale_den=1000, reduction_given=True) + assert Quantity(Fraction(1, 3), KG) == Quantity(Fraction(1000, 3), gram) + + +def test_a_rational_vector_component_round_trips(): + pb = Vector((Fraction(1, 3), 2)).to_pb() + assert pb.components[0].WhichOneof("kind") == "rational_value" + assert Vector.from_pb(pb).components == (Fraction(1, 3), 2) + + +def test_a_rational_document_value_round_trips(): + (binding,) = build_bindings({"x": Fraction(1, 3)}) + assert binding.values[0].WhichOneof("kind") == "rational_value" + assert _value_of(binding.values[0]) == Fraction(1, 3) + (half,) = build_bindings({"x": Fraction(1, 2)}) + assert half.values[0].real_value == 0.5 + + +def make_connection(stub, capabilities): + """Build a Connection over a mock stub reporting ``capabilities``.""" + stub.GetServerInfo.return_value = sysml_pb2.ServerInfoResponse( + version="test", capabilities=list(capabilities) + ) + with patch("grpc.insecure_channel"): + with patch( + "opensysml.proto.sysml_pb2_grpc.SysMLServiceStub", return_value=stub + ): + return Connection(auto_start=False) + + +OLD = ( + CAPABILITY_SET_VALUES, + CAPABILITY_STRUCTURED_VALUES, + CAPABILITY_TENSOR_VALUES, + CAPABILITY_VERIFICATION, +) + + +def test_a_rational_is_not_sent_to_a_service_without_the_capability(): + """An older service would read the unknown arm as null, so nothing is sent.""" + third = Quantity(Fraction(1, 3), KG) + stub = Mock() + conn = make_connection(stub, OLD) + for value in ( + Fraction(1, 3), + [1, [Fraction(1, 10)]], + third, + Vector((1, Fraction(1, 3))), + VectorQuantity((third,)), + TensorQuantity((1,), (third,)), + ): + with pytest.raises(MissingCapabilityError) as excinfo: + conn.calc("W::f", "hash", arguments=[value]) + assert excinfo.value.capability == CAPABILITY_RATIONAL_VALUES + stub.EvaluateCalc.assert_not_called() + assert conn._python_to_value(Fraction(1, 2)).real_value == 0.5 + + +def test_a_rational_is_sent_to_a_service_with_the_capability(): + conn = make_connection(Mock(), OLD + (CAPABILITY_RATIONAL_VALUES,)) + pb = conn._python_to_value([1, Fraction(1, 3)]).sequence.elements[1] + assert (pb.rational_value.numerator, pb.rational_value.denominator) == ("1", "3") + + +def test_a_rational_document_binding_needs_the_capability(): + stub = Mock() + conn = make_connection(stub, OLD + (CAPABILITY_DOCUMENT_QUERY,)) + for value in (Fraction(1, 3), Quantity(Fraction(1, 3), KG)): + with pytest.raises(MissingCapabilityError) as excinfo: + conn.run_document_query("hash", "Q::q", bindings={"x": value}) + assert excinfo.value.capability == CAPABILITY_RATIONAL_VALUES + stub.RunDocumentQuery.assert_not_called() From 3a0a1305758e1e155c27e91ffd2f3360a355ddd0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:32:00 +0000 Subject: [PATCH 03/26] feat(export): state rational literals exactly in RDF Co-Authored-By: jason.han --- docs/reference/rdf-mapping.md | 10 +++- internal/translate/export/api_json_in.go | 2 +- internal/translate/export/rdf_expr.go | 72 +++++++++++++++++++++++- tests/export/api_json_test.go | 3 +- tests/export/result_expression_test.go | 37 ++++++++++++ 5 files changed, 118 insertions(+), 6 deletions(-) diff --git a/docs/reference/rdf-mapping.md b/docs/reference/rdf-mapping.md index e51300ab29..821951da6a 100644 --- a/docs/reference/rdf-mapping.md +++ b/docs/reference/rdf-mapping.md @@ -1087,9 +1087,15 @@ The rules the tree follows: - **A literal's `sysml:value` is a typed literal** whose lexical form is the token the notation spells it with: `"2"^^xsd:integer`, `"1.5"^^xsd:decimal`, `"1.5E3"^^xsd:double` (an exponent is outside `xsd:decimal`'s lexical space), - `"true"^^xsd:boolean`, and a string with its escapes resolved. Read back, a + `"true"^^xsd:boolean`, and a string with its escapes resolved. The literal + denotes exactly the Rational the token does: an exponent token no double holds + is written as the `xsd:decimal` it denotes (`1E-1` is `"0.1"^^xsd:decimal`), + and a JSON number in the API JSON form is read the same way. Read back, a value is spelled as that token again: a rational with no fractional digits - (`"3"^^xsd:decimal`) gains them (`3.0`), a boolean is `true` or `false`, a + (`"3"^^xsd:decimal`) gains them (`3.0`), an `xsd:double` or `xsd:float` whose + binary value differs from its lexical form is spelled as the exact decimal of + that binary value (`"0.1"^^xsd:float` is `0.100000001490116119384765625`), a + boolean is `true` or `false`, a string is quoted and escaped; a value no token spells — a signed number, `INF`, `NaN` — is reported as unsupported, naming the node, since the notation states a sign as an operator applied to a literal. diff --git a/internal/translate/export/api_json_in.go b/internal/translate/export/api_json_in.go index 28bd5bb65b..591b7161b8 100644 --- a/internal/translate/export/api_json_in.go +++ b/internal/translate/export/api_json_in.go @@ -542,7 +542,7 @@ func apiJSONScalarOf(subject rdf.Term, predicate rdf.Term, sysmlKey string, valu if apiJSONInteger.MatchString(lexical) { return rdf.TypedLiteral(lexical, rdf.XSD+"integer"), nil } - return rdf.TypedLiteral(lexical, realDatatype(lexical)), nil + return exactRealLiteral(lexical) case string: if apiJSONIsExpressionText(sysmlKey, v) { return rdf.TypedLiteral(v, rdf.OpenSysML+dtExpression), nil diff --git a/internal/translate/export/rdf_expr.go b/internal/translate/export/rdf_expr.go index 8e5c86bae3..43cb72180c 100644 --- a/internal/translate/export/rdf_expr.go +++ b/internal/translate/export/rdf_expr.go @@ -5,11 +5,15 @@ package export // sysx:sourceText. See docs/reference/rdf-mapping.md § Expressions. import ( + "errors" "fmt" + "math" + "math/big" "sort" "strconv" "strings" + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" "github.com/Open-MBEE/OpenSysML/internal/syntax/lexer" "github.com/Open-MBEE/OpenSysML/internal/syntax/source" @@ -165,7 +169,11 @@ func (e *encoder) expressionOperands(subject rdf.Term, owner string, node ast.No e.graph.Add(subject, e.sysml(pValue), rdf.TypedLiteral(n.Value, rdf.XSD+"integer")) case *ast.LiteralReal: - e.graph.Add(subject, e.sysml(pValue), rdf.TypedLiteral(n.Value, realDatatype(n.Value))) + literal, err := exactRealLiteral(n.Value) + if err != nil { + return &UnsupportedError{What: fmt.Sprintf("the rational literal %s at %s", n.Value, e.where(n)), Note: err.Error()} + } + e.graph.Add(subject, e.sysml(pValue), literal) case *ast.QualifiedName: // A position whose notation is a bare name holds the feature it names. @@ -330,6 +338,65 @@ func realDatatype(value string) string { return rdf.XSD + "decimal" } +// exactRealLiteral is the typed literal denoting exactly the Rational a decimal +// token does: the token under realDatatype where that datatype's value is the +// token's, else the same value as an xsd:decimal (`1E-1` is `0.1`). +func exactRealLiteral(token string) (rdf.Term, error) { + datatype := realDatatype(token) + if datatype == rdf.XSD+"decimal" { + return rdf.TypedLiteral(token, datatype), nil + } + exact, err := semantics.ParseRational(strings.TrimPrefix(token, "+"), semantics.DefaultMaxIntegerBits) + if err != nil { + return rdf.Term{}, err + } + if _, ok := exact.BinaryExact(); ok { + return rdf.TypedLiteral(token, datatype), nil + } + return rdf.TypedLiteral(decimalText(exact.Rat(), decimalPlaces(token)), rdf.XSD+"decimal"), nil +} + +// decimalPlaces bounds the fraction digits the decimal a token denotes needs. +func decimalPlaces(token string) int { + mantissa, exponent, _ := strings.Cut(strings.ToLower(token), "e") + _, fraction, _ := strings.Cut(mantissa, ".") + e, _ := strconv.Atoi(exponent) + return max(len(fraction)-e, 1) +} + +// decimalText spells a terminating r with at most places fraction digits, +// trailing zeros dropped but for the one after the point. +func decimalText(r *big.Rat, places int) string { + text := strings.TrimRight(r.FloatString(places), "0") + if strings.HasSuffix(text, ".") { + text += "0" + } + return text +} + +// binaryLiteralText is the token a binary xsd:double or xsd:float literal +// denotes exactly: its own lexical form where that is its value, else the +// exact decimal of the binary value (`"0.1"^^xsd:float` is 0.100000001490116119384765625). +func binaryLiteralText(term rdf.Term) string { + bits := 64 + switch term.Datatype { + case rdf.XSD + "double": + case rdf.XSD + "float": + bits = 32 + default: + return term.Value + } + binary, err := strconv.ParseFloat(term.Value, bits) + if math.IsInf(binary, 0) || math.IsNaN(binary) || (err != nil && !errors.Is(err, strconv.ErrRange)) { + return term.Value + } + value := new(big.Rat).SetFloat64(binary) + if exact, err := semantics.ParseRational(term.Value, semantics.DefaultMaxIntegerBits); err == nil && exact.Rat().Cmp(value) == 0 { + return term.Value + } + return decimalText(value, max(int(value.Denom().TrailingZeroBits()), 1)) +} + func (e *encoder) typed(subject rdf.Term, metaclass string) { e.graph.Add(subject, rdf.IRI(rdf.RDFType), rdf.SysMLTerm(metaclass)) } @@ -1182,10 +1249,11 @@ func (d *decoder) expressionForm(node rdf.Term, in *element) (operand, error) { } return primary(value, nil) case mLiteralRational: - value, ok := d.graph.Lexical(node, rdf.SysML+pValue) + term, ok := d.graph.Object(node, rdf.SysML+pValue) if !ok { return primary("", unsupported(literalStatesValue)) } + value := binaryLiteralText(term) text, ok := realValueText(value) if !ok { return primary("", unsupported(fmt.Sprintf("the notation spells a rational literal as an unsigned finite number, not %q; a sign is an OperatorExpression applied to it", value))) diff --git a/tests/export/api_json_test.go b/tests/export/api_json_test.go index 8e1480746f..e1af5a2dc0 100644 --- a/tests/export/api_json_test.go +++ b/tests/export/api_json_test.go @@ -241,7 +241,7 @@ func TestReadAPIJSONRejectsNonElements(t *testing.T) { func TestReadAPIJSONValueForms(t *testing.T) { document := `[{ "@type": "Package", "@id": "P", - "integer": 42, "decimal": 0.0, "double": 1.5e3, "flag": true, + "integer": 42, "decimal": 0.0, "double": 1.5e3, "tenth": 1e-1, "flag": true, "absent": null, "empty": [], "declaredName": "P", "specializes": "A < B", "ownedMember": [{"@id": "Q"}, {"@id": "R"}], "sysx:sourceText": "package P {}", @@ -256,6 +256,7 @@ func TestReadAPIJSONValueForms(t *testing.T) { {Subject: subject, Predicate: rdf.SysMLTerm("integer"), Object: rdf.TypedLiteral("42", rdf.XSD+"integer")}, {Subject: subject, Predicate: rdf.SysMLTerm("decimal"), Object: rdf.TypedLiteral("0.0", rdf.XSD+"decimal")}, {Subject: subject, Predicate: rdf.SysMLTerm("double"), Object: rdf.TypedLiteral("1.5e3", rdf.XSD+"double")}, + {Subject: subject, Predicate: rdf.SysMLTerm("tenth"), Object: rdf.TypedLiteral("0.1", rdf.XSD+"decimal")}, {Subject: subject, Predicate: rdf.SysMLTerm("flag"), Object: rdf.Bool(true)}, {Subject: subject, Predicate: rdf.SysMLTerm("declaredName"), Object: rdf.String("P")}, {Subject: subject, Predicate: rdf.SysMLTerm("specializes"), Object: rdf.TypedLiteral("A < B", rdf.OpenSysML+"Expression")}, diff --git a/tests/export/result_expression_test.go b/tests/export/result_expression_test.go index 6db67f09ba..9b46d73aba 100644 --- a/tests/export/result_expression_test.go +++ b/tests/export/result_expression_test.go @@ -526,6 +526,9 @@ func TestLiteralValuesAreSpelledAsTokens(t *testing.T) { {rational, `sysml:value "3."^^xsd:decimal ;`, "x * 3.0", ""}, {rational, `sysml:value "3.E2"^^xsd:double ;`, "x * 3.0E2", ""}, {rational, `sysml:value ".5"^^xsd:decimal ;`, "x * .5", ""}, + {rational, `sysml:value "0.1"^^xsd:double ;`, "x * 0.1000000000000000055511151231257827021181583404541015625", ""}, + {rational, `sysml:value "0.1"^^xsd:float ;`, "x * 0.100000001490116119384765625", ""}, + {rational, `sysml:value "0.5"^^xsd:float ;`, "x * 0.5", ""}, {boolean, `sysml:value "1"^^xsd:boolean .`, "in expr always {\n true\n }", ""}, {boolean, `sysml:value "0"^^xsd:boolean .`, "in expr always {\n false\n }", ""}, {integer, `sysml:value "-2"^^xsd:integer ;`, "", `the expression : the notation spells an integer literal as digits alone, not "-2"`}, @@ -557,6 +560,40 @@ func TestLiteralValuesAreSpelledAsTokens(t *testing.T) { } } +// A rational literal's sysml:value denotes exactly the Rational its token does: +// an exponent token no double holds is written as the xsd:decimal it denotes. +func TestRationalLiteralValuesAreExact(t *testing.T) { + src := "package P {\n attribute a : Real = 1E-1;\n attribute b : Real = 1.5E3;\n attribute c : Real = 0.1;\n attribute d : Real = 25E-3;\n}\n" + turtle, err := convert.Convert("m.sysml", []byte(src), convert.FormatSysML, convert.FormatTurtle) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + `sysml:value "0.1"^^xsd:decimal`, + `sysml:value "1.5E3"^^xsd:double`, + `sysml:value "0.025"^^xsd:decimal`, + } { + if !strings.Contains(string(turtle), want) { + t.Errorf("the graph lacks %q:\n%s", want, turtle) + } + } + if strings.Contains(string(turtle), `"1E-1"^^xsd:double`) || strings.Contains(string(turtle), `"25E-3"^^xsd:double`) { + t.Errorf("an exponent token no double holds is not an xsd:double:\n%s", turtle) + } + stripped := withoutTriples(t, turtle, "sysx:sourceText") + back, err := convert.Convert("m.ttl", stripped, convert.FormatTurtle, convert.FormatSysML) + if err != nil { + t.Fatal(err) + } + again, err := convert.Convert("m.sysml", back, convert.FormatSysML, convert.FormatTurtle) + if err != nil { + t.Fatal(err) + } + if string(withoutTriples(t, again, "sysx:sourceText")) != string(stripped) { + t.Errorf("the exact literals should round-trip unchanged:\n%s\nthen\n%s", stripped, again) + } +} + // A declaration inside an expression body is a subject with the declaration's // metaclass and properties, so the graph alone writes it back; untyped, it is refused. func TestExpressionBodyDeclarationIsStructure(t *testing.T) { From d4cbaaa786e81750cc6cd8d868f0f09f18f1564a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:34:43 +0000 Subject: [PATCH 04/26] test(runtime): cover exact Rational arithmetic, accumulation and budgets Co-Authored-By: jason.han --- .../runtime/robustness_exact_rational_test.go | 93 +++++++++++++++++++ ..._exact_rational_accumulation.expected.json | 10 ++ .../action_exact_rational_accumulation.sysml | 23 +++++ ...n_exact_rational_accumulation.trace.golden | 93 +++++++++++++++++++ ...lc_exact_rational_arithmetic.expected.json | 32 +++++++ .../calc_exact_rational_arithmetic.sysml | 37 ++++++++ .../parse/exact_rational_literals.golden | 62 +++++++++++++ .../parse/exact_rational_literals.sysml | 15 +++ 8 files changed, 365 insertions(+) create mode 100644 internal/exec/runtime/robustness_exact_rational_test.go create mode 100644 internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.sysml create mode 100644 internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.trace.golden create mode 100644 internal/exec/runtime/testdata/conformance/calc_exact_rational_arithmetic.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/calc_exact_rational_arithmetic.sysml create mode 100644 tests/parser/testdata/parse/exact_rational_literals.golden create mode 100644 tests/parser/testdata/parse/exact_rational_literals.sysml diff --git a/internal/exec/runtime/robustness_exact_rational_test.go b/internal/exec/runtime/robustness_exact_rational_test.go new file mode 100644 index 0000000000..9adeef16a7 --- /dev/null +++ b/internal/exec/runtime/robustness_exact_rational_test.go @@ -0,0 +1,93 @@ +package runtime + +import ( + "errors" + "testing" + "time" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// TestRuntimeRobustnessExactRational covers the failure modes of exact +// Rationals: a denominator that grows past the size budget, a power or a +// literal beyond it, and a zero divisor are typed errors, never a rounded +// value, a hang or a panic. +func TestRuntimeRobustnessExactRational(t *testing.T) { + t.Run("denominator_growth_beyond_a_lowered_budget", testDenominatorGrowthBeyondALoweredBudget) + t.Run("power_beyond_the_size_budget", testRationalPowerBeyondTheSizeBudget) + t.Run("literal_beyond_the_size_budget", testRationalLiteralBeyondTheSizeBudget) + t.Run("division_by_zero", testRationalDivisionByZero) +} + +// The harmonic sum's denominator is lcm(1..n): within a 64-bit budget ten +// terms are exact (7381/2520) and two hundred are refused, not rounded. +func testDenominatorGrowthBeyondALoweredBudget(t *testing.T) { + src := ` + package test { + private import ScalarValues::*; + private import ControlFunctions::*; + calc harmonic { + in n : Integer; + return : Rational = RationalFunctions::sum((1..n)->collect { in i : Integer; 1 / i }); + } + } + ` + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, src)) + budgets := DefaultBudgets() + budgets.MaxIntegerBits = MinMaxIntegerBits + if err := ctx.SetBudgets(budgets); err != nil { + t.Fatal(err) + } + root := idx.DocumentRoot("") + sym := findSymbolByName(root, "harmonic", ast.DefCalc) + if sym == nil { + t.Fatal("harmonic calc not found") + } + got, err := ctx.InvokeCalc(sym, []Value{{Kind: ValConst, Const: semantics.IntValue(10)}}, root) + if err != nil || got.Kind != ValConst || got.Const.Kind != semantics.ValRational || got.Const.FormatRational() != "7381/2520" { + t.Fatalf("harmonic(10) = %s, %v; want 7381/2520", FormatValue(got), err) + } + start := time.Now() + if got, err := ctx.InvokeCalc(sym, []Value{{Kind: ValConst, Const: semantics.IntValue(200)}}, root); !errors.Is(err, semantics.ErrRationalSizeLimit) { + t.Fatalf("harmonic(200) under a 64-bit budget = %s, %v; want %v", FormatValue(got), err, semantics.ErrRationalSizeLimit) + } + if elapsed := time.Since(start); elapsed > 5*time.Second { + t.Fatalf("refusing harmonic(200) took %v", elapsed) + } +} + +// (1/3) ** 2000000 needs about 3.2 million bits: refused at once. +func testRationalPowerBeyondTheSizeBudget(t *testing.T) { + start := time.Now() + if got, err := evalCollectionExpr(t, "(1 / 3) ** 2000000"); !errors.Is(err, semantics.ErrRationalSizeLimit) { + t.Fatalf("(1 / 3) ** 2000000 = %s, %v; want %v", FormatValue(got), err, semantics.ErrRationalSizeLimit) + } + if elapsed := time.Since(start); elapsed > 5*time.Second { + t.Fatalf("refusing (1 / 3) ** 2000000 took %v", elapsed) + } + got, err := evalCollectionExpr(t, "(2 / 3) ** -3") + if err != nil || got.Kind != ValConst || got.Const.FormatRational() != "3.375" { + t.Fatalf("(2 / 3) ** -3 = %s, %v; want 3.375", FormatValue(got), err) + } +} + +// A literal is the exact Rational it spells, so one past the budget is refused +// rather than read as the nearest double, zero or infinity. +func testRationalLiteralBeyondTheSizeBudget(t *testing.T) { + for _, src := range []string{"1.0e400000", "1.0e-400000"} { + if got, err := evalCollectionExpr(t, src); !errors.Is(err, semantics.ErrRationalSizeLimit) { + t.Errorf("%s = %s, %v; want %v", src, FormatValue(got), err, semantics.ErrRationalSizeLimit) + } + } +} + +// A zero divisor of an exact quotient is a typed error, for a Rational and an +// Integer dividend alike. +func testRationalDivisionByZero(t *testing.T) { + for _, src := range []string{"(1 / 3) / 0", "0.1 / 0.0", "RationalFunctions::rat(1, 0)"} { + if got, err := evalCollectionExpr(t, src); !errors.Is(err, ErrDivisionByZero) { + t.Errorf("%s = %s, %v; want %v", src, FormatValue(got), err, ErrDivisionByZero) + } + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.expected.json b/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.expected.json new file mode 100644 index 0000000000..4a44ab9f91 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "evaluate": "test::accumulate", + "trace": true, + "outputs": { + "tenths": {"type": "Rational", "value": "1.0"}, + "harmonic": {"type": "Rational", "value": "25/12"}, + "reachesOne": {"type": "Boolean", "value": true} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.sysml b/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.sysml new file mode 100644 index 0000000000..e79e892103 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.sysml @@ -0,0 +1,23 @@ +package test { + private import ScalarValues::*; + + // Rational assignments accumulate exactly, in order (KerML 1.0 §9.3.2.2.8): + // ten steps of 0.1 reach exactly 1.0, and 1/1 + 1/2 + 1/3 + 1/4 is 25/12. + action accumulate { + out attribute tenths : Rational = 0.0; + out attribute harmonic : Rational = 0.0; + out attribute reachesOne : Boolean = false; + + first start; + then action sum { + for i in 1..10 { + assign tenths := tenths + 0.1; + } + for n in 1..4 { + assign harmonic := harmonic + 1 / n; + } + assign reachesOne := tenths == 1; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.trace.golden b/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.trace.golden new file mode 100644 index 0000000000..6cd17217b9 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.trace.golden @@ -0,0 +1,93 @@ +step 1: token 1@sum +stmt for i + eval literal 1 -> 1 + eval literal 10 -> 10 + eval operator .. -> (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + iteration 1 + stmt assign tenths + eval feature tenths -> 0.0 + eval literal 0.1 -> 0.1 + eval operator + -> 0.1 + iteration 2 + stmt assign tenths + eval feature tenths -> 0.1 + eval literal 0.1 -> 0.1 + eval operator + -> 0.2 + iteration 3 + stmt assign tenths + eval feature tenths -> 0.2 + eval literal 0.1 -> 0.1 + eval operator + -> 0.3 + iteration 4 + stmt assign tenths + eval feature tenths -> 0.3 + eval literal 0.1 -> 0.1 + eval operator + -> 0.4 + iteration 5 + stmt assign tenths + eval feature tenths -> 0.4 + eval literal 0.1 -> 0.1 + eval operator + -> 0.5 + iteration 6 + stmt assign tenths + eval feature tenths -> 0.5 + eval literal 0.1 -> 0.1 + eval operator + -> 0.6 + iteration 7 + stmt assign tenths + eval feature tenths -> 0.6 + eval literal 0.1 -> 0.1 + eval operator + -> 0.7 + iteration 8 + stmt assign tenths + eval feature tenths -> 0.7 + eval literal 0.1 -> 0.1 + eval operator + -> 0.8 + iteration 9 + stmt assign tenths + eval feature tenths -> 0.8 + eval literal 0.1 -> 0.1 + eval operator + -> 0.9 + iteration 10 + stmt assign tenths + eval feature tenths -> 0.9 + eval literal 0.1 -> 0.1 + eval operator + -> 1.0 +stmt for n + eval literal 1 -> 1 + eval literal 4 -> 4 + eval operator .. -> (1, 2, 3, 4) + iteration 1 + stmt assign harmonic + eval feature harmonic -> 0.0 + eval literal 1 -> 1 + eval feature n -> 1 + eval operator / -> 1.0 + eval operator + -> 1.0 + iteration 2 + stmt assign harmonic + eval feature harmonic -> 1.0 + eval literal 1 -> 1 + eval feature n -> 2 + eval operator / -> 0.5 + eval operator + -> 1.5 + iteration 3 + stmt assign harmonic + eval feature harmonic -> 1.5 + eval literal 1 -> 1 + eval feature n -> 3 + eval operator / -> 1/3 + eval operator + -> 11/6 + iteration 4 + stmt assign harmonic + eval feature harmonic -> 11/6 + eval literal 1 -> 1 + eval feature n -> 4 + eval operator / -> 0.25 + eval operator + -> 25/12 +stmt assign reachesOne + eval feature tenths -> 1.0 + eval literal 1 -> 1 + eval operator == -> true +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/calc_exact_rational_arithmetic.expected.json b/internal/exec/runtime/testdata/conformance/calc_exact_rational_arithmetic.expected.json new file mode 100644 index 0000000000..560e85b204 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_exact_rational_arithmetic.expected.json @@ -0,0 +1,32 @@ +{ + "libraries": true, + "type": "calcUsage", + "evaluate": "test::c", + "outputs": { + "tenthsSum": {"type": "Rational", "value": "0.3"}, + "tenthsSumIsThreeTenths": {"type": "Boolean", "value": true}, + "third": {"type": "Rational", "value": "1/3"}, + "thirdTimesThree": {"type": "Rational", "value": "1.0"}, + "thirdTimesThreeIsOne": {"type": "Boolean", "value": true}, + "thirds": {"type": "Rational", "value": "2/3"}, + "difference": {"type": "Rational", "value": "0.2"}, + "quotient": {"type": "Rational", "value": "1/3"}, + "power": {"type": "Rational", "value": "8/27"}, + "inversePower": {"type": "Rational", "value": "2.25"}, + "exponentLiteral": {"type": "Rational", "value": "1500.0"}, + "smallExponentLiteral": {"type": "Rational", "value": "0.025"}, + "thirdIsBelow": {"type": "Boolean", "value": true}, + "absolute": {"type": "Rational", "value": "1/3"}, + "floored": {"type": "Integer", "value": -4}, + "rounded": {"type": "Integer", "value": 3}, + "largest": {"type": "Rational", "value": "1/3"}, + "smallest": {"type": "Rational", "value": "0.3"}, + "total": {"type": "Rational", "value": "0.6"}, + "product": {"type": "Rational", "value": "0.25"}, + "gcd": {"type": "Integer", "value": 2}, + "text": {"type": "String", "value": "1/3"}, + "fromText": {"type": "Rational", "value": "1/3"}, + "fromDecimalText": {"type": "Rational", "value": "0.125"}, + "root": {"type": "Real", "value": 0.5} + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_exact_rational_arithmetic.sysml b/internal/exec/runtime/testdata/conformance/calc_exact_rational_arithmetic.sysml new file mode 100644 index 0000000000..8dcb275d93 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_exact_rational_arithmetic.sysml @@ -0,0 +1,37 @@ +// A Rational is exact (KerML 1.0 §9.3.2.2.8; RationalFunctions): a decimal +// literal is the Rational it spells, Integer `/` and the Rational operators +// compute without rounding, a whole Rational stays a Rational, and only a +// Real-typed function (sqrt) yields a binary64 Real. +package test { + private import ScalarValues::*; + + calc def ExactRationals { + out tenthsSum = 0.1 + 0.2; + out tenthsSumIsThreeTenths = 0.1 + 0.2 == 0.3; + out third = 1 / 3; + out thirdTimesThree = 1 / 3 * 3; + out thirdTimesThreeIsOne = 1 / 3 * 3 == 1; + out thirds = 1 / 3 + 1 / 3; + out difference = 0.3 - 0.1; + out quotient = 0.1 / 0.3; + out power = (2 / 3) ** 3; + out inversePower = (2 / 3) ** -2; + out exponentLiteral = 1.5e3; + out smallExponentLiteral = 25e-3; + out thirdIsBelow = 1 / 3 < 0.3334; + out absolute = RationalFunctions::abs(-1 / 3); + out floored = RationalFunctions::floor(-7 / 2); + out rounded = RationalFunctions::round(5 / 2); + out largest = RationalFunctions::max(1 / 3, 0.3); + out smallest = RationalFunctions::min(1 / 3, 0.3); + out total = RationalFunctions::sum((0.1, 0.2, 0.3)); + out product = RationalFunctions::product((1 / 2, 2 / 3, 3 / 4)); + out gcd = RationalFunctions::gcd(6, 4); + out text = RationalFunctions::ToString(1 / 3); + out fromText = RationalFunctions::ToRational("2/6"); + out fromDecimalText = RationalFunctions::ToRational("0.125"); + out root = RealFunctions::sqrt(0.25); + } + + calc c : ExactRationals; +} diff --git a/tests/parser/testdata/parse/exact_rational_literals.golden b/tests/parser/testdata/parse/exact_rational_literals.golden new file mode 100644 index 0000000000..d4c4e47130 --- /dev/null +++ b/tests/parser/testdata/parse/exact_rational_literals.golden @@ -0,0 +1,62 @@ +(RootNamespace + (Membership visibility="default" + (Package name="P" library=false standard=false + (Import visibility="private" all=false kind=namespace recursive=false imported="ScalarValues" filtered=false) + (Membership visibility="default" + (Definition kind="calc" abstract=false variation=false name="ExactRationals" + (Membership visibility="default" + (Usage kind="attribute" name="tenthsSum" ref=false direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Rational + (*ast.QualifiedName)) + (OperatorExpr operator="+" + (LiteralReal value="0.1") + (LiteralReal value="0.2")))) + (Membership visibility="default" + (Usage kind="attribute" name="third" ref=false direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Rational + (*ast.QualifiedName)) + (OperatorExpr operator="/" + (LiteralInteger value="1") + (LiteralInteger value="3")))) + (Membership visibility="default" + (Usage kind="attribute" name="exponent" ref=false direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Rational + (*ast.QualifiedName)) + (LiteralReal value="1.5e3"))) + (Membership visibility="default" + (Usage kind="attribute" name="small" ref=false direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Rational + (*ast.QualifiedName)) + (LiteralReal value="25E-3"))) + (Membership visibility="default" + (Usage kind="attribute" name="leadingPoint" ref=false direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Rational + (*ast.QualifiedName)) + (LiteralReal value=".5"))) + (Membership visibility="default" + (Usage kind="attribute" name="power" ref=false direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Rational + (*ast.QualifiedName)) + (OperatorExpr operator="**" + (OperatorExpr operator="/" + (LiteralInteger value="2") + (LiteralInteger value="3")) + (OperatorExpr operator="-" + (LiteralInteger value="2"))))) + (Membership visibility="default" + (Usage kind="attribute" name="terms" ref=false direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (InvocationExpr type="RationalFunctions::numer" + (InvocationExpr type="RationalFunctions::rat" + (LiteralInteger value="6") + (LiteralInteger value="4"))))) + (Membership visibility="default" + (Usage kind="attribute" name="root" ref=false direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Real + (*ast.QualifiedName)) + (InvocationExpr type="RealFunctions::sqrt" + (LiteralReal value="0.25")))) + (Membership visibility="default" + (Usage kind="attribute" name="unbounded" ref=false direction="out" composite=false derived=false ordered=false nonunique=false + (LiteralInfinity)))))))) \ No newline at end of file diff --git a/tests/parser/testdata/parse/exact_rational_literals.sysml b/tests/parser/testdata/parse/exact_rational_literals.sysml new file mode 100644 index 0000000000..f4a848f092 --- /dev/null +++ b/tests/parser/testdata/parse/exact_rational_literals.sysml @@ -0,0 +1,15 @@ +package P { + private import ScalarValues::*; + + calc def ExactRationals { + out tenthsSum : Rational = 0.1 + 0.2; + out third : Rational = 1 / 3; + out exponent : Rational = 1.5e3; + out small : Rational = 25E-3; + out leadingPoint : Rational = .5; + out power : Rational = (2 / 3) ** -2; + out terms : Integer = RationalFunctions::numer(RationalFunctions::rat(6, 4)); + out root : Real = RealFunctions::sqrt(0.25); + out unbounded = *; + } +} From a39be7ec8b1bb419bf6e6d1e69b22da5f9c141b2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:48:07 +0000 Subject: [PATCH 05/26] test(referee): bucket exact-Rational pilot disagreements as differs-by-design Co-Authored-By: jason.han --- .../testing-pilot-execution-referee/SKILL.md | 12 ++++--- docs/project/pilot-execution-referee.md | 32 +++++++++++++++---- tools/referee/exec/cases.go | 13 +++++++- tools/referee/exec/execref_cases_test.go | 22 +++++++++++++ tools/referee/exec/execref_normalize_test.go | 5 +++ tools/referee/exec/main.go | 14 +++++++- tools/referee/exec/normalize.go | 6 ++-- .../exec/testdata/cases/exact_rationals.cases | 16 ++++++++++ .../exec/testdata/cases/rational_terms.cases | 2 +- .../testdata/models/exact_rationals.sysml | 6 ++++ .../exec/testdata/models/rational_terms.sysml | 4 +-- 11 files changed, 114 insertions(+), 18 deletions(-) create mode 100644 tools/referee/exec/testdata/cases/exact_rationals.cases create mode 100644 tools/referee/exec/testdata/models/exact_rationals.sysml diff --git a/.agents/skills/testing-pilot-execution-referee/SKILL.md b/.agents/skills/testing-pilot-execution-referee/SKILL.md index 7b9d620281..95f4bc2bc9 100644 --- a/.agents/skills/testing-pilot-execution-referee/SKILL.md +++ b/.agents/skills/testing-pilot-execution-referee/SKILL.md @@ -60,14 +60,16 @@ go run -C tools ./cmd/pilot-exec-diff # ~13 s wall with the default fix Use `-cases DIR` for another directory of `.cases` files, `-out DIR`, `-launcher PATH`, `-repo DIR`. Each `.cases` file lists `model: ` -lines followed by `id :: target :: expression` lines. Reports go to +lines followed by `id :: target :: expression` lines; a `by-design: ` line marks the +cases after it, so a disagreement among them buckets `differs-by-design` with the clause in the +report. Reports go to `build/pilot-exec-diff/pilot-exec-diff.{txt,json}`. -Reference values at the current implementation (446 cases, all twenty default +Reference values at the current implementation (456 cases, all twenty-one default fixtures): -`agree 211 · kind-only 1 · order-only 0 · disagree 30 · pilot-unevaluated 124 · -pilot-silent 21 · pilot-error 9 · ours-error 10 · ours-undetermined 28 · both-error 12 · -nondeterministic 0`. +`agree 217 · kind-only 1 · order-only 0 · disagree 29 · differs-by-design 5 · +pilot-unevaluated 124 · pilot-silent 21 · pilot-error 5 · ours-error 10 · ours-undetermined 28 · +both-error 16 · nondeterministic 0`. Four of the nine `pilot-error` are the whole of `unknown_bounds.cases`: the pilot rejects a model whose multiplicity bound names a valueless feature (`a : Real[n]`, `Must have a Natural value`) and resolves nothing in it afterwards, which is why those cases have a model of their diff --git a/docs/project/pilot-execution-referee.md b/docs/project/pilot-execution-referee.md index 42e57c4fce..6683ce1fdb 100644 --- a/docs/project/pilot-execution-referee.md +++ b/docs/project/pilot-execution-referee.md @@ -189,6 +189,11 @@ No compliance row's status flag is changed on the strength of this work. this harness. It is no longer a display limit — we now print `1.0 / 3.0` as `0.3333333333333333`, the same digits the pilot reports as `LiteralRational`, so the tolerance can be tightened on its own once the buckets it moves are adjudicated. +- **A disagreement the specification decides is reported in its own bucket.** A `by-design:` + line in a `.cases` file names the clause the cases after it follow; a case so marked that would + bucket `disagree` buckets `differs-by-design` instead, and the report carries the clause beside + the raw outputs of both sides. Only a disagreement moves: a marked case that agrees still + buckets `agree`, and one that errors on either side keeps its error bucket. - **Integer vs Rational is reported, never normalized away.** `2 ** 40` gives the pilot `LiteralRational 1.099511627776E12` and gives us `1099511627776`; the harness buckets that `kind-only`, not `agree`. @@ -216,7 +221,7 @@ Run it with `go run -C tools ./cmd/pilot-exec-diff` after `./scripts/download-pi execution artifact absent it prints a provisioning instruction, exits 0 and writes nothing, so `tools/referee/diff` and its committed baseline are untouched. The bucket counts below are as measured when this record was last updated and are not the current baseline — `go run -C tools ./cmd/pilot-exec-diff` -prints the current ones. State of the 446 committed cases, the original 32, the 62 the +prints the current ones. State of the 456 committed cases, the original 32, the 62 the expression round added (one of them, `intdiv`, since moved to `integer_quotient.cases`), the 14 of `value_classification.cases`, the 3 of `contextual_names.cases`, the 14 of `rational_terms.cases`, the 5 the empty-aggregate and subsetting round added to `w6d_expr_depth.cases` the 12 of @@ -225,14 +230,27 @@ the 27 of `scalar_classification.cases`, the 24 of `literal_types.cases`, the 23 `enumeration_classification.cases`, the 24 of `metadata_access.cases`, the 13 of `extent_expressions.cases`, the 1 of `extent_count_mismatch.cases`, the 166 of `undetermined_operands.cases`, the 4 of `unknown_bounds.cases`, the 2 of `vast_bounds.cases` and -the 4 of `body_local_conformance.cases`: +the 4 of `body_local_conformance.cases` and the 10 of `exact_rationals.cases`: ``` -agree: 211 · kind-only: 1 · order-only: 0 · disagree: 30 -pilot-unevaluated: 124 · pilot-silent: 21 · pilot-error: 9 · ours-error: 10 · ours-undetermined: 28 -both-error: 12 · nondeterministic: 0 +agree: 217 · kind-only: 1 · order-only: 0 · disagree: 29 · differs-by-design: 5 +pilot-unevaluated: 124 · pilot-silent: 21 · pilot-error: 5 · ours-error: 10 · ours-undetermined: 28 +both-error: 16 · nondeterministic: 0 ``` +The ten `exact_rationals.cases` are the binary64 probes of +[exact-rational-evaluation.md](exact-rational-evaluation.md#the-pilot-differs-by-design), marked +`by-design` with KerML 1.0 §9.3.2.2.8 (a `Rational` is a rational number) and §8.4.4.9.2 (a +`LiteralRational`'s result is a `Rational`). The pinned pilot holds a `LiteralRational` as a Java +`double`; we hold it exactly. Five land in `differs-by-design`: `0.1 + 0.2 == 0.3`, `0.1 + 0.2 <= +0.3`, `0.3 < 0.1 + 0.2`, `(1.0 / 49.0) * 49.0 == 1.0` and ten sums of a `0.1` attribute compared +with `1.0`, each the opposite Boolean on the two sides. The other five agree, and only through the +harness's own tolerance: `0.1 + 0.2` is `0.30000000000000004` from the pilot and `0.3` here, `0.1 + +0.2 - 0.3` is `5.551115123125783E-17` and `0.0`, `1.0 / 3.0` and `1 / 3` are `0.3333333333333333` +and `1/3`, all equal to two decimal places; `(1.0 / 3.0) * 3.0 == 1.0` is `true` on both sides +because binary64 rounding happens to return `1.0`. No other case moved bucket when Rationals became +exact. + The first six `extent_expressions.cases` probe `all T` (KerML 1.0 §7.4.9.2, §8.2.5.8.1 `ExtentExpression`, `BaseFunctions::'all'`), added with the evaluation they were meant to referee and could not: the pilot does not evaluate the operator, as Table 5 of §8.2.5.8.1 foretells by @@ -607,7 +625,9 @@ with the implementation they were meant to referee and could not: the pilot answ or as an attribute's value — with the unevaluated `InvocationExpression rat`/`numer`/`denom`, so thirteen land in `pilot-unevaluated` and only `quotient-by-operator` (`6 / 4`, `1.5`) agrees. The semantics are therefore self-assessed, in -[exact-rational-evaluation.md](exact-rational-evaluation.md#rationalfunctionsrat-numer-and-denom-over-a-binary64-rational). +[exact-rational-evaluation.md](exact-rational-evaluation.md#rationalfunctionsrat-numer-and-denom). +Our answers are exact — `rat(1, 3)` is `1/3`, `numer(0.1)` `1`, `denom(1.0 / 3.0)` `3` — where the +pilot, had it evaluated them, would hold the binary64 nearest. The fourteen `value_classification.cases` all agree, and they were added with the fixes they referee: `x @ T` with a value subject is a classification test like `x istype T` — `a : Integer = diff --git a/tools/referee/exec/cases.go b/tools/referee/exec/cases.go index 89fe81a3ba..ca7c67d015 100644 --- a/tools/referee/exec/cases.go +++ b/tools/referee/exec/cases.go @@ -13,6 +13,9 @@ type execCase struct { ID string Target string Expression string + // ByDesign is the specification clause a disagreement of the case follows + // from, set by a `by-design:` line for the cases after it. + ByDesign string } type execModel struct { @@ -57,12 +60,20 @@ func readCaseFile(path string) (execCaseFile, error) { file := execCaseFile{Path: path} scanner := bufio.NewScanner(f) lineNumber := 0 + byDesign := "" for scanner.Scan() { lineNumber++ line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } + if strings.HasPrefix(line, "by-design:") { + byDesign = strings.TrimSpace(strings.TrimPrefix(line, "by-design:")) + if byDesign == "" { + return execCaseFile{}, fmt.Errorf("%s:%d: by-design states no clause", path, lineNumber) + } + continue + } if strings.HasPrefix(line, "model:") { model := strings.TrimSpace(strings.TrimPrefix(line, "model:")) if model == "" { @@ -82,7 +93,7 @@ func readCaseFile(path string) (execCaseFile, error) { if expression == "" { return execCaseFile{}, fmt.Errorf("%s:%d: expression is empty", path, lineNumber) } - file.Cases = append(file.Cases, execCase{ID: id, Target: target, Expression: expression}) + file.Cases = append(file.Cases, execCase{ID: id, Target: target, Expression: expression, ByDesign: byDesign}) } if err := scanner.Err(); err != nil { return execCaseFile{}, fmt.Errorf("read %s: %w", path, err) diff --git a/tools/referee/exec/execref_cases_test.go b/tools/referee/exec/execref_cases_test.go index fe4967d13b..741b84687d 100644 --- a/tools/referee/exec/execref_cases_test.go +++ b/tools/referee/exec/execref_cases_test.go @@ -25,6 +25,28 @@ func TestReadCaseFile(t *testing.T) { } } +// A by-design line marks every case after it with its clause. +func TestReadCaseFileByDesign(t *testing.T) { + path := filepath.Join(t.TempDir(), "sample.cases") + content := "plain :: :: 1\nby-design: KerML 1.0 §9.3.2.2.8\nexact :: :: 0.1 + 0.2 == 0.3\nalso :: :: 1 / 3\n" + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + got, err := readCaseFile(path) + if err != nil { + t.Fatal(err) + } + if got.Cases[0].ByDesign != "" || got.Cases[1].ByDesign != "KerML 1.0 §9.3.2.2.8" || got.Cases[2].ByDesign != got.Cases[1].ByDesign { + t.Fatalf("by-design clauses = %+v", got.Cases) + } + if err := os.WriteFile(path, []byte("by-design:\none :: :: 1\n"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := readCaseFile(path); err == nil || !strings.Contains(err.Error(), path+":1: by-design states no clause") { + t.Fatalf("readCaseFile() error = %v", err) + } +} + func TestReadCaseFileMalformed(t *testing.T) { path := filepath.Join(t.TempDir(), "bad.cases") if err := os.WriteFile(path, []byte("bad line\n"), 0o644); err != nil { diff --git a/tools/referee/exec/execref_normalize_test.go b/tools/referee/exec/execref_normalize_test.go index 675d12a6e3..f6ddfaa846 100644 --- a/tools/referee/exec/execref_normalize_test.go +++ b/tools/referee/exec/execref_normalize_test.go @@ -57,6 +57,8 @@ func TestNormalizeOursSamples(t *testing.T) { {"✓ seq\n = [3, 1, 2]", normalized{Kind: kindSequence, Elements: []normalized{{Kind: kindInt, Value: "3"}, {Kind: kindInt, Value: "1"}, {Kind: kindInt, Value: "2"}}}}, {"✓ s\n = \"abc\"", normalized{Kind: kindString, Value: "abc"}}, {"✓ q\n = 3.00 [SI::kg]", normalized{Kind: kindQuantity, Value: "3.00 [SI::kg]"}}, + {"✓ rat(1, 3)\n = 1/3", normalized{Kind: kindReal, Value: "1/3"}}, + {"✓ rat(-2, 3)\n = -2/3", normalized{Kind: kindReal, Value: "-2/3"}}, {"", normalized{}}, } for _, test := range tests { @@ -146,6 +148,9 @@ func TestRealRoundingAndExponent(t *testing.T) { if got := roundedReal(normalized{Kind: kindReal, Value: "0.3333333333333333"}); got != "0.33" { t.Fatalf("roundedReal() = %q, want 0.33", got) } + if got := compareValues(normalized{Kind: kindReal, Value: "0.3333333333333333"}, normalized{Kind: kindReal, Value: "1/3"}); got != "agree" { + t.Fatalf("compareValues(binary third, 1/3) = %q, want agree", got) + } } func TestCanonicalPilotStripsUUIDsFromEveryLine(t *testing.T) { diff --git a/tools/referee/exec/main.go b/tools/referee/exec/main.go index b1fc5ef6dc..3248905430 100644 --- a/tools/referee/exec/main.go +++ b/tools/referee/exec/main.go @@ -22,11 +22,16 @@ import ( // reconciled. const bucketKindOnly = "kind-only" +// bucketByDesign is the verdict for a disagreement the case documents as +// following from a specification clause the pilot departs from. +const bucketByDesign = "differs-by-design" + var bucketNames = []string{ "agree", bucketKindOnly, "order-only", "disagree", + bucketByDesign, "pilot-unevaluated", "pilot-silent", "pilot-error", @@ -46,6 +51,7 @@ type caseReport struct { Pilot normalized `json:"pilotNormalized"` Ours normalized `json:"oursNormalized"` Bucket string `json:"bucket"` + ByDesign string `json:"byDesign,omitempty"` RealPrecision string `json:"realPrecision"` } @@ -164,12 +170,15 @@ func execute(repo, launcher string, files []execCaseFile) (*execReport, error) { oursOne[testCase.ID].Error != oursTwo[testCase.ID].Error { bucket = "nondeterministic" } + if bucket == "disagree" && testCase.ByDesign != "" { + bucket = bucketByDesign + } report.Cases = append(report.Cases, caseReport{ ID: testCase.ID, Models: modelPaths(file.Models), Target: testCase.Target, Expression: testCase.Expression, RawPilot: pilotRaw, RawOurs: oursRaw.Raw, Pilot: pilot.Value, Ours: ours.Value, - Bucket: bucket, RealPrecision: "real values compared after rounding both sides to 2 decimal places", + Bucket: bucket, ByDesign: testCase.ByDesign, RealPrecision: "real values compared after rounding both sides to 2 decimal places", }) report.Buckets[bucket]++ } @@ -338,6 +347,9 @@ func writeReport(dir string, report *execReport) error { for _, result := range report.Cases { fmt.Fprintf(&text, "%s: %s [%s]\n", result.ID, result.Bucket, strings.Join(result.Models, ", ")) fmt.Fprintf(&text, " target: %s\n expression: %s\n", result.Target, result.Expression) + if result.ByDesign != "" { + fmt.Fprintf(&text, " by design: %s\n", result.ByDesign) + } fmt.Fprintf(&text, " raw pilot:\n%s\n raw ours:\n%s\n", result.RawPilot, result.RawOurs) fmt.Fprintf(&text, " pilot normalized: %s\n ours normalized: %s\n", normalizedText(result.Pilot), normalizedText(result.Ours)) fmt.Fprintf(&text, " real comparison: %s\n\n", result.RealPrecision) diff --git a/tools/referee/exec/normalize.go b/tools/referee/exec/normalize.go index e398f4fd63..0cd37a92b5 100644 --- a/tools/referee/exec/normalize.go +++ b/tools/referee/exec/normalize.go @@ -25,7 +25,9 @@ var ( pilotUUID = regexp.MustCompile(` \([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\)$`) intText = regexp.MustCompile(`^-?[0-9]+$`) realText = regexp.MustCompile(`^-?[0-9]+\.[0-9]+$`) - quantity = regexp.MustCompile(`^(-?[0-9]+(?:\.[0-9]+)?) \[(.+)\]$`) + // fraction is a Rational no decimal spells, printed as `numer/denom`. + fraction = regexp.MustCompile(`^-?[0-9]+/[0-9]+$`) + quantity = regexp.MustCompile(`^(-?[0-9]+(?:\.[0-9]+)?) \[(.+)\]$`) ) type normalized struct { @@ -148,7 +150,7 @@ func parseOurValue(text string) (normalized, bool) { if intText.MatchString(text) { return normalized{Kind: kindInt, Value: text}, true } - if realText.MatchString(text) { + if realText.MatchString(text) || fraction.MatchString(text) { return normalized{Kind: kindReal, Value: text}, true } return normalized{}, false diff --git a/tools/referee/exec/testdata/cases/exact_rationals.cases b/tools/referee/exec/testdata/cases/exact_rationals.cases new file mode 100644 index 0000000000..2c31751f49 --- /dev/null +++ b/tools/referee/exec/testdata/cases/exact_rationals.cases @@ -0,0 +1,16 @@ +model: tools/referee/exec/testdata/models/exact_rationals.sysml + +# The binary64 probes of docs/project/exact-rational-evaluation.md. The pinned +# pilot holds a LiteralRational as a Java double; a Rational is a rational +# number, so every comparison below is exact here and rounded there. +by-design: KerML 1.0 §9.3.2.2.8 Rational is the rational numbers; §8.4.4.9.2 a LiteralRational's result is a Rational +tenths-sum :: :: 0.1 + 0.2 +tenths-sum-equals :: :: 0.1 + 0.2 == 0.3 +tenths-sum-at-most :: :: 0.1 + 0.2 <= 0.3 +tenths-sum-exceeds :: :: 0.3 < 0.1 + 0.2 +tenths-residue :: :: 0.1 + 0.2 - 0.3 +forty-ninths-equal :: :: (1.0 / 49.0) * 49.0 == 1.0 +thirds-equal :: :: (1.0 / 3.0) * 3.0 == 1.0 +tenfold-tenth-equals :: :: ExactRationals::tenfold == 1.0 +rational-third :: :: 1.0 / 3.0 +integer-third :: :: 1 / 3 diff --git a/tools/referee/exec/testdata/cases/rational_terms.cases b/tools/referee/exec/testdata/cases/rational_terms.cases index 9a0c35261a..6023d1fe88 100644 --- a/tools/referee/exec/testdata/cases/rational_terms.cases +++ b/tools/referee/exec/testdata/cases/rational_terms.cases @@ -1,6 +1,6 @@ model: tools/referee/exec/testdata/models/rational_terms.sysml -# RationalFunctions::rat, numer and denom over the binary64 Rational +# RationalFunctions::rat, numer and denom over the exact Rational # (docs/project/exact-rational-evaluation.md). The pinned pilot leaves every # one of these calls unevaluated, so they land in pilot-unevaluated; only the # operator quotient is refereeable, and it agrees. diff --git a/tools/referee/exec/testdata/models/exact_rationals.sysml b/tools/referee/exec/testdata/models/exact_rationals.sysml new file mode 100644 index 0000000000..baaddd0a5e --- /dev/null +++ b/tools/referee/exec/testdata/models/exact_rationals.sysml @@ -0,0 +1,6 @@ +package ExactRationals { + private import ScalarValues::*; + + attribute tenth : Rational = 0.1; + attribute tenfold : Rational = tenth + tenth + tenth + tenth + tenth + tenth + tenth + tenth + tenth + tenth; +} diff --git a/tools/referee/exec/testdata/models/rational_terms.sysml b/tools/referee/exec/testdata/models/rational_terms.sysml index f6129efcc1..4c8dc1b81b 100644 --- a/tools/referee/exec/testdata/models/rational_terms.sysml +++ b/tools/referee/exec/testdata/models/rational_terms.sysml @@ -1,8 +1,8 @@ package RationalTerms { private import ScalarValues::*; - // A Rational is a binary64 on both sides; rat is the quotient `/` computes, - // numer/denom the exact terms of that binary64. + // A Rational is exact here and a binary64 in the pilot; rat is the quotient + // `/` computes, numer/denom its terms in lowest form. attribute third = RationalFunctions::rat(1, 3); attribute sixQuarters = RationalFunctions::rat(6, 4); attribute undefined = RationalFunctions::rat(1, 0); From 5e09c5da2628f2646eccb2ec27e763684258f2e5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:05:08 +0000 Subject: [PATCH 06/26] feat(client): carry exact Rationals in the Java and Rust clients Co-Authored-By: jason.han --- .../org/openmbee/opensysml/Capabilities.java | 6 + .../org/openmbee/opensysml/DocumentValue.java | 21 + .../java/org/openmbee/opensysml/Model.java | 3 + .../java/org/openmbee/opensysml/Quantity.java | 27 +- .../java/org/openmbee/opensysml/Rational.java | 164 ++++++++ .../java/org/openmbee/opensysml/Value.java | 74 +++- .../openmbee/opensysml/internal/Protos.java | 74 ++++ .../internal/RationalProtosTest.java | 171 ++++++++ client/rust/opensysml/src/capabilities.rs | 3 + client/rust/opensysml/src/document.rs | 52 ++- client/rust/opensysml/src/domain.rs | 53 ++- client/rust/opensysml/src/encode.rs | 73 +++- client/rust/opensysml/src/lib.rs | 2 + client/rust/opensysml/src/operations.rs | 7 +- client/rust/opensysml/src/rational.rs | 367 ++++++++++++++++++ 15 files changed, 1057 insertions(+), 40 deletions(-) create mode 100644 client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Rational.java create mode 100644 client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java create mode 100644 client/rust/opensysml/src/rational.rs diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Capabilities.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Capabilities.java index 1d1e01e30f..90fb219118 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Capabilities.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Capabilities.java @@ -54,6 +54,12 @@ public final class Capabilities { /** An Integer beyond int64 travels as {@code big_int_value} rather than as an unsupported null. */ public static final String BIG_INT_VALUES = "big_int_values"; + /** + * An exact Rational no double holds travels as {@code rational_value} rather than as an + * unsupported null. + */ + public static final String RATIONAL_VALUES = "rational_values"; + /** A complex number travels as itself rather than as an unsupported null. */ public static final String COMPLEX_VALUES = "complex_values"; diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java index 552550896a..5a7f64c422 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java @@ -118,6 +118,27 @@ record BigIntegerValue(java.math.BigInteger value) implements DocumentValue { */ record RealValue(double value) implements DocumentValue {} + /** + * An exact rational no {@code double} holds; one a {@code double} holds is always a {@link + * RealValue}. + * + * @param value the rational, not a {@code double} + */ + record RationalValue(Rational value) implements DocumentValue { + /** + * Creates an exact rational. + * + * @param value the rational, never {@code null} + * @throws IllegalArgumentException if a {@code double} holds the rational exactly + */ + public RationalValue { + Objects.requireNonNull(value, "value"); + if (value.isBinary64()) { + throw new IllegalArgumentException(value + " is a double, which RealValue carries"); + } + } + } + /** * A boolean. * diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Model.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Model.java index 30455ef2fe..3afdb24833 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Model.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Model.java @@ -1569,6 +1569,9 @@ public DocumentQueryResult runDocumentQuery( if (request.getBindingsList().stream().anyMatch(Protos::holdsBigInt)) { connection.capabilities().require(Capabilities.BIG_INT_VALUES); } + if (request.getBindingsList().stream().anyMatch(Protos::holdsRational)) { + connection.capabilities().require(Capabilities.RATIONAL_VALUES); + } RunDocumentQueryResponse response = connection.call( "RunDocumentQuery", request.build(), RunDocumentQueryResponse.getDefaultInstance()); diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Quantity.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Quantity.java index 62032e71dd..fb13106f0b 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Quantity.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Quantity.java @@ -8,10 +8,12 @@ * A magnitude and the measurement reference it is expressed in. * *

The magnitude keeps {@code Integer} and {@code Real} apart the way {@link Value} does: it is a - * {@link Long} or, beyond {@code long}, a {@link java.math.BigInteger} for an {@code Integer}, and a - * {@link Double} for a {@code Real}, never converted on the way in. + * {@link Long} or, beyond {@code long}, a {@link java.math.BigInteger} for an {@code Integer}, a + * {@link Rational} for an exact {@code Rational} no {@code double} holds, and a {@link Double} for a + * {@code Real}, never converted on the way in. * - * @param magnitude the magnitude, a {@link Long}, {@link java.math.BigInteger} or {@link Double} + * @param magnitude the magnitude, a {@link Long}, {@link java.math.BigInteger}, {@link Rational} or + * {@link Double} * @param unit the unit as written ({@code "km/h"}), or empty for one never written down * @param reduction what the unit reduces to, absent when the service sent none */ @@ -21,7 +23,7 @@ public record Quantity(Number magnitude, Optional unit, Optional unit, OptionalA Rational a {@code double} holds exactly crosses the wire as a {@code Real}; only one no + * {@code double} holds, such as {@code 1/3} or {@code 1/10}, arrives as this. + */ +public final class Rational extends Number implements Comparable { + + private static final long serialVersionUID = 1L; + + private final BigInteger numerator; + private final BigInteger denominator; + + private Rational(BigInteger numerator, BigInteger denominator) { + this.numerator = numerator; + this.denominator = denominator; + } + + /** + * The rational {@code numerator / denominator}, reduced to lowest terms with a positive + * denominator. + * + * @param numerator the numerator + * @param denominator the denominator, nonzero + * @return the rational + * @throws ArithmeticException if the denominator is zero + */ + public static Rational of(BigInteger numerator, BigInteger denominator) { + Objects.requireNonNull(numerator, "numerator"); + Objects.requireNonNull(denominator, "denominator"); + if (denominator.signum() == 0) { + throw new ArithmeticException("a rational's denominator is zero"); + } + BigInteger gcd = numerator.gcd(denominator); + if (denominator.signum() < 0) { + gcd = gcd.negate(); + } + return new Rational(numerator.divide(gcd), denominator.divide(gcd)); + } + + /** + * The rational {@code numerator / denominator}, reduced to lowest terms. + * + * @param numerator the numerator + * @param denominator the denominator, nonzero + * @return the rational + * @throws ArithmeticException if the denominator is zero + */ + public static Rational of(long numerator, long denominator) { + return of(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator)); + } + + /** + * The numerator in lowest terms. + * + * @return the numerator, signed + */ + public BigInteger numerator() { + return numerator; + } + + /** + * The denominator in lowest terms. + * + * @return the denominator, positive + */ + public BigInteger denominator() { + return denominator; + } + + /** + * Whether a {@code double} holds this rational exactly: it is a dyadic fraction within the + * {@code double} range and precision. + * + * @return {@code true} when {@link #doubleValue()} loses nothing + */ + public boolean isBinary64() { + double nearest = doubleValue(); + return !Double.isInfinite(nearest) && of(new BigDecimal(nearest)).equals(this); + } + + private static Rational of(BigDecimal exact) { + BigInteger unscaled = exact.unscaledValue(); + int scale = exact.scale(); + return scale >= 0 + ? of(unscaled, BigInteger.TEN.pow(scale)) + : of(unscaled.multiply(BigInteger.TEN.pow(-scale)), BigInteger.ONE); + } + + /** + * The nearest {@code double}, ties to even; infinite only past the finite range. + * + * @return the nearest double + */ + @Override + public double doubleValue() { + if (numerator.signum() == 0) { + return 0.0; + } + BigInteger a = numerator.abs(); + // 62 or 63 significant bits of the quotient, and a sticky bit for any remainder, round once. + int shift = 62 - (a.bitLength() - denominator.bitLength()); + BigInteger[] qr = + shift >= 0 + ? a.shiftLeft(shift).divideAndRemainder(denominator) + : a.divideAndRemainder(denominator.shiftLeft(-shift)); + BigInteger sticky = + qr[0].shiftLeft(1).add(qr[1].signum() == 0 ? BigInteger.ZERO : BigInteger.ONE); + int exponent = shift + 1; + BigDecimal dyadic = + exponent >= 0 + ? new BigDecimal(sticky.multiply(BigInteger.valueOf(5).pow(exponent)), exponent) + : new BigDecimal(sticky.shiftLeft(-exponent)); + double magnitude = Double.parseDouble(dyadic.toString()); + return numerator.signum() < 0 ? -magnitude : magnitude; + } + + @Override + public float floatValue() { + return (float) doubleValue(); + } + + /** The integer part, truncated toward zero, as {@link BigInteger#intValue} narrows it. */ + @Override + public int intValue() { + return numerator.divide(denominator).intValue(); + } + + /** The integer part, truncated toward zero, as {@link BigInteger#longValue} narrows it. */ + @Override + public long longValue() { + return numerator.divide(denominator).longValue(); + } + + @Override + public int compareTo(Rational other) { + return numerator.multiply(other.denominator).compareTo(other.numerator.multiply(denominator)); + } + + @Override + public boolean equals(Object other) { + return other instanceof Rational r + && numerator.equals(r.numerator) + && denominator.equals(r.denominator); + } + + @Override + public int hashCode() { + return Objects.hash(numerator, denominator); + } + + /** {@code numerator/denominator}, as the wire and the evaluator's {@code ToRational} spell it. */ + @Override + public String toString() { + return numerator + "/" + denominator; + } +} diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java index cf63da2831..ebc989e034 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java @@ -45,6 +45,28 @@ record BigIntegerValue(BigInteger value) implements Value { /** A {@code Real} value. */ record RealValue(double value) implements Value {} + /** + * An exact {@code Rational} no {@code double} holds, such as {@code 1/3}: one a {@code double} + * holds exactly is always a {@link RealValue}, never this, so two numbers are the same value + * exactly when their arms and values are. + * + * @param value the rational, not a {@code double} + */ + record RationalValue(Rational value) implements Value { + /** + * Creates an exact rational value. + * + * @param value the rational, never {@code null} + * @throws IllegalArgumentException if a {@code double} holds the rational exactly + */ + public RationalValue { + Objects.requireNonNull(value, "value"); + if (value.isBinary64()) { + throw new IllegalArgumentException(value + " is a double, which RealValue carries"); + } + } + } + /** * A {@code Complex} value in rectangular form: one number, never a sequence of two reals. * @@ -387,8 +409,8 @@ public Value get(long... index) { } /** - * A vector of numbers, each an {@link IntegerValue}, {@link BigIntegerValue} or {@link RealValue} - * as the model computed + * A vector of numbers, each an {@link IntegerValue}, {@link BigIntegerValue}, {@link + * RationalValue} or {@link RealValue} as the model computed * it: one value, never a sequence of numbers. * *

Only a service advertising the {@code structured_values} capability reports one as itself @@ -400,8 +422,8 @@ record VectorValue(List components) implements Value { /** * Creates a vector, copying the components. * - * @param components the components, each an {@link IntegerValue}, {@link BigIntegerValue} or - * {@link RealValue} + * @param components the components, each an {@link IntegerValue}, {@link BigIntegerValue}, + * {@link RationalValue} or {@link RealValue} * @throws IllegalArgumentException if a component is not a number */ public VectorValue { @@ -409,6 +431,7 @@ record VectorValue(List components) implements Value { for (Value component : components) { if (!(component instanceof IntegerValue) && !(component instanceof BigIntegerValue) + && !(component instanceof RationalValue) && !(component instanceof RealValue)) { throw new IllegalArgumentException( "vector component is not a number: " + component.getClass().getSimpleName()); @@ -713,6 +736,7 @@ default boolean sameValue(Value other) { } if (this instanceof IntegerValue || this instanceof BigIntegerValue + || this instanceof RationalValue || this instanceof RealValue || this instanceof ComplexValue) { return numbersEqual(this, other); @@ -779,8 +803,8 @@ private static boolean numbersEqual(Value a, Value b) { } /** - * A number's magnitude as a {@link Long}, {@link BigInteger} or {@link Double}; {@code null} off - * the real axis. + * A number's magnitude as a {@link Long}, {@link BigInteger}, {@link Rational} or {@link + * Double}; {@code null} off the real axis. */ private static Number onRealAxis(Value value) { if (value instanceof IntegerValue integer) { @@ -789,6 +813,9 @@ private static Number onRealAxis(Value value) { if (value instanceof BigIntegerValue integer) { return integer.value(); } + if (value instanceof RationalValue rational) { + return rational.value(); + } if (value instanceof RealValue real) { return real.value(); } @@ -799,6 +826,10 @@ private static Number onRealAxis(Value value) { } private static boolean magnitudesEqual(Number a, Number b) { + if (a instanceof Rational || b instanceof Rational) { + // A rational is never whole and never a double, so it equals only the same rational. + return a.equals(b); + } if (a instanceof BigInteger x) { return b instanceof BigInteger y ? x.equals(y) : b instanceof Double r && realIsBig(r, x); } @@ -873,23 +904,29 @@ private static double baseMagnitude(Quantity quantity) { } /** - * The base magnitude as an exact numerator/denominator while an integer scales by whole factors; - * empty otherwise. + * The base magnitude as an exact numerator/denominator while an integer or a rational scales by + * whole factors; empty otherwise. */ private static BigInteger[] exactBaseMagnitude(Quantity quantity) { Quantity.UnitTerm term = quantity.reduction().get(); - if (!quantity.isIntegral() + if (!quantity.isExact() || !isWhole(term.scaleNumerator()) || !isWhole(term.scaleDenominator())) { return new BigInteger[0]; } - BigInteger magnitude = - quantity.magnitude() instanceof BigInteger big - ? big - : BigInteger.valueOf(quantity.magnitude().longValue()); + BigInteger numerator; + BigInteger denominator = BigInteger.ONE; + if (quantity.magnitude() instanceof Rational rational) { + numerator = rational.numerator(); + denominator = rational.denominator(); + } else if (quantity.magnitude() instanceof BigInteger big) { + numerator = big; + } else { + numerator = BigInteger.valueOf(quantity.magnitude().longValue()); + } return new BigInteger[] { - magnitude.multiply(wholeOf(term.scaleNumerator())), - wholeOf(term.scaleDenominator()) + numerator.multiply(wholeOf(term.scaleNumerator())), + denominator.multiply(wholeOf(term.scaleDenominator())) }; } @@ -922,8 +959,8 @@ private static boolean sameQuantities(List a, List b) { } /** - * This value as a {@code double}, for the numeric arms; an integer beyond {@code long} is the - * nearest {@code double}, ties to even, and infinite only past the finite range. + * This value as a {@code double}, for the numeric arms; an integer beyond {@code long} or a + * rational is the nearest {@code double}, ties to even, and infinite only past the finite range. * * @return the magnitude of an integer, real or quantity value * @throws IllegalStateException if this value is not numeric @@ -935,6 +972,9 @@ default double asDouble() { if (this instanceof BigIntegerValue integer) { return integer.value().doubleValue(); } + if (this instanceof RationalValue rational) { + return rational.value().doubleValue(); + } if (this instanceof RealValue real) { return real.value(); } diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/internal/Protos.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/internal/Protos.java index bacf0c51b9..c1587dfded 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/internal/Protos.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/internal/Protos.java @@ -26,6 +26,7 @@ import org.openmbee.opensysml.MigrationReport; import org.openmbee.opensysml.Outcome; import org.openmbee.opensysml.Quantity; +import org.openmbee.opensysml.Rational; import org.openmbee.opensysml.Query; import org.openmbee.opensysml.QueryElement; import org.openmbee.opensysml.Referrer; @@ -109,6 +110,7 @@ public static Optional value(org.openmbee.opensysml.proto.Value value) { case INT_VALUE -> Optional.of(new Value.IntegerValue(value.getIntValue())); case BIG_INT_VALUE -> Optional.of(new Value.BigIntegerValue(bigInteger(value.getBigIntValue()))); + case RATIONAL_VALUE -> Optional.of(new Value.RationalValue(rational(value.getRationalValue()))); case REAL_VALUE -> Optional.of(new Value.RealValue(value.getRealValue())); case COMPLEX -> Optional.of( @@ -157,6 +159,54 @@ private static java.math.BigInteger bigInteger(String digits) { return integer; } + /** + * The rational a {@code rational_value} spells: canonical decimal terms in lowest terms over a + * positive denominator, and not a number a {@code double} holds, which {@code real_value} + * carries. + */ + private static Rational rational(org.openmbee.opensysml.proto.Rational wire) { + String spelled = wire.getNumerator() + "/" + wire.getDenominator(); + if (!canonicalDigits(wire.getNumerator(), true) + || !canonicalDigits(wire.getDenominator(), false) + || wire.getDenominator().equals("0")) { + throw new TransportException( + "the service answered a malformed rational: " + spelled + " is not decimal terms", null); + } + java.math.BigInteger numerator = new java.math.BigInteger(wire.getNumerator()); + java.math.BigInteger denominator = new java.math.BigInteger(wire.getDenominator()); + Rational rational = Rational.of(numerator, denominator); + if (!rational.numerator().equals(numerator) || !rational.denominator().equals(denominator)) { + throw new TransportException( + "the service answered a malformed rational: " + spelled + " is not in lowest terms", + null); + } + if (rational.isBinary64()) { + throw new TransportException( + "the service answered a malformed rational: " + spelled + " is a double, which" + + " real_value carries", + null); + } + return rational; + } + + // Decimal digits with no leading zero or '+', a '-' only where signed; zero is "0", not "-0". + private static boolean canonicalDigits(String digits, boolean signed) { + if (digits.equals("0")) { + return true; + } + String magnitude = signed && digits.startsWith("-") ? digits.substring(1) : digits; + return !magnitude.isEmpty() + && !magnitude.startsWith("0") + && magnitude.chars().allMatch(c -> c >= '0' && c <= '9'); + } + + private static org.openmbee.opensysml.proto.Rational proto(Rational rational) { + return org.openmbee.opensysml.proto.Rational.newBuilder() + .setNumerator(rational.numerator().toString()) + .setDenominator(rational.denominator().toString()) + .build(); + } + /** Only an asserted arm carries the unbounded value. */ private static Value infinity(org.openmbee.opensysml.proto.Value value) { if (!value.getInfinity()) { @@ -194,6 +244,7 @@ private static Value vector(org.openmbee.opensysml.proto.Vector vector) { case INT_VALUE -> new Value.IntegerValue(component.getIntValue()); case BIG_INT_VALUE -> new Value.BigIntegerValue(bigInteger(component.getBigIntValue())); + case RATIONAL_VALUE -> new Value.RationalValue(rational(component.getRationalValue())); case REAL_VALUE -> new Value.RealValue(component.getRealValue()); default -> throw new TransportException( @@ -311,6 +362,7 @@ public static Quantity quantity(org.openmbee.opensysml.proto.Quantity quantity) switch (quantity.getMagnitudeCase()) { case INT_MAGNITUDE -> Long.valueOf(quantity.getIntMagnitude()); case BIG_INT_MAGNITUDE -> bigInteger(quantity.getBigIntMagnitude()); + case RATIONAL_MAGNITUDE -> rational(quantity.getRationalMagnitude()); case REAL_MAGNITUDE -> Double.valueOf(quantity.getRealMagnitude()); case MAGNITUDE_NOT_SET -> throw new TransportException( @@ -499,6 +551,8 @@ public static org.openmbee.opensysml.proto.Value proto(Value value) { builder.setIntValue(integral.value()); } else if (value instanceof Value.BigIntegerValue integral) { builder.setBigIntValue(integral.value().toString()); + } else if (value instanceof Value.RationalValue rational) { + builder.setRationalValue(proto(rational.value())); } else if (value instanceof Value.RealValue real) { builder.setRealValue(real.value()); } else if (value instanceof Value.ComplexValue complex) { @@ -610,6 +664,8 @@ private static org.openmbee.opensysml.proto.Quantity proto(Quantity quantity) { builder.setIntMagnitude(integral); } else if (quantity.magnitude() instanceof java.math.BigInteger integral) { builder.setBigIntMagnitude(integral.toString()); + } else if (quantity.magnitude() instanceof Rational rational) { + builder.setRationalMagnitude(proto(rational)); } else { builder.setRealMagnitude(quantity.magnitude().doubleValue()); } @@ -1476,6 +1532,7 @@ public static DocumentValue documentValue(org.openmbee.opensysml.proto.DocumentV case INT_VALUE -> new DocumentValue.IntegerValue(value.getIntValue()); case BIG_INT_VALUE -> new DocumentValue.BigIntegerValue(bigInteger(value.getBigIntValue())); + case RATIONAL_VALUE -> new DocumentValue.RationalValue(rational(value.getRationalValue())); case REAL_VALUE -> new DocumentValue.RealValue(value.getRealValue()); case BOOL_VALUE -> new DocumentValue.BooleanValue(value.getBoolValue()); case INFINITY -> new DocumentValue.InfinityValue(); @@ -1573,6 +1630,21 @@ public static boolean holdsBigInt(org.openmbee.opensysml.proto.DocumentQueryBind || (value.hasQuantity() && value.getQuantity().hasBigIntMagnitude())); } + /** + * Whether a document-query binding sends an exact Rational no double holds, which a service + * reads only when it advertises {@code rational_values}. + * + * @param binding the wire binding + * @return whether a value or a quantity magnitude is a {@code rational_value} + */ + public static boolean holdsRational(org.openmbee.opensysml.proto.DocumentQueryBinding binding) { + return binding.getValuesList().stream() + .anyMatch( + value -> + value.hasRationalValue() + || (value.hasQuantity() && value.getQuantity().hasRationalMagnitude())); + } + /** * A document-query value as a request's binding carries it. * @@ -1602,6 +1674,8 @@ public static org.openmbee.opensysml.proto.DocumentValue proto(DocumentValue val builder.setIntValue(integer.value()); } else if (value instanceof DocumentValue.BigIntegerValue integer) { builder.setBigIntValue(integer.value().toString()); + } else if (value instanceof DocumentValue.RationalValue rational) { + builder.setRationalValue(proto(rational.value())); } else if (value instanceof DocumentValue.RealValue real) { builder.setRealValue(real.value()); } else if (value instanceof DocumentValue.BooleanValue flag) { diff --git a/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java b/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java new file mode 100644 index 0000000000..c238de771c --- /dev/null +++ b/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java @@ -0,0 +1,171 @@ +package org.openmbee.opensysml.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigInteger; +import java.util.List; +import org.openmbee.opensysml.DocumentValue; +import org.openmbee.opensysml.Quantity; +import org.openmbee.opensysml.Rational; +import org.openmbee.opensysml.TransportException; +import org.openmbee.opensysml.Value; +import org.openmbee.opensysml.proto.DocumentQueryBinding; +import org.openmbee.opensysml.proto.UnitFactor; +import org.openmbee.opensysml.proto.UnitTerm; +import org.openmbee.opensysml.proto.Vector; +import org.junit.jupiter.api.Test; + +class RationalProtosTest { + + private static org.openmbee.opensysml.proto.Rational wire(String numerator, String denominator) { + return org.openmbee.opensysml.proto.Rational.newBuilder() + .setNumerator(numerator) + .setDenominator(denominator) + .build(); + } + + @Test + void aRationalIsHeldInLowestTermsAndRoundedOnce() { + Rational third = Rational.of(2, -6); + assertEquals(BigInteger.valueOf(-1), third.numerator()); + assertEquals(BigInteger.valueOf(3), third.denominator()); + assertEquals("-1/3", third.toString()); + assertEquals(-1.0 / 3.0, third.doubleValue()); + assertEquals(0.1, Rational.of(1, 10).doubleValue()); + assertEquals(0.3, Rational.of(3, 10).doubleValue()); + assertFalse(Rational.of(1, 10).isBinary64()); + assertTrue(Rational.of(1, 4).isBinary64()); + assertTrue(Rational.of(6, 3).isBinary64()); + assertEquals(0, Rational.of(1, 3).compareTo(Rational.of(2, 6))); + assertTrue(Rational.of(1, 3).compareTo(Rational.of(1, 2)) < 0); + assertThrows(ArithmeticException.class, () -> Rational.of(1, 0)); + // A tie between two doubles rounds to even: 2^53 + 1 is halfway, 2^53 + 3 rounds up. + assertEquals(0x1p53, Rational.of(BigInteger.ONE.shiftLeft(53).add(BigInteger.ONE), + BigInteger.ONE).doubleValue()); + assertEquals(0x1p53 + 4, Rational.of(BigInteger.ONE.shiftLeft(53).add(BigInteger.valueOf(3)), + BigInteger.ONE).doubleValue()); + assertEquals(Double.MIN_VALUE, Rational.of(BigInteger.ONE, + BigInteger.ONE.shiftLeft(1074)).doubleValue()); + assertEquals(Double.POSITIVE_INFINITY, Rational.of(BigInteger.TEN.pow(400), + BigInteger.valueOf(3)).doubleValue()); + assertEquals(0, Rational.of(1, 3).longValue()); + assertEquals(-2, Rational.of(-7, 3).intValue()); + } + + @Test + void aRationalIsReadAndWrittenExactly() { + var proto = org.openmbee.opensysml.proto.Value.newBuilder().setRationalValue(wire("1", "3")).build(); + Value read = Protos.value(proto).orElseThrow(); + assertEquals(new Value.RationalValue(Rational.of(1, 3)), read); + assertEquals(proto, Protos.proto(read)); + assertEquals(1.0 / 3.0, read.asDouble()); + assertTrue(read.sameValue(new Value.RationalValue(Rational.of(2, 6)))); + assertFalse(read.sameValue(new Value.RealValue(1.0 / 3.0))); + assertFalse(read.sameValue(new Value.IntegerValue(0))); + var whole = + org.openmbee.opensysml.proto.Value.newBuilder() + .setRationalValue(wire(BigInteger.TEN.pow(400).toString(), "1")) + .build(); + assertEquals( + new Value.RationalValue(Rational.of(BigInteger.TEN.pow(400), BigInteger.ONE)), + Protos.value(whole).orElseThrow()); + assertThrows( + IllegalArgumentException.class, () -> new Value.RationalValue(Rational.of(1, 2))); + } + + @Test + void aRationalOnTheWireIsCanonical() { + List> malformed = + List.of( + List.of("2", "6"), + List.of("1", "-3"), + List.of("-1", "-3"), + List.of("1", "0"), + List.of("1", "2"), + List.of("3", "1"), + List.of("0", "1"), + List.of("-0", "3"), + List.of("01", "3"), + List.of("+1", "3"), + List.of("1.5", "7"), + List.of("", "3"), + List.of("1", "")); + for (List terms : malformed) { + assertThrows( + TransportException.class, + () -> + Protos.value( + org.openmbee.opensysml.proto.Value.newBuilder() + .setRationalValue(wire(terms.get(0), terms.get(1))) + .build()), + terms.toString()); + } + } + + @Test + void aRationalMagnitudeAndComponentCrossExactly() { + var wireQuantity = + org.openmbee.opensysml.proto.Quantity.newBuilder() + .setRationalMagnitude(wire("1", "3")) + .setUnit("km") + .setUnitTerm( + UnitTerm.newBuilder() + .setScaleNum(1000) + .setScaleDen(1) + .addFactors(UnitFactor.newBuilder().setUnitId("SI::m").setExponent(1))) + .build(); + Quantity third = Protos.quantity(wireQuantity); + assertEquals(Rational.of(1, 3), third.magnitude()); + assertTrue(third.isExact()); + assertFalse(third.isIntegral()); + var metres = + org.openmbee.opensysml.proto.Quantity.newBuilder() + .setRationalMagnitude(wire("1000", "3")) + .setUnit("m") + .setUnitTerm( + UnitTerm.newBuilder() + .setScaleNum(1) + .setScaleDen(1) + .addFactors(UnitFactor.newBuilder().setUnitId("SI::m").setExponent(1))) + .build(); + assertTrue( + new Value.QuantityValue(third).sameValue(new Value.QuantityValue(Protos.quantity(metres)))); + assertThrows( + IllegalArgumentException.class, + () -> new Quantity(Rational.of(1, 2), java.util.Optional.empty(), java.util.Optional.empty())); + + var vector = + org.openmbee.opensysml.proto.Value.newBuilder() + .setVector( + Vector.newBuilder() + .addComponents( + org.openmbee.opensysml.proto.Value.newBuilder().setIntValue(1)) + .addComponents( + org.openmbee.opensysml.proto.Value.newBuilder() + .setRationalValue(wire("2", "3")))) + .build(); + assertEquals( + new Value.VectorValue( + List.of(new Value.IntegerValue(1), new Value.RationalValue(Rational.of(2, 3)))), + Protos.value(vector).orElseThrow()); + } + + @Test + void aDocumentBindingHoldingARationalNeedsTheCapability() { + DocumentValue third = new DocumentValue.RationalValue(Rational.of(1, 3)); + var proto = Protos.proto(third); + assertEquals(wire("1", "3"), proto.getRationalValue()); + assertEquals(third, Protos.documentValue(proto)); + assertTrue( + Protos.holdsRational(DocumentQueryBinding.newBuilder().setParameter("x").addValues(proto).build())); + assertFalse( + Protos.holdsRational( + DocumentQueryBinding.newBuilder() + .setParameter("x") + .addValues(Protos.proto(new DocumentValue.RealValue(0.5))) + .build())); + } +} diff --git a/client/rust/opensysml/src/capabilities.rs b/client/rust/opensysml/src/capabilities.rs index 9e9efcc584..02e3959c17 100644 --- a/client/rust/opensysml/src/capabilities.rs +++ b/client/rust/opensysml/src/capabilities.rs @@ -116,6 +116,9 @@ pub const CAPABILITY_UNDETERMINED_VALUE: &str = "undetermined_value"; /// An Integer beyond int64 as `Value.big_int_value`, `Quantity.big_int_magnitude` and /// `DocumentValue.big_int_value`; a service without it reads one sent to it as null. pub const CAPABILITY_BIG_INT_VALUES: &str = "big_int_values"; +/// An exact Rational no f64 holds as `Value.rational_value`, `Quantity.rational_magnitude` and +/// `DocumentValue.rational_value`; a service without it reads one sent to it as null. +pub const CAPABILITY_RATIONAL_VALUES: &str = "rational_values"; /// The remedy for a service lacking `capability`, naming both routes to one that has it. pub fn upgrade_remedy(capability: &str) -> String { diff --git a/client/rust/opensysml/src/document.rs b/client/rust/opensysml/src/document.rs index f3c3f5eab9..fa080ebe76 100644 --- a/client/rust/opensysml/src/document.rs +++ b/client/rust/opensysml/src/document.rs @@ -2,9 +2,12 @@ use std::fmt; -use crate::domain::{quantity_from_wire, BigInteger, Quantity}; +use crate::domain::{ + quantity_from_wire, rational_from_wire, rational_to_wire, BigInteger, Quantity, +}; use crate::encode::quantity_to_wire; use crate::error::Error; +use crate::rational::Rational; use crate::wire; /// A model element a document query names: its qualified name and the standard's type. @@ -173,6 +176,8 @@ pub enum DocumentValue { Integer(i64), /// An Integer beyond `i64`. BigInteger(BigInteger), + /// An exact Rational no `f64` holds. + Rational(Rational), /// A real number. Real(f64), /// A boolean. @@ -197,6 +202,7 @@ impl fmt::Display for DocumentValue { Self::Text(v) => f.write_str(v), Self::Integer(v) => v.fmt(f), Self::BigInteger(v) => v.fmt(f), + Self::Rational(v) => v.fmt(f), Self::Real(v) => v.fmt(f), Self::Boolean(v) => v.fmt(f), Self::Quantity(v) => v.fmt(f), @@ -270,6 +276,19 @@ pub(crate) fn bindings_to_wire>( .collect() } +/// Whether a wire binding sends an exact Rational, which needs `rational_values`. +pub(crate) fn binding_holds_rational(binding: &wire::DocumentQueryBinding) -> bool { + use wire::document_value::Kind; + binding.values.iter().any(|value| match &value.kind { + Some(Kind::RationalValue(_)) => true, + Some(Kind::Quantity(quantity)) => matches!( + quantity.magnitude, + Some(wire::quantity::Magnitude::RationalMagnitude(_)) + ), + _ => false, + }) +} + /// Whether a wire binding sends an Integer beyond int64, which needs `big_int_values`. pub(crate) fn binding_holds_big_int(binding: &wire::DocumentQueryBinding) -> bool { use wire::document_value::Kind; @@ -305,6 +324,7 @@ fn bound_value(parameter: &str, value: &DocumentValue) -> Result Kind::StringValue(text.clone()), DocumentValue::Integer(v) => Kind::IntValue(*v), DocumentValue::BigInteger(v) => Kind::BigIntValue(v.as_str().to_owned()), + DocumentValue::Rational(v) => Kind::RationalValue(rational_to_wire(v)), DocumentValue::Real(v) => Kind::RealValue(*v), DocumentValue::Boolean(v) => Kind::BoolValue(*v), DocumentValue::Quantity(quantity) => { @@ -442,6 +462,7 @@ fn value_of(value: wire::DocumentValue) -> Result { Some(Kind::StringValue(v)) => DocumentValue::Text(v), Some(Kind::IntValue(v)) => DocumentValue::Integer(v), Some(Kind::BigIntValue(v)) => DocumentValue::BigInteger(BigInteger::parse(&v)?), + Some(Kind::RationalValue(v)) => DocumentValue::Rational(rational_from_wire(&v)?), Some(Kind::RealValue(v)) => DocumentValue::Real(v), Some(Kind::BoolValue(v)) => DocumentValue::Boolean(v), Some(Kind::Infinity(true)) => DocumentValue::Infinity, @@ -561,6 +582,35 @@ mod tests { assert!(!binding_holds_big_int(&narrow[0])); } + #[test] + fn an_exact_rational_crosses_as_rational_value() { + use wire::document_value::Kind; + let third = Rational::parse("1", "3").unwrap(); + let bound = + bindings_to_wire(&[("q", vec![DocumentValue::Rational(third.clone())])]).unwrap(); + assert!(matches!( + &bound[0].values[0].kind, + Some(Kind::RationalValue(r)) if r.numerator == "1" && r.denominator == "3" + )); + assert!(binding_holds_rational(&bound[0])); + assert!(!binding_holds_big_int(&bound[0])); + let read = DocumentValue::try_from(bound[0].values[0].clone()).unwrap(); + assert_eq!(read, DocumentValue::Rational(third.clone())); + assert_eq!(read.to_string(), "1/3"); + let magnitude = bindings_to_wire(&[( + "m", + vec![DocumentValue::Quantity(Quantity { + magnitude: Magnitude::Rational(third), + unit: String::new(), + unit_term: None, + })], + )]) + .unwrap(); + assert!(binding_holds_rational(&magnitude[0])); + let real = bindings_to_wire(&[("x", vec![DocumentValue::Real(0.5)])]).unwrap(); + assert!(!binding_holds_rational(&real[0])); + } + #[test] fn answers_are_refused_as_bindings() { for value in [ diff --git a/client/rust/opensysml/src/domain.rs b/client/rust/opensysml/src/domain.rs index 8c625f1d45..2a18e5bf25 100644 --- a/client/rust/opensysml/src/domain.rs +++ b/client/rust/opensysml/src/domain.rs @@ -3,6 +3,7 @@ use std::fmt; use std::path::{Path, PathBuf}; use std::sync::Arc; +use crate::rational::Rational; use crate::{error::Error, wire, Connection}; /// Language accepted by the parser. @@ -172,13 +173,15 @@ impl Diagnostic { } } -/// An integer or real quantity magnitude. +/// An integer, rational or real quantity magnitude. #[derive(Clone, Debug, PartialEq)] pub enum Magnitude { /// An exact integer magnitude within `i64`. Integer(i64), /// An exact integer magnitude beyond `i64`. BigInteger(BigInteger), + /// An exact rational magnitude no `f64` holds. + Rational(Rational), /// A floating-point magnitude. Real(f64), } @@ -269,6 +272,7 @@ impl fmt::Display for Magnitude { match self { Self::Integer(n) => n.fmt(f), Self::BigInteger(n) => n.fmt(f), + Self::Rational(q) => q.fmt(f), Self::Real(r) => r.fmt(f), } } @@ -655,6 +659,8 @@ pub enum Value { Integer(i64), /// Integer value beyond `i64`. BigInteger(BigInteger), + /// Exact Rational value no `f64` holds. + Rational(Rational), /// Real value. Real(f64), /// Complex value. @@ -728,9 +734,14 @@ impl Value { return self.is_absent() && other.is_absent(); } match (self, other) { - (Value::Integer(_) | Value::BigInteger(_) | Value::Real(_) | Value::Complex(_), _) => { - numbers_equal(self, other) - } + ( + Value::Integer(_) + | Value::BigInteger(_) + | Value::Rational(_) + | Value::Real(_) + | Value::Complex(_), + _, + ) => numbers_equal(self, other), (Value::Sequence(a), Value::Sequence(b)) => sequences_equal(a, b), (Value::Quantity(a), Value::Quantity(b)) => quantities_equal(a, b), (Value::Array(a), Value::Array(b)) => { @@ -791,6 +802,7 @@ fn numbers_equal(a: &Value, b: &Value) -> bool { Value::Complex(z) if z.imaginary == 0.0 => Some(Magnitude::Real(z.real)), Value::Integer(n) => Some(Magnitude::Integer(*n)), Value::BigInteger(n) => Some(Magnitude::BigInteger(n.clone())), + Value::Rational(q) => Some(Magnitude::Rational(q.clone())), Value::Real(r) => Some(Magnitude::Real(*r)), _ => None, }; @@ -811,6 +823,9 @@ fn magnitudes_equal(a: &Magnitude, b: &Magnitude) -> bool { | (Magnitude::Real(r), Magnitude::BigInteger(n)) => n.equals_real(*r), (Magnitude::Integer(_), Magnitude::BigInteger(_)) | (Magnitude::BigInteger(_), Magnitude::Integer(_)) => false, + // A rational is never whole and never an f64, so it is only the same rational. + (Magnitude::Rational(x), Magnitude::Rational(y)) => x == y, + (Magnitude::Rational(_), _) | (_, Magnitude::Rational(_)) => false, } } @@ -864,21 +879,24 @@ fn base_magnitude(magnitude: &Magnitude, term: &UnitTerm) -> f64 { let m = match magnitude { Magnitude::Integer(n) => *n as f64, Magnitude::BigInteger(n) => n.to_f64(), + Magnitude::Rational(q) => q.to_f64(), Magnitude::Real(r) => *r, }; m * term.scale_num / term.scale_den } // The base magnitude as an exact rational (numerator, denominator), while an -// integer magnitude scales by whole factors that fit. +// integer or rational magnitude scales by whole factors that fit. fn exact_base_magnitude(q: &Quantity) -> Option { - let Magnitude::Integer(n) = q.magnitude else { - return None; + let (n, d): (i128, i128) = match &q.magnitude { + Magnitude::Integer(n) => (i128::from(*n), 1), + Magnitude::Rational(r) => (r.numerator().parse().ok()?, r.denominator().parse().ok()?), + _ => return None, }; let term = q.unit_term.as_ref()?; let num = i128::from(whole(term.scale_num)?); let den = i128::from(whole(term.scale_den)?); - Some(ExactRational::new(i128::from(n).checked_mul(num)?, den)) + Some(ExactRational::new(n.checked_mul(num)?, d.checked_mul(den)?)) } fn whole(scale: f64) -> Option { @@ -930,6 +948,7 @@ pub(crate) fn value_from_wire(value: wire::Value) -> Result { match kind { wire::value::Kind::IntValue(v) => Ok(Value::Integer(v)), wire::value::Kind::BigIntValue(v) => Ok(Value::BigInteger(BigInteger::parse(&v)?)), + wire::value::Kind::RationalValue(v) => Ok(Value::Rational(rational_from_wire(&v)?)), wire::value::Kind::RealValue(v) => Ok(Value::Real(v)), wire::value::Kind::Complex(v) => Ok(Value::Complex(Complex { real: v.real, @@ -965,6 +984,9 @@ pub(crate) fn value_from_wire(value: wire::Value) -> Result { Some(wire::value::Kind::BigIntValue(value)) => { Ok(Magnitude::BigInteger(BigInteger::parse(&value)?)) } + Some(wire::value::Kind::RationalValue(value)) => { + Ok(Magnitude::Rational(rational_from_wire(&value)?)) + } Some(wire::value::Kind::RealValue(value)) => Ok(Magnitude::Real(value)), other => Err(Error::Decode(format!( "vector component is not a number: {}", @@ -1049,6 +1071,7 @@ fn kind_name(kind: &wire::value::Kind) -> &'static str { match kind { wire::value::Kind::IntValue(_) => "int_value", wire::value::Kind::BigIntValue(_) => "big_int_value", + wire::value::Kind::RationalValue(_) => "rational_value", wire::value::Kind::RealValue(_) => "real_value", wire::value::Kind::BoolValue(_) => "bool_value", wire::value::Kind::StringValue(_) => "string_value", @@ -1110,12 +1133,26 @@ fn unit_term_from_wire(term: wire::UnitTerm) -> UnitTerm { } } +pub(crate) fn rational_from_wire(v: &wire::Rational) -> Result { + Rational::parse(&v.numerator, &v.denominator) +} + +pub(crate) fn rational_to_wire(q: &Rational) -> wire::Rational { + wire::Rational { + numerator: q.numerator().to_owned(), + denominator: q.denominator().to_owned(), + } +} + pub(crate) fn quantity_from_wire(v: wire::Quantity) -> Result { let magnitude = match v.magnitude { Some(wire::quantity::Magnitude::IntMagnitude(value)) => Magnitude::Integer(value), Some(wire::quantity::Magnitude::BigIntMagnitude(value)) => { Magnitude::BigInteger(BigInteger::parse(&value)?) } + Some(wire::quantity::Magnitude::RationalMagnitude(value)) => { + Magnitude::Rational(rational_from_wire(&value)?) + } Some(wire::quantity::Magnitude::RealMagnitude(value)) => Magnitude::Real(value), None => return Err(Error::Decode("Quantity has no magnitude".to_owned())), }; diff --git a/client/rust/opensysml/src/encode.rs b/client/rust/opensysml/src/encode.rs index 637525aed9..0c62e6a35c 100644 --- a/client/rust/opensysml/src/encode.rs +++ b/client/rust/opensysml/src/encode.rs @@ -3,10 +3,10 @@ use crate::capabilities::{ upgrade_remedy, CAPABILITY_BIG_INT_VALUES, CAPABILITY_COMPLEX_VALUES, CAPABILITY_ENUM_VALUES, CAPABILITY_FUNCTION_VALUES, CAPABILITY_INFINITY_VALUE, CAPABILITY_MEASUREMENT_REFS, - CAPABILITY_METAOBJECT_VALUES, CAPABILITY_SET_VALUES, CAPABILITY_STRUCTURED_VALUES, - CAPABILITY_TENSOR_VALUES, + CAPABILITY_METAOBJECT_VALUES, CAPABILITY_RATIONAL_VALUES, CAPABILITY_SET_VALUES, + CAPABILITY_STRUCTURED_VALUES, CAPABILITY_TENSOR_VALUES, }; -use crate::domain::{Capabilities, Magnitude, Quantity, UnitTerm, Value}; +use crate::domain::{rational_to_wire, Capabilities, Magnitude, Quantity, UnitTerm, Value}; use crate::error::Error; use crate::wire; @@ -29,13 +29,17 @@ fn require(capabilities: &Capabilities, capability: &str) -> Result<(), Error> { capabilities.require(capability, upgrade_remedy(capability)) } -/// Refuse an Integer beyond int64 to a service that would read its arm as null. +/// Refuse an Integer beyond int64 or an exact Rational to a service that would read its arm as null. fn require_magnitudes<'a>( capabilities: &Capabilities, - mut magnitudes: impl Iterator, + magnitudes: impl Iterator, ) -> Result<(), Error> { - if magnitudes.any(|magnitude| matches!(magnitude, Magnitude::BigInteger(_))) { - require(capabilities, CAPABILITY_BIG_INT_VALUES)?; + for magnitude in magnitudes { + match magnitude { + Magnitude::BigInteger(_) => require(capabilities, CAPABILITY_BIG_INT_VALUES)?, + Magnitude::Rational(_) => require(capabilities, CAPABILITY_RATIONAL_VALUES)?, + Magnitude::Integer(_) | Magnitude::Real(_) => {} + } } Ok(()) } @@ -54,6 +58,10 @@ fn encode(value: &Value, capabilities: &Capabilities, depth: usize) -> Result { + require(capabilities, CAPABILITY_RATIONAL_VALUES)?; + kind(Kind::RationalValue(rational_to_wire(v))) + } Value::Real(v) => kind(Kind::RealValue(*v)), Value::Boolean(v) => kind(Kind::BoolValue(*v)), Value::Text(v) => kind(Kind::StringValue(v.clone())), @@ -112,6 +120,7 @@ fn encode(value: &Value, capabilities: &Capabilities, depth: usize) -> Result Kind::IntValue(*v), Magnitude::BigInteger(v) => Kind::BigIntValue(v.as_str().to_owned()), + Magnitude::Rational(v) => Kind::RationalValue(rational_to_wire(v)), Magnitude::Real(v) => Kind::RealValue(*v), }) }) @@ -263,6 +272,9 @@ pub(crate) fn quantity_to_wire(q: &Quantity) -> Result { Magnitude::BigInteger(v) => { wire::quantity::Magnitude::BigIntMagnitude(v.as_str().to_owned()) } + Magnitude::Rational(v) => { + wire::quantity::Magnitude::RationalMagnitude(rational_to_wire(v)) + } Magnitude::Real(v) => wire::quantity::Magnitude::RealMagnitude(*v), }), }) @@ -304,6 +316,7 @@ mod tests { CAPABILITY_METAOBJECT_VALUES, CAPABILITY_INFINITY_VALUE, CAPABILITY_BIG_INT_VALUES, + CAPABILITY_RATIONAL_VALUES, ]; fn capabilities(names: &[&str]) -> Capabilities { @@ -593,6 +606,52 @@ mod tests { } } + #[test] + fn an_exact_rational_needs_rational_values_bare_or_nested() { + let third = crate::Rational::parse("1", "3").unwrap(); + let quantity = metres(Magnitude::Rational(third.clone())); + let without: Vec<&str> = ALL + .iter() + .copied() + .filter(|name| *name != CAPABILITY_RATIONAL_VALUES) + .collect(); + for value in [ + Value::Rational(third.clone()), + Value::Sequence(vec![Value::Sequence(vec![Value::Rational(third.clone())])]), + Value::Quantity(quantity.clone()), + Value::Vector(Vector { + components: vec![Magnitude::Integer(1), Magnitude::Rational(third.clone())], + }), + Value::VectorQuantity(VectorQuantity::new(vec![quantity.clone()]).unwrap()), + ] { + match value_to_wire(&value, &capabilities(&without)) { + Err(Error::MissingCapability { capability, .. }) => { + assert_eq!(capability, CAPABILITY_RATIONAL_VALUES, "{value:?}") + } + other => panic!("{value:?}: expected rational_values, got {other:?}"), + } + let sent = value_to_wire(&value, &capabilities(ALL)).unwrap(); + assert!( + Value::try_from(sent).unwrap().same_value(&value), + "{value:?}" + ); + } + assert!(!Value::Rational(third.clone()).same_value(&Value::Real(third.to_f64()))); + let kilometres = Quantity { + magnitude: Magnitude::Rational(third), + unit: "km".to_owned(), + unit_term: Some(UnitTerm { + scale_num: 1000.0, + scale_den: 1.0, + factors: quantity.unit_term.clone().unwrap().factors, + }), + }; + let metres_value = metres(Magnitude::Rational( + crate::Rational::parse("1000", "3").unwrap(), + )); + assert!(Value::Quantity(kilometres).same_value(&Value::Quantity(metres_value))); + } + #[test] fn an_integer_beyond_int64_needs_big_int_values_bare_or_nested() { let wide = BigInteger::parse("1180591620717411303424").unwrap(); diff --git a/client/rust/opensysml/src/lib.rs b/client/rust/opensysml/src/lib.rs index cefde22bb2..db9e29e0f5 100644 --- a/client/rust/opensysml/src/lib.rs +++ b/client/rust/opensysml/src/lib.rs @@ -15,6 +15,7 @@ mod migration; mod model; mod operations; mod query; +mod rational; mod results; mod sources; mod typefacts; @@ -33,6 +34,7 @@ pub use domain::{ TensorQuantity, Undetermined, UnitFactor, UnitTerm, Value, Vector, VectorQuantity, }; pub use error::{Error, Status}; +pub use rational::Rational; pub use wire::FailureReason; pub use capabilities::*; diff --git a/client/rust/opensysml/src/operations.rs b/client/rust/opensysml/src/operations.rs index 2f29ef9c71..fa43c2300c 100644 --- a/client/rust/opensysml/src/operations.rs +++ b/client/rust/opensysml/src/operations.rs @@ -5,8 +5,8 @@ use std::collections::{BTreeMap, HashMap}; use crate::capabilities::*; use crate::conversion::{conversion_of, request_of, Conversion, ConvertOptions, ConvertSource}; use crate::document::{ - binding_holds_big_int, bindings_to_wire, result_of, DocumentForm, DocumentQueryResult, - DocumentValue, + binding_holds_big_int, binding_holds_rational, bindings_to_wire, result_of, DocumentForm, + DocumentQueryResult, DocumentValue, }; use crate::domain::{Model, Value}; use crate::encode::value_to_wire; @@ -366,6 +366,9 @@ impl Connection { if bindings.iter().any(binding_holds_big_int) { self.require_all(&[CAPABILITY_BIG_INT_VALUES])?; } + if bindings.iter().any(binding_holds_rational) { + self.require_all(&[CAPABILITY_RATIONAL_VALUES])?; + } let response = self.gated_rpc( "RunDocumentQuery", wire::RunDocumentQueryRequest { diff --git a/client/rust/opensysml/src/rational.rs b/client/rust/opensysml/src/rational.rs new file mode 100644 index 0000000000..0dc575d123 --- /dev/null +++ b/client/rust/opensysml/src/rational.rs @@ -0,0 +1,367 @@ +//! Exact KerML Rationals, as the wire's `rational_value` carries them. + +use std::cmp::Ordering; +use std::fmt; + +use crate::error::Error; + +/// An exact Rational no `f64` holds, such as `1/3` or `1/10`: a numerator over a +/// positive denominator in lowest terms, each held as its decimal digits. One an +/// `f64` holds exactly is always a `Real`, never this, so two numbers are equal +/// exactly when their arms and terms are. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Rational { + numerator: String, + denominator: String, +} + +impl Rational { + /// The rational `numerator/denominator` spells: decimal digits with no leading + /// zero, a `-` only on the numerator, in lowest terms, and not a number an + /// `f64` holds, which a `Real` carries. + pub fn parse(numerator: &str, denominator: &str) -> Result { + let malformed = |why: &str| { + Error::Decode(format!( + "not a canonical rational: {numerator}/{denominator} {why}" + )) + }; + let negative = numerator.starts_with('-'); + let (Some(n), Some(d)) = ( + Natural::parse(numerator.strip_prefix('-').unwrap_or(numerator)), + Natural::parse(denominator), + ) else { + return Err(malformed("is not decimal terms")); + }; + if d.is_zero() || (negative && n.is_zero()) { + return Err(malformed("has no value")); + } + if !Natural::gcd(&n, &d).is_one() { + return Err(malformed("is not in lowest terms")); + } + if binary64(&n, &d) { + return Err(malformed("is an f64, which real_value carries")); + } + Ok(Self { + numerator: numerator.to_owned(), + denominator: denominator.to_owned(), + }) + } + + /// The numerator's decimal digits, `-` signed. + pub fn numerator(&self) -> &str { + &self.numerator + } + + /// The denominator's decimal digits, positive. + pub fn denominator(&self) -> &str { + &self.denominator + } + + /// The nearest `f64`, ties to even; infinite only beyond the finite range. + pub fn to_f64(&self) -> f64 { + let digits = self.numerator.strip_prefix('-').unwrap_or(&self.numerator); + let magnitude = match (Natural::parse(digits), Natural::parse(&self.denominator)) { + (Some(n), Some(d)) => nearest(&n, &d), + _ => f64::NAN, + }; + if self.numerator.starts_with('-') { + -magnitude + } else { + magnitude + } + } +} + +impl fmt::Display for Rational { + /// Writes `numerator/denominator`, as the evaluator spells a non-terminating Rational. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}", self.numerator, self.denominator) + } +} + +// Whether the positive n/d, in lowest terms, is a finite f64: a dyadic fraction of +// at most 53 significant bits whose least bit is no finer than 2^-1074. +fn binary64(n: &Natural, d: &Natural) -> bool { + let shift = d.trailing_zeros(); + if d.bit_len() != shift + 1 { + return false; + } + let zeros = n.trailing_zeros(); + let significant = n.bit_len() - zeros.min(n.bit_len()); + let least = zeros as i64 - shift as i64; + let top = n.bit_len() as i64 - 1 - shift as i64; + n.is_zero() || (significant <= 53 && least >= -1074 && top <= 1023) +} + +// n/d, both positive, rounded once to the nearest f64. +fn nearest(n: &Natural, d: &Natural) -> f64 { + if n.is_zero() { + return 0.0; + } + // A quotient of 63 or 64 bits and a sticky remainder hold enough to round once. + let scale = 63 - (n.bit_len() as i64 - d.bit_len() as i64); + let (q, exact) = if scale >= 0 { + n.shl(scale as usize).div_u64(d) + } else { + n.div_u64(&d.shl((-scale) as usize)) + }; + let q = u128::from(q); + let width = 128 - q.leading_zeros() as i64; + let top = width - 1 - scale; + if top > 1023 { + return f64::INFINITY; + } + let precision = if top >= -1022 { 53 } else { 53 - (-1022 - top) }; + let drop = width - precision; + if drop > 127 { + return 0.0; + } + let drop = drop as u32; + let mut kept = q >> drop; + let rest = q & ((1u128 << drop) - 1); + let half = 1u128 << (drop - 1); + if rest > half || (rest == half && (!exact || kept & 1 == 1)) { + kept += 1; + } + let least = drop as i32 - scale as i32; + let mut x = kept as f64; + if least < -1022 { + x *= pow2(least + 1022); + x * pow2(-1022) + } else { + x * pow2(least) + } +} + +// 2^e for a normal exponent e. +fn pow2(e: i32) -> f64 { + f64::from_bits(((e + 1023) as u64) << 52) +} + +// An unsigned integer as base-2^32 limbs, least significant first, with no high zero limb. +#[derive(Clone, Debug, PartialEq, Eq)] +struct Natural(Vec); + +impl Natural { + fn parse(digits: &str) -> Option { + if digits.is_empty() + || !digits.bytes().all(|b| b.is_ascii_digit()) + || (digits.len() > 1 && digits.starts_with('0')) + { + return None; + } + let mut limbs: Vec = Vec::new(); + for b in digits.bytes() { + let mut carry = u64::from(b - b'0'); + for limb in &mut limbs { + let wide = u64::from(*limb) * 10 + carry; + *limb = wide as u32; + carry = wide >> 32; + } + if carry != 0 { + limbs.push(carry as u32); + } + } + Some(Self(limbs)) + } + + fn is_zero(&self) -> bool { + self.0.is_empty() + } + + fn is_one(&self) -> bool { + self.0 == [1] + } + + fn bit_len(&self) -> usize { + self.0 + .last() + .map_or(0, |top| self.0.len() * 32 - top.leading_zeros() as usize) + } + + fn trailing_zeros(&self) -> usize { + self.0 + .iter() + .position(|limb| *limb != 0) + .map_or(0, |i| i * 32 + self.0[i].trailing_zeros() as usize) + } + + fn trimmed(mut limbs: Vec) -> Self { + while limbs.last() == Some(&0) { + limbs.pop(); + } + Self(limbs) + } + + fn shl(&self, bits: usize) -> Self { + let (words, bits) = (bits / 32, bits % 32); + let mut limbs = vec![0u32; words]; + let mut carry = 0u32; + for limb in &self.0 { + limbs.push(if bits == 0 { + *limb + } else { + (limb << bits) | carry + }); + carry = if bits == 0 { 0 } else { limb >> (32 - bits) }; + } + limbs.push(carry); + Self::trimmed(limbs) + } + + fn shr(&self, bits: usize) -> Self { + let (words, bits) = (bits / 32, bits % 32); + if words >= self.0.len() { + return Self(Vec::new()); + } + let rest = &self.0[words..]; + let limbs = (0..rest.len()) + .map(|i| { + let high = rest.get(i + 1).copied().unwrap_or(0); + if bits == 0 { + rest[i] + } else { + (rest[i] >> bits) | (high << (32 - bits)) + } + }) + .collect(); + Self::trimmed(limbs) + } + + fn cmp(&self, other: &Self) -> Ordering { + self.0 + .len() + .cmp(&other.0.len()) + .then_with(|| self.0.iter().rev().cmp(other.0.iter().rev())) + } + + // self - other, for self >= other. + fn sub(&self, other: &Self) -> Self { + let mut borrow = 0i64; + let limbs = self + .0 + .iter() + .enumerate() + .map(|(i, limb)| { + let mut wide = + i64::from(*limb) - i64::from(other.0.get(i).copied().unwrap_or(0)) - borrow; + borrow = i64::from(wide < 0); + if wide < 0 { + wide += 1 << 32; + } + wide as u32 + }) + .collect(); + Self::trimmed(limbs) + } + + // The quotient self/d, known to fit 64 bits, and whether it is exact. + fn div_u64(&self, d: &Self) -> (u64, bool) { + let mut rest = self.clone(); + let mut q = 0u64; + for bit in (0..64).rev() { + let shifted = d.shl(bit); + if rest.cmp(&shifted) != Ordering::Less { + rest = rest.sub(&shifted); + q |= 1 << bit; + } + } + (q, rest.is_zero()) + } + + // Stein's binary gcd, which needs only shifts and subtraction. + fn gcd(a: &Self, b: &Self) -> Self { + if a.is_zero() { + return b.clone(); + } + if b.is_zero() { + return a.clone(); + } + let common = a.trailing_zeros().min(b.trailing_zeros()); + let mut a = a.shr(a.trailing_zeros()); + let mut b = b.shr(b.trailing_zeros()); + while !b.is_zero() { + if a.cmp(&b) == Ordering::Greater { + std::mem::swap(&mut a, &mut b); + } + b = b.sub(&a); + b = b.shr(b.trailing_zeros()); + } + a.shl(common) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn q(n: &str, d: &str) -> Rational { + Rational::parse(n, d).unwrap() + } + + #[test] + fn a_rational_rounds_once_to_the_nearest_f64() { + assert_eq!(q("1", "3").to_f64(), 1.0 / 3.0); + assert_eq!(q("-1", "10").to_f64(), -0.1); + assert_eq!(q("3", "10").to_f64(), 0.3); + assert_eq!(q("2", "3").to_f64(), 2.0 / 3.0); + let googol = format!("1{}", "0".repeat(100)); + assert_eq!(q(&googol, "3").to_f64(), 1e100 / 3.0); + assert_eq!(q("1", &googol).to_f64(), 1e-100); + assert_eq!( + q(&format!("1{}", "0".repeat(400)), "3").to_f64(), + f64::INFINITY + ); + // Below the least subnormal's half, and just above it. + let tiny = format!("1{}", "0".repeat(330)); + assert_eq!(q("1", &tiny).to_f64(), 0.0); + assert_eq!( + q("3", &format!("1{}", "0".repeat(324))).to_f64(), + f64::from_bits(1) + ); + // 2^53 + 1 is halfway between two f64s and rounds to even. + assert_eq!(q("9007199254740993", "1").to_f64(), 9007199254740992.0); + assert_eq!(q("9007199254740995", "1").to_f64(), 9007199254740996.0); + } + + #[test] + fn only_a_canonical_rational_no_f64_holds_parses() { + for (n, d) in [ + ("2", "6"), + ("1", "0"), + ("1", "-3"), + ("-0", "3"), + ("01", "3"), + ("+1", "3"), + ("1.5", "7"), + ("", "3"), + ("1", "2"), + ("3", "1"), + ("0", "1"), + ("-5", "4"), + ("1", "4503599627370496"), + ] { + assert!(Rational::parse(n, d).is_err(), "{n}/{d}"); + } + let r = q("-1", "3"); + assert_eq!((r.numerator(), r.denominator()), ("-1", "3")); + assert_eq!(r.to_string(), "-1/3"); + assert!(Rational::parse("9007199254740993", "1").is_ok()); + assert!(Rational::parse("9007199254740993", "2").is_ok()); + assert!(Rational::parse(&format!("1{}", "0".repeat(400)), "1").is_ok()); + } + + #[test] + fn natural_arithmetic_is_exact() { + let n = Natural::parse("340282366920938463463374607431768211456").unwrap(); + assert_eq!(n.bit_len(), 129); + assert_eq!(n.trailing_zeros(), 128); + let a = Natural::parse("123456789012345678901234567890").unwrap(); + let b = Natural::parse("987654321098765432109876543210").unwrap(); + assert_eq!( + Natural::gcd(&a, &b), + Natural::parse("9000000000900000000090").unwrap() + ); + assert_eq!(a.shl(37).shr(37), a); + } +} From 6f31120cfce70feba3d8a2b9811f04f07c286c4b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:05:08 +0000 Subject: [PATCH 07/26] feat(client): carry exact Rationals in the Julia and MATLAB clients Co-Authored-By: jason.han --- client/julia/OpenSysML/src/OpenSysML.jl | 3 +- client/julia/OpenSysML/src/capabilities.jl | 1 + client/julia/OpenSysML/src/document.jl | 10 ++++ client/julia/OpenSysML/src/values.jl | 46 +++++++++++++++++-- client/julia/OpenSysML/test/runtests.jl | 20 +++++++- .../+internal/decodeDocumentValue.m | 4 +- .../matlab/+opensysml/+internal/rationalOf.m | 26 +++++++++++ .../matlab/+opensysml/+internal/valueArms.m | 4 +- .../matlab/+opensysml/buildDocumentBindings.m | 3 ++ client/matlab/+opensysml/capabilities.m | 3 +- client/matlab/+opensysml/decodeValue.m | 7 ++- client/matlab/+opensysml/encodeValue.m | 19 ++++++++ client/matlab/+opensysml/runDocumentQuery.m | 10 ++++ client/matlab/README.md | 3 +- client/matlab/tests/test_decode_value.m | 14 +++++- client/matlab/tests/test_encode_value.m | 7 +++ client/matlab/tests/test_surface_offline.m | 23 ++++++++++ 17 files changed, 189 insertions(+), 14 deletions(-) create mode 100644 client/matlab/+opensysml/+internal/rationalOf.m diff --git a/client/julia/OpenSysML/src/OpenSysML.jl b/client/julia/OpenSysML/src/OpenSysML.jl index 77af37882b..47316971b8 100644 --- a/client/julia/OpenSysML/src/OpenSysML.jl +++ b/client/julia/OpenSysML/src/OpenSysML.jl @@ -115,7 +115,8 @@ for capability in ( :CAPABILITY_VERIFICATION_VERDICTS, :CAPABILITY_INFINITY_VALUE, :CAPABILITY_DIAGNOSTIC_CODES, :CAPABILITY_SCHEDULE, :CAPABILITY_CASE_EVALUATIONS, :CAPABILITY_SCHEDULE_EXPLORE, :CAPABILITY_PERFORMER, :CAPABILITY_FINAL_TIME, - :CAPABILITY_ENGINES, :CAPABILITY_UNDETERMINED_VALUE, :CAPABILITY_BIG_INT_VALUES + :CAPABILITY_ENGINES, :CAPABILITY_UNDETERMINED_VALUE, :CAPABILITY_BIG_INT_VALUES, + :CAPABILITY_RATIONAL_VALUES ) @eval export $capability end diff --git a/client/julia/OpenSysML/src/capabilities.jl b/client/julia/OpenSysML/src/capabilities.jl index dcc87a5f30..4e80de3fe3 100644 --- a/client/julia/OpenSysML/src/capabilities.jl +++ b/client/julia/OpenSysML/src/capabilities.jl @@ -53,6 +53,7 @@ const CAPABILITY_FINAL_TIME = "final_time" const CAPABILITY_ENGINES = "engines" const CAPABILITY_UNDETERMINED_VALUE = "undetermined_value" const CAPABILITY_BIG_INT_VALUES = "big_int_values" +const CAPABILITY_RATIONAL_VALUES = "rational_values" """The version and advertised capabilities reported by a sysml-grpc service.""" struct ServerInfo diff --git a/client/julia/OpenSysML/src/document.jl b/client/julia/OpenSysML/src/document.jl index 5e0c5517e2..66c0341b14 100644 --- a/client/julia/OpenSysML/src/document.jl +++ b/client/julia/OpenSysML/src/document.jl @@ -91,6 +91,7 @@ function _document_value(raw) haskey(raw, "stringValue") && return String(raw["stringValue"]) haskey(raw, "intValue") && return parse(Int64, string(raw["intValue"])) haskey(raw, "bigIntValue") && return parse_big_integer(string(raw["bigIntValue"])) + haskey(raw, "rationalValue") && return parse_rational(raw["rationalValue"]) haskey(raw, "realValue") && return asreal(raw["realValue"]) haskey(raw, "boolValue") && return Bool(raw["boolValue"]) haskey(raw, "infinity") && return Infinity() @@ -180,6 +181,8 @@ function build_document_bindings(bindings=Dict()) Dict{String,Any}("stringValue" => String(value)) elseif value isa Integer Dict{String,Any}(integer_arm(value, "intValue", "bigIntValue")) + elseif value isa Rational + Dict{String,Any}(rational_arm(value, "realValue", "rationalValue")) elseif value isa AbstractFloat Dict{String,Any}("realValue" => Float64(value)) elseif value isa Quantity @@ -201,6 +204,12 @@ function build_document_bindings(bindings=Dict()) result end +# Whether a wire binding sends an exact Rational no Float64 holds, which needs rational_values. +_binding_holds_rational(binding) = any(binding["values"]) do value + haskey(value, "rationalValue") || + (haskey(value, "quantity") && haskey(value["quantity"], "rationalMagnitude")) +end + # Whether a wire binding sends an Integer beyond int64, which needs big_int_values. _binding_holds_big_int(binding) = any(binding["values"]) do value haskey(value, "bigIntValue") || @@ -213,6 +222,7 @@ function run_document_query(model::Model, query_id::AbstractString; bindings=Dic require_capability(conn, CAPABILITY_DOCUMENT_QUERY) wire = build_document_bindings(bindings) any(_binding_holds_big_int, wire) && require_capability(conn, CAPABILITY_BIG_INT_VALUES) + any(_binding_holds_rational, wire) && require_capability(conn, CAPABILITY_RATIONAL_VALUES) request = Dict{String,Any}("modelHash" => model.hash, "queryId" => String(query_id), "bindings" => wire) answer = _translate(; not_found=SymbolNotFoundError, diff --git a/client/julia/OpenSysML/src/values.jl b/client/julia/OpenSysML/src/values.jl index 87ccf6b284..a4e359055d 100644 --- a/client/julia/OpenSysML/src/values.jl +++ b/client/julia/OpenSysML/src/values.jl @@ -81,7 +81,7 @@ Base.string(unit::Unit) = isempty(unit.text) ? reduction(unit) : unit.text """A numeric magnitude associated with a named unit and optional reduction.""" struct Quantity - magnitude::Union{Int64,BigInt,Float64} + magnitude::Union{Int64,BigInt,Rational{BigInt},Float64} unit::String unit_term::Union{Nothing,Unit} end @@ -90,6 +90,9 @@ Quantity(magnitude::Bool, unit::AbstractString, term=nothing) = Quantity(magnitude::Integer, unit::AbstractString, term=nothing) = Quantity(Int64(magnitude), String(unit), term === nothing ? nothing : term isa Unit ? term : term isa AbstractDict ? Unit(unit, term) : throw(ArgumentError("invalid unit term"))) +Quantity(magnitude::Rational, unit::AbstractString, term=nothing) = + Quantity(_exact_rational(magnitude), String(unit), term === nothing ? nothing : + term isa Unit ? term : term isa AbstractDict ? Unit(unit, term) : throw(ArgumentError("invalid unit term"))) Quantity(magnitude::AbstractFloat, unit::AbstractString, term=nothing) = Quantity(Float64(magnitude), String(unit), term === nothing ? nothing : term isa Unit ? term : term isa AbstractDict ? Unit(unit, term) : throw(ArgumentError("invalid unit term"))) @@ -274,15 +277,14 @@ Base.:*(n::Real, q::Quantity) = q * n Base.:/(q::Quantity, n::Real) = Quantity(q.magnitude / n, q.unit, q.unit_term) function _exact_base_magnitude(q::Quantity, unit::Unit) magnitude = q.magnitude - magnitude isa Integer && !(magnitude isa Bool) || + magnitude isa Union{Integer,Rational} && !(magnitude isa Bool) || return nothing isfinite(unit.scale_num) && isinteger(unit.scale_num) || return nothing isfinite(unit.scale_den) && isinteger(unit.scale_den) || return nothing (unit.scale_num == 0 || unit.scale_den == 0) && return nothing - Rational{BigInt}(BigInt(magnitude) * BigInt(round(unit.scale_num)), - BigInt(round(unit.scale_den))) + Rational{BigInt}(magnitude) * BigInt(round(unit.scale_num)) // BigInt(round(unit.scale_den)) end function Base.:(==)(left::Quantity, right::Quantity) a, b = _unit(left), _unit(right) @@ -335,7 +337,7 @@ function asreal(x) end const VALUE_ARMS = Set([ - "intValue", "bigIntValue", "realValue", "boolValue", "stringValue", "instanceId", "sequence", + "intValue", "bigIntValue", "rationalValue", "realValue", "boolValue", "stringValue", "instanceId", "sequence", "null", "unset", "quantity", "enumLiteral", "complex", "array", "vector", "vectorQuantity", "measurementRef", "infinity", "function", "set", "tensorQuantity", "metaobject", "undetermined", @@ -359,6 +361,32 @@ function parse_big_integer(digits::AbstractString) return n end +# The exact Rational a rationalValue spells: canonical decimal terms in lowest +# terms over a positive denominator, of a value no Float64 holds. +function parse_rational(terms) + terms isa AbstractDict || error("not a rational: $(repr(terms))") + numerator, denominator = string(get(terms, "numerator", "")), string(get(terms, "denominator", "")) + occursin(r"^(0|-?[1-9][0-9]*)$", numerator) && occursin(r"^[1-9][0-9]*$", denominator) || + error("not the decimal terms of a rational: $(repr(numerator))/$(repr(denominator))") + n, d = parse(BigInt, numerator), parse(BigInt, denominator) + gcd(n, d) == 1 || error("$numerator/$denominator is not in lowest terms") + q = n // d + _binary64(q) && error("$numerator/$denominator is a Float64, which realValue carries") + return q +end + +# Whether a Float64 holds the rational exactly. +_binary64(q::Rational) = (x = Float64(q); isfinite(x) && Rational{BigInt}(x) == q) + +# A Rational as KerML holds it: lowest terms over BigInt. +_exact_rational(q::Rational) = Rational{BigInt}(q) + +# A Rational on the wire: realValue when a Float64 holds it exactly, rationalValue otherwise. +rational_arm(q::Rational, real::String, rational::String) = + _binary64(q) ? (real => _json_real(Float64(q))) : + (rational => Dict{String,Any}("numerator" => string(numerator(q)), + "denominator" => string(denominator(q)))) + # An Integer on the wire: intValue within Int64, bigIntValue beyond it. integer_arm(x::Integer, small::String, big::String) = typemin(Int64) <= x <= typemax(Int64) ? (small => string(Int64(x))) : (big => string(BigInt(x))) @@ -368,6 +396,7 @@ _unsupported_value(message) = throw(UnsupportedValueError(String(message))) function decode_quantity(q) magnitude = haskey(q, "intMagnitude") ? parse(Int64, q["intMagnitude"]) : haskey(q, "bigIntMagnitude") ? parse_big_integer(q["bigIntMagnitude"]) : + haskey(q, "rationalMagnitude") ? parse_rational(q["rationalMagnitude"]) : haskey(q, "realMagnitude") ? asreal(q["realMagnitude"]) : _unsupported_value("quantity carries neither intMagnitude nor realMagnitude") term = get(q, "unitTerm", nothing) @@ -383,6 +412,7 @@ function decode_value(v) v === nothing && return missing haskey(v, "intValue") && return parse(Int64, v["intValue"]) haskey(v, "bigIntValue") && return parse_big_integer(v["bigIntValue"]) + haskey(v, "rationalValue") && return parse_rational(v["rationalValue"]) haskey(v, "realValue") && return asreal(v["realValue"]) haskey(v, "boolValue") && return v["boolValue"]::Bool haskey(v, "stringValue") && return v["stringValue"]::String @@ -414,6 +444,7 @@ function decode_value(v) components = map(get(v["vector"], "components", Any[])) do c haskey(c, "intValue") && return parse(Int64, c["intValue"]) haskey(c, "bigIntValue") && return parse_big_integer(c["bigIntValue"]) + haskey(c, "rationalValue") && return parse_rational(c["rationalValue"]) haskey(c, "realValue") && return asreal(c["realValue"]) _unsupported_value("vector component is not an intValue or realValue: $(first(keys(c)))") end @@ -487,6 +518,8 @@ function encode_quantity(q::Quantity) body = Dict{String,Any}() if q.magnitude isa Integer push!(body, integer_arm(q.magnitude, "intMagnitude", "bigIntMagnitude")) + elseif q.magnitude isa Rational + push!(body, rational_arm(q.magnitude, "realMagnitude", "rationalMagnitude")) else body["realMagnitude"] = _json_real(Float64(q.magnitude)) end @@ -506,6 +539,7 @@ function encode_value(x::Bool) Dict{String,Any}("boolValue" => x) end encode_value(x::Integer) = Dict{String,Any}(integer_arm(x, "intValue", "bigIntValue")) +encode_value(x::Rational) = Dict{String,Any}(rational_arm(x, "realValue", "rationalValue")) encode_value(x::AbstractFloat) = Dict{String,Any}("realValue" => _json_real(x)) encode_value(x::AbstractString) = Dict{String,Any}("stringValue" => String(x)) encode_value(::Nothing) = Dict{String,Any}("null" => "") @@ -636,6 +670,8 @@ function value_capabilities(value) function visit(item) if item isa Integer && !(item isa Bool) typemin(Int64) <= item <= typemax(Int64) || push!(capabilities, CAPABILITY_BIG_INT_VALUES) + elseif item isa Rational + _binary64(item) || push!(capabilities, CAPABILITY_RATIONAL_VALUES) elseif item isa Quantity visit(item.magnitude) elseif item isa Complex diff --git a/client/julia/OpenSysML/test/runtests.jl b/client/julia/OpenSysML/test/runtests.jl index 6870f3044c..49342a0004 100644 --- a/client/julia/OpenSysML/test/runtests.jl +++ b/client/julia/OpenSysML/test/runtests.jl @@ -9,7 +9,7 @@ include(joinpath(@__DIR__, "..", "conformance", "compare.jl")) const FIXTURES = normpath(joinpath(@__DIR__, "..", "..", "..", "..", "conformance", "fixtures")) -@testset "decode_value: the twenty-two arms" begin +@testset "decode_value: the twenty-three arms" begin @test decode_value(nothing) === missing @test decode_value(JSON.parse("""{"intValue":"9007199254740993"}""")) === Int64(9007199254740993) @test decode_value(JSON.parse("""{"intValue":"-9223372036854775808"}""")) === typemin(Int64) @@ -20,6 +20,24 @@ const FIXTURES = normpath(joinpath(@__DIR__, "..", "..", "..", "..", "conformanc end @test decode_value(JSON.parse("""{"quantity":{"bigIntMagnitude":"9223372036854775808","unit":"kg"}}""")).magnitude == big(2)^63 @test encode_value(big(2)^70) == Dict("bigIntValue" => "1180591620717411303424") + third = decode_value(JSON.parse("""{"rationalValue":{"numerator":"-1","denominator":"3"}}""")) + @test third isa Rational{BigInt} && third == -1//3 + @test encode_value(third) == Dict("rationalValue" => Dict("numerator" => "-1", "denominator" => "3")) + @test encode_value(1//4) == Dict("realValue" => 0.25) + @test encode_value(big(10)^400 // 1)["rationalValue"]["denominator"] == "1" + for (n, d) in (("2", "6"), ("1", "-3"), ("1", "0"), ("1", "2"), ("3", "1"), ("0", "1"), + ("-0", "3"), ("01", "3"), ("+1", "3"), ("1.5", "7"), ("", "3")) + @test_throws ErrorException decode_value(Dict("rationalValue" => Dict("numerator" => n, "denominator" => d))) + end + km = decode_value(JSON.parse("""{"quantity":{"rationalMagnitude":{"numerator":"1","denominator":"3"},"unit":"km","unitTerm":{"scaleNum":1000,"scaleDen":1,"factors":[{"unitId":"SI::m","exponent":1}]}}}""")) + @test km.magnitude == 1//3 + m = Quantity(1000//3, "m", Dict("scaleNum" => 1, "scaleDen" => 1, "factors" => Any[Dict("unitId" => "SI::m", "exponent" => 1)])) + @test same_value(km, m) + @test encode_value(km)["quantity"]["rationalMagnitude"] == Dict("numerator" => "1", "denominator" => "3") + for value in (1//3, Any[Any[1//10]], Set([1//3]), km, VectorQuantity([km])) + @test CAPABILITY_RATIONAL_VALUES in value_capabilities(value) + end + @test !(CAPABILITY_RATIONAL_VALUES in value_capabilities(1//2)) @test encode_value(big(7)) == Dict("intValue" => "7") @test encode_value(Quantity(big(2)^63, "kg", nothing)) == Dict("quantity" => Dict("bigIntMagnitude" => "9223372036854775808", "unit" => "kg")) wide = Quantity(big(2)^63, "kg", nothing) diff --git a/client/matlab/+opensysml/+internal/decodeDocumentValue.m b/client/matlab/+opensysml/+internal/decodeDocumentValue.m index ed0b53bf60..953195e170 100644 --- a/client/matlab/+opensysml/+internal/decodeDocumentValue.m +++ b/client/matlab/+opensysml/+internal/decodeDocumentValue.m @@ -1,7 +1,7 @@ function value = decodeDocumentValue(raw) %DECODEDOCUMENTVALUE Decode one answered DocumentValue. - arms = {'elementId','stringValue','intValue','bigIntValue','realValue','boolValue', ... + arms = {'elementId','stringValue','intValue','bigIntValue','rationalValue','realValue','boolValue', ... 'infinity','quantity','object','verdict','state','event'}; arm = ''; for i = 1:numel(arms) @@ -22,6 +22,8 @@ value = opensysml.parseInt64(valueString(raw.intValue)); case 'bigIntValue' value = opensysml.decodeValue(struct('bigIntValue', raw.bigIntValue)); + case 'rationalValue' + value = opensysml.decodeValue(struct('rationalValue', raw.rationalValue)); case 'realValue' value = realValue(raw.realValue); case 'boolValue' diff --git a/client/matlab/+opensysml/+internal/rationalOf.m b/client/matlab/+opensysml/+internal/rationalOf.m new file mode 100644 index 0000000000..ba7c16655c --- /dev/null +++ b/client/matlab/+opensysml/+internal/rationalOf.m @@ -0,0 +1,26 @@ +function r = rationalOf(raw) +%RATIONALOF The exact Rational a rationalValue spells, kept as its decimal terms. +% Terms are canonical: lowest terms over a positive denominator, of a value no +% double holds. Lowest terms and double exactness are checked where both terms +% are exact doubles; larger terms are taken as the service sent them. + + if ~isstruct(raw) || ~isscalar(raw) || ~isfield(raw, 'numerator') || ~isfield(raw, 'denominator') + error('opensysml:decode', 'a rational carries a numerator and a denominator'); + end + n = char(raw.numerator); + d = char(raw.denominator); + if isempty(regexp(n, '^(0|-?[1-9][0-9]*)$', 'once')) || isempty(regexp(d, '^[1-9][0-9]*$', 'once')) + error('opensysml:decode', 'not the decimal terms of a rational: %s/%s', n, d); + end + nd = str2double(n); + dd = str2double(d); + if abs(nd) <= flintmax && dd <= flintmax + if gcd(nd, dd) ~= 1 + error('opensysml:decode', '%s/%s is not in lowest terms', n, d); + end + if bitand(uint64(dd), uint64(dd) - 1) == 0 + error('opensysml:decode', '%s/%s is a double, which realValue carries', n, d); + end + end + r = struct('numerator', n, 'denominator', d); +end diff --git a/client/matlab/+opensysml/+internal/valueArms.m b/client/matlab/+opensysml/+internal/valueArms.m index a49e895686..207e592e27 100644 --- a/client/matlab/+opensysml/+internal/valueArms.m +++ b/client/matlab/+opensysml/+internal/valueArms.m @@ -1,7 +1,7 @@ function arms = valueArms() -%VALUEARMS The twenty-two Value arm names on the wire. +%VALUEARMS The twenty-three Value arm names on the wire. - arms = {'intValue', 'bigIntValue', 'realValue', 'boolValue', 'stringValue', 'instanceId', ... + arms = {'intValue', 'bigIntValue', 'rationalValue', 'realValue', 'boolValue', 'stringValue', 'instanceId', ... 'sequence', 'null', 'unset', 'quantity', 'enumLiteral', 'complex', ... 'array', 'vector', 'vectorQuantity', 'measurementRef', 'infinity', ... 'function', 'set', 'tensorQuantity', 'metaobject', 'undetermined'}; diff --git a/client/matlab/+opensysml/buildDocumentBindings.m b/client/matlab/+opensysml/buildDocumentBindings.m index 9e794b53f4..6b22f16059 100644 --- a/client/matlab/+opensysml/buildDocumentBindings.m +++ b/client/matlab/+opensysml/buildDocumentBindings.m @@ -60,6 +60,9 @@ elseif isstruct(item) && isscalar(item) && isfield(item, 'bigInteger') wide = opensysml.encodeValue(item); value = struct('bigIntValue', wide.bigIntValue); + elseif isstruct(item) && isscalar(item) && isfield(item, 'numerator') && isfield(item, 'denominator') + exact = opensysml.encodeValue(item); + value = struct('rationalValue', exact.rationalValue); elseif isstruct(item) && isfield(item, 'magnitude') && isfield(item, 'unit') quantity = opensysml.encodeValue(item); value = struct('quantity', quantity.quantity); diff --git a/client/matlab/+opensysml/capabilities.m b/client/matlab/+opensysml/capabilities.m index 3a5f649478..fdd4194533 100644 --- a/client/matlab/+opensysml/capabilities.m +++ b/client/matlab/+opensysml/capabilities.m @@ -54,5 +54,6 @@ 'finalTime', 'final_time', ... 'engines', 'engines', ... 'undeterminedValue', 'undetermined_value', ... - 'bigIntValues', 'big_int_values'); + 'bigIntValues', 'big_int_values', ... + 'rationalValues', 'rational_values'); end diff --git a/client/matlab/+opensysml/decodeValue.m b/client/matlab/+opensysml/decodeValue.m index 9ac7ca022d..f2385fc1c5 100644 --- a/client/matlab/+opensysml/decodeValue.m +++ b/client/matlab/+opensysml/decodeValue.m @@ -12,7 +12,7 @@ function val = decodeValueInner(v, resolveInstance) %DECODEVALUE Decode one wire Value: an object with exactly one of the -%twenty-two arm keys. Contract violations are errors, never defaults. +%twenty-three arm keys. Contract violations are errors, never defaults. if isa(v, 'containers.Map') kinds = v.keys; @@ -36,6 +36,7 @@ switch kind case 'intValue', val = asInt64(v.intValue); case 'bigIntValue', val = bigIntegerOf(v.bigIntValue); + case 'rationalValue', val = opensysml.internal.rationalOf(v.rationalValue); case 'realValue', val = asReal(v.realValue); case 'boolValue', val = logical(v.boolValue); case 'stringValue', val = char(v.stringValue); @@ -208,6 +209,8 @@ n = opensysml.parseInt64(c.intValue); elseif isfield(c, 'bigIntValue') n = bigIntegerOf(c.bigIntValue); + elseif isfield(c, 'rationalValue') + n = opensysml.internal.rationalOf(c.rationalValue); elseif isfield(c, 'realValue') n = realOf(c.realValue); else @@ -220,6 +223,8 @@ mag = opensysml.parseInt64(b.intMagnitude); elseif isfield(b, 'bigIntMagnitude') mag = bigIntegerOf(b.bigIntMagnitude); + elseif isfield(b, 'rationalMagnitude') + mag = opensysml.internal.rationalOf(b.rationalMagnitude); elseif isfield(b, 'realMagnitude') mag = realOf(b.realMagnitude); else diff --git a/client/matlab/+opensysml/encodeValue.m b/client/matlab/+opensysml/encodeValue.m index d009a529f3..39fcdc4d3d 100644 --- a/client/matlab/+opensysml/encodeValue.m +++ b/client/matlab/+opensysml/encodeValue.m @@ -52,6 +52,9 @@ elseif isfield(x, 'bigInteger') requireCapability(conn, 'big_int_values'); v = struct('bigIntValue', char(x.bigInteger)); + elseif isRational(x) + requireCapability(conn, 'rational_values'); + v = struct('rationalValue', rationalTerms(x)); elseif isfield(x, 'instanceRef') id = integerValue(x.instanceRef, 'instance reference id'); v = struct('instanceId', sprintf('%d', id)); @@ -166,6 +169,9 @@ if isstruct(magnitude) && isfield(magnitude, 'bigInteger') requireCapability(conn, 'big_int_values'); body.bigIntMagnitude = char(magnitude.bigInteger); + elseif isRational(magnitude) + requireCapability(conn, 'rational_values'); + body.rationalMagnitude = rationalTerms(magnitude); elseif ~isnumeric(magnitude) || ~isscalar(magnitude) || ~isreal(magnitude) opensysml.internal.raise('opensysml:encode', 'quantity magnitude must be a real scalar'); elseif isinteger(magnitude) @@ -249,6 +255,19 @@ function validateShape(dimensions, count, kind) end end +function tf = isRational(value) + tf = isstruct(value) && isscalar(value) && isfield(value, 'numerator') && ... + isfield(value, 'denominator'); +end + +function terms = rationalTerms(value) + try + terms = opensysml.internal.rationalOf(value); + catch e + opensysml.internal.raise('opensysml:encode', e.message); + end +end + function tf = isQuantity(value) tf = isstruct(value) && isfield(value, 'magnitude') && isfield(value, 'unit'); end diff --git a/client/matlab/+opensysml/runDocumentQuery.m b/client/matlab/+opensysml/runDocumentQuery.m index a18e17de3c..759d50834d 100644 --- a/client/matlab/+opensysml/runDocumentQuery.m +++ b/client/matlab/+opensysml/runDocumentQuery.m @@ -18,6 +18,9 @@ if any(cellfun(@bindingHoldsBigInt, wire)) model.connection.require('big_int_values'); end + if any(cellfun(@bindingHoldsRational, wire)) + model.connection.require('rational_values'); + end request = struct('modelHash', model.hash, 'queryId', char(queryId), ... 'bindings', {wire}); answer = opensysml.call(model.connection, 'RunDocumentQuery', request, ... @@ -25,6 +28,13 @@ result = opensysml.internal.decodeDocumentResult(answer); end +function exact = bindingHoldsRational(binding) +%BINDINGHOLDSRATIONAL Whether a wire binding sends an exact Rational no double holds. + exact = any(cellfun(@(value) isfield(value, 'rationalValue') || ... + (isfield(value, 'quantity') && isfield(value.quantity, 'rationalMagnitude')), ... + binding.values)); +end + function wide = bindingHoldsBigInt(binding) %BINDINGHOLDSBIGINT Whether a wire binding sends an Integer beyond int64. wide = any(cellfun(@(value) isfield(value, 'bigIntValue') || ... diff --git a/client/matlab/README.md b/client/matlab/README.md index 14175535ea..2bb84c121a 100644 --- a/client/matlab/README.md +++ b/client/matlab/README.md @@ -93,13 +93,14 @@ A `Model` exposes `hash`, `connection`, `diagnostics`, `documents`, ## Values -`opensysml.decodeValue` reads all twenty-two `Value` arms; +`opensysml.decodeValue` reads all twenty-three `Value` arms; `opensysml.encodeValue` writes request values. | Wire arm | MATLAB/Octave value | | --- | --- | | `intValue` | Exact `int64`; JSON decimal digits are never routed through a double. | | `bigIntValue` | `struct('bigInteger', char)`: an Integer beyond `int64`, kept as its decimal digits since no MATLAB number holds it, and sent back as written. A quantity's `bigIntMagnitude` and a vector component decode the same way. | +| `rationalValue` | `struct('numerator', char, 'denominator', char)`: an exact Rational no `double` holds (such as `1/3`), kept as its lowest-terms decimal numerator and positive denominator, and sent back as written; sending one needs `rational_values`. A Rational a `double` holds exactly arrives as `realValue`. A quantity's `rationalMagnitude` and a vector component decode the same way. | | `realValue` | `double`, including `"NaN"`, `"Infinity"`, and `"-Infinity"`. | | `boolValue` / `stringValue` | `logical` / `char`. | | `instanceId` | `struct('instanceRef', int64)`. | diff --git a/client/matlab/tests/test_decode_value.m b/client/matlab/tests/test_decode_value.m index 5e4cf6d9cd..9c2dff9aae 100644 --- a/client/matlab/tests/test_decode_value.m +++ b/client/matlab/tests/test_decode_value.m @@ -1,5 +1,5 @@ function test_decode_value() -%TEST_DECODE_VALUE All twenty-two Value arms and the failure modes. +%TEST_DECODE_VALUE All twenty-three Value arms and the failure modes. % the double literal cannot write -2^63; decodeValue must go through % the int64 accumulator @@ -14,6 +14,18 @@ function test_decode_value() assert_error(@() opensysml.decodeValue(struct('bigIntValue', '007')), 'opensysml:decode', 'bigIntValue leading zero'); q = opensysml.decodeValue(struct('quantity', struct('bigIntMagnitude', '9223372036854775808', 'unit', 'kg'))); assert_equal(q.magnitude.bigInteger, '9223372036854775808', 'quantity bigIntMagnitude'); + third = struct('numerator', '-1', 'denominator', '3'); + assert_equal(opensysml.decodeValue(struct('rationalValue', third)), third, 'rationalValue'); + q = opensysml.decodeValue(struct('quantity', struct('rationalMagnitude', third, 'unit', 'kg'))); + assert_equal(q.magnitude, third, 'quantity rationalMagnitude'); + wideRational = struct('numerator', '1', 'denominator', ['1' repmat('0', 1, 30)]); + assert_equal(opensysml.decodeValue(struct('rationalValue', wideRational)), wideRational, 'rationalValue beyond double terms'); + bad = {{'2', '6'}, {'1', '-3'}, {'1', '0'}, {'1', '2'}, {'3', '1'}, {'0', '1'}, {'01', '3'}, {'+1', '3'}, {'', '3'}}; + for i = 1:numel(bad) + assert_error(@() opensysml.decodeValue(struct('rationalValue', ... + struct('numerator', bad{i}{1}, 'denominator', bad{i}{2}))), 'opensysml:decode', ... + sprintf('rationalValue %s/%s', bad{i}{1}, bad{i}{2})); + end assert_equal(opensysml.decodeValue(struct('realValue', 1.5)), 1.5, 'realValue'); assert_equal(opensysml.decodeValue(struct('realValue', 'NaN')), NaN, 'realValue NaN'); assert_equal(opensysml.decodeValue(struct('realValue', 'Infinity')), Inf, 'realValue Infinity'); diff --git a/client/matlab/tests/test_encode_value.m b/client/matlab/tests/test_encode_value.m index ab2975aca1..292ffd975c 100644 --- a/client/matlab/tests/test_encode_value.m +++ b/client/matlab/tests/test_encode_value.m @@ -8,6 +8,13 @@ function test_encode_value() assert_equal(opensysml.decodeValue(opensysml.encodeValue(big)), big, 'bigInteger round trip'); q = opensysml.encodeValue(struct('magnitude', big, 'unit', 'kg', 'unitTerm', [])); assert_equal(q.quantity.bigIntMagnitude, '1180591620717411303424', 'quantity bigIntMagnitude'); + third = struct('numerator', '1', 'denominator', '3'); + assert_equal(opensysml.encodeValue(third), struct('rationalValue', third), 'rational'); + assert_equal(opensysml.decodeValue(opensysml.encodeValue(third)), third, 'rational round trip'); + q = opensysml.encodeValue(struct('magnitude', third, 'unit', 'kg', 'unitTerm', [])); + assert_equal(q.quantity.rationalMagnitude, third, 'quantity rationalMagnitude'); + assert_error(@() opensysml.encodeValue(struct('numerator', '1', 'denominator', '4')), ... + 'opensysml:encode', 'a double-exact rational is a Real'); assert_equal(opensysml.encodeValue(1.5), struct('realValue', 1.5), 'real'); assert_equal(opensysml.encodeValue('x'), struct('stringValue', 'x'), 'string'); assert_equal(opensysml.encodeValue([]), struct('null', ''), 'null'); diff --git a/client/matlab/tests/test_surface_offline.m b/client/matlab/tests/test_surface_offline.m index 930525564f..4d4e3237f4 100644 --- a/client/matlab/tests/test_surface_offline.m +++ b/client/matlab/tests/test_surface_offline.m @@ -72,6 +72,18 @@ function test_surface_offline() wire = opensysml.encodeValue({int64(1), {wide}}, bigConn); assert_equal(wire.sequence.elements{2}.sequence.elements{1}.bigIntValue, ... '1180591620717411303424', 'nested Integer beyond int64 with big_int_values'); + third = struct('numerator', '1', 'denominator', '3'); + thirdQuantity = struct('magnitude', third, 'unit', 'kg'); + for value = {third, {int64(1), {third}}, thirdQuantity, struct('components', {{thirdQuantity}})} + assert_error(@() opensysml.encodeValue(value{1}, conn), ... + 'opensysml:missingCapability', 'exact Rational without rational_values'); + end + rationalConn = opensysml.external('127.0.0.1:1'); + rationalConn.primeServerInfo(struct('version', 'test', 'capabilities', ... + {[capabilities, {'rational_values'}]})); + wire = opensysml.encodeValue({int64(1), {third}}, rationalConn); + assert_equal(wire.sequence.elements{2}.sequence.elements{1}.rationalValue, third, ... + 'nested exact Rational with rational_values'); assert_equal(opensysml.parseUint64('18446744073709551615'), ... intmax('uint64'), 'exact uint64 parsing'); @@ -199,6 +211,11 @@ function test_surface_offline() assert_equal(wide{1}.values{1}.bigIntValue, '9223372036854775808', 'Integer binding beyond int64'); decodedWide = opensysml.internal.decodeDocumentValue(struct('bigIntValue', '-1180591620717411303424')); assert_equal(decodedWide.bigInteger, '-1180591620717411303424', 'document Integer beyond int64'); + exact = opensysml.buildDocumentBindings(struct('r', {{struct('numerator', '2', 'denominator', '3')}})); + assert_equal(exact{1}.values{1}.rationalValue.denominator, '3', 'exact Rational binding'); + decodedExact = opensysml.internal.decodeDocumentValue(struct('rationalValue', ... + struct('numerator', '-2', 'denominator', '3'))); + assert_equal(decodedExact.numerator, '-2', 'document exact Rational'); queryConn = opensysml.external('127.0.0.1:1'); queryConn.primeServerInfo(struct('version', 'test', 'capabilities', ... {{'document_query'}})); @@ -209,6 +226,12 @@ function test_surface_offline() 'bindings', struct('n', {bound})), ... 'opensysml:missingCapability', 'document binding beyond int64 without big_int_values'); end + thirdBound = struct('numerator', '1', 'denominator', '3'); + for bound = {thirdBound, struct('magnitude', thirdBound, 'unit', '')} + assert_error(@() opensysml.runDocumentQuery(queryModel, 'Q::q', ... + 'bindings', struct('n', {bound})), ... + 'opensysml:missingCapability', 'document exact Rational without rational_values'); + end assert_error(@() opensysml.buildDocumentBindings(struct('bad', {struct('type', 'verdict')})), ... 'opensysml:argument', 'unsupported document binding'); From 3b340d41c0c6cd5adc462950d2d0ee36598cdcfc Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:05:08 +0000 Subject: [PATCH 08/26] test(semantics): cover exact Rational storage, parsing and wire validation Co-Authored-By: jason.han --- client/opensysml/edges_test.go | 32 +++ .../frontend/grpc/convert_rational_test.go | 198 ++++++++++++++++ internal/semantic/semantics/integer.go | 19 -- internal/semantic/semantics/integer_test.go | 4 +- internal/semantic/semantics/rational_test.go | 224 ++++++++++++++++++ 5 files changed, 456 insertions(+), 21 deletions(-) create mode 100644 internal/frontend/grpc/convert_rational_test.go create mode 100644 internal/semantic/semantics/rational_test.go diff --git a/client/opensysml/edges_test.go b/client/opensysml/edges_test.go index 7a9ee37f5b..a52f465bac 100644 --- a/client/opensysml/edges_test.go +++ b/client/opensysml/edges_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "math" + "math/big" "strings" "testing" @@ -35,6 +36,37 @@ func TestIntegerArithmeticBeyondInt64IsExact(t *testing.T) { } } +// TestRationalArithmeticIsExact: a KerML Rational is a rational number, so a +// quotient or decimal no binary64 holds arrives as the exact Rational it is, +// and one a binary64 holds as that Real. +func TestRationalArithmeticIsExact(t *testing.T) { + client := newClient(t) + model := parseVehicle(t, client) + + for expression, want := range map[string]string{ + "1 / 3": "1/3", + "0.1 + 0.2": "0.3", + "(2 / 3) ** 3": "8/27", + "1.0e400 / 4": "2.5e+399", + } { + value, err := client.Evaluate(context.Background(), model, expression) + got, ok := value.(opensysml.Rational) + if err != nil || !ok || got.String() != want { + t.Errorf("Evaluate(%q) = %#v, %v; want Rational %s", expression, value, err, want) + } + } + if value, err := client.Evaluate(context.Background(), model, "0.25 + 0.25"); err != nil || value != opensysml.Real(0.5) { + t.Errorf("Evaluate(0.25 + 0.25) = %#v, %v; want Real 0.5", value, err) + } + if value, err := client.Evaluate(context.Background(), model, "0.1 + 0.2 == 0.3"); err != nil || value != opensysml.Bool(true) { + t.Errorf("Evaluate(0.1 + 0.2 == 0.3) = %#v, %v; want true", value, err) + } + third := opensysml.NewRational(big.NewRat(1, 3)) + if third.Float64() != 1.0/3.0 || third.Rat().Cmp(big.NewRat(2, 6)) != 0 { + t.Errorf("Rational 1/3 = %v (%v)", third, third.Float64()) + } +} + // TestRealOutsideItsRangeIsAFailure: a Real no float64 holds is reported, not // read as an infinity. The literal 1e400 is an exact Rational; ToReal is Real. func TestRealOutsideItsRangeIsAFailure(t *testing.T) { diff --git a/internal/frontend/grpc/convert_rational_test.go b/internal/frontend/grpc/convert_rational_test.go new file mode 100644 index 0000000000..b3477f9135 --- /dev/null +++ b/internal/frontend/grpc/convert_rational_test.go @@ -0,0 +1,198 @@ +package grpc + +import ( + "context" + "errors" + "strings" + "testing" + + "connectrpc.com/connect" + "google.golang.org/protobuf/proto" + + pb "github.com/Open-MBEE/OpenSysML/api/proto" + "github.com/Open-MBEE/OpenSysML/internal/exec/runtime" + "github.com/Open-MBEE/OpenSysML/internal/frontend/protoconv" + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" +) + +func ratConst(t *testing.T, text string) runtime.Value { + t.Helper() + v, err := semantics.ParseRationalText(text, semantics.DefaultMaxIntegerBits) + if err != nil { + t.Fatalf("bad rational %q: %v", text, err) + } + return runtime.Value{Kind: runtime.ValConst, Const: v} +} + +// A Rational a double holds exactly keeps the real_value arm; any other crosses +// as its lowest terms in rational_value, and both read back as the Rational sent. +func TestRationalValueRoundTripsOverTheWire(t *testing.T) { + idx := symbols.NewIndex() + for _, tc := range []struct { + text string + num, den string + }{ + {text: "0.5"}, {text: "1800"}, {text: "-0.375"}, + {text: "1/3", num: "1", den: "3"}, + {text: "0.1", num: "1", den: "10"}, + {text: "-25/12", num: "-25", den: "12"}, + {text: "1e400", num: "1" + strings.Repeat("0", 400), den: "1"}, + } { + t.Run(tc.text, func(t *testing.T) { + value := ratConst(t, tc.text) + sent := protoconv.ValueToProto(value, idx) + if tc.num == "" { + if _, ok := sent.GetKind().(*pb.Value_RealValue); !ok { + t.Fatalf("crossed as %T, want real_value", sent.GetKind()) + } + } else if r := sent.GetRationalValue(); r.GetNumerator() != tc.num || r.GetDenominator() != tc.den { + t.Fatalf("rational_value = %v (kind %T), want %s/%s", r, sent.GetKind(), tc.num, tc.den) + } + wire, err := proto.Marshal(sent) + if err != nil { + t.Fatal(err) + } + var received pb.Value + if err := proto.Unmarshal(wire, &received); err != nil { + t.Fatal(err) + } + back, err := protoconv.ProtoToValueIn(&received, idx, nil) + if err != nil { + t.Fatalf("ProtoToValueIn: %v", err) + } + if tc.num == "" { + if f, _ := value.Const.BinaryExact(); back.Const.Kind != semantics.ValReal || back.Const.Real != f { + t.Errorf("read back %s, want the Real %v", runtime.FormatValue(back), f) + } + } else if back.Const.Kind != semantics.ValRational || semantics.CompareRat(back.Const, value.Const) != 0 { + t.Errorf("read back %s, want %s", runtime.FormatValue(back), runtime.FormatValue(value)) + } + }) + } +} + +// A quantity's Rational magnitude crosses in rational_magnitude unless a double +// holds it exactly. +func TestRationalQuantityMagnitudeCrossesExactly(t *testing.T) { + sent := protoconv.QuantityToProto(&runtime.Quantity{Num: ratConst(t, "1/3").Const}) + if r := sent.GetRationalMagnitude(); r.GetNumerator() != "1" || r.GetDenominator() != "3" { + t.Fatalf("rational_magnitude = %v (magnitude %T)", r, sent.GetMagnitude()) + } + exact := protoconv.QuantityToProto(&runtime.Quantity{Num: ratConst(t, "0.25").Const}) + if m, ok := exact.GetMagnitude().(*pb.Quantity_RealMagnitude); !ok || m.RealMagnitude != 0.25 { + t.Errorf("a binary64 magnitude crossed as %v, want real_magnitude 0.25", exact.GetMagnitude()) + } +} + +// Each Rational has one encoding: a fraction not in lowest terms, over a +// non-positive denominator, or one a double holds exactly is refused. +func TestRationalValueReadsOnlyCanonicalTerms(t *testing.T) { + idx := symbols.NewIndex() + for _, terms := range [][2]string{{"2", "6"}, {"1", "-3"}, {"-1", "-3"}, {"1", "0"}, {"1", "2"}, {"3", "1"}, {"0", "1"}, {"1.5", "7"}, {"", "3"}, {"1", ""}} { + pv := &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: &pb.Rational{Numerator: terms[0], Denominator: terms[1]}}} + if _, err := protoconv.ProtoToValueIn(pv, idx, nil); !errors.Is(err, protoconv.ErrRationalNotCanonical) { + t.Errorf("rational_value %s/%s: err = %v, want %v", terms[0], terms[1], err, protoconv.ErrRationalNotCanonical) + } + } +} + +const rationalWireModel = ` +package R { + private import ScalarValues::*; + calc def Third { in x : Rational; return : Rational = x / 3; } + calc third : Third; + + attribute oneThird = 1 / 3; + attribute half = 1 / 2; + attribute both [*] = (1, 1 / 3); + + action pass { + in x; + out y; + first start; + action inner { assign y := x; } + then done; + succession first start then inner; + } +} +` + +// A calc invoked over gRPC takes and returns exact Rationals. +func TestEvaluateCalcTakesAndReturnsRationals(t *testing.T) { + srv := mustNewService(t, 10) + hash := mustParsedModel(t, srv, rationalWireModel) + for _, tc := range []struct { + arg *pb.Value + num, den string + }{ + {arg: &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: &pb.Rational{Numerator: "1", Denominator: "10"}}}, num: "1", den: "30"}, + {arg: intValue(1), num: "1", den: "3"}, + } { + resp, err := srv.EvaluateCalc(context.Background(), &pb.EvaluateCalcRequest{ + ModelHash: hash, + SymbolId: "R::third", + Arguments: []*pb.Value{tc.arg}, + }) + if err != nil { + t.Fatalf("EvaluateCalc: %v", err) + } + if resp.Error != "" { + t.Fatalf("EvaluateCalc reported %q", resp.Error) + } + if r := resp.Result.GetRationalValue(); r.GetNumerator() != tc.num || r.GetDenominator() != tc.den { + t.Errorf("third(%v) = %v, want rational_value %s/%s", tc.arg, resp.Result, tc.num, tc.den) + } + } +} + +// A service withholding rational_values reports an exact Rational as +// unsupported — nested in a sequence included — and refuses one sent to it, as +// big_int_values does; a Rational a double holds is unaffected. +func TestRationalCapability(t *testing.T) { + ctx := context.Background() + withheld := mustNewServiceWithout(t, CapabilityRationalValues) + modelHash := mustParse(t, withheld, rationalWireModel) + + const want = "unsupported: constant 1/3" + if got := mustEvaluate(t, withheld, modelHash, "R::oneThird"); got.GetNull() != want { + t.Errorf("R::oneThird without %s = %v, want null %q", CapabilityRationalValues, got, want) + } + if got := mustEvaluate(t, withheld, modelHash, "R::half"); got.GetRealValue() != 0.5 { + t.Errorf("R::half without %s = %v, want real_value 0.5", CapabilityRationalValues, got) + } + both := mustEvaluate(t, withheld, modelHash, "R::both").GetSequence().GetElements() + if len(both) != 2 || both[0].GetIntValue() != 1 || both[1].GetNull() != want { + t.Errorf("R::both without %s = %v, want (1, withheld)", CapabilityRationalValues, both) + } + + third := &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: &pb.Rational{Numerator: "1", Denominator: "3"}}} + quantity := &pb.Value{Kind: &pb.Value_Quantity{Quantity: &pb.Quantity{ + Magnitude: &pb.Quantity_RationalMagnitude{RationalMagnitude: &pb.Rational{Numerator: "1", Denominator: "3"}}, + UnitTerm: &pb.UnitTerm{ScaleNum: 1, ScaleDen: 1}, + }}} + filtered := proto.Clone(quantity).(*pb.Value) + withheld.filterValueCapabilities(filtered) + if _, ok := protoconv.UnsupportedReason(filtered); !ok { + t.Errorf("quantity without %s = %v, want it reported unsupported", CapabilityRationalValues, filtered) + } + nested := &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: []*pb.Value{third}}}} + vector := &pb.Value{Kind: &pb.Value_Vector{Vector: &pb.Vector{Components: []*pb.Value{third}}}} + for name, input := range map[string]*pb.Value{"rational": third, "nested": nested, "vector": vector, "quantity": quantity} { + _, err := withheld.ExecuteAction(ctx, &pb.ExecuteActionRequest{ + ModelHash: modelHash, + ActionSymbolId: "R::pass", + Inputs: map[string]*pb.Value{"x": input}, + }) + if connect.CodeOf(err) != connect.CodeUnimplemented || !strings.Contains(err.Error(), CapabilityRationalValues) { + t.Errorf("ExecuteAction with %s input without %s: err = %v, want UNIMPLEMENTED naming the capability", name, CapabilityRationalValues, err) + } + } + + available := mustNewService(t, 10) + t.Cleanup(available.Close) + availableHash := mustParse(t, available, rationalWireModel) + if r := mustEvaluate(t, available, availableHash, "R::oneThird").GetRationalValue(); r.GetNumerator() != "1" || r.GetDenominator() != "3" { + t.Errorf("R::oneThird = %v, want rational_value 1/3", r) + } +} diff --git a/internal/semantic/semantics/integer.go b/internal/semantic/semantics/integer.go index a85071bc5c..1415a726e8 100644 --- a/internal/semantic/semantics/integer.go +++ b/internal/semantic/semantics/integer.go @@ -318,25 +318,6 @@ func IntDivTrunc(a, b Value) (Value, bool) { return BigIntValue(new(big.Int).Quo(a.bigView(), b.bigView())), true } -// IntQuotient is the exact rational quotient of two Integers rounded once to -// the nearest float64, shared by the folder and the runtime; ok=false on a -// zero divisor. Rounding each operand first would move quotients of operands -// beyond 2^53, where float64 loses exactness. A quotient beyond the float64 -// range rounds to an infinity, which callers report as a Real overflow. -func IntQuotient(a, b Value) (float64, bool) { - if b.IntSign() == 0 { - return 0, false - } - var r *big.Rat - if a.ext == nil && b.ext == nil { - r = new(big.Rat).SetFrac64(a.Int, b.Int) - } else { - r = new(big.Rat).SetFrac(a.bigView(), b.bigView()) - } - q, _ := r.Float64() - return q, true -} - // IntPow is a**n for an Integer base and a non-negative Integer exponent, // refused with ErrIntegerSizeLimit before it is computed when the result would // need more than maxBits. diff --git a/internal/semantic/semantics/integer_test.go b/internal/semantic/semantics/integer_test.go index 9027a862e1..5611aea77b 100644 --- a/internal/semantic/semantics/integer_test.go +++ b/internal/semantic/semantics/integer_test.go @@ -94,8 +94,8 @@ func TestIntegerToReal(t *testing.T) { func TestIntegerQuotientAndModulo(t *testing.T) { p70, _ := IntPow(intVal(2), intVal(70), DefaultMaxIntegerBits) p69, _ := IntPow(intVal(2), intVal(69), DefaultMaxIntegerBits) - if q, ok := IntQuotient(p70, p69); !ok || q != 2 { - t.Errorf("2**70 / 2**69 = %v", q) + if q, err := RatArith(ast.OpDiv, p70, p69, DefaultMaxIntegerBits); err != nil || CompareRat(q, intVal(2)) != 0 || q.Kind != ValRational { + t.Errorf("2**70 / 2**69 = %s, %v, want the Rational 2", q.FormatRational(), err) } if r, ok := IntRem(p70, intVal(7)); !ok || r != intVal(2) { t.Errorf("2**70 mod 7 = %+v", r) diff --git a/internal/semantic/semantics/rational_test.go b/internal/semantic/semantics/rational_test.go new file mode 100644 index 0000000000..7b9d678de8 --- /dev/null +++ b/internal/semantic/semantics/rational_test.go @@ -0,0 +1,224 @@ +package semantics + +import ( + "errors" + "math" + "math/big" + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +func frac(t *testing.T, num, den int64) Value { + t.Helper() + v, ok := FracValue(num, den) + if !ok { + t.Fatalf("FracValue(%d, %d) refused", num, den) + } + return v +} + +func parsed(t *testing.T, text string) Value { + t.Helper() + v, err := ParseRational(text, DefaultMaxIntegerBits) + if err != nil { + t.Fatalf("ParseRational(%q): %v", text, err) + } + return v +} + +func TestRationalNormalization(t *testing.T) { + if v := frac(t, 2, -4); v.Kind != ValRational || v.FormatRational() != "-0.5" || v.RatDenom() != intVal(2) || v.RatNumer() != intVal(-1) { + t.Errorf("2/-4 = %s over %s", v.RatNumer().FormatInt(), v.RatDenom().FormatInt()) + } + if _, ok := FracValue(1, 0); ok { + t.Error("1/0 accepted") + } + // One Rational, one representation: a big.Rat whose terms fit is held inline. + wide := RatValue(new(big.Rat).SetFrac64(1, 1<<33)) + inline := RatValue(new(big.Rat).SetFrac64(6, 9)) + if inline != frac(t, 2, 3) { + t.Errorf("RatValue(6/9) = %+v, want the inline 2/3", inline) + } + if CompareRat(wide, frac(t, 1, 1<<33)) != 0 || wide.FormatRational() != "1.16415321826934814453125e-10" { + t.Errorf("1/2^33 = %s", wide.FormatRational()) + } + if v := frac(t, math.MinInt64, 2); v.RatNumer().FormatInt() != "-4611686018427387904" || v.RatDenom() != intVal(1) { + t.Errorf("MinInt64/2 = %s", v.FormatRational()) + } + if v := frac(t, 1, math.MinInt64); v.RatNumer() != intVal(-1) || v.RatDenom().FormatInt() != "9223372036854775808" { + t.Errorf("1/MinInt64 = %s", v.FormatRational()) + } + if v := RatOf(intVal(6)); v.Kind != ValRational || !v.RatIsWhole() || CompareRat(v, intVal(6)) != 0 { + t.Errorf("RatOf(6) = %+v", v) + } +} + +func TestParseRationalIsExact(t *testing.T) { + for _, c := range []struct{ text, want string }{ + {"0.1", "0.1"}, {".5", "0.5"}, {"1.5e3", "1500.0"}, {"25E-3", "0.025"}, {"-0.0", "0.0"}, + {"+2.50", "2.5"}, {"1.0e400", "1e+400"}, {"3e-7", "3e-07"}, {"123456789012345678901234567890.5", "1.234567890123456789012345678905e+29"}, {"12345678901234567890.25", "12345678901234567890.25"}, + } { + if got := parsed(t, c.text); got.Kind != ValRational || got.FormatRational() != c.want { + t.Errorf("ParseRational(%q) = %s, want %s", c.text, got.FormatRational(), c.want) + } + } + if v := parsed(t, "0.1"); v.RatNumer() != intVal(1) || v.RatDenom() != intVal(10) { + t.Errorf("0.1 = %s/%s, want 1/10", v.RatNumer().FormatInt(), v.RatDenom().FormatInt()) + } + for _, text := range []string{"1e100000", "1e-100000", "1e9223372036854775807", strings.Repeat("7", 100) + ".1"} { + if _, err := ParseRational(text, 128); !errors.Is(err, ErrRationalSizeLimit) { + t.Errorf("ParseRational(%.20q, 128) = %v, want ErrRationalSizeLimit", text, err) + } + } + for _, text := range []string{"", "abc", "1.2.3", "NaN", "Inf", "0x1p3", "1e"} { + if _, err := ParseRational(text, DefaultMaxIntegerBits); !errors.Is(err, ErrRealNotation) { + t.Errorf("ParseRational(%q) = %v, want ErrRealNotation", text, err) + } + } + if v, err := ParseRationalText("2/6", DefaultMaxIntegerBits); err != nil || CompareRat(v, frac(t, 1, 3)) != 0 { + t.Errorf("ParseRationalText(2/6) = %s, %v", v.FormatRational(), err) + } + if _, err := ParseRationalText("1/0", DefaultMaxIntegerBits); !errors.Is(err, ErrDivisionByZero) { + t.Errorf("ParseRationalText(1/0) = %v", err) + } + if _, err := ParseRationalText("1/x", DefaultMaxIntegerBits); !errors.Is(err, ErrRealNotation) { + t.Errorf("ParseRationalText(1/x) = %v", err) + } +} + +func TestRationalArithmeticIsExact(t *testing.T) { + arith := func(op ast.OperatorKind, a, b Value) Value { + t.Helper() + v, err := RatArith(op, a, b, DefaultMaxIntegerBits) + if err != nil { + t.Fatalf("%s %s %s: %v", a.FormatRational(), op, b.FormatRational(), err) + } + return v + } + if sum := arith(ast.OpAdd, parsed(t, "0.1"), parsed(t, "0.2")); CompareRat(sum, parsed(t, "0.3")) != 0 { + t.Errorf("0.1 + 0.2 = %s", sum.FormatRational()) + } + third := arith(ast.OpDiv, intVal(1), intVal(3)) + if third.Kind != ValRational || third.FormatRational() != "1/3" { + t.Errorf("1 / 3 = %s", third.FormatRational()) + } + if whole := arith(ast.OpMul, third, intVal(3)); whole.Kind != ValRational || !whole.RatIsWhole() || CompareRat(whole, intVal(1)) != 0 { + t.Errorf("1 / 3 * 3 = %+v, want the Rational 1", whole) + } + if rem := arith(ast.OpMod, frac(t, -7, 2), intVal(1)); CompareRat(rem, frac(t, -1, 2)) != 0 { + t.Errorf("-7/2 %% 1 = %s, want -0.5 (truncated)", rem.FormatRational()) + } + // Past int64 the terms move to big.Rat and stay exact. + wide := arith(ast.OpAdd, frac(t, math.MaxInt64, 1), frac(t, 1, 2)) + if wide.FormatRational() != "9223372036854775807.5" { + t.Errorf("MaxInt64 + 1/2 = %s", wide.FormatRational()) + } + if back := arith(ast.OpSub, wide, frac(t, 1, 2)); back != frac(t, math.MaxInt64, 1) { + t.Errorf("(MaxInt64 + 1/2) - 1/2 = %+v, want the inline MaxInt64", back) + } + if _, err := RatArith(ast.OpDiv, third, intVal(0), DefaultMaxIntegerBits); !errors.Is(err, ErrDivisionByZero) { + t.Errorf("1/3 / 0 = %v", err) + } + if _, err := RatArith(ast.OpMul, frac(t, 1, 1<<40), frac(t, 1, 1<<40), 64); !errors.Is(err, ErrRationalSizeLimit) { + t.Errorf("2^-40 * 2^-40 under a 64-bit budget = %v, want ErrRationalSizeLimit", err) + } + if n := RatNeg(third); n.FormatRational() != "-1/3" || RatAbs(n) != third { + t.Errorf("-(1/3) = %s", n.FormatRational()) + } +} + +func TestRationalPower(t *testing.T) { + pow := func(base Value, n int64) (Value, error) { return RatPow(base, intVal(n), DefaultMaxIntegerBits) } + for _, c := range []struct { + base Value + n int64 + want string + }{ + {frac(t, 2, 3), 3, "8/27"}, {frac(t, 2, 3), -2, "2.25"}, {frac(t, -1, 1), 3, "-1.0"}, {frac(t, -1, 1), 4, "1.0"}, + {frac(t, 5, 7), 0, "1.0"}, {frac(t, 0, 1), 5, "0.0"}, + } { + if v, err := pow(c.base, c.n); err != nil || v.Kind != ValRational || v.FormatRational() != c.want { + t.Errorf("(%s) ** %d = %s, %v, want %s", c.base.FormatRational(), c.n, v.FormatRational(), err, c.want) + } + } + if _, err := pow(frac(t, 0, 1), -1); !errors.Is(err, ErrArithmeticDomain) { + t.Errorf("0 ** -1 = %v", err) + } + // Refused from a lower bound, before 3^(2^62) is computed. + if _, err := RatPow(frac(t, 2, 3), intVal(1<<62), 1024); !errors.Is(err, ErrRationalSizeLimit) { + t.Errorf("(2/3) ** 2^62 = %v, want ErrRationalSizeLimit", err) + } +} + +func TestRationalComparisonAndRounding(t *testing.T) { + tenth := parsed(t, "0.1") + if CompareRat(frac(t, 1, 3), parsed(t, "0.3334")) >= 0 || CompareRat(frac(t, 2, 6), frac(t, 1, 3)) != 0 { + t.Error("exact order") + } + if CompareRat(frac(t, math.MaxInt64, 3), frac(t, math.MaxInt64-1, 3)) <= 0 { + t.Error("order of terms whose cross products overflow int64") + } + // The double nearest 1/10 lies above it. + if CompareExactReal(tenth, 0.1) != -1 || CompareExactReal(frac(t, 1, 2), 0.5) != 0 || CompareExactReal(tenth, math.Inf(1)) != -1 || CompareExactReal(tenth, math.Inf(-1)) != 1 { + t.Error("exact order against a Real") + } + for _, c := range []struct { + v Value + floor, round, truncate int64 + }{ + {frac(t, -7, 2), -4, -4, -3}, {frac(t, 5, 2), 2, 3, 2}, {frac(t, -5, 2), -3, -3, -2}, {frac(t, 1, 3), 0, 0, 0}, {frac(t, 4, 1), 4, 4, 4}, + } { + if f, r, tr := RatFloor(c.v), RatRound(c.v), RatTrunc(c.v); f != intVal(c.floor) || r != intVal(c.round) || tr != intVal(c.truncate) { + t.Errorf("floor/round/trunc(%s) = %s %s %s", c.v.FormatRational(), f.FormatInt(), r.FormatInt(), tr.FormatInt()) + } + } +} + +func TestRationalFormatting(t *testing.T) { + for _, c := range []struct { + v Value + want string + }{ + {frac(t, 1, 3), "1/3"}, {frac(t, -25, 12), "-25/12"}, {frac(t, 3, 10), "0.3"}, {frac(t, 2, 1), "2.0"}, + {frac(t, 3, 200000), "1.5e-05"}, {frac(t, 1, 10000), "0.0001"}, {frac(t, 0, 7), "0.0"}, + } { + if got := c.v.FormatRational(); got != c.want { + t.Errorf("FormatRational(%d/%d) = %s, want %s", c.v.RatNumer().Int, c.v.RatDenom().Int, got, c.want) + } + } + // A Rational binary64 holds prints as that Real prints. + for _, f := range []float64{0.5, 1.5, 2.0, 1e21, 0x1p-16, 1800} { + v, _ := RationalOfReal(f) + if v.FormatRational() != FormatReal(f) { + t.Errorf("FormatRational(%v) = %s, FormatReal = %s", f, v.FormatRational(), FormatReal(f)) + } + } +} + +func TestRationalBinaryBoundary(t *testing.T) { + if f, ok := frac(t, 1, 2).BinaryExact(); !ok || f != 0.5 { + t.Error("1/2 is a binary64") + } + if _, ok := parsed(t, "0.1").BinaryExact(); ok { + t.Error("1/10 is no binary64") + } + if _, ok := parsed(t, "1e400").BinaryExact(); ok { + t.Error("1e400 is no finite binary64") + } + if v, ok := RationalOfReal(0.1); !ok || v.RatNumer() != intVal(3602879701896397) || v.RatDenom() != intVal(36028797018963968) { + t.Errorf("RationalOfReal(0.1) = %s", v.FormatRational()) + } + if _, ok := RationalOfReal(math.NaN()); ok { + t.Error("NaN has no Rational") + } + if v, ok := CanonicalRational("1", "3"); !ok || CompareRat(v, frac(t, 1, 3)) != 0 { + t.Error("1/3 is canonical") + } + for _, c := range [][2]string{{"2", "6"}, {"1", "-3"}, {"1", "0"}, {"1", "2"}, {"3", "1"}, {"x", "3"}} { + if _, ok := CanonicalRational(c[0], c[1]); ok { + t.Errorf("CanonicalRational(%s, %s) accepted a noncanonical encoding", c[0], c[1]) + } + } +} From d2ae53fd36c538920498d2190124b2e3cab8301f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:13:31 +0000 Subject: [PATCH 09/26] feat(client): type generated Rational features exactly in Python and Node Co-Authored-By: jason.han --- client/node/src/core/index.ts | 2 ++ client/node/src/core/typed.ts | 19 ++++++++++++++++- client/node/src/core/values.ts | 14 +++++++++++++ client/node/src/node/generate.ts | 4 ++-- client/node/test/generate.test.ts | 4 ++-- client/node/test/golden/vehicle_types.ts | 10 ++++----- client/node/test/typed-runtime.test.ts | 9 ++++---- client/node/test/typed.test.ts | 17 +++++++++++++++ client/python/opensysml/generate.py | 22 +++++++++++--------- client/python/opensysml/typed.py | 23 +++++++++++++++++---- client/python/tests/golden/vehicle_types.py | 10 ++++----- client/python/tests/test_generate.py | 21 ++++++++++--------- client/python/tests/test_generate_golden.py | 8 ++++--- client/python/tests/test_typed.py | 13 ++++++++++++ 14 files changed, 130 insertions(+), 46 deletions(-) diff --git a/client/node/src/core/index.ts b/client/node/src/core/index.ts index eca605fb6e..4de17e07ac 100644 --- a/client/node/src/core/index.ts +++ b/client/node/src/core/index.ts @@ -241,6 +241,7 @@ export { asInt, asObject, asQuantity, + asRational, asReal, asString, asTyped, @@ -265,6 +266,7 @@ export { formatRational, formatValue, rational, + rationalOfDouble, rationalToNumber, requireInput, toValue, diff --git a/client/node/src/core/typed.ts b/client/node/src/core/typed.ts index 91b9bc19a7..e33d1ddef8 100644 --- a/client/node/src/core/typed.ts +++ b/client/node/src/core/typed.ts @@ -13,10 +13,12 @@ import { } from "./errors.js"; import type { Instance } from "./model.js"; import { + rationalOfDouble, rationalToNumber, type ComplexValue, type EnumValue, type QuantityValue, + type RationalValue, type SysMLValue, } from "./values.js"; @@ -175,7 +177,7 @@ export function asInt(featureName: string, value: SysMLValue): bigint { throw mismatch(featureName, "int", value); } -/** Decode a Real/Rational feature value; an integer value widens to float. */ +/** Decode a Real feature value; an Integer or exact Rational widens to the nearest double. */ export function asReal(featureName: string, value: SysMLValue): number { if (value.kind === "real") { return value.value; @@ -189,6 +191,21 @@ export function asReal(featureName: string, value: SysMLValue): number { throw mismatch(featureName, "float", value); } +/** Decode a Rational feature value exactly, whichever wire arm carried it. */ +export function asRational(featureName: string, value: SysMLValue): RationalValue { + if (value.kind === "rational") { + return { numerator: value.numerator, denominator: value.denominator }; + } + if (value.kind === "int") { + return { numerator: value.value, denominator: 1n }; + } + if (value.kind === "real" && Number.isFinite(value.value)) { + const { numerator, denominator } = rationalOfDouble(value.value); + return { numerator, denominator }; + } + throw mismatch(featureName, "Rational", value); +} + /** Decode a Complex feature value; a real value widens to complex. */ export function asComplex(featureName: string, value: SysMLValue): ComplexValue { if (value.kind === "complex") { diff --git a/client/node/src/core/values.ts b/client/node/src/core/values.ts index 2ea29113f1..2f9d56d2a6 100644 --- a/client/node/src/core/values.ts +++ b/client/node/src/core/values.ts @@ -902,6 +902,20 @@ function bitLength(n: bigint): number { return n === 0n ? 0 : (n < 0n ? -n : n).toString(2).length; } +/** The exact Rational a finite double holds. */ +export function rationalOfDouble(x: number): { kind: "rational" } & RationalValue { + if (!Number.isFinite(x)) { + throw new RangeError(`${x} is no Rational`); + } + let whole = x; + let denominator = 1n; + while (!Number.isInteger(whole)) { + whole *= 2; + denominator *= 2n; + } + return rational(BigInt(whole), denominator); +} + /** The double that holds a reduced Rational exactly, or undefined where none does. */ export function rationalAsDouble(value: RationalValue): number | undefined { const { numerator, denominator } = value; diff --git a/client/node/src/node/generate.ts b/client/node/src/node/generate.ts index 6901f30fe6..a5c3c70d4d 100644 --- a/client/node/src/node/generate.ts +++ b/client/node/src/node/generate.ts @@ -28,7 +28,7 @@ const OBJECT_DECODER = "_t.asObject"; // Emission schema, bumped when this generator's output changes for an // unchanged model. Not the opensysml release version, which would churn // every module. -export const GENERATOR_VERSION = "4"; +export const GENERATOR_VERSION = "5"; // The stamp hashes the model source client-side. A path, a timestamp or the // service version would churn without the module's content changing. @@ -65,7 +65,7 @@ const PRIMITIVE_TYPES = new Map ["String", { annotation: "string", decoder: "_t.asString" }], ["Natural", { annotation: "bigint", decoder: "_t.asInt" }], ["Integer", { annotation: "bigint", decoder: "_t.asInt" }], - ["Rational", { annotation: "number", decoder: "_t.asReal" }], + ["Rational", { annotation: "_t.RationalValue", decoder: "_t.asRational" }], ["Real", { annotation: "number", decoder: "_t.asReal" }], ["Complex", { annotation: "_t.ComplexValue", decoder: "_t.asComplex" }], ]); diff --git a/client/node/test/generate.test.ts b/client/node/test/generate.test.ts index 7e4f751c1a..526ef77d7e 100644 --- a/client/node/test/generate.test.ts +++ b/client/node/test/generate.test.ts @@ -101,7 +101,7 @@ test("elementType maps library scalars to their counterparts", () => { ["String", "string", "_t.asString"], ["Natural", "bigint", "_t.asInt"], ["Integer", "bigint", "_t.asInt"], - ["Rational", "number", "_t.asReal"], + ["Rational", "_t.RationalValue", "_t.asRational"], ["Real", "number", "_t.asReal"], ]; for (const [primitive, annotation, decoder] of cases) { @@ -304,7 +304,7 @@ test("renderModule emits the header and stamps, deterministically", () => { const source = renderModule(definitions, "abc"); assert.equal(source, renderModule(definitions, "abc")); assert.ok(source.startsWith("// Generated by opensysml-generate. Do not edit.")); - assert.ok(source.includes('SYSML_GENERATOR_VERSION = "4"')); + assert.ok(source.includes('SYSML_GENERATOR_VERSION = "5"')); assert.ok(source.includes('SYSML_MODEL_HASH = "sha256:abc"')); assert.ok(source.includes('import * as _t from "@openmbee/opensysml"')); assert.ok(source.includes("export class Engine extends _t.TypedObject")); diff --git a/client/node/test/golden/vehicle_types.ts b/client/node/test/golden/vehicle_types.ts index 89ce2db323..8686272dd1 100644 --- a/client/node/test/golden/vehicle_types.ts +++ b/client/node/test/golden/vehicle_types.ts @@ -3,7 +3,7 @@ import * as _t from "@openmbee/opensysml"; -export const SYSML_GENERATOR_VERSION = "4"; +export const SYSML_GENERATOR_VERSION = "5"; export const SYSML_MODEL_HASH = "sha256:31eb18ac5d0577683b21c4854fc751385d8b92f724943e91f8548d00c6986016"; @@ -13,8 +13,8 @@ export class Engine extends _t.TypedObject { static { _t.registerTyped(this); } /** attributeUsage Demo::Engine::power. */ - get power(): number { - return _t.featureValue(this, "power", _t.asReal); + get power(): _t.RationalValue { + return _t.featureValue(this, "power", _t.asRational); } } @@ -30,7 +30,7 @@ export class Vehicle extends _t.TypedObject { } /** attributeUsage Demo::Vehicle::mass. */ - get mass(): number { - return _t.featureValue(this, "mass", _t.asReal); + get mass(): _t.RationalValue { + return _t.featureValue(this, "mass", _t.asRational); } } diff --git a/client/node/test/typed-runtime.test.ts b/client/node/test/typed-runtime.test.ts index ef39022a9b..1e67081853 100644 --- a/client/node/test/typed-runtime.test.ts +++ b/client/node/test/typed-runtime.test.ts @@ -10,6 +10,7 @@ import { connect } from "../src/node/index.js"; import { generateSource } from "../src/node/generate.js"; import { packageRoot, useServiceBinary } from "./support/service.js"; import type { Instance } from "../src/core/model.js"; +import type { RationalValue } from "../src/core/values.js"; const MODEL = `package Demo { part def Engine { @@ -27,8 +28,8 @@ const MODEL = `package Demo { interface GeneratedModule { Vehicle: new (instance: Instance, resolve?: (id: bigint) => Instance | undefined) => { - mass: number; - engine: { power: number }; + mass: RationalValue; + engine: { power: RationalValue }; }; } @@ -78,6 +79,6 @@ test("a generated module compiles and reads an instantiated model", async () => const tree = await model.instantiate("vehicle"); const vehicle = new generated.Vehicle(tree.root, (id) => tree.byId(id)); - assert.equal(vehicle.mass, 1500.0); - assert.equal(vehicle.engine.power, 300.0); + assert.deepEqual(vehicle.mass, { numerator: 1500n, denominator: 1n }); + assert.deepEqual(vehicle.engine.power, { numerator: 300n, denominator: 1n }); }); diff --git a/client/node/test/typed.test.ts b/client/node/test/typed.test.ts index 74db1382c1..2af7805eb7 100644 --- a/client/node/test/typed.test.ts +++ b/client/node/test/typed.test.ts @@ -14,6 +14,7 @@ import { TypedObject, asEnum, asQuantity, + asRational, asReal, asInt, asString, @@ -156,6 +157,22 @@ test("an integer widens to float but a boolean does not", () => { assert.throws(() => asInt("x", { kind: "boolean", value: true }), TypeMismatchError); }); +test("a Rational decodes exactly whichever arm carried it; a Real rounds one once", () => { + const third = { kind: "rational", numerator: 1n, denominator: 3n } as const; + assert.deepEqual(asRational("x", third), { numerator: 1n, denominator: 3n }); + assert.deepEqual(asRational("x", { kind: "real", value: 0.1 }), { + numerator: 3602879701896397n, + denominator: 36028797018963968n, + }); + assert.deepEqual(asRational("x", { kind: "real", value: -0.25 }), { numerator: -1n, denominator: 4n }); + assert.deepEqual(asRational("x", { kind: "int", value: 7n }), { numerator: 7n, denominator: 1n }); + assert.equal(asReal("x", third), 1 / 3); + for (const bad of [Infinity, NaN]) { + assert.throws(() => asRational("x", { kind: "real", value: bad }), TypeMismatchError); + } + assert.throws(() => asRational("x", { kind: "boolean", value: true }), TypeMismatchError); +}); + test("optionalFeatureValue returns undefined when absent, present when held", () => { const { root, resolve } = vehicleInstance(); assert.equal(Vehicle.fromInstance(root, resolve).spare, undefined); diff --git a/client/python/opensysml/generate.py b/client/python/opensysml/generate.py index a631297d2a..240bba9cdd 100644 --- a/client/python/opensysml/generate.py +++ b/client/python/opensysml/generate.py @@ -32,7 +32,7 @@ # Emission schema, bumped when this generator's output changes for an unchanged # model. Not the opensysml release version, which would churn every module. -GENERATOR_VERSION = "4" +GENERATOR_VERSION = "5" # The stamp hashes the model source client-side. A path, a timestamp or the # service version would churn without the module's content changing. @@ -66,14 +66,16 @@ # How far a generalization chain is followed for a fact a declaration leaves out. MAX_GENERALIZATION_DEPTH = 16 +# Each library scalar's annotation and decoder. A Rational is exact, so it is a +# Fraction whichever wire arm carried it. PRIMITIVE_TYPES = { - "Boolean": "bool", - "String": "str", - "Natural": "int", - "Integer": "int", - "Rational": "float", - "Real": "float", - "Complex": "complex", + "Boolean": ("bool", "_t.as_bool"), + "String": ("str", "_t.as_str"), + "Natural": ("int", "_t.as_int"), + "Integer": ("int", "_t.as_int"), + "Rational": ("_t.Fraction", "_t.as_rational"), + "Real": ("float", "_t.as_float"), + "Complex": ("complex", "_t.as_complex"), } # Library scalars with no sound Python counterpart, mapped to `object`: Number is @@ -327,8 +329,8 @@ def element_type(type_facts: Optional[TypeFacts], names: Dict[str, str]) -> Pyth # instance, even when it has a generated class of its own. primitive = type_facts.primitive if primitive in PRIMITIVE_TYPES: - python = PRIMITIVE_TYPES[primitive] - return PythonType(python, f"_t.as_{python}") + annotation, decoder = PRIMITIVE_TYPES[primitive] + return PythonType(annotation, decoder) # An enumeration-typed feature holds one of the enumeration's literals, which # is a value of its own rather than an instance of the generated class. diff --git a/client/python/opensysml/typed.py b/client/python/opensysml/typed.py index 281884d7ae..c30463e8e2 100644 --- a/client/python/opensysml/typed.py +++ b/client/python/opensysml/typed.py @@ -9,6 +9,8 @@ returning a wrongly typed value. """ +import math +from fractions import Fraction from typing import Callable, ClassVar, List, Optional, Set, TypeVar from opensysml.enumeration import EnumLiteral @@ -16,9 +18,10 @@ from opensysml.instance import Instance from opensysml.values import UNSET, Quantity -# Re-exported so a generated module annotates a quantity property as `_t.Quantity` -# and needs no import of its own. +# Re-exported so a generated module annotates a property as `_t.Quantity` or +# `_t.Fraction` and needs no import of its own. __all__ = [ + "Fraction", "Quantity", "TypedObject", "as_bool", @@ -26,6 +29,7 @@ "as_int", "as_object", "as_quantity", + "as_rational", "as_str", "as_typed", "feature_value", @@ -146,16 +150,27 @@ def as_int(feature_name: str, value: object) -> int: def as_float(feature_name: str, value: object) -> float: - """Decode a Real/Rational feature value; an integer value widens to float.""" + """Decode a Real feature value; an Integer or exact Rational widens to the nearest float.""" if isinstance(value, bool): raise _mismatch(feature_name, "float", value) if isinstance(value, float): return value - if isinstance(value, int): + if isinstance(value, (int, Fraction)): return float(value) raise _mismatch(feature_name, "float", value) +def as_rational(feature_name: str, value: object) -> Fraction: + """Decode a Rational feature value exactly, whether it arrived as a Fraction, an int or a float.""" + if isinstance(value, bool): + raise _mismatch(feature_name, "Fraction", value) + if isinstance(value, Fraction): + return value + if isinstance(value, int) or (isinstance(value, float) and math.isfinite(value)): + return Fraction(value) + raise _mismatch(feature_name, "Fraction", value) + + def as_complex(feature_name: str, value: object) -> complex: """Decode a Complex feature value; a real value widens to complex.""" if isinstance(value, bool): diff --git a/client/python/tests/golden/vehicle_types.py b/client/python/tests/golden/vehicle_types.py index da5fba146d..1ecb0358be 100644 --- a/client/python/tests/golden/vehicle_types.py +++ b/client/python/tests/golden/vehicle_types.py @@ -5,7 +5,7 @@ from opensysml import typed as _t -SYSML_GENERATOR_VERSION = "4" +SYSML_GENERATOR_VERSION = "5" """Emission schema this module was generated with.""" SYSML_MODEL_HASH = "sha256:31eb18ac5d0577683b21c4854fc751385d8b92f724943e91f8548d00c6986016" @@ -18,9 +18,9 @@ class Engine(_t.TypedObject): sysml_id = "Demo::Engine" @property - def power(self) -> float: + def power(self) -> _t.Fraction: """attributeUsage Demo::Engine::power.""" - return _t.feature_value(self, "power", _t.as_float) + return _t.feature_value(self, "power", _t.as_rational) class Vehicle(_t.TypedObject): @@ -34,6 +34,6 @@ def engine(self) -> Engine: return _t.feature_value(self, "engine", _t.as_typed(Engine)) @property - def mass(self) -> float: + def mass(self) -> _t.Fraction: """attributeUsage Demo::Vehicle::mass.""" - return _t.feature_value(self, "mass", _t.as_float) + return _t.feature_value(self, "mass", _t.as_rational) diff --git a/client/python/tests/test_generate.py b/client/python/tests/test_generate.py index df2527efb3..9e81d2c6ea 100644 --- a/client/python/tests/test_generate.py +++ b/client/python/tests/test_generate.py @@ -94,21 +94,22 @@ def test_is_feature_kind_excludes_behavioral_usages(): @pytest.mark.parametrize( - "primitive,expected", + "primitive,expected,decoder", [ - ("Boolean", "bool"), - ("String", "str"), - ("Natural", "int"), - ("Integer", "int"), - ("Rational", "float"), - ("Real", "float"), + ("Boolean", "bool", "_t.as_bool"), + ("String", "str", "_t.as_str"), + ("Natural", "int", "_t.as_int"), + ("Integer", "int", "_t.as_int"), + ("Rational", "_t.Fraction", "_t.as_rational"), + ("Real", "float", "_t.as_float"), + ("Complex", "complex", "_t.as_complex"), ], ) -def test_element_type_primitives(primitive, expected): - """Library scalars map to their Python counterparts.""" +def test_element_type_primitives(primitive, expected, decoder): + """Library scalars map to their Python counterparts; a Rational is an exact Fraction.""" mapped = element_type(TypeFacts(primitive=primitive), {}) assert mapped.annotation == expected - assert mapped.decoder == f"_t.as_{expected}" + assert mapped.decoder == decoder assert mapped.comment == "" diff --git a/client/python/tests/test_generate_golden.py b/client/python/tests/test_generate_golden.py index 8ed9c34c5c..2bd56bac74 100644 --- a/client/python/tests/test_generate_golden.py +++ b/client/python/tests/test_generate_golden.py @@ -94,7 +94,7 @@ def test_golden_module_is_importable_and_typed(): "engine": module.Vehicle.engine.fget.__annotations__["return"], "power": module.Engine.power.fget.__annotations__["return"], } - assert annotations == {"mass": "float", "engine": "Engine", "power": "float"} + assert annotations == {"mass": "_t.Fraction", "engine": "Engine", "power": "_t.Fraction"} @pytest.mark.skipif(not mypy_available(), reason="mypy not installed") @@ -104,11 +104,13 @@ def test_generated_code_is_mypy_clean_and_flags_misuse(tmp_path): (tmp_path / "usage_ok.py").write_text( textwrap.dedent( """ + from fractions import Fraction + from opensysml.instance import Instance from vehicle_types import Vehicle - def check(inst: Instance) -> float: + def check(inst: Instance) -> Fraction: v: Vehicle = Vehicle.from_instance(inst) return v.mass + v.engine.power """ @@ -135,7 +137,7 @@ def check(inst: Instance) -> None: misuse = run_mypy(tmp_path, "usage_bad.py") assert misuse.returncode != 0 assert 'has no attribute "mas"' in misuse.stdout - assert 'Unsupported operand types for + ("float" and "str")' in misuse.stdout + assert 'No overload variant of "__add__" of "Fraction" matches argument type "str"' in misuse.stdout @pytest.mark.integration diff --git a/client/python/tests/test_typed.py b/client/python/tests/test_typed.py index 082a535ec4..d678b7f9d5 100644 --- a/client/python/tests/test_typed.py +++ b/client/python/tests/test_typed.py @@ -1,5 +1,7 @@ """Tests for the typed view runtime that generated classes are built on.""" +from fractions import Fraction + import pytest import opensysml @@ -113,6 +115,17 @@ def test_integer_widens_to_float_but_bool_does_not(): _t.as_int("x", True) +def test_rational_decodes_exactly_and_real_widens_to_nearest(): + """A Rational is the exact Fraction whichever arm carried it; a Real rounds a Fraction once.""" + assert _t.as_rational("x", Fraction(1, 3)) == Fraction(1, 3) + assert _t.as_rational("x", 0.25) == Fraction(1, 4) + assert _t.as_rational("x", 3) == Fraction(3) + assert _t.as_float("x", Fraction(1, 3)) == 1 / 3 + for bad in (True, float("inf"), float("nan"), "1/3"): + with pytest.raises(TypeMismatchError): + _t.as_rational("x", bad) + + def test_optional_feature_value_returns_none_when_absent(): """An absent 0..1 slot is None; a present one is decoded.""" v = Vehicle.from_instance(vehicle_instance()) From fac823da38a539bc76f8b3910f777f3281c8d19e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:15:16 +0000 Subject: [PATCH 10/26] docs: record exact KerML Rational evaluation and its wire contract Co-Authored-By: jason.han --- changes/unreleased/exact-rationals.changed.md | 2 + conformance/README.md | 1 + conformance/scenarios/01-server-info.json | 3 +- docs/guide/09-clients.md | 10 +- docs/project/exact-rational-evaluation.md | 477 +++++++++--------- docs/project/native-compilation.md | 24 +- docs/project/pilot-execution-referee.md | 6 +- docs/project/spec-compliance.md | 41 +- docs/reference/java-api.md | 1 + docs/reference/julia-api.md | 4 +- docs/reference/matlab-api.md | 6 +- docs/reference/node-api.md | 1 + docs/reference/python-api.md | 3 +- docs/reference/rust-api.md | 3 +- docs/reference/service-transports.md | 2 +- docs/reference/wire-contract.md | 48 +- 16 files changed, 346 insertions(+), 286 deletions(-) create mode 100644 changes/unreleased/exact-rationals.changed.md diff --git a/changes/unreleased/exact-rationals.changed.md b/changes/unreleased/exact-rationals.changed.md new file mode 100644 index 0000000000..27cf8a047b --- /dev/null +++ b/changes/unreleased/exact-rationals.changed.md @@ -0,0 +1,2 @@ +- **Rational values are exact.** A KerML `Rational` is a rational number (KerML 1.0 §9.3.2.2.8), so a decimal or exponent literal is the exact Rational it spells and Integer `/`, the `RationalFunctions` operators, `rat`, `numer`, `denom`, `floor`, `round`, `abs`, `max`, `min`, `gcd`, `sum`, `product`, `ToString` and `ToRational` compute without rounding: `0.1 + 0.2 == 0.3` is `true`, `1 / 3` is `1/3`, and `numer(rat(1, 3))` is `1` where it was `6004799503160661`. A whole result of rational arithmetic stays a Rational unless the declaration returns `Integer`. A terminating Rational prints as a decimal (`0.1`), any other as `numer/denom` (`1/3`). A Rational whose numerator and denominator outgrow the evaluation's size budget is the typed `ErrRationalSizeLimit`, never a rounded double. `Real` stays IEEE 754 binary64: a value declared `Real`, an operation with no rational result (`sqrt`, trigonometry, `exp`, `ln`, a non-Integer exponent) and mixed Rational/Real arithmetic answer a binary64 Real. +- **Exact Rationals cross every boundary.** gRPC adds `Value.rational_value`, `Quantity.rational_magnitude` and `DocumentValue.rational_value` under the new `rational_values` capability, carrying a Rational no double holds in lowest terms (a service without the capability reports it unsupported and refuses one sent, as `big_int_values` does); the JSON engine adds `rationalValue`; the Go client decodes `opensysml.Rational`, the Python client `fractions.Fraction`, the Node client `{ kind: "rational", numerator, denominator }`, the Java client `Value.RationalValue` over `org.openmbee.opensysml.Rational`, the Rust client `Value::Rational` over `opensysml::Rational`, the Julia client `Rational{BigInt}` and the MATLAB client `struct('numerator', …, 'denominator', …)`; each refuses to send one to a service without `rational_values`. RDF export keeps an exponent literal no double holds as its exact `xsd:decimal`, unit-scale composition stays exact, the SMT witness replay is exact over Integers and Rationals (only queries a binary64 Real takes part in are still marked rounded), and `sysml -compile` refuses, by construct, exact Rational arithmetic binary64 cannot reproduce. diff --git a/conformance/README.md b/conformance/README.md index 2033682bdb..2498acf421 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -160,6 +160,7 @@ What a request asks for is fixed per capability: | `function_values` | Encode functions (a calc read as a value) as unsupported nulls; refuse a request carrying one, at any depth. | | `metaobject_values` | Encode metaobjects (an element reflected on by `meta` or `.metadata`) as unsupported nulls; refuse a request carrying one, at any depth. | | `big_int_values` | Encode an Integer beyond `int64` (`big_int_value`, a quantity's `big_int_magnitude`) as an unsupported null; refuse a request carrying one, at any depth. | +| `rational_values` | Encode an exact Rational no double holds (`rational_value`, a quantity's `rational_magnitude`) as an unsupported null; refuse a request carrying one, at any depth, and a document query bound to or answering one. | The default service reports and supports every capability above. `make conformance` also starts a second service with `strict_conformance` and `oslc_query` withheld, verifies that its advertisement diff --git a/conformance/scenarios/01-server-info.json b/conformance/scenarios/01-server-info.json index 8350fe5e9f..9f784bbd1a 100644 --- a/conformance/scenarios/01-server-info.json +++ b/conformance/scenarios/01-server-info.json @@ -40,7 +40,8 @@ "schedule", "performer", "engines", - "big_int_values" + "big_int_values", + "rational_values" ] } } diff --git a/docs/guide/09-clients.md b/docs/guide/09-clients.md index ec6a545786..5a1deed1ed 100644 --- a/docs/guide/09-clients.md +++ b/docs/guide/09-clients.md @@ -615,7 +615,9 @@ inst.features # {'mass': 1500.0, 'engine': Instance(...)} inst.get("missing", 0) # 0 ``` -Integers, reals, booleans, strings and sequences map to `int`, `float`, `bool`, `str` and `list`. +Integers, reals, booleans, strings and sequences map to `int`, `float`, `bool`, `str` and `list`; +an exact Rational a double holds (`0.5`) maps to `float` and any other (`1/3`, `0.1`) to +`fractions.Fraction`. Unknown names raise `AttributeError` for attribute access or `KeyError` for item access, so `hasattr`, `copy` and `pickle` behave as expected. @@ -688,8 +690,8 @@ A `Metaobject` is what `x meta T` and the last element of `x.metadata` evaluate identity and its reflective metaclass, whose features (`.name`, `.qualifiedName`) evaluate through it. Each kind is capability-negotiated (`complex_values`, `structured_values`, `measurement_refs`, `function_values`, `set_values`, `tensor_values`, `metaobject_values`, -`infinity_value`, `undetermined_value`, `enum_values`, and `big_int_values` for an Integer -beyond `int64`): a value a service predating one +`infinity_value`, `undetermined_value`, `enum_values`, `big_int_values` for an Integer +beyond `int64`, and `rational_values` for an exact Rational no double holds): a value a service predating one cannot send arrives as `UnsupportedValueError` (in place, for a feature value or an output; raised, from `eval`), and sending such a value as an argument to that service is refused before anything goes over the wire, so a script can check @@ -1456,7 +1458,7 @@ html = model.render_document("Observatory::SubsystemReport", form="html") ``` A binding value is an element (`opensysml.ElementRef("Demo::optics")`), a `str`, an `int`, a -`float`, a `bool`, or a list of these; anything else raises `DocumentQueryError` before anything +`float`, a `fractions.Fraction`, a `bool`, or a list of these; anything else raises `DocumentQueryError` before anything is sent. Cell values come back with those Python types, an element as `ElementRef` and an unbounded multiplicity as `opensysml.INFINITY`. `render_document` takes no bindings, because a document binds its queries' parameters in the model; it returns the Markdown text, identical to diff --git a/docs/project/exact-rational-evaluation.md b/docs/project/exact-rational-evaluation.md index 8873326660..d865c78725 100644 --- a/docs/project/exact-rational-evaluation.md +++ b/docs/project/exact-rational-evaluation.md @@ -1,166 +1,224 @@ -# Exact-rational evaluation: adjudicated and declined - -The solver work left one asymmetry standing: the evaluator computes `Real`/`Rational` -arithmetic in IEEE 754 binary64 while the SMT translation reasons over the exact `Real` -(rational) sort, so the two can disagree wherever a value is not float64-representable. -The [solver soundness record](spec-compliance.md#exact-reals-against-a-rounding-evaluator--what-agreement-is-claimed) made the -*verdict* sound rather than exact — sat witnesses are replayed through the evaluator's -own arithmetic, and a query the evaluator rounds does not report an exact-real `unsat` -as an evaluator verdict — at the cost of completeness: rounded queries answer undecided -where a float64-aware solver might have decided them. - -The remaining way to close that gap would be to make the evaluator itself exact — a -`big.Rat`-backed value representation, so there is nothing for the solver to disagree -with. This record adjudicates that change: what the reference implementation actually -computes, what the specification actually requires, what the change would touch, what -it would cost, and why it is **declined**. - -## What the reference computes: binary64, observably and structurally - -The pinned pilot (`jupyter-sysml-kernel` 0.60.1, provisioned by -`scripts/download-pilot-validator.sh` + `scripts/download-pilot-evaluator.sh`, driven -by `build/pilot-evaluator/eval-sysml --cases`) was probed with cases chosen to -distinguish exact-rational from binary64 evaluation. Its answers, verbatim: - -| Case | Pilot answer | Exact-rational answer would be | -|------|--------------|--------------------------------| +# Exact-rational evaluation + +A KerML `Rational` is a rational number, and OpenSysML evaluates it as one: `0.1 + 0.2 == 0.3` +is `true`, `1 / 3` is the Rational `1/3`, `RationalFunctions::numer(RationalFunctions::rat(1, 3))` +is `1`. `Real` stays IEEE 754 binary64. This record derives that behavior from the +specification, states the choices the specification leaves to an implementation and how each was +made, and keeps the probes of the pinned pilot, which evaluates a `Rational` as a Java `double` and +so differs from this implementation by design. + +An earlier version of this record adjudicated the change and declined it, on the grounds that the +specification is silent on precision and the pilot computes in binary64. That decision is +superseded: the specification is not silent on what a `Rational` *is*, and the pilot's binary64 is +an implementation artifact, not a reading of the text. The pilot's answers are kept below as the +documented divergence. + +## What the specification requires + +KerML 1.0 (formal/2025-02-01) and the Kernel libraries vendored under `internal/workspace/libs`: + +- **A Rational is a rational number.** §9.3.2.2.8: "Rational is the type of rational numbers, + extended with values for positive and negative infinity." §9.3.2.2.9: "Real is the type of + mathematical (extended) real numbers. This includes both rational and irrational numbers". + `ScalarValues.kerml` orders them `Natural :> Integer :> Rational :> Real :> Complex`. +- **A decimal literal is a Rational.** §8.3.4.8.13 `LiteralRational`: `value : Real`, "the value + whose rational approximation is the result of evaluating this LiteralRational"; §8.4.4.9.2: "only + the rational-number subset of the real numbers can be represented using a finite literal. So the + result of a LiteralRational is actually always classified in the KerML DataType Rational." A + finite decimal — `0.1`, `1.5e3`, `25E-3` — names a rational exactly (1/10, 1500, 1/40), so the + rational approximation of the value it spells is that value: the literal evaluates to it, not to + a binary64 near it. +- **The library declares which operations stay Rational.** `RationalFunctions.kerml` (§9.4.10): + `rat(numer: Integer, denum: Integer): Rational`, `numer`/`denom(rat: Rational): Integer`, `abs`, + `'+'`, `'-'`, `'*'`, `'/'`, `max`, `min`, `sum`, `product` over Rationals returning `Rational`; + `'<'`, `'>'`, `'<='`, `'>='`, `'=='` returning `Boolean`; `gcd`, `floor`, `round`, `ToInteger` + returning `Integer`; `ToString` returning `String`; `ToRational(x: String): Rational`. + `IntegerFunctions.kerml` (§9.4.11): `'/'(x: Integer, y: Integer): Rational` — an Integer + quotient is a Rational — while `'+'`, `'-'`, `'*'`, `'%'`, `'**'`, `'^'` (Natural exponent) + return `Integer`. +- **Classification follows the declared result, not the number held.** A function's result is + classified by its declared return type (§8.4.4.9.2's rule for `LiteralRational`, applied to the + library signatures above). So a whole-valued result of Rational arithmetic is still a + `Rational` — `6 / 3` is the Rational `2`, `6 / 3 istype Integer` is `false` — and only an + operation declared to return `Integer` (`floor`, `round`, `numer`, `denom`, `gcd`, Integer + `+ - * % **`) yields one. +- **Real operations have no Rational result.** `sqrt`, the trigonometric functions, `exp`, `ln` + and every function `RealFunctions` alone declares return `Real`; their results are in general + irrational, so no Rational value is available to return. `RationalFunctions::'**'` declares + `y: Rational` and `return : Rational`, but `(2 / 3) ** 0.5` is irrational: the declaration can + hold only for an Integer exponent, which is what is evaluated exactly; any other exponent is the + `RealFunctions::'**'` the declaration specializes. +- **Infinity.** §9.3.2.2.8 extends Rational with the two infinities. KerML's only notation for an + infinite value is `LiteralInfinity` (`*`, §8.4.4.6, typed `Positive`); its existing rules apply + unchanged to Rationals — `*` exceeds every finite Rational (`1 / 3 < *`), equals itself, and any + arithmetic over it is a typed error. No finite Rational operation produces an infinity: a zero + divisor is a division-by-zero error. + +**What the specification is silent on**, and so is decided here rather than derived: the internal +representation, any bound on a Rational's size, the precision of `Real` (the text describes the +mathematical reals; it has no conformance clause on precision), how a Rational meets a `Real` that +an implementation holds approximately, and the text a Rational prints as. UML, fUML and PSSM say +nothing about KerML Rationals and were not consulted. + +## What is implemented + +- **Representation** (`internal/semantic/semantics/rational.go`). `ValRational` is held exactly, in + lowest terms with a positive denominator. A Rational whose terms fit `int64` (denominator within + 32 bits) lives inline in `Value`; any other in an immutable `big.Rat`, so each Rational has one + representation and `Value` stays within its 64-byte bound. Integer stays `ValInt` (`IntValue`/ + `BigIntValue`). +- **Literals.** `0.1`, `.5`, `1.5e3`, `25E-3`, `1.0e400` parse exactly (`semantics.ParseRational`); + an integer literal stays an Integer. A literal declared `Real` — the value of an attribute typed + `Real` — is held as the binary64 nearest it, which is that declaration's boundary (below). +- **Arithmetic** (`semantics.RatArith`, `RatPow`, `RatNeg`, `RatAbs`). `+ - * /` over Integer and + Rational operands are exact; Integer `/` is the exact Rational quotient (`1 / 3` is `1/3`, which + replaces the once-rounded `IntQuotient`); `**` and `^` with an Integer exponent are exact, a + negative exponent inverting (`(2 / 3) ** -2` is `2.25`); `0 ** -1` is a domain error. +- **Comparison and equality** (`semantics.CompareRat`, `CompareExactReal`) are exact between exact + values. Between a Rational and a binary64 Real they are currently exact, as the Integer precedent + `CompareIntReal` is; see [the open choice](#comparing-a-rational-with-a-binary64-real) below. +- **Library functions** (`internal/exec/runtime/library_conversions.go`, + `library_functions.go`). `rat`, `numer`, `denom`, `gcd`, `abs`, `floor`, `round`, `max`, `min`, + `sum`, `product`, `ToString`, `ToRational`, `ToInteger` compute exactly over exact operands; + `RationalFunctions::sum((0.1, 0.2))` is `0.3`. The `RealFunctions` versions take `Real` + operands, so `RealFunctions::sum((0.1, 0.2))` is binary64 `0.30000000000000004`. A finite + binary64 Real passed to a Rational function is the rational number that double is — every finite + binary64 is one — so for `x : Real = 0.1`, `numer(x)` is `3602879701896397`: the declaration, + not the function, rounded. +- **Size budget.** A Rational result whose numerator and denominator together need more bits than + the numeric size budget (`DefaultMaxIntegerBits`, shared with Integers, settable per run) is + `semantics.ErrRationalSizeLimit`, never rounded. A power is refused from a lower bound before it + is computed. A long accumulation of distinct fractions — a harmonic sum — therefore stops with a + typed error instead of growing without bound (`TestRuntimeRobustnessExactRational`). +- **Printing** (`Value.FormatRational`). A Rational whose denominator has no prime factor other + than 2 and 5 is a terminating decimal and prints as one, in the layout a Real prints in (`0.3`, + `2.0`, `1.5e-05`, `1e+400`); any other prints as `numer/denom` (`1/3`, `25/12`). So every value + that was already a binary fraction (`0.5`, `1.5`, `2.0`) prints as before, and REPL, JSON and + trace output change only where the old output was a rounding. +- **Quantities and units** (`semantics/units.go`, `quantity_eval.go`). A quantity's magnitude is + whatever number it holds, exact or binary64. Unit scale factors are ratios, composed exactly + (`Scale.Times`, `DividedBy`, `Pow`) while their terms are whole and within 2^53, so `0.1 [m] + 1 + [mm]` is exactly `0.101 [m]` and `1 [km] / 3` is `1/3 [km]`; a scale beyond that, or a conversion + through a binary64 magnitude, is binary64 as before. +- **The Real boundary** (`semantics.RealOf`). A value written to a feature typed `Real`, the + operands of `RealFunctions`, and a Rational meeting a binary64 Real in arithmetic are converted to + the nearest binary64 once. A nonzero Rational below the least positive binary64 is an overflow + error rather than a silent zero. + +### Every boundary + +- **gRPC** (`api/proto/sysml.proto`, `internal/frontend/protoconv`, `internal/frontend/grpc`). A + Rational binary64 holds exactly crosses as the existing `real_value`/`real_magnitude`, so old + clients see what they saw. Any other crosses as the additive `Rational` message (numerator and + denominator as decimal text) in `Value`, `Quantity` and `DocumentValue`, negotiated by the + `rational_values` capability exactly as `big_int_values` negotiates `big_int_value`: a service + that does not advertise it answers such a Rational as an unsupported value naming the + capability, refuses a request or document query carrying or answering one with `UNIMPLEMENTED`, + and never sends a nearest double; the bundled clients refuse to send one to it. A wire Rational + must be canonical (positive denominator, lowest terms, not a binary64), so no Rational has two + encodings, and an inbound `real_value` is a binary64 Real (see + [the open choice](#a-rational-a-double-holds-on-input)). +- **JSON** (`internal/frontend/engine`, `internal/frontend/core`): `"rationalValue": {"numerator": + "1", "denominator": "3"}` and `"rationalMagnitude"`, with the same canonical rule + ([wire contract](../reference/wire-contract.md)). +- **Clients.** Go: `opensysml.Rational` over `*big.Rat`. Python: `fractions.Fraction`, a + binary64-exact Rational arriving as `float`; generated typed classes declare a `Rational` + feature `Fraction` and decode it exactly (`as_rational`). Node: `{kind: "rational", numerator: + bigint, denominator: bigint}` with `rational()`, `formatRational()`, `rationalToNumber()` and + `rationalOfDouble()`; generated classes declare `RationalValue` (`asRational`). Java and Rust + carry an exact numerator/denominator pair, Julia `Rational{BigInt}`, MATLAB a struct of decimal + strings. Each declares `rational_values` and refuses a noncanonical encoding. +- **RDF** (`internal/translate/export/rdf_expr.go`). A literal is written as the exact Rational it + denotes: a decimal token as `xsd:decimal`, an exponent token binary64 holds exactly as + `xsd:double`, any other exponent token as its exact `xsd:decimal` (`1E-1` → `"0.1"^^xsd:decimal`). + An imported `xsd:double`/`xsd:float` is the binary value it names. The round trip is exact. +- **SMT** (`internal/exec/solve`). The encoding was always exact; now the evaluator is too for + Integer and Rational arithmetic, so replay (`solve/replay.go`) replays it exactly and only + arithmetic a binary64 Real takes part in is replayed, and marked rounded (`Query.Rounded`, + `RoundingSound`), as binary64. A declared `Real` variable is binary64 (`Var.Binary64`). +- **Compiled calcs** (`internal/translate/codegen/rational.go`, `sysml -compile`). Compiled code + holds numbers as `int64` and binary64, so it compiles exact Rational arithmetic only where one + binary64 rounding gives the exact answer: constants are folded exactly, a single operation over + values binary64 holds exactly is compiled with guards, an Integer quotient compared with a whole + number is compared exactly. Anything else — a `Rational` parameter, `a * 0.1` over a variable `a`, + `(a * 0.5) ** 2`, `a / 3 < 0.1`, collection operations over exact Rationals — is refused at + compile time with a typed error naming the construct, never compiled to a rounding. + +## Comparing a Rational with a binary64 Real + +With exact Rationals and binary64 Reals, `attribute x : Real = 0.1; x == 0.1` compares the double +nearest 1/10 with 1/10 itself. KerML has Rational ⊂ Real and so asks for the mathematical +comparison; but the binary64 Real is this implementation's approximation, not the text's, and +the text does not say how an approximate Real meets an exact Rational. Two readings: + +- **Exact comparison** (implemented): `x == 0.1` is `false` and `x > 0.1` is `true`, as the + Integer precedent `CompareIntReal` compares a large Integer with a Real exactly. +- **Round the Rational once**, as mixed arithmetic does: `x == 0.1` is `true`. + +The choice is recorded as open; the implementation follows the Integer precedent until it is made. + +## A Rational a double holds, on input + +A Rational binary64 holds exactly (`1/4`) crosses the wire as `real_value`, so a client cannot +tell the service that a `0.25` it sends is the Rational rather than the Real, and the service +reads it as a Real: `in x : Rational; x + 1 / 3` with `x` sent as `0.25` is binary64 +`0.5833333333333333`, while `x` sent as the `rational_value` `1/3` gives the exact `2/3`. KerML +says nothing about a wire. Three readings: + +- **An inbound `real_value` is always a Real** (implemented): the encoding decides. +- **The declaration decides**: a binary64 value held by a feature or parameter declared + `Rational` is the exact Rational equal to it, as a `Real` declaration already rounds. This + changes in-model semantics too. +- **Wire only**: clients send every exact Rational as `rational_value` to a service with + `rational_values`, which then accepts a binary64-exact `rational_value` on input while + answering canonically. + +The choice is recorded as open; the implementation reads the encoding until it is made. + +## The pilot differs by design + +The pinned pilot (`jupyter-sysml-kernel`, provisioned by `scripts/download-pilot-validator.sh` + +`scripts/download-pilot-evaluator.sh`) holds a `LiteralRational` as a Java `double` +(`LiteralRationalImpl.value`: `protected double value`). Its answers, verbatim, beside ours: + +| Case | Pilot answer | OpenSysML | +|------|--------------|-----------| | `0.1 + 0.2` | `LiteralRational 0.30000000000000004` | `0.3` | | `0.1 + 0.2 == 0.3` | `LiteralBoolean false` | `true` | | `0.1 + 0.2 <= 0.3` | `LiteralBoolean false` | `true` | | `0.3 < 0.1 + 0.2` | `LiteralBoolean true` | `false` | -| `0.1 + 0.2 - 0.3` | `LiteralRational 5.551115123125783E-17` | `0` | +| `0.1 + 0.2 - 0.3` | `LiteralRational 5.551115123125783E-17` | `0.0` | | `(1.0 / 49.0) * 49.0 == 1.0` | `LiteralBoolean false` | `true` | | `(1.0 / 3.0) * 3.0 == 1.0` | `LiteralBoolean true` (double rounding happens to land on 1.0) | `true` | | `0.1` ten-fold sum `== 1.0` | `LiteralBoolean false` | `true` | -| `1.0 / 3.0`, `1 / 3` | `LiteralRational 0.3333333333333333` | an exact third | - -`5.551115123125783E-17` is exactly the binary64 value of `0.1 + 0.2 - 0.3`; every row is -the IEEE 754 double answer, none is the exact-rational one. The evidence is structural -too: in the pinned jar, `LiteralRationalImpl.value` is a Java `double` -(`javap`: `protected double value; public double getValue(); public void setValue(double)`), -so a rational literal is rounded to binary64 at parse time, before any arithmetic runs. -(Its integer literals are narrower still: `9007199254740993` answers -`ERROR:For input string: "9007199254740993"` — a Java 32-bit `parseInt`, the limit the -division work had already recorded.) - -OpenSysML today answers **identically on every one of these probes** (`0.30000000000000004`, -`false`, `false`, `true`, `5.551115123125783e-17`, `false`, `true`, `false`, -`0.3333333333333333`). An exact-rational evaluator would therefore not close a gap with -the reference — it would **open one**, flipping the observable answer of every probe row -above against the pilot, and `tools/referee/exec` would report each as a disagreement. -This inverts the premise of the change: the evaluator's binary64 arithmetic *is* the -reference behavior. - -## What the specification requires: nothing about precision - -KerML 1.0 (formal/2025-02-01; the 1.1 RTF has not published changes to these clauses) -describes the data types mathematically: - -- §9.3.2.2.8 Rational: "Rational is the type of rational numbers, extended with values - for positive and negative infinity." -- §9.3.2.2.9 Real: "Real is the type of mathematical (extended) real numbers. This - includes both rational and irrational numbers, and values for positive and negative - infinity." -- §8.3.4.8.13 LiteralRational: the abstract-syntax `value` attribute is typed `Real` - ("The value whose rational approximation is the result of evaluating this - LiteralRational"), and §8.4.4.9.2 notes that "only the rational-number subset of the - real numbers can be represented using a finite literal. So the result of a - LiteralRational is actually always classified in the KerML DataType Rational." - -That is a statement about what the *values are*, not about what arithmetic an -implementation must perform. The Kernel Function Library (§9.4; `RealFunctions.kerml`, -`RationalFunctions.kerml`) declares only signatures — `function '+' … in x: Real[1]; in -y: Real[0..1]; return : Real[1];` — with no precision, rounding, or exactness clause, -and no conformance clause elsewhere in the specification constrains numeric precision. -The spec is **silent on precision**; the reference implementation chose binary64. There -is no clause an exact-rational evaluator could point to as mandating it, and the only -executable oracle contradicts it. - -## What the change would touch - -For completeness of the adjudication, the blast radius of a `big.Rat`-backed (or -exact-until-formatted) `Real`/`Rational` value, mapped concretely: - -- `internal/semantic/semantics`: `Value` carries `Real float64` (`eval.go`); the constant - folder's `evalRealArith`/`RealArith`, `IntQuotient`, `Pow`, comparisons and equality, - and the numeric-widening lattice all move to a rational representation. -- `internal/exec/runtime`: the evaluator (`eval.go`, `toReal`), `value.go` - (`FormatReal` and all printing), `library_functions.go` (34 `math.*` call sites — - `sqrt`, trig, `floor`/`round`, `exp`/`ln` — which have no exact form), quantities and - unit scaling, collections, overflow handling; 69 `float64` sites in the package. -- Wire and clients: `api/proto/sysml.proto` carries `double real_value`, - `double real_magnitude`, unit `scale_num`/`scale_den`, `double exponent`; an exact - value needs a new wire form and migrations in the Go service and the Python client - (`Real, Rational → float` is a documented mapping in the Python API reference). -- Fixtures: conformance `.expected.json` files and golden traces that print reals, the - REPL output contract, and the pilot execution referee's normalization (which today - matches the pilot digit-for-digit because both sides are binary64). -- The solver seam: `solve/replay.go` — the witness replay and `Query.Rounded` marking — - encodes the evaluator's rounding; an exact evaluator rewrites that contract and its - tests (`TestSolvedWitnessRejectedByEvaluatorIsUndecided`, - `TestRoundedMarksFloatComputingQueries`, …), the very behavior the soundness work - just pinned down. -- The irrational remainder: `sqrt`, trig, `**` with a fractional exponent and the - transcendentals cannot be exact, so the result is necessarily a **hybrid** — exact - for the field operations, rounded for irrational functions — and any query touching - an irrational operation re-enters exactly the rounded-query incompleteness this - change was meant to remove. Exactness only ever covers the rational-closed fragment. - -## What it would cost, measured - -Microbenchmarks on this machine (Go 1.23, `math/big`; float64 loop vs the equivalent -`big.Rat` loop, reusing allocated `Rat`s): - -| Workload | float64 | big.Rat | Ratio | -|----------|---------|---------|-------| -| mul + quo + add per iteration | 0.33 ns/op | 731 ns/op | ~2,200× | -| accumulating sums of distinct small fractions | 0.30 ns/op | 10,380 ns/op | ~35,000× | - -The second row is the structural problem, not just a constant factor: exact rational -accumulation grows denominators without bound (the running sum's denominator tends -toward the lcm of everything added), so operand size — and per-operation cost and -memory — grows with the computation. A simulation loop accumulating quantities is the -runtime's hot path. - -## The options, compared - -| Option | Solver agreement bought | Pilot agreement | Cost | -|--------|------------------------|-----------------|------| -| (a) Full exact-rational values | Closes the gap on the rational-closed fragment only; irrational ops re-open it | **Diverges** on every probe row above | Every package listed above, a wire-format migration, fixture rewrites, 3–4 orders of magnitude on numeric hot paths | -| (b) Exact-rational only where it closes the solver gap, binary64 elsewhere | Same fragment as (a) | Diverges wherever the exact path is used | The same expression yields different values depending on whether a solver looks at it — a worse contract than either uniform choice | -| (c) Keep binary64 + sound-but-incomplete verdicts (status quo) | Sound verdicts; rounded queries stay undecided | **Agrees** (verified digit-for-digit on the probes) | Zero; already landed and tested | -| (d) Narrow `Query.Rounded` by proving exactness per term | Recovers decided verdicts for provably-exact float64 computations (dyadic constants, small-magnitude sums) without touching the value representation | Agrees (no evaluator change) | Contained in `solve/replay.go` `roundedTerm`; subtle — a term over free Real variables can round on some witness values and not others, so unmarking is only sound for terms whose *whole value set* is exact | - -## Decision - -**Declined — the evaluator stays binary64; option (c) stands.** The reference -implementation computes in binary64 (behaviorally and structurally verified above), the -specification is silent on precision, and this project's conformance posture is -agreement with the pinned pilot wherever it can speak. An exact-rational evaluator -would diverge from the reference on observable answers, cost measured orders of -magnitude on hot paths, force a hybrid contract that re-admits the same solver gap at -every irrational operation, and rewrite the wire format and the just-landed soundness -seam. The incompleteness it would remove is narrow (rounded queries answer undecided -rather than wrong) and already documented as the deliberate trade. - -Option (d) is recorded as the one refinement worth holding open: it narrows the -undecided surface without moving the value contract or the pilot agreement, and it is -contained in one function — but it is subtle enough (exactness must hold for a term's -whole value set, not one witness) that it should wait until the undecided verdicts are -observed to bite in practice. - -## RationalFunctions::rat, numer and denom over a binary64 Rational - -The Kernel Function Library declares three functions that presuppose an exact ratio: +| `1.0 / 3.0`, `1 / 3` | `LiteralRational 0.3333333333333333` | `1/3` | + +Every pilot row is the binary64 answer, `5.551115123125783E-17` being exactly the double `0.1 + +0.2 - 0.3`; every OpenSysML row is the rational one §9.3.2.2.8 and §8.4.4.9.2 define. (The pilot's +integer literals are narrower still: `9007199254740993` answers `ERROR:For input string: +"9007199254740993"`, a 32-bit `parseInt`.) The probes are committed as +`tools/referee/exec/testdata/cases/exact_rationals.cases`, marked `by-design` with those clauses: +the five whose answers differ land in the [execution referee's](pilot-execution-referee.md) +`differs-by-design` bucket, the clause recorded beside both raw outputs, and the other five agree +(the referee compares reals to two decimal places). None is hidden or normalized away. + +## RationalFunctions::rat, numer and denom + `rat(numer: Integer, denum: Integer): Rational`, `numer(rat: Rational): Integer` and -`denom(rat: Rational): Integer`. They were registered as unevaluable, citing this record, -and that verdict was re-adjudicated: not whether to make the evaluator exact — the -decision above stands — but what the three should compute *given* a binary64 Rational. +`denom(rat: Rational): Integer` presuppose an exact ratio, and now have one: + +- `rat(n, d)` is the Rational `n/d` in lowest terms, the value `n / d` computes: `rat(1, 3)` is + `1/3`, `rat(6, 4)` is `1.5`, `rat(1, 0)` is the typed division-by-zero error `1 / 0` reports, + never an infinity. The library's `RationalFunctions::sum`/`product` bodies start from + `rat(0, 1)`/`rat(1, 1)`. +- `numer(x)` and `denom(x)` are the numerator and positive denominator of `x` in lowest terms; an + Integer is itself over `1`. `numer(0.1)` is `1` and `denom(0.1)` `10`, `numer(rat(1, 3))` is + `1` and `denom(1.0 / 3.0)` `3`, `denom(0.0001)` is `10000`, and `rat(numer(x), denom(x)) == x` + holds for every finite `x`. A binary64 Real argument is the Rational it is exactly + (`big.Rat.SetFloat64`); an infinity or NaN has no ratio and is `semantics.ErrArithmeticDomain`. -The pinned pilot (`jupyter-sysml-kernel` 0.60.1) was probed first, each call written both -qualified at the prompt and as the value of a model attribute. Its answers, verbatim -(UUIDs elided), are all the unevaluated invocation — it has no implementation of any of -the three: +The pilot implements none of the three. Each call, qualified at the prompt and as an attribute's +value, answers the unevaluated invocation (UUIDs elided): | Case | Pilot answer | |------|--------------| @@ -183,91 +241,44 @@ and finds them unequal, so `rat(1, 3)` is "not equal" even to itself. That is th unevaluated-operand artifact `w6d:complex-is-zero-qualified` records in the [execution referee](pilot-execution-referee.md). `RationalFunctions::abs`, `floor` and `'/'` called by name are unevaluated too; only operator syntax folds. So the pilot cannot -referee any of the three, the standing decision is not contradicted (nothing here shows an -exact numerator/denominator pair for `1/3`), and the semantics are self-assessed. The +referee any of the three, and the semantics are self-assessed. The probes are committed as `tools/referee/exec/testdata/cases/rational_terms.cases`, where every call lands in `pilot-unevaluated` and the operator quotient agrees. -**Adjudicated: implement all three over the binary64 the runtime already holds.** - -- `rat(numer, denum)` is the binary64 quotient — the value `RationalFunctions::'/'` and - the `/` operator compute, the exact Integer ratio rounded once (`semantics.IntQuotient`). - `rat(1, 0)` is the typed division-by-zero error `1 / 0` reports, never an infinity. The - library's own `RationalFunctions::sum`/`product` bodies start from `rat(0, 1)`/`rat(1, 1)`, - which is one more reason the constructor must have a value. -- `numer(rat)` and `denom(rat)` are the exact numerator and positive denominator, in - lowest terms, of the rational the binary64 *is* (`math/big.Rat.SetFloat64`, which is - exact for every finite double); an Integer is itself over `1`. So `numer(0.75) = 3`, - `denom(0.75) = 4`, `numer(-0.75) = -3`, a whole value reads as `n/1` (`numer(2) = 2`, - `denom(2.0) = 1`, `denom(0.0) = 1`), and `rat(numer(x), denom(x)) == x` holds exactly - for every finite `x` whose terms are Integers. A term beyond `int64` is the exact Integer it - is, KerML Integers being unbounded — `denom(0.0001)` is `2^66 = 73786976294838206464`, - `numer(1.0e19)` is `10000000000000000000`; an infinity or NaN has no finite ratio and is - `semantics.ErrArithmeticDomain`. - -**The binary64 consequence, stated plainly.** A Rational here holds the double nearest -the value written, so `numer`/`denom` answer the terms of that double, not of the -rational the model meant: `numer(rat(1, 3))` is `6004799503160661` and `denom(rat(1, 3))` -is `2^54 = 18014398509481984`; `numer(0.1)` is `3602879701896397` and `denom(0.1)` is -`2^55 = 36028797018963968`. The pure-spec answers — `1` and `3`, `1` and `10` — need an -exact Rational value kind, which this record declines. The 16-digit numerator is the same -class of artifact as `0.1 + 0.2 != 0.3` in the probe table at the top: a faithful -reading of the binary64 the reference implementation also stores (`LiteralRationalImpl.value` -is a Java `double`), accepted as pilot parity rather than hidden behind a decimal-derived -pair that `rat` could not reproduce. The alternatives were weighed and set aside: the -reduced pair of the shortest decimal rendering (`numer(0.1) = 1`, `denom(0.1) = 10`) would -make `rat(numer(x), denom(x))` a different double from `x` for most `x`, breaking the one -identity the three functions owe each other; keeping them unevaluable would leave the -library's `sum`/`product` bodies without a starting value and report a typed error where -the runtime can honestly compute one. - -## Option (d) measured: the rounded-query census - -Whether the narrowing is worth building is an empirical question — how many queries -does the conservative `Query.Rounded` marker sweep in that are in fact provably exact? -`TestRoundedCensus` (`internal/exec/solve/rounded_census_test.go`) answers it -reproducibly: it enumerates every constraint, requirement and analysis case in the -repository's solver-facing corpora, translates each through the same -`Condition`/`Analysis` path the REPL's `%check`/`%solve`/`%configure all`/`%optimize` -commands use, and classifies every translated query. + +## Cost + +COST_PLACEHOLDER + +## The rounded-query census + +The solver reasons over SMT-LIB's exact `Real` sort, so a query is marked rounded +(`Query.Rounded`) where the evaluator rounds: an exact-real `unsat` about it is reported +undecided, and `%configure all` and `%optimize` decline the completeness claim (see +[spec-compliance](spec-compliance.md#exact-reals-against-a-rounding-evaluator--what-agreement-is-claimed)). +With exact Rationals only arithmetic a binary64 `Real` takes part in still rounds. +`TestRoundedCensus` (`internal/exec/solve/rounded_census_test.go`) translates every constraint, +requirement, satisfaction assertion and analysis case in the repository's solver-facing corpora +through the path `%check`/`%solve`/`%configure all`/`%optimize` use, and counts the marked ones: ``` OPENSYSML_SMT=/usr/bin/z3 go test -count=1 -run TestRoundedCensus -v ./internal/exec/solve OPENSYSML_SMT=/usr/local/bin/cvc5 go test -count=1 -run TestRoundedCensus -v ./internal/exec/solve ``` -A marked query is *recoverable* only if every asserted or optimized term is exact over -its **whole value set**: exact-float64 real literals, and real-valued arithmetic only -when it folds to a constant whose every intermediate is exactly representable. -Anything over free variables — real arithmetic, division, integer→real widening — -stays conservative, because a witness-dependent value set cannot be proven exact -statically. - -Counted (both solvers give identical numbers): the training corpus, the three pilot -corpora, the runtime conformance fixtures, the repository examples and manual -examples, the solver test fixtures, and the bundled standard library — 890 files, -2,170 candidate elements, 415 translated queries. Excluded: elements the translator -refuses (unsupported operations, unresolved names), which never reach the solver and -so never see the marker. - -| Corpus | Files | Translated queries | Marked rounded | Provably exact | -|--------|-------|--------------------|----------------|----------------| -| Training corpus | 100 | 13 | 2 | 0 | -| Pilot corpora | 212 | 82 | 8 | 0 | -| Conformance fixtures | 444 | 17 | 0 | 0 | -| Examples + manual | 27 | 56 | 4 | 0 | -| Solver fixtures | 10 | 54 | 7 | 0 | -| Standard library | 97 | 193 | 0 | 0 | -| **Total** | **890** | **415** | **21** | **0** | - -Every one of the 21 marked queries is genuinely inexact: 4 contain a real literal with -no exact float64 (e.g. a `0.4` efficiency, a degree-unit scale factor), and 17 perform -real arithmetic, division, or integer widening over free variables — value sets no static -analysis can prove exact. The false-undecided rate over the real corpus is **zero**. - -**Adjudication: option (d) stays unbuilt.** The narrowing would recover no verdict in -any realistic model in the repository; the recoverable class (constant-folded dyadic -arithmetic asserted directly) does not occur in practice, because models constrain -free attributes, not constants. The census harness remains as the reproducible -instrument: re-run it if future corpora accumulate undecided verdicts, and revisit -only if it reports a material provably-exact population. +| Corpus | Files | Translated queries | Rounded, binary64 Rationals | Rounded, exact Rationals | +|--------|-------|--------------------|-----------------------------|--------------------------| +| Training corpus | 100 | 18 | 3 | 0 | +| Pilot corpora | 213 | 95 | 9 | 2 | +| Conformance fixtures | 1,361 | 17 | 0 | 0 | +| Examples + manual | 53 | 177 | 17 | 3 | +| Solver fixtures | 10 | 65 | 7 | 1 | +| Standard library | 107 | 203 | 0 | 0 | +| **Total** | **1,844** | **575** | **36** | **6** | + +Both solvers give the same counts. Thirty of the 36 queries a binary64 Rational marked were +rounded only by a decimal literal or an Integer quotient, which is now exact, so their `unsat` +verdicts are reported as verdicts. The six left are genuinely binary64 — arithmetic over a +feature declared `Real` (`powerMargin` in `examples/verdicts-demo/rover.sysml`, `pwr` and `acc` in +the pilot's `HSUVDynamics.sysml`) or a quotient of one (`MassPerCrate` in the solver fixtures) — +and none of them is provably exact. diff --git a/docs/project/native-compilation.md b/docs/project/native-compilation.md index a4b73ef356..aef88a3037 100644 --- a/docs/project/native-compilation.md +++ b/docs/project/native-compilation.md @@ -43,7 +43,7 @@ A calc compiles when everything it reaches is in this subset: | Construct | Compiled as | |---|---| -| `in` parameters typed `Integer`, `Natural`, `Positive`, `Real`/`Rational`, `Boolean`, with no multiplicity or `[1]` | Integer: `int64_t` in C, an `int64` promoted to `math/big` in Go ([Integers](#integers)); `double` / `bool` | +| `in` parameters typed `Integer`, `Natural`, `Positive`, `Real`, `Boolean`, with no multiplicity or `[1]` | Integer: `int64_t` in C, an `int64` promoted to `math/big` in Go ([Integers](#integers)); `double` / `bool` | | The same types with any multiplicity (`[0..*]`, `[2..3]`, `[0..1]`, …), as parameters, results and body-local attributes | a sequence of the element type with its shape (null, one value, many); the bounds are checked where the interpreter checks them, and a sequence bound to a feature not declared `nonunique` is refused where it repeats a value, with the interpreter's `uniqueness violation` reason and positions | | Result: the body's trailing expression, or `return : T = ;` | function result | | `attribute x : T;` with no value | null, until assigned | @@ -124,13 +124,23 @@ arithmetic rather than the host language's: - **Integer** is unbounded, as the interpreter's is ([Integers](#integers)): Go computes it exactly, C refuses at compile time a calc whose Integer result may leave `int64`. `/` and `%` by zero are errors. -- **Integer `/`** is the exact rational quotient rounded once to binary64, as the interpreter's - `IntQuotient` does — `7 / 2` is `3.5`, `1 / 3` is `0.3333333333333333`, and - `9007199254740993 / 1` rounds the way the interpreter rounds. C does this with `__int128` - remainder refinement; Go uses `math/big.Rat`. +- **Rational** is exact in the interpreter (`exact-rational-evaluation.md`) and binary64 in + generated code, so a calc compiles only where binary64 gives the interpreter's answer. A + `Rational` parameter, result or attribute is refused (`type ScalarValues::Rational is not + Integer, Real or Boolean`). Exact arithmetic between Rational literals is folded at compile + time when the result is a double (`0.1 + 0.2` compiles as `0.3`); one operation over values + binary64 holds exactly is one correct rounding, guarded at run time where an Integer operand + might not be held; and an Integer `/` whose quotient reaches a `Real` declaration is the + exact quotient rounded once there, as the interpreter's Real declaration rounds it (`7 / 2` is + `3.5`, `1 / 3` is `0.3333333333333333`; C refines with `__int128`, Go uses `math/big.Rat`). + A comparison between a Real and a Rational literal compares exactly, through the literal's + nearest double and the side the Rational lies on, and an Integer quotient against a whole + number compares exactly (`a / 3 >= 2`). Anything else is refused, naming the + construct: `exact Rational arithmetic '*' over a value binary64 does not hold exactly`, + `'**' of an exact Rational by an Integer exponent`, `'<' of an exact Rational binary64 does + not hold exactly`. - **Real** is binary64 and every result is checked finite; `1.0 / 0.0` and `1e308 * 10.0` are - errors, not `inf`. `0.1 + 0.2` prints `0.30000000000000004`, exactly as the interpreter - (see `exact-rational-evaluation.md`; no exact arithmetic is introduced here). + errors, not `inf`. - **Mixed** Integer/Real operands widen the Integer in arithmetic. A comparison between them is exact, as the interpreter's `CompareIntReal`: `9007199254740993 > 9007199254740992.0` holds although the Integer rounds to that Real. diff --git a/docs/project/pilot-execution-referee.md b/docs/project/pilot-execution-referee.md index 6683ce1fdb..be35ad3bbf 100644 --- a/docs/project/pilot-execution-referee.md +++ b/docs/project/pilot-execution-referee.md @@ -186,9 +186,9 @@ No compliance row's status flag is changed on the strength of this work. - **Reals are compared to two decimal places**, a tolerance of the harness itself (both sides are rounded in `tools/referee/exec`, `roundedReal`). A divergence below 2dp is invisible to - this harness. It is no longer a display limit — we now print `1.0 / 3.0` as - `0.3333333333333333`, the same digits the pilot reports as `LiteralRational`, so the - tolerance can be tightened on its own once the buckets it moves are adjudicated. + this harness. Our Rationals are exact (`1.0 / 3.0` prints `1/3`) and the pilot's are binary64, + so the harness rounds both to compare them; the tolerance can be tightened on its own once + the buckets it moves are adjudicated. - **A disagreement the specification decides is reported in its own bucket.** A `by-design:` line in a `.cases` file names the clause the cases after it follow; a case so marked that would bucket `disagree` buckets `differs-by-design` instead, and the report carries the clause beside diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 943b049a15..39c68957fb 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -1572,13 +1572,21 @@ restores library symbols without their AST). | **Domain library** `VectorCalculations` (16): the ten over a numeric vector compute as their `VectorFunctions` counterparts (`inner`, `norm`, `angle` over vector quantities answer the declared `Number`, the magnitude over the `num` components with the unit dropped by declaration — drafted in `omg-issues.md`), and `scalarQuantityVectorMult`, `vectorScalarQuantityMult`, `vectorScalarQuantityDiv` answer a **vector quantity** (`ValVectorQuantity`, `⟨2.0, 4.0⟩ [m]`) whose unit is composed by the scalar quantities' own rule; the five `MeasurementRefCalculations` over a scalar unit (`'*'`, `'/'`, `'**'`, `'^'`, `ToString`) compute over the measurement reference value; **`TensorCalculations`** (13): `'['` over a model-declared `TensorMeasurementReference`, `'+'`, `'-'`, the four scalar multiplications, `isZeroTensorQuantity` and `isUnitTensorQuantity` (square order two) compute over a **tensor quantity** (`ValTensorQuantity`, `Tensor(2, 2)[1.0, 2.0, 3.0, 4.0] [Pa]`) and over the scalar and vector quantities the subtype chain admits; `'CoordinateFrame*'`/`'CoordinateFrame/'`, `VectorCalculations::'['` and `transform` compute over the **coordinate frame** value (`ValCoordinateFrame`); `outer`, and `TensorCalculations`' three products and `transform`, report themselves by name, each with the reason (*Structured values* above) | `runtime/quantity_functions.go` `registerVectorCalculations`, `registerMeasurementRefCalculations`, `registerTensorCalculations`; `runtime/measurement_ref_functions.go`; `runtime/vector_functions.go` `scaleVectorQuantity` over `quantity.go` `scaleQuantities`; `runtime/tensor.go`, `runtime/tensor_functions.go` | `TestVectorCalculations`, `TestQuantityCalculationsReport`, `TestQuantityCalculationsAreAllDispatchable`, `TestMeasurementRef*`, `TestTensorQuantity*`, `semantics/tensor_type_test.go`, `passes/typecheck_tensor_test.go`; conformance `calc_library_vector_quantity`, `calc_library_vector_quantity_norm`, `instance_measurement_references`, `instance_tensor_quantity`, `instance_tensor_quantity_failures`, `calc_library_unevaluable_function` (`TensorCalculations::tensorTensorMult`); robustness `library_function_errors` (vector-quantity rows), `measurement_reference_failure_modes`, `tensor_quantity_failure_modes` | ⚠️ Approximate (below) | | A declaration this runtime has no representation for the values of reports itself by name (`ErrUnevaluableLibraryFunction`), never a wrong result | `runtime/library_functions.go` `registerUnevaluable` | `TestUnevaluableLibraryFunctionsNameThemselves`, `TestVendoredFunctionsAreAllDispatchable` (every vendored declaration of these packages either computes or names itself); conformance `calc_library_unevaluable_function` | ✅ Faithful | | **Every declaration of every vendored Kernel Function Library package is dispatchable by name** — the 17 packages `BaseFunctions`, `BooleanFunctions`, `CollectionFunctions`, `ComplexFunctions`, `ControlFunctions`, `DataFunctions`, `IntegerFunctions`, `NaturalFunctions`, `NumericalFunctions`, `OccurrenceFunctions`, `RationalFunctions`, `RealFunctions`, `ScalarFunctions`, `SequenceFunctions`, `StringFunctions`, `TrigFunctions`, `VectorFunctions`, and `OpenSysMLMathFunctions`. Each calc or function declaration, operator-named ones included, is registered as a value function (with the declared parameter names in declared order), as a builtin, or as unevaluable by its own name with a reason; no package and no name is skipped | `runtime/library_functions.go` `libraryFunctions`, `runtime/builtins.go` `builtins`, `runtime/library_unevaluable.go` | `TestVendoredFunctionsAreAllDispatchable` (reads the vendored `.kerml`, so a declaration the library adds fails the gate until it is registered) | ✅ Faithful | -| Conversions: `BaseFunctions::ToString` (`x: Anything[0..1]`: the notation a value is written with — a Boolean, number or String literal, `*` for the unbounded value, an enumeration literal by name, a quantity with its unit as `1.5 [m]`; an omitted or null `x` reads `"null"`; two or more values are `ErrMultiplicityViolation`; an instance, a variant or a body has no notation and is `ErrTypeMismatch`; a Complex stays unevaluable, see below), `BooleanFunctions::ToString`/`ToBoolean`, `IntegerFunctions::ToString`/`ToInteger`/`ToNatural`, `NaturalFunctions::ToString`/`ToNatural`, `RationalFunctions::ToString`/`ToInteger`/`ToRational`, `RealFunctions::ToString`/`ToInteger`/`ToRational`/`ToReal`. A String is parsed as the type's literal notation — decimal digits for an Integer, `true`/`false` for a Boolean, a decimal with an optional fraction and exponent for a Real or Rational — and a String that is no such notation is `ErrInvalidNotation`, never 0 or a panic (`"NaN"`, `"Inf"`, a hex float and a padded `" 1.5 "` are refused too — the notation is the whole String, nothing is trimmed). A negative given to `ToNatural` is `semantics.ErrArithmeticDomain`; an Integer is unbounded, so a String of any number of digits or a whole Real beyond `int64` converts to the exact Integer it spells (`ToInteger("123456789012345678901234567890")`, `ToInteger(1.0e20)` is `100000000000000000000`); a Real notation float64 cannot hold — one that overflows to an infinity (`"1e400"`) or a nonzero one that underflows to zero (`"1e-400"`), as a literal or a String — is `semantics.ErrArithmeticOverflow`, never `0.0`. `ToInteger` of a Real or Rational truncates toward zero: the vendored declarations carry no body and the library declares `floor` and `round` beside them, so a rounding conversion would duplicate one of those. `RealFunctions::ToRational` of a number answers the Real its `x: Real` parameter binds it as, an Integer widened like every `RealFunctions` form (`ToRational(2) === 2.0`), a Rational being a float64 here; `RationalFunctions::ToInteger` of an Integer keeps it | `semantics/eval.go` `ParseReal` (decimal notation only — `isRealNotation`, any other text `semantics.ErrRealNotation` — shared by the Real literal in `runtime/eval.go` `evalLiteralReal`, `runtime/compile.go`, `codegen/compile.go` and the model-level folder); `runtime/library_conversions.go` `registerConversionFunctions`, `parseReal`, `parseInteger` | `semantics/eval_test.go:TestParseReal`, `:TestParseRealRejectsNonDecimalNotation`; `TestConversionFunctionValues`, `TestConversionFunctionErrors`, `TestRealToStringRoundTrips`; conformance `calc_library_conversions`, `calc_library_to_real_invalid_string`, `calc_library_to_natural_negative`; robustness `named_library_call_that_has_no_value`, `real_literal_that_underflows` | ✅ Faithful | +| Conversions: `BaseFunctions::ToString` (`x: Anything[0..1]`: the notation a value is written with — a Boolean, number or String literal, `*` for the unbounded value, an enumeration literal by name, a quantity with its unit as `1.5 [m]`; an omitted or null `x` reads `"null"`; two or more values are `ErrMultiplicityViolation`; an instance, a variant or a body has no notation and is `ErrTypeMismatch`; a Complex stays unevaluable, see below), `BooleanFunctions::ToString`/`ToBoolean`, `IntegerFunctions::ToString`/`ToInteger`/`ToNatural`, `NaturalFunctions::ToString`/`ToNatural`, `RationalFunctions::ToString`/`ToInteger`/`ToRational`, `RealFunctions::ToString`/`ToInteger`/`ToRational`/`ToReal`. A String is parsed as the type's literal notation — decimal digits for an Integer, `true`/`false` for a Boolean, a decimal with an optional fraction and exponent for a Real or Rational — and a String that is no such notation is `ErrInvalidNotation`, never 0 or a panic (`"NaN"`, `"Inf"`, a hex float and a padded `" 1.5 "` are refused too — the notation is the whole String, nothing is trimmed). A negative given to `ToNatural` is `semantics.ErrArithmeticDomain`; an Integer is unbounded, so a String of any number of digits or a whole Real beyond `int64` converts to the exact Integer it spells (`ToInteger("123456789012345678901234567890")`, `ToInteger(1.0e20)` is `100000000000000000000`); a Real notation float64 cannot hold — one that overflows to an infinity (`"1e400"`) or a nonzero one that underflows to zero (`"1e-400"`), as a literal or a String — is `semantics.ErrArithmeticOverflow`, never `0.0`. `ToInteger` of a Real or Rational truncates toward zero: the vendored declarations carry no body and the library declares `floor` and `round` beside them, so a rounding conversion would duplicate one of those. `RealFunctions::ToRational` of a number answers the exact Rational it is — an Integer or Rational itself as a Rational (`ToRational(2)` is the Rational `2.0`), a finite binary64 Real the rational number that double is, an infinity or NaN `semantics.ErrArithmeticDomain`; `RationalFunctions::ToInteger` of an Integer keeps it | `semantics/eval.go` `ParseReal` (decimal notation only — `isRealNotation`, any other text `semantics.ErrRealNotation` — shared by the Real literal in `runtime/eval.go` `evalLiteralReal`, `runtime/compile.go`, `codegen/compile.go` and the model-level folder); `runtime/library_conversions.go` `registerConversionFunctions`, `parseReal`, `parseInteger` | `semantics/eval_test.go:TestParseReal`, `:TestParseRealRejectsNonDecimalNotation`; `TestConversionFunctionValues`, `TestConversionFunctionErrors`, `TestRealToStringRoundTrips`; conformance `calc_library_conversions`, `calc_library_to_real_invalid_string`, `calc_library_to_natural_negative`; robustness `named_library_call_that_has_no_value`, `real_literal_that_underflows` | ✅ Faithful | | **`ToString` of a Real** is the shortest decimal that reads back as the same float64 (`FormatReal`: `2.5` → `"2.5"`, `1.0` → `"1.0"`, `0.1 + 0.2` → `"0.30000000000000004"`, `1e21` → `"1e+21"`), the rendering the REPL already prints a Real with, so `ToReal(ToString(x)) == x` for every finite Real. The pinned pilot evaluator was asked for `ToString(1.0)`, `ToString(0.1 + 0.2)`, `ToString(1e21)` and `ToString(2.5)` and answered each with the unevaluated `InvocationExpression ToString` — its `ToString` has no evaluable body — so there is no pilot rendering to match, and the round-tripping one was chosen | `runtime/library_conversions.go` `numberToString` → `runtime/value.go` `FormatReal` | `TestRealToStringRoundTrips`, `TestConversionFunctionValues`; conformance `calc_library_conversions` (`roundTrip`) | ⚠️ Approximate (self-assessed: the pilot does not evaluate it, so the rendering is a decision, recorded here) | -| `RationalFunctions::floor`/`round` delegate to the Real implementations, a Rational being a float64 here (see `docs/project/exact-rational-evaluation.md`); `RationalFunctions::gcd` is the Integer gcd over whole-valued operands, computed exactly (`math/big`) over their magnitudes so any whole Rational is an operand — `gcd(-2^63, 2)` is `2`, `gcd(1.0e20, 6.0e18)` is `2000000000000000000` — and a divisor beyond `int64` is the Integer it is (`gcd(2^63, 0)` is `9223372036854775808`, `gcd(1.0e19, 1.0e19)` is `10000000000000000000`); `gcd(0, 0)` is `0`, and a non-whole operand `semantics.ErrArithmeticDomain`. `RealFunctions::re`/`im`/`arg` answer for a Real as the Complex it is with a zero imaginary part — `re(x)` is `x`, `im(x)` is `0.0`, `arg(x)` is `0.0` or `pi` — rather than being marked unevaluable | `runtime/library_conversions.go` `registerConversionFunctions` (`rationalGCD`, `realPartOfReal`, `imagPartOfReal`, `argumentOfReal`) | `TestConversionFunctionValues`, `TestConversionFunctionErrors`; conformance `calc_library_conversions` | ✅ Faithful | -| `RationalFunctions::rat(numer, denum)` is the binary64 quotient, the same value `RationalFunctions::'/'(numer, denum)` and the `/` operator compute (`semantics.IntQuotient`: the exact Integer ratio rounded once), so `rat(1, 3)` is `0.3333333333333333`, `rat(6, 4)` is `1.5` and `rat(1, 0)` is `ErrDivisionByZero` as `1 / 0` is, never an infinity; a non-Integer operand is `ErrTypeMismatch`. `numer(rat)`/`denom(rat)` read the exact numerator and positive denominator, in lowest terms, of the ratio the binary64 holds (`math/big.Rat.SetFloat64`) — an Integer is itself over `1` — so `numer(0.75)` is `3`, `denom(0.75)` is `4`, `numer(-0.75)` is `-3`, `numer(2)`/`denom(2)` are `2`/`1`, `denom(0.0)` is `1`, and `rat(numer(x), denom(x)) == x` holds for every finite `x` whose terms are Integers (`TestRationalTermsRoundTrip`: negatives, zero, whole values, `0.1`, `1.0 / 3.0`, `pi`, `2^63 - 1`). A term beyond `int64` is the exact Integer it is (`denom(0.0001)` is `73786976294838206464`, 2⁶⁶; `numer(1.0e19)` is `10000000000000000000`), an infinity or NaN `semantics.ErrArithmeticDomain`, each naming the function | `runtime/library_conversions.go` `integersToRational`, `rationalNumerator`, `rationalDenominator`, `exactRationalTerm` | `TestConversionFunctionValues`, `TestConversionFunctionErrors`, `TestRationalTermsRoundTrip`, `TestVendoredFunctionsAreAllDispatchable`; conformance `calc_library_rational_terms`, `calc_library_rational_zero_denominator`, `calc_library_rational_denominator_beyond_int64`; robustness `named_library_call_that_has_no_value`; pilot-exec-diff `rational_terms.cases` (`rat-third`, `numer-tenth`, … all `pilot-unevaluated`; `quotient-by-operator` agrees) | ⚠️ Approximate, by design: a Rational is a binary64 here, so `numer`/`denom` answer the terms of the *double nearest* the written value, not of the rational the model wrote — `numer(rat(1, 3))` is `6004799503160661` over `2^54`, `numer(0.1)` is `3602879701896397` over `2^55` (`36028797018963968`) — the same class of artifact as `0.1 + 0.2 != 0.3`, accepted as parity with the pinned pilot, which stores every `LiteralRational` as a Java `double`. The pure-spec answer (`numer(rat(1, 3))` is `1`) needs an exact Rational value kind, which [exact-rational-evaluation.md](exact-rational-evaluation.md#rationalfunctionsrat-numer-and-denom-over-a-binary64-rational) declines; that record holds the verbatim probes showing the pilot evaluates none of the three (`InvocationExpression rat`/`numer`/`denom` for every case, `rat(1, 0)` included), so they are self-assessed | +| `RationalFunctions::floor`/`round` are exact over a Rational, answering the Integer their declarations return (`floor(-7/2)` is `-4`, `round(5/2)` is `3`, a half rounding away from zero as `RealFunctions::round` does; `semantics.RatFloor`, `RatRound`), and over a binary64 Real stay the Real implementations; `RationalFunctions::gcd` is the Integer gcd over whole-valued operands, computed exactly (`math/big`) over their magnitudes so any whole Rational is an operand — `gcd(-2^63, 2)` is `2`, `gcd(1.0e20, 6.0e18)` is `2000000000000000000` — and a divisor beyond `int64` is the Integer it is (`gcd(2^63, 0)` is `9223372036854775808`, `gcd(1.0e19, 1.0e19)` is `10000000000000000000`); `gcd(0, 0)` is `0`, and a non-whole operand `semantics.ErrArithmeticDomain`. `RealFunctions::re`/`im`/`arg` answer for a Real as the Complex it is with a zero imaginary part — `re(x)` is `x`, `im(x)` is `0.0`, `arg(x)` is `0.0` or `pi` — rather than being marked unevaluable | `runtime/library_conversions.go` `registerConversionFunctions` (`rationalGCD`, `realPartOfReal`, `imagPartOfReal`, `argumentOfReal`) | `TestConversionFunctionValues`, `TestConversionFunctionErrors`; conformance `calc_library_conversions` | ✅ Faithful | +| `RationalFunctions::rat(numer, denum)` is the exact Rational `numer / denum` in lowest terms (KerML 1.0 §9.3.2.2.8; `rat(Integer, Integer): Rational`), the same value `RationalFunctions::'/'(numer, denum)` and the `/` operator compute: `rat(1, 3)` is `1/3`, `rat(6, 4)` is `1.5`, `rat(6, 3)` the Rational `2.0`, and `rat(1, 0)` is `ErrDivisionByZero` as `1 / 0` is, never an infinity; a non-Integer operand is `ErrTypeMismatch`. `numer(rat)`/`denom(rat)` read the exact numerator and positive denominator in lowest terms — an Integer is itself over `1` — so `numer(rat(1, 3))` is `1`, `denom(0.1)` is `10`, `numer(-0.75)` is `-3`, `numer(2)`/`denom(2)` are `2`/`1`, `denom(0.0)` is `1`, and `rat(numer(x), denom(x)) == x` holds for every finite `x`. A finite binary64 Real is the rational number that double is, so `numer` of a Real `x = 0.1` reads the terms of the double the declaration rounded to (`3602879701896397` over `2^55`); a term beyond `int64` is the exact Integer it is, an infinity or NaN `semantics.ErrArithmeticDomain`, each naming the function | `runtime/library_conversions.go` `integersToRational`, `rationalNumerator`, `rationalDenominator`, `exactRationalTerm` over `semantics/rational.go` `RatArith`, `Value.RatNumer`, `Value.RatDenom` | `TestConversionFunctionValues`, `TestConversionFunctionErrors`, `TestRationalTermsRoundTrip`, `TestVendoredFunctionsAreAllDispatchable`; conformance `calc_library_rational_terms`, `calc_library_rational_zero_denominator`, `calc_library_rational_denominator_beyond_int64`, `calc_exact_rational_arithmetic`; robustness `named_library_call_that_has_no_value`; pilot-exec-diff `rational_terms.cases` (`rat-third`, `numer-tenth`, … all `pilot-unevaluated`: the pilot evaluates none of the three, see [exact-rational-evaluation.md](exact-rational-evaluation.md#rationalfunctionsrat-numer-and-denom)) | ✅ Faithful | +| A decimal or exponent literal (`0.1`, `1.5e3`, `25E-3`) is the exact Rational it denotes (§8.4.4.9.2: a `LiteralRational`'s result is classified in `Rational`), and an Integer-only literal stays an Integer; `0.1 + 0.2 == 0.3` is `true`. An exponent beyond the size budget is `ErrRationalSizeLimit` at evaluation, never an infinity or a rounded double | `semantics/rational.go` `ParseRational`; `semantics/eval.go` (literal folding); `runtime/eval.go` (literal evaluation) | `semantics/rational_test.go:TestParseRationalIsExact`; golden `tests/parser/testdata/parse/exact_rational_literals`; conformance `calc_exact_rational_arithmetic`; robustness `TestRuntimeRobustnessExactRational` (`oversized exponent literal`) | ✅ Faithful | +| Rational arithmetic is exact: `+`, `-`, `*`, `/`, `%` and `**`/`^` with an Integer exponent over Integers and Rationals answer the exact Rational `RationalFunctions` declares, the Integer `/` included (`1 / 3` is `1/3`, `IntegerFunctions::'/'` returning `Rational`); a whole result stays a Rational (`6 / 3` is the Rational `2.0`) unless the declaration returns `Integer` (`+`, `-`, `*`, `%`, `**` over Integers, `floor`, `round`, `numer`, `denom`, `ToInteger`). Comparison and equality are exact (`semantics.CompareRat`); `abs`, `max`, `min`, `sum`, `product` keep exactness. A `*` operand follows the existing `LiteralInfinity` rules | `semantics/rational.go` `RatArith`, `smallRatArith`, `RatPow`, `CompareRat`, `RatNeg`, `RatAbs`; `runtime/eval.go` `arithmeticValues`, `comparisonValues`; `runtime/library_functions.go`, `runtime/collections.go` (aggregation) | `semantics/rational_test.go:TestRationalArithmeticIsExact`, `:TestRationalPower`, `:TestRationalComparisonAndRounding`, `runtime/division_test.go:TestWholeNumberQuotientIsRational`, `:TestQuotientBeyondFloatExactRangeIsExact`; conformance `calc_exact_rational_arithmetic`, `action_exact_rational_accumulation` (trace), `constraint_rational_quotient`, `requirement_rational_quotient`; pilot-exec-diff `exact_rationals.cases` | ✅ Faithful | +| A Rational is held in lowest terms over a positive denominator, inline (`int64` numerator, `uint32` denominator) when it fits and as an immutable `math/big.Rat` otherwise, so each Rational has one representation; a result whose numerator-plus-denominator bits exceed the evaluation's budget (`DefaultMaxIntegerBits`, shared with Integers) is `ErrRationalSizeLimit`, refused before an oversized power is computed and never rounded | `semantics/rational.go` `RatValue`, `FracValue`, `smallRat`, `sizedRat`, `RationalSizeExceeded` | `semantics/rational_test.go:TestRationalNormalization`; robustness `TestRuntimeRobustnessExactRational` (denominator growth, harmonic sum, oversized power) | ✅ Faithful | +| A Rational prints exactly: a terminating decimal as the Real of that value prints (`0.1`, `2.0`, `1.5e-05`), any other as `numer/denom` (`1/3`), in the REPL, JSON, traces and `ToString`; `ToRational` reads both forms. The specification fixes no output form | `semantics/rational.go` `Value.FormatRational`, `formatDecimal`, `ParseRationalText` | `semantics/rational_test.go:TestRationalFormatting`, `:TestParseRationalIsExact` | ✅ Faithful (presentation is an OpenSysML choice) | +| `Real` stays IEEE 754 binary64 (§9.3.2.2.9 leaves precision to the implementation): a value declared `Real` is rounded once to the nearest double, operations with no rational result (`sqrt`, trigonometry, `exp`, `ln`, `**` with a non-Integer exponent, anything typed only `Real`) answer a Real, and mixed Rational/Real arithmetic rounds the Rational once to the nearest double. Mixed Rational/Real *comparison* is exact, as Integer/Real comparison already is (`x : Real = 0.1; x == 0.1` is `false`, the double nearest `1/10` not being `1/10`) — a policy the specification does not settle, recorded in [exact-rational-evaluation.md](exact-rational-evaluation.md#comparing-a-rational-with-a-binary64-real) | `semantics/quantity_eval.go` `RealOf`; `semantics/rational.go` `CompareExactReal`, `ratToFloat`; `runtime/eval.go` `arithmeticValues` | `semantics/rational_test.go:TestRationalComparisonAndRounding`, `semantics/quantity_eval_test.go`; conformance `calc_exact_rational_arithmetic` (`root`) | ⚠️ Approximate — the mixed comparison rule is open | +| Every boundary keeps a Rational exact: gRPC carries one no double holds as `Value.rational_value`/`Quantity.rational_magnitude`/`DocumentValue.rational_value` in lowest terms under the `rational_values` capability (a service without it reports the value unsupported and refuses one sent, as `big_int_values` does; a noncanonical encoding is `ErrRationalNotCanonical`), the JSON engine as `rationalValue`, the Go, Python and Node clients as `Rational`, `fractions.Fraction` and `{numerator, denominator}` bigints, and a Rational a double holds exactly as that Real | `api/proto/sysml.proto` `Rational`; `frontend/protoconv/convert.go` `RationalToProto`, `ProtoToRational`, `ValueCarriesRational`; `frontend/grpc/service.go` `CapabilityRationalValues`; `semantics/rational.go` `CanonicalRational`; `client/opensysml/value.go` `Rational`; `client/python/opensysml/values.py`; `client/node/src/core/values.ts` | `grpc/convert_rational_test.go:TestRationalValueRoundTripsOverTheWire`, `:TestRationalQuantityMagnitudeCrossesExactly`, `:TestRationalValueReadsOnlyCanonicalTerms`, `:TestEvaluateCalcTakesAndReturnsRationals`, `:TestRationalCapability`; `client/opensysml/edges_test.go:TestRationalArithmeticIsExact`; `client/python/tests/test_rational.py`; `client/node/test/rational.test.ts` | ✅ Faithful | +| RDF keeps a literal's exact value: a decimal exports as `xsd:decimal`, an exponent literal as `xsd:double` only when a double holds it exactly and as the exact `xsd:decimal` otherwise, and an imported `xsd:double`/`xsd:float` is read as the binary value it names | `translate/export/rdf_expr.go` `exactRealLiteral`; `translate/export/api_json_in.go` | `tests/export/result_expression_test.go:TestRationalLiteralValuesAreExact` | ✅ Faithful | +| `sysml -compile` compiles exact arithmetic where one binary64 rounding gives the exact result (whole values within `2^53`, Rationals a double holds), folds exact constants, and refuses any other exact Rational operation with a typed error naming the construct, never emitting code that rounds a Rational the interpreter keeps exact; a declared `Real` stays binary64 | `translate/codegen/rational.go` `exactness`, `translate/codegen/ir.go` | `translate/codegen/library_test.go` | ⚠️ Approximate — exact Rationals outside binary64 are refused, not compiled | | `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, the unary sign), `'*'`, `'/'`, `'%'`, `'**'`, `'^'`, `'<'`, `'<='`, `'>'`, `'>='` 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` keep an Integer's kind as `RationalFunctions::abs`/`max`/`min` do (a Rational is a `float64` here, 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 | +| **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, the unary sign), `'*'`, `'/'`, `'%'`, `'**'`, `'^'`, `'<'`, `'<='`, `'>'`, `'>='` 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.0 §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 | | `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` `builtinDeferredParams`, `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 `ErrNoResultExpression`, 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` (`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 | @@ -3210,9 +3218,9 @@ A solver is run as a process speaking SMT-LIB2 on standard input: `OPENSYSML_SMT | The three verdicts stay distinct to the user: `sat`, `unsat`, `unknown`. A timeout, or arithmetic the solver gave up on, is `unknown` with the reason it gave — never reported as either of the others | `solve/solver.go` `Solver.Solve`, `session.verdict`, `session.reasonUnknown`; `repl/check.go` `SolveStatus` | `solve/solver_test.go:TestSolverVerdicts`, `:TestSolverTimeout`, `repl/check_test.go:TestCheckReportsAnUndecidedAnswer` | ✅ | | A solver process failure — crash, non-zero exit, malformed or missing reply, a model naming an undeclared variable — is a typed `SolverProcessError`, distinguished from `unknown` | `solve/solver.go` `session.read`, `session.finish`, `Solver.processError`; `errors.go` `SolverProcessError` | `solve/solver_test.go:TestSolverProcessFailures`, `:TestSolverBadModel`, `repl/check_test.go:TestCheckReportsASolverProcessFailure` | ✅ | | On `sat` the model is read back and rendered in OpenSysML's own terms: the feature's qualified name, a quantity's magnitude in the base unit the query normalized to, an enumeration literal or variant by name — never raw SMT-LIB. A value the notation has no literal for is marked as the solver's own rather than mistaken for one | `solve/model.go` `assign`, `renderValue`; `solve/sort.go` `smtName` | `solve/model_test.go`, `solve/solver_test.go:TestSolverVerdicts` | ✅ | -| Integer `/` and `%` mean what the evaluator means: a whole-number quotient is a Real (`7 / 2` is `3.5`), the remainder truncates toward zero, and for a spread of sign combinations the solved quotient and remainder equal what `internal/exec/runtime` computes for the same expression. The encoding divides the exact `Int` terms and the evaluator divides as an exact rational rounded once to float64 (`semantics.IntQuotient`), so the two agree even for operands beyond 2^53, where rounding each operand first would move the quotient. A quotient float64 cannot represent — `(2^53 + 1) / 2` — is where the exact ratio and the evaluator's rounded answer differ in the last bit; the witness replay and the rounded-query marking below keep that difference out of the verdicts | `solve/term.go` `TruncRem`, `RatioDiv`; `translate.go` `multiplicative`, `remainder` | `solve/agreement_test.go:TestSolvedIntegerDivisionAgreesWithEvaluator`, `:TestSolvedDivisionRejectsEuclideanAnswer`, `:TestSolvedQuotientAgreesBeyondFloatExactRange`, `solve/replay_test.go:TestSolvedHalfUlpQuotientAgreesWithEvaluator` | ✅ | -| A `sat` witness is confirmed against the evaluator's own arithmetic before it is reported: every assignment is replayed through float64 (`semantics.RealArith`, `IntArith`, `IntQuotient`), and a witness the replay rejects — one satisfying the exact rationals but not the rounding evaluator — is reported `unknown` with the reason, never `sat` | `solve/replay.go` `replayWitness`, `replayTerm`, `replayRatio`; `solver.go` `Solver.Solve` | `solve/replay_test.go:TestSolvedWitnessRejectedByEvaluatorIsUndecided`, `:TestSolvedWitnessConfirmedByEvaluatorStaysSat` | ✅ | -| A query whose conditions the evaluator computes in float64 is marked rounded (`Query.Rounded`): an exact-real `unsat` about it does not rule out values the evaluator's rounding would accept, so `%check` and `%solve` report it undecided rather than unsatisfiable, and `%configure all` and `%optimize` decline the completeness claim outright | `solve/replay.go` `Query.Rounded`, `roundedTerm`; `repl/check.go` `roundedUnsatReport`; `repl/synth.go` `roundedNoValuesReport`, `Session.enumerateConfigurations`; `repl/optimize.go` `optimizeQuery` | `solve/replay_test.go:TestRoundedMarksFloatComputingQueries`, `repl/check_test.go:TestCheckLeavesRoundedUnsatUndecided` | ✅ | +| Integer `/` and `%` mean what the evaluator means: a quotient is the exact Rational `IntegerFunctions::'/'` declares (`7 / 2` is `3.5`, `1 / 3` is `1/3`), the remainder truncates toward zero, and for a spread of sign combinations the solved quotient and remainder equal what `internal/exec/runtime` computes for the same expression. The encoding divides the exact `Int` terms and the evaluator divides exactly too, so the two agree for every operand, beyond 2^53 included — `(2^53 + 1) / 2` is `4503599627370496.5` in both | `solve/term.go` `TruncRem`, `RatioDiv`; `translate.go` `multiplicative`, `remainder` | `solve/agreement_test.go:TestSolvedIntegerDivisionAgreesWithEvaluator`, `:TestSolvedDivisionRejectsEuclideanAnswer`, `:TestSolvedQuotientAgreesBeyondFloatExactRange`, `solve/replay_test.go:TestSolvedHalfUlpQuotientAgreesWithEvaluator` | ✅ | +| A `sat` witness is confirmed against the evaluator's own arithmetic before it is reported: every assignment is replayed as the evaluator holds it — Integer and Rational arithmetic exactly (`semantics.IntArith`, `RatArith`), a declared `Real` variable as the binary64 it is (`Var.Binary64`), and arithmetic a Real takes part in through binary64 (`semantics.RealArith`) — and a witness the replay rejects is reported `unknown` with the reason, never `sat` | `solve/replay.go` `replayWitness`, `replayTerm`, `replayArithmetic`, `replayIntDiv`; `solve/reference.go` (`Var.Binary64`); `solver.go` `Solver.Solve` | `solve/replay_test.go:TestSolvedWitnessRejectedByEvaluatorIsUndecided`, `:TestSolvedWitnessConfirmedByEvaluatorStaysSat`, `:TestSolvedExactRationalWitnessStaysSat` | ✅ | +| A query whose conditions the evaluator computes in binary64 — arithmetic a Real takes part in — is marked rounded (`Query.Rounded`), and one over Integers and Rationals alone is not: an exact-real `unsat` about it does not rule out values the evaluator's rounding would accept, so `%check` and `%solve` report it undecided rather than unsatisfiable, and `%configure all` and `%optimize` decline the completeness claim outright | `solve/replay.go` `Query.Rounded`, `roundedTerm`; `repl/check.go` `roundedUnsatReport`; `repl/synth.go` `roundedNoValuesReport`, `Session.enumerateConfigurations`; `repl/optimize.go` `optimizeQuery` | `solve/replay_test.go:TestRoundedMarksFloatComputingQueries`, `:TestSolvedExactRationalWitnessStaysSat`, `solve/rounded_census_test.go`, `repl/check_test.go:TestCheckLeavesRoundedUnsatUndecided` | ✅ | | Division by zero, which the evaluator refuses and SMT-LIB leaves total: a literal zero divisor refuses translation, and any other divisor — integer or real — carries a non-zero side condition, so no model is found by choosing zero | `solve/translate.go` `divisor`, `guard` | `solve/translate_test.go:TestRefusals`, `solve/agreement_test.go:TestDivisorGuardRulesOutDivisionByZero` | ✅ | | `%check ` reports the verdict for a constraint, requirement or satisfaction assertion, and on `sat` the assignment. It is a read: nothing is materialized, and an action or state debugging session keeps running | `repl/check.go` `CheckSolve`, `doCheck`; `repl/meta.go` (command table, dispatch) | `repl/check_test.go` | ✅ Faithful — an experimental surface, kept apart from `%constraint`/`%satisfy`, whose `VerdictStatus` it never collapses into | | An `unsat` verdict is explained by an unsat core: the script names each assertion, cores are turned on, and the core is asked for only once the verdict is `unsat`. Labels are assertion positions, so each core member is the `Assertion` — and `Provenance` — it came from, rather than a table beside the query | `solve/smtlib.go` `CoreScript`, `CoreLabel`, `coreLabelIndex`; `solve/core.go` `session.explain`, `session.unsatCore` | `solve/core_test.go:TestCoreScriptShape`, `:TestCoreLabelsRoundTrip`, `:TestCoreGolden`, `:TestExplainedTwoConditionConflict` | ✅ | @@ -3226,25 +3234,22 @@ A solver is run as a process speaking SMT-LIB2 on standard input: `OPENSYSML_SMT ### Exact reals against a rounding evaluator — what agreement is claimed -The evaluator computes Real arithmetic in IEEE 754 binary64; the translation reasons over SMT-LIB's exact `Real` (rational) sort. Those are different arithmetics, and the difference is reachable, not theoretical — checked against z3 4.8.12 and cvc5 1.3.4: +The translation reasons over SMT-LIB's exact `Real` (rational) sort. The evaluator holds Integers and Rationals exactly (KerML 1.0 §9.3.2.2.8; see [exact-rational evaluation](exact-rational-evaluation.md)), so over them the two arithmetics are the same: `0.1 + 0.2 == 0.3` holds in both, `x * 3.0 == 0.3` is satisfied by `x = 1/10` in both, and `(2^53 + 1) / 2 == 4503599627370496.5` holds in both. A declared `Real` is IEEE 754 binary64, and there the difference is reachable — checked against z3 4.8.12 and cvc5 1.3.4: -- `0.1 + 0.2 == 0.30000000000000004` — the evaluator holds it; the exact encoding is `unsat`. -- `0.1 + 0.2 == 0.3`, `0.1 + 0.2 <= 0.3` — the evaluator rejects them; the exact encoding is `sat`. -- `x * 3.0 == 0.3` — exactly `x = 1/10` satisfies it, a value the evaluator's `0.1 * 3.0` does not confirm. -- `(2^53 + 1) / 2 == 4503599627370496.0` — the evaluator's rounded quotient; the exact encoding is `unsat`. -- `(2^53 + 1) / 2 == 4503599627370496.5` — the exact ratio; the evaluator rounds to the same float64, so both accept it. +- `x : Real = 0.1` holds the double nearest `1/10`, so `x + 0.2 == 0.3` is not what the exact encoding of the written literals says. +- `x * 3.0 == 0.3` over a `Real` `x`: exactly `x = 1/10` satisfies it, a value no binary64 `x` is. -So an unqualified exact-real verdict can be wrong in both directions about the normative evaluator: a `sat` whose witness the evaluator rejects, and an `unsat` about conditions the evaluator's rounding would accept. +So an unqualified exact-real verdict about a query a Real takes part in can be wrong in both directions about the evaluator: a `sat` whose witness the evaluator rejects, and an `unsat` about conditions the evaluator's rounding would accept. **Why the encoding is not IEEE 754 (`FloatingPoint`/`Float64`):** prototyped and measured under both backends rather than assumed. Formulas over pinned float64 constants are fast in both (milliseconds). Formulas with free FP variables are materially slower under z3 (~1s against cvc5's ~0.15s for one equality), and the mixed fragment this project's subset needs — `Int` division and truncating remainder joined to FP arithmetic by `to_fp`/`fp.to_real` — is not a fragment both backends decide: z3 answers `unknown` for a free Int quotient converted to FP (with or without `(set-logic ALL)`), and cvc5 ran past a 20s budget on division-plus-remainder cases z3 also gave up on. A fix that holds under only one backend is not acceptable here, so the FP encoding is recorded as not viable for the translatable subset on the supported solvers, not merely unattempted. **What is done instead — the verdict is narrowed to what both arithmetics support:** -- The exact encoding stays: integer bounds, remainders, enumerations, quantities and unit conversions keep exact reasoning, and every existing verdict about them is unchanged. -- A `sat` witness is replayed through the evaluator's arithmetic (`solve/replay.go`) — float64 operations, `IntQuotient` for whole-number division marked by `RatioDiv` — and reported `sat` only if the replay confirms every assertion; a witness the evaluator would reject is reported `unknown` with the reason. -- A query whose conditions the evaluator rounds (`Query.Rounded`: any float64-computing Real arithmetic, or a Real literal float64 cannot hold exactly) does not report exact-real `unsat` as an evaluator verdict: `%check` and `%solve` say undecided and why, `%configure all` and `%optimize` decline rather than claim exact-real completeness for a rounding arithmetic. +- The exact encoding stays: integer bounds, remainders, Rationals, enumerations, quantities and unit conversions keep exact reasoning. +- A `sat` witness is replayed through the evaluator's arithmetic (`solve/replay.go`) — exact over Integers and Rationals, binary64 for a declared `Real` and for arithmetic one takes part in — and reported `sat` only if the replay confirms every assertion; a witness the evaluator would reject is reported `unknown` with the reason. +- A query whose conditions the evaluator rounds (`Query.Rounded`: arithmetic a Real takes part in) does not report exact-real `unsat` as an evaluator verdict: `%check` and `%solve` say undecided and why, `%configure all` and `%optimize` decline rather than claim exact-real completeness for a rounding arithmetic. Exact Rational arithmetic is not marked; the census of marked queries over every corpus is in [exact-rational evaluation](exact-rational-evaluation.md#the-rounded-query-census). -**The agreement claimed after this:** a `sat` reported to the user carries a witness the evaluator itself confirms, and an `unsat` is reported only where evaluator and exact arithmetic coincide (no rounded Real computation in the query). What is *not* claimed: completeness — a rounded query whose exact encoding answers `unsat`, or whose witness fails replay, is reported undecided even where a float64-aware solver might have decided it. Narrower, and sound; the alternatives — an exact-rational evaluator value representation (a contract change across `semantics`, `runtime`, formatting and serialization, not a modelling fix) and refusing to translate rounded queries at all (losing the confirmed-witness answers the replay preserves) — are recorded here so they are not re-derived. The exact-rational alternative has since been adjudicated against the pinned pilot and the specification text and **declined**; the evidence and the option comparison are in [exact-rational evaluation](exact-rational-evaluation.md). +**The agreement claimed after this:** a `sat` reported to the user carries a witness the evaluator itself confirms, and an `unsat` is reported only where evaluator and exact arithmetic coincide (no binary64 Real computation in the query). What is *not* claimed: completeness — a rounded query whose exact encoding answers `unsat`, or whose witness fails replay, is reported undecided even where a float64-aware solver might have decided it. ### SMT-LIB portability — which backends the extension supports diff --git a/docs/reference/java-api.md b/docs/reference/java-api.md index 0c862d1ce2..233f48ddd8 100644 --- a/docs/reference/java-api.md +++ b/docs/reference/java-api.md @@ -383,6 +383,7 @@ which JDK 17 offers only as a preview: String rendered; if (value instanceof Value.IntegerValue v) rendered = Long.toString(v.value()); else if (value instanceof Value.RealValue v) rendered = Double.toString(v.value()); +else if (value instanceof Value.RationalValue v) rendered = v.value().toString(); // exact; numerator()/denominator() as BigInteger else if (value instanceof Value.ComplexValue v) rendered = v.real() + " + " + v.imaginary() + "i"; // one value else if (value instanceof Value.BooleanValue v) rendered = Boolean.toString(v.value()); else if (value instanceof Value.StringValue v) rendered = v.value(); diff --git a/docs/reference/julia-api.md b/docs/reference/julia-api.md index 622b44b04f..cd48c227f3 100644 --- a/docs/reference/julia-api.md +++ b/docs/reference/julia-api.md @@ -102,7 +102,9 @@ returns a `StateRun` with `states_visited`, `final_context`, and `final_time`. `decode_value` decodes the service's value oneof and `encode_value` creates request values. The mapping retains exact `Int64` values and `BigInt` ones -beyond `Int64`, decodes sequences and sets recursively, and exposes +beyond `Int64`, decodes an exact Rational no `Float64` holds (`rationalValue`, a quantity's +`rationalMagnitude`) as `Rational{BigInt}` and sends one back the same way, which needs the +service's `rational_values` capability, decodes sequences and sets recursively, and exposes structured values through `Quantity`, `EnumLiteral`, `ArrayValue`, `VectorValue`, `VectorQuantity`, `TensorQuantity`, `MeasurementRef`, `FunctionRef`, `Metaobject`, `Undetermined`, `Unset`, and `Infinity`. diff --git a/docs/reference/matlab-api.md b/docs/reference/matlab-api.md index 9f0c5137fd..0bf9a479c3 100644 --- a/docs/reference/matlab-api.md +++ b/docs/reference/matlab-api.md @@ -89,11 +89,13 @@ scenario requests exactly as written. ## Values -`opensysml.decodeValue` reads all twenty-two Connect-JSON `Value` arms and `opensysml.encodeValue` +`opensysml.decodeValue` reads all twenty-three Connect-JSON `Value` arms and `opensysml.encodeValue` writes supported request values. `intValue` uses decimal JSON strings and exact `int64` accumulation, including `-9223372036854775808`; an Integer beyond `int64` arrives as `bigIntValue` and decodes to `struct('bigInteger', digits)`, its decimal digits kept as -`char` because no MATLAB number holds it, and is sent back as written; `realValue` supports `"NaN"`, `"Infinity"`, and +`char` because no MATLAB number holds it, and is sent back as written; an exact Rational no double holds arrives as `rationalValue` and +decodes to `struct('numerator', digits, 'denominator', digits)`, sent back as written to a service +with the `rational_values` capability; `realValue` supports `"NaN"`, `"Infinity"`, and `"-Infinity"`. Other primitive mappings include `boolValue` to `logical`, `stringValue` to `char`, `complex` to a complex scalar, and `sequence` to a cell array. `null` becomes `[]`. diff --git a/docs/reference/node-api.md b/docs/reference/node-api.md index 57dea455a8..84b37492d7 100644 --- a/docs/reference/node-api.md +++ b/docs/reference/node-api.md @@ -98,6 +98,7 @@ than a message with optional fields: switch (value.kind) { case "int": value.value; // bigint, never lossy, beyond int64 too case "real": value.value; // number + case "rational": value.numerator; value.denominator; // bigint terms: an exact Rational no double holds case "complex": value.value.real; value.value.imaginary; // one value, not two floats case "boolean": case "string": value.value; diff --git a/docs/reference/python-api.md b/docs/reference/python-api.md index 57d234ce1d..c421d0f2c3 100644 --- a/docs/reference/python-api.md +++ b/docs/reference/python-api.md @@ -155,7 +155,8 @@ collection property. | SysML | Python | | --- | --- | -| `Real`, `Rational` | `float` | +| `Real` | `float` | +| `Rational` | `fractions.Fraction`, exact whichever wire arm carried it (`as_rational`); a value read untyped is a `float` where a double holds it exactly (`0.5`) and a `Fraction` otherwise (`1/3`, `0.1`) | | `Complex` | `complex`, one number rather than two floats | | `Array` | `opensysml.Array`: `dimensions` and row-major `elements`, `nested()` unfolds them | | `CartesianVectorValue` and the other numeric vectors | `opensysml.Vector`: a tuple of `int`/`float` components, kept apart | diff --git a/docs/reference/rust-api.md b/docs/reference/rust-api.md index c5bf3fd46a..13c5d8ca5c 100644 --- a/docs/reference/rust-api.md +++ b/docs/reference/rust-api.md @@ -153,12 +153,13 @@ the elements that still refer to it. match value { Value::Integer(v) => (), Value::Real(v) => (), + Value::Rational(r) => (), // exact, no double holds it: r.numerator(), r.denominator() as decimal text; r.to_f64() rounds once Value::Complex(z) => (), // z.real, z.imaginary; one value, Display as `1.5 - 2.0i` Value::Boolean(v) => (), Value::Text(v) => (), Value::InstanceRef(id) => (), Value::Sequence(values) => (), - Value::Quantity(q) => (), // Magnitude::Integer | ::Real, unit, unit_term + Value::Quantity(q) => (), // Magnitude::Integer | ::BigInteger | ::Rational | ::Real, unit, unit_term Value::Array(a) => (), // a.dimensions(), a.elements() row-major, a.get(&[i, j]) Value::Vector(v) => (), // v.components: Vec, Integer and Real apart Value::VectorQuantity(q) => (), // q.components(): one Quantity per component; q.unit() when shared diff --git a/docs/reference/service-transports.md b/docs/reference/service-transports.md index 4b10b6a6fb..16f8b2c0b7 100644 --- a/docs/reference/service-transports.md +++ b/docs/reference/service-transports.md @@ -57,7 +57,7 @@ capability's definition rather than something a client has to guess: | The capability describes | A request that needs it | What a client should do | |---|---|---| | what the service can be *asked*: `strict_conformance`, `inline_language`, `parse_sources`, `evaluate_subject`, `verification`, `convert`, `apply_edits`, `authoring`, `connection_authoring`, `edit_documents`, `query`, `oslc_query`, `document_query`, `render_document`, `render_document_html`, `schedule`, `performer`, `satisfy_authoring`, `requirement_constraint_authoring`, `member_modifiers`, `transition_authoring`, `verification_objective_authoring`, `metadata_authoring`, `metadata_prefix_authoring`, `sequence_authoring`, `implicit_parameters`, `action_body_statement_authoring`, `constraint_body_authoring`, `state_action_authoring`, `import_authoring`, `documentation_authoring`, `comment_authoring`, `migrate` | is **refused** with `UNIMPLEMENTED`, naming the capability | check the advertised list first, and report the missing capability locally rather than spending a round trip | -| how a response is *populated*: `type_facts`, `symbol_attributes`, `feature_values`, `enum_values`, `unset_value`, `undetermined_value`, `complex_values`, `structured_values`, `measurement_refs`, `function_values`, `metaobject_values`, `big_int_values`, `verification_verdicts`, `case_evaluations`, `infinity_value`, `diagnostic_codes`, `final_time`, `edit_documents` | is answered with those fields **omitted** | check before reading the fields; an omitted field is not an error | +| how a response is *populated*: `type_facts`, `symbol_attributes`, `feature_values`, `enum_values`, `unset_value`, `undetermined_value`, `complex_values`, `structured_values`, `measurement_refs`, `function_values`, `metaobject_values`, `big_int_values`, `rational_values`, `verification_verdicts`, `case_evaluations`, `infinity_value`, `diagnostic_codes`, `final_time`, `edit_documents` | is answered with those fields **omitted** | check before reading the fields; an omitted field is not an error | `edit_documents` sits in both rows: without it `ApplyEdits` still edits a model of one document and answers `content`, but omits `documents`, `referrers` and each applied edit's `document`; diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index 2683631416..a4c04c5471 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -192,10 +192,10 @@ Note that `not_found` is also the status for an unknown *symbol* on some methods which (`model not found:`, `symbol not found:`, `file not found:`), and a client that recovers by re-parsing must read it. -## `Value`: twenty-two arms, exactly one present +## `Value`: twenty-three arms, exactly one present Every value the engine returns — an expression result, a feature of an instance, an action -output, a state-machine context variable — is a `Value`, which is a proto `oneof` of twenty-two +output, a state-machine context variable — is a `Value`, which is a proto `oneof` of twenty-three arms. In JSON that is **an object with exactly one key**, and the key is the discriminator. A decoder therefore does not look for a `kind` field: it looks at which key is present. The arms, each captured from `Evaluate` against the model at the end of this section: @@ -204,13 +204,14 @@ arms, each captured from `Evaluate` against the model at the end of this section |---|---|---|---| | `intValue` | string | `{"result":{"intValue":"4"}}` | Integer within 64 bits; string because `int64` | | `bigIntValue` | string | `{"result":{"bigIntValue":"1180591620717411303424"}}` | Integer beyond 64 bits, in decimal; KerML Integers are unbounded | -| `realValue` | number | `{"result":{"realValue":0.3333333333333333}}` | Real (IEEE-754 double) | +| `realValue` | number | `{"result":{"realValue":1.4142135623730951}}` | Real (IEEE-754 double), or a Rational a double holds exactly | +| `rationalValue` | object | `{"result":{"rationalValue":{"numerator":"1","denominator":"3"}}}` | Rational no double holds, in lowest terms; KerML Rationals are exact | | `boolValue` | boolean | `{"result":{"boolValue":true}}` | Boolean | | `stringValue` | string | `{"result":{"stringValue":"abc"}}` | String | | `instanceId` | string | `{"result":{"instanceId":"2"}}` | A reference to a runtime instance, by id | | `sequence` | object | `{"result":{"sequence":{"elements":[{"stringValue":"nav"},{"stringValue":"sci"}]}}}` | Ordered collection; `elements` are `Value`s | | `null` | string | `{"result":{"null":""}}` | The SysML `null`, or an unsupported value (non-empty string) | -| `quantity` | object | `{"result":{"quantity":{"realMagnitude":5.4,"unit":"SI::km/SI::h","unitTerm":{…}}}}` | Magnitude with a unit | +| `quantity` | object | `{"result":{"quantity":{"rationalMagnitude":{"numerator":"27","denominator":"5"},"unit":"SI::km/SI::h","unitTerm":{…}}}}` | Magnitude with a unit | | `enumLiteral` | object | `{"result":{"enumLiteral":{"literalId":"Rover::Mode::idle","enumerationId":"Rover::Mode","name":"Mode::idle"}}}` | Enumeration literal; a scalar-valued one (`high = 3`) also carries `value` | | `unset` | boolean | `{"result":{"unset":true}}` | A feature that exists and has no value | | `complex` | object | `{"result":{"complex":{"real":1.5,"imaginary":-2}}}` | Complex number | @@ -232,7 +233,7 @@ The `array`, `vector` and `vectorQuantity` rows were captured against `conformance/fixtures/set_tensor.sysml` (`T::s.elements`, `T::cube`), `metaobject` against `conformance/fixtures/metaobject.sysml` (`(Meta::seatBelt meta KerML::Feature)#(1)`); the rest against the model below, with requests of the form `{"modelHash":"59c4…a654","expression":"","contextSymbolId":"Rover"}` with `rover.count`, -`1.0 / 3.0`, `rover.armed`, `"abc"`, `rover.wheel`, `rover.tags`, `null`, `rover.speed`, +`RealFunctions::sqrt(2.0)`, `1.0 / 3.0`, `rover.armed`, `"abc"`, `rover.wheel`, `rover.tags`, `null`, `rover.speed`, `Mode::idle`, `rover.serial` and `rover.z`, and the model was: ```sysml @@ -276,6 +277,8 @@ decode(v): bigIntValue → parse the decimal string as an arbitrary-precision integer (it is always outside int64); never as a double realValue → the number, as a double + rationalValue → numerator / denominator, each a decimal string, as an exact rational; + never as a double boolValue → the boolean stringValue → the string instanceId → an opaque reference; parse as 64-bit integer, keep it a reference @@ -289,7 +292,7 @@ decode(v): array → shape v.array.dimensions (parse each as int64); elements := map decode over v.array.elements; require len(elements) == product(dimensions), else an error vector → map over v.vector.components: intValue or bigIntValue → integer, - realValue → double, + rationalValue → exact rational, realValue → double, anything else → an error vectorQuantity → map the quantity rule over v.vectorQuantity.components; empty → an error measurementRef → unit := v.measurementRef.unit, id := v.measurementRef.unitId; @@ -343,6 +346,22 @@ with `UNIMPLEMENTED` naming the capability, since a document value has no unsupp since such a service would read an unknown arm as `null`, the bundled clients refuse to send one to it, document-query bindings included, before the call. +**`rationalValue`.** A KerML `Rational` is a rational number (KerML 1.0 §9.3.2.2.8), held +exactly: `1 / 3` answers `{"result":{"rationalValue":{"numerator":"1","denominator":"3"}}}` and +`0.1 + 0.2` answers `{"numerator":"3","denominator":"10"}`. `numerator` and `denominator` are +canonical decimal strings (no `+`, no leading zero), the fraction is in lowest terms and the +denominator is positive. A Rational a double holds exactly (`0.5`, `2.0 ** 70`) is sent as +`realValue`, so the two arms never spell the same number; an Integer is never sent here, though a +whole Rational no double holds is (denominator `1`). A decoder rejects a `rationalValue` that is +not in lowest terms or that a double holds. The service accepts `rationalValue` on input under the +same rule, and reads a `realValue` sent to it as a binary64 Real. A value declared `Real` is IEEE 754 binary64 and always +answers `realValue` (`rover.mass` is `{"realValue":20}`). The arm is negotiated by the +`rational_values` capability exactly as `big_int_values` negotiates `bigIntValue`: a service +that does not advertise it sends such a Rational (bare, nested, or as a `rationalMagnitude`) as +an unsupported `null` naming it, refuses a document query bound to or answering one with +`UNIMPLEMENTED` naming the capability, and the bundled clients refuse to send one to it before +the call. + MATLAB's `jsondecode` gives you a `char` array, which `int64(str2double(...))` corrupts and `sscanf(s, '%ld')` does not; R needs `bit64::as.integer64`; Julia's `parse(Int64, s)` and C's `strtoll` are exact. @@ -440,12 +459,13 @@ input. A service that does not advertise `undetermined_value` sends the arm as a **`quantity`.** A magnitude with a unit: ```json -{"quantity":{"realMagnitude":5.4,"unit":"SI::km/SI::h","unitTerm":{"scaleNum":5,"scaleDen":18,"factors":[{"unitId":"SI::metre","exponent":1},{"unitId":"SI::second","exponent":-1}]}}} +{"quantity":{"rationalMagnitude":{"numerator":"27","denominator":"5"},"unit":"SI::km/SI::h","unitTerm":{"scaleNum":5,"scaleDen":18,"factors":[{"unitId":"SI::metre","exponent":1},{"unitId":"SI::second","exponent":-1}]}}} ``` - The magnitude is its own `oneof`: **`intMagnitude`** (a string, same rule as `intValue`), - **`bigIntMagnitude`** (a string, same rule as `bigIntValue`) or **`realMagnitude`** (a - number). Exactly one is present. + **`bigIntMagnitude`** (a string, same rule as `bigIntValue`), **`rationalMagnitude`** (same + rule as `rationalValue`) or **`realMagnitude`** (a number). Exactly one is present. The + literal `5.4` is the exact Rational 27/5, which no double holds, so it is a `rationalMagnitude`. - `unit` is the unit expression as written, by fully qualified name of each unit; it is the display form and the identity of the unit *as declared*. - `unitTerm` is the same unit reduced to base units: `factors` are `(unitId, exponent)` pairs @@ -489,7 +509,7 @@ $ … /Evaluate -d '{"modelHash":"42cc…54b0","expression":"S::grid"}' the same rule to an array sent to it. **`vector`.** `components` is a list of `Value`s each of which is an `intValue`, a -`bigIntValue` or a `realValue` — nothing else — so an Integer and a Real component stay distinct, as they do in +`bigIntValue`, a `rationalValue` or a `realValue` — nothing else — so an Integer and a Real component stay distinct, as they do in a `sequence`. A `vector` is not a `sequence`: `VectorOf((3.0, 4.0))` is one value with a dimension, and the engine's vector functions accept it where a sequence of numbers would be read element by element. A component of any other arm is an error, on both sides. @@ -681,8 +701,8 @@ $ … /Evaluate -d '{"modelHash":"07a0…b5ca","expression":"Meta::notADefinitio ### What a client must not do - **Do not compare enum literals by `name`.** Compare `literalId`. -- **Do not read `intValue` or `bigIntValue` (or `intMagnitude`, `bigIntMagnitude`, `id`, - `instanceId`) as a double.** Above 2^53 the +- **Do not read `intValue`, `bigIntValue` or `rationalValue` (or `intMagnitude`, + `bigIntMagnitude`, `rationalMagnitude`, `id`, `instanceId`) as a double.** Above 2^53 the digits are gone and nothing tells you. - **Do not read `unset` as a boolean.** Its presence is the fact; a missing `result` is a different fact (no value), `{"null":""}` a third (the null value), and `undetermined` a @@ -2023,8 +2043,8 @@ a `DocumentValue`, here always `elementId` plus `elementType`) and `cells` **pos aligned with `columns`**. A cell holds `values`, a list of `DocumentValue`s (several for a multi-valued property, none for a missing one, in which case `values` is absent). A `DocumentValue` decodes like a `Value` — one arm present — but its arms are the eight above plus -the answer-only `verdict` below and `bigIntValue` for an Integer beyond `int64` (same rule as on -`Value`), and never a nested sequence or enum. `SubsystemTable` projects two columns and shows a +the answer-only `verdict` below, `bigIntValue` for an Integer beyond `int64` and `rationalValue` +for a Rational no double holds (same rules as on `Value`), and never a nested sequence or enum. `SubsystemTable` projects two columns and shows a `realValue` cell: ```console From d1eb16edb588575c69d2c17266d6ecc0119a0c7f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:32:38 +0000 Subject: [PATCH 11/26] perf(semantics): keep exact Integer and Rational fast paths allocation-free Co-Authored-By: jason.han --- internal/exec/runtime/collections.go | 5 ++-- internal/semantic/semantics/integer.go | 33 +++++++++++++++++++------ internal/semantic/semantics/rational.go | 21 ++++++++++++++++ 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/internal/exec/runtime/collections.go b/internal/exec/runtime/collections.go index 04d55135ce..ec7f270ab0 100644 --- a/internal/exec/runtime/collections.go +++ b/internal/exec/runtime/collections.go @@ -1569,16 +1569,15 @@ func (ctx *Context) aggregate(op string, args []Value, operator ast.OperatorKind if elem.Kind != ValConst || !elem.Const.IsNumeric() { return Value{}, fmt.Errorf("%w: %s requires numeric elements, got %s", ErrTypeMismatch, op, describeValue(elem)) } - x := kind.element(elem.Const) if a, ok := acc.Int64(); ok && operator == ast.OpAdd { - if b, ok := x.Int64(); ok { + if b, ok := elem.Const.Int64(); ok { if sum := a + b; (sum^a)&(sum^b) >= 0 { acc = semantics.IntValue(sum) continue } } } - next, err := foldNumeric(op, operator, acc, x, ctx.maxIntegerBits) + next, err := foldNumeric(op, operator, acc, kind.element(elem.Const), ctx.maxIntegerBits) if err != nil { return Value{}, err } diff --git a/internal/semantic/semantics/integer.go b/internal/semantic/semantics/integer.go index 1415a726e8..b85779e633 100644 --- a/internal/semantic/semantics/integer.go +++ b/internal/semantic/semantics/integer.go @@ -222,12 +222,23 @@ func IntegerSizeExceeded(bits, maxBits int64) error { return fmt.Errorf("%w: the result needs at least %d bits, beyond the %d-bit Integer size budget", ErrIntegerSizeLimit, bits, maxBits) } -// sizedInt is the Integer b, refused when its magnitude exceeds maxBits. -func sizedInt(b *big.Int, maxBits int64) (Value, error) { - if bits := int64(b.BitLen()); bits > maxBits { +// intCell is a fresh big.Int an Integer Value holds without a copy: the numerator +// of a big.Rat over one. +func intCell() (*big.Rat, *big.Int) { + r := new(big.Rat) + return r, r.Num() +} + +// sizedInt is the Integer cell r holds, refused when its magnitude exceeds maxBits. +func sizedInt(r *big.Rat, maxBits int64) (Value, error) { + n := r.Num() + if bits := int64(n.BitLen()); bits > maxBits { return Value{}, IntegerSizeExceeded(bits, maxBits) } - return BigIntValue(b), nil + if n.IsInt64() { + return IntValue(n.Int64()), nil + } + return Value{Kind: ValInt, ext: r}, nil } // IntArith is Integer addition, subtraction and multiplication, shared by the @@ -246,11 +257,14 @@ func IntArith(op ast.OperatorKind, a, b Value, maxBits int64) (Value, error) { // bigIntArith is IntArith over math/big, for a result or operand beyond int64. func bigIntArith(op ast.OperatorKind, a, b Value, maxBits int64) (Value, error) { x, y := a.bigView(), b.bigView() + r, n := intCell() switch op { case ast.OpAdd: - return sizedInt(new(big.Int).Add(x, y), maxBits) + n.Add(x, y) + return sizedInt(r, maxBits) case ast.OpSub: - return sizedInt(new(big.Int).Sub(x, y), maxBits) + n.Sub(x, y) + return sizedInt(r, maxBits) case ast.OpMul: if x.Sign() == 0 || y.Sign() == 0 { return IntValue(0), nil @@ -259,7 +273,8 @@ func bigIntArith(op ast.OperatorKind, a, b Value, maxBits int64) (Value, error) if bits := int64(x.BitLen()) + int64(y.BitLen()) - 1; bits > maxBits { return Value{}, IntegerSizeExceeded(bits, maxBits) } - return sizedInt(new(big.Int).Mul(x, y), maxBits) + n.Mul(x, y) + return sizedInt(r, maxBits) } return Value{}, fmt.Errorf("%w: '%s' is no Integer arithmetic operator", ErrArithmeticDomain, op) } @@ -348,5 +363,7 @@ func IntPow(a, n Value, maxBits int64) (Value, error) { } return Value{}, IntegerSizeExceeded(bits, maxBits) } - return sizedInt(new(big.Int).Exp(a.bigView(), n.bigView(), nil), maxBits) + r, p := intCell() + p.Exp(a.bigView(), n.bigView(), nil) + return sizedInt(r, maxBits) } diff --git a/internal/semantic/semantics/rational.go b/internal/semantic/semantics/rational.go index 7aea0d3d8a..098392ebfc 100644 --- a/internal/semantic/semantics/rational.go +++ b/internal/semantic/semantics/rational.go @@ -188,6 +188,14 @@ func RationalOfReal(r float64) (Value, bool) { if r == math.Trunc(r) && math.Abs(r) < 1<<53 { return Value{Kind: ValRational, Int: int64(r), den: 1}, true } + // r is m·2^e with m odd; a denominator 2^-e within uint32 stays inline. + frac, exp := math.Frexp(r) + m, e := int64(frac*(1<<53)), exp-53 + tz := bits.TrailingZeros64(uint64(m)) + m, e = m>>tz, e+tz + if e < 0 && e >= -31 { + return Value{Kind: ValRational, Int: m, den: 1 << -e}, true + } return RatValue(new(big.Rat).SetFloat64(r)), true } @@ -468,6 +476,19 @@ func ParseRational(text string, maxBits int64) (Value, error) { mantissa = mantissa[1:] } whole, frac, _ := strings.Cut(mantissa, ".") + if exponent == "" && len(whole)+len(frac) <= 18 { + n := int64(0) + for _, digits := range [2]string{whole, frac} { + for i := 0; i < len(digits); i++ { + n = n*10 + int64(digits[i]-'0') + } + } + if neg { + n = -n + } + v, _ := FracValue(n, pow10(int64(len(frac)))) + return v, nil + } digits := strings.TrimLeft(whole+frac, "0") if digits == "" { return Value{Kind: ValRational, Int: 0, den: 1}, nil From 492c98c9ba4f28b18d95194cc5bf9cea19185c0a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:48:50 +0000 Subject: [PATCH 12/26] test(runtime): benchmark exact Rational arithmetic and record its cost Co-Authored-By: jason.han --- docs/project/exact-rational-evaluation.md | 28 +++++++- internal/exec/runtime/rational_bench_test.go | 76 ++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 internal/exec/runtime/rational_bench_test.go diff --git a/docs/project/exact-rational-evaluation.md b/docs/project/exact-rational-evaluation.md index d865c78725..50a1c0ce9f 100644 --- a/docs/project/exact-rational-evaluation.md +++ b/docs/project/exact-rational-evaluation.md @@ -248,7 +248,33 @@ every call lands in `pilot-unevaluated` and the operator quotient agrees. ## Cost -COST_PLACEHOLDER +Small Rationals are held inline — an `int64` numerator over a `uint32` denominator in the +`Value` itself — and only terms beyond that use a normalized `math/big.Rat`, so the `Value` stays +32 bytes and ordinary arithmetic allocates nothing it did not allocate as binary64. Literals, a +double converted exactly, and Integer arithmetic beyond `int64` take the same allocation-free paths. + +Interleaved runs on one machine (8 cores, six samples each side, `benchstat`), against the +binary64 implementation: + +| Benchmark | Time | Bytes/op | Allocs/op | +|-----------|------|----------|-----------| +| `internal/exec/runtime`, all seven existing benchmarks | geomean +2.1%, no significant change | geomean +3.5% | unchanged | +| `IntegerArithmeticLoopBeyondInt64` | no significant change | +27% (227.6 KiB → 290.1 KiB) | unchanged | +| `tests/perf` (`REPLEvalExpr`, `ExecuteAction`, `BatchConstraints`, `SameConstraintManyInstances`, `BatchSatisfy`, `Instantiate`, `GRPCEvaluate`) | no significant change on any | at most +2.8% (`GRPCEvaluate`) | unchanged | +| `RationalDecimalLoop` (1,000 steps of exact decimal arithmetic) | no significant change | unchanged | unchanged | +| `RealDecimalLoop` (the same loop over a feature declared `Real`) | no significant change | unchanged | unchanged | +| `RationalHarmonicLoop` (200 exact terms of 1/i) | +47% to +55% | ×4.3 | +69% | + +``` +go test ./internal/exec/runtime -run '^$' -bench . -benchmem -count=6 +go test ./tests/perf -run '^$' -bench 'Instantiate|BatchConstraints|BatchSatisfy|ExecuteAction$|GRPCEvaluate|REPLEvalExpr|SameConstraint' -benchtime=20x -benchmem -count=6 +``` + +An Integer beyond `int64` is the numerator of a `big.Rat` over one, which is the byte increase in +`IntegerArithmeticLoopBeyondInt64`. The harmonic sum is what exactness costs where it is real: +its denominator outgrows `int64` within a few dozen terms and reaches hundreds of bits, where +binary64 kept 53. An accumulation whose terms keep growing is stopped by +`ErrRationalSizeLimit` at the configured size budget rather than slowing without bound. ## The rounded-query census diff --git a/internal/exec/runtime/rational_bench_test.go b/internal/exec/runtime/rational_bench_test.go new file mode 100644 index 0000000000..137a7566ec --- /dev/null +++ b/internal/exec/runtime/rational_bench_test.go @@ -0,0 +1,76 @@ +package runtime + +import "testing" + +// rationalBenchModel holds Rational-heavy calcs: a loop of decimal arithmetic +// whose terms stay small, a harmonic sum whose denominator grows past int64, +// and the decimal loop over features declared Real. +const rationalBenchModel = ` +package bench { + private import ScalarValues::*; + calc def DecimalLoop { + in n : Integer; + attribute acc : Rational = 0.0; + attribute i : Integer = 0; + while i < n { + assign acc := acc + 0.25 * i - 0.1; + assign i := i + 1; + } + acc + } + calc def HarmonicLoop { + in n : Integer; + attribute acc : Rational = 0.0; + attribute i : Integer = 1; + while i <= n { + assign acc := acc + 1 / i; + assign i := i + 1; + } + acc + } + calc def RealLoop { + in n : Integer; + attribute acc : Real = 0.0; + attribute i : Integer = 0; + while i < n { + assign acc := acc + 0.25 * i - 0.1; + assign i := i + 1; + } + acc + } +} +` + +func benchRationalCalc(b *testing.B, calc string, args ...Value) { + b.Helper() + ctx, idx := benchRuntime(b, rationalBenchModel) + matches := idx.LookupQualified("bench::" + calc) + if len(matches) != 1 { + b.Fatalf("bench::%s: %d matching symbols, want 1", calc, len(matches)) + } + scope := idx.LookupQualified("bench")[0].Scope + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := ctx.InvokeCalc(matches[0], args, scope); err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkRationalDecimalLoop runs exact decimal arithmetic whose terms stay +// within the inline numerator and denominator. +func BenchmarkRationalDecimalLoop(b *testing.B) { + benchRationalCalc(b, "DecimalLoop", constInt(1000)) +} + +// BenchmarkRationalHarmonicLoop sums 1/i exactly; its denominator outgrows +// int64 after a few dozen terms, so most additions are arbitrary-precision. +func BenchmarkRationalHarmonicLoop(b *testing.B) { + benchRationalCalc(b, "HarmonicLoop", constInt(200)) +} + +// BenchmarkRealDecimalLoop runs the decimal loop over a binary64 Real. +func BenchmarkRealDecimalLoop(b *testing.B) { + benchRationalCalc(b, "RealLoop", constInt(1000)) +} From a26bfabd19a8a6ec4b246c7c645d24829c689d6c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:22:15 +0000 Subject: [PATCH 13/26] chore(semantics): justify bounded integer conversions for gosec Co-Authored-By: jason.han --- internal/semantic/semantics/rational.go | 12 ++++++------ internal/translate/codegen/rational.go | 2 +- internal/translate/export/rdf_expr.go | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/semantic/semantics/rational.go b/internal/semantic/semantics/rational.go index 098392ebfc..d6f0e01a11 100644 --- a/internal/semantic/semantics/rational.go +++ b/internal/semantic/semantics/rational.go @@ -26,7 +26,7 @@ var ErrRationalSizeLimit = errors.New("rational size limit exceeded") // must not modify it afterwards. func RatValue(r *big.Rat) Value { if r.Num().IsInt64() && r.Num().Int64() != math.MinInt64 && r.Denom().IsUint64() && r.Denom().Uint64() <= math.MaxUint32 { - return Value{Kind: ValRational, Int: r.Num().Int64(), den: uint32(r.Denom().Uint64())} + return Value{Kind: ValRational, Int: r.Num().Int64(), den: uint32(r.Denom().Uint64())} // #nosec G115 -- checked within uint32 above } return Value{Kind: ValRational, ext: r} } @@ -56,7 +56,7 @@ func smallRat(num, den int64) (Value, bool) { if den > math.MaxUint32 { return Value{}, false } - return Value{Kind: ValRational, Int: num, den: uint32(den)}, true + return Value{Kind: ValRational, Int: num, den: uint32(den)}, true // #nosec G115 -- checked within uint32 above } func abs64(x int64) int64 { @@ -153,7 +153,7 @@ func (v Value) RatSign() int { // magnitude plus the bits of its denominator. func (v Value) RatBitLen() int64 { if num, den, ok := v.small(); ok { - return int64(bits.Len64(uint64(abs64(num))) + bits.Len64(uint64(den))) + return int64(bits.Len64(uint64(abs64(num))) + bits.Len64(uint64(den))) // #nosec G115 -- both non-negative } r := v.ratView() return int64(r.Num().BitLen() + r.Denom().BitLen()) @@ -191,7 +191,7 @@ func RationalOfReal(r float64) (Value, bool) { // r is m·2^e with m odd; a denominator 2^-e within uint32 stays inline. frac, exp := math.Frexp(r) m, e := int64(frac*(1<<53)), exp-53 - tz := bits.TrailingZeros64(uint64(m)) + tz := bits.TrailingZeros64(uint64(m)) // #nosec G115 -- only the trailing zero bits are read m, e = m>>tz, e+tz if e < 0 && e >= -31 { return Value{Kind: ValRational, Int: m, den: 1 << -e}, true @@ -579,9 +579,9 @@ func (v Value) FormatRational() string { } k := max(twos, fives) scaled := num.BigInt() - scaled.Mul(scaled, new(big.Int).Exp(ten, big.NewInt(int64(k)), nil)) + scaled.Mul(scaled, new(big.Int).Exp(ten, big.NewInt(int64(k)), nil)) // #nosec G115 -- a trailing-zero count fits scaled.Quo(scaled, den.bigView()) - return formatDecimal(scaled, int64(k)) + return formatDecimal(scaled, int64(k)) // #nosec G115 -- a trailing-zero count fits } // formatDecimal renders n·10^-k in FormatReal's layout: positional between diff --git a/internal/translate/codegen/rational.go b/internal/translate/codegen/rational.go index f8fc5d84b6..92c7be419a 100644 --- a/internal/translate/codegen/rational.go +++ b/internal/translate/codegen/rational.go @@ -215,7 +215,7 @@ func scalesExactly(x Expr) bool { return false } n := r.Num() - return n.BitLen()-1 == int(n.TrailingZeroBits()) + return n.BitLen()-1 == int(n.TrailingZeroBits()) // #nosec G115 -- a trailing-zero count fits } // exactWiden guards x's Integer leaves and whole results, so that binary64 diff --git a/internal/translate/export/rdf_expr.go b/internal/translate/export/rdf_expr.go index 43cb72180c..7307e21a3f 100644 --- a/internal/translate/export/rdf_expr.go +++ b/internal/translate/export/rdf_expr.go @@ -394,7 +394,7 @@ func binaryLiteralText(term rdf.Term) string { if exact, err := semantics.ParseRational(term.Value, semantics.DefaultMaxIntegerBits); err == nil && exact.Rat().Cmp(value) == 0 { return term.Value } - return decimalText(value, max(int(value.Denom().TrailingZeroBits()), 1)) + return decimalText(value, max(int(value.Denom().TrailingZeroBits()), 1)) // #nosec G115 -- a trailing-zero count fits } func (e *encoder) typed(subject rdf.Term, metaclass string) { From da7235accc91ff69ce07d5afd021a0077cefcd9c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:17:09 +0000 Subject: [PATCH 14/26] feat(semantics): compare Rationals with binary64 Reals at Real precision and read exact Rational inputs A Rational meeting a binary64 Real is rounded once before comparison, as in mixed arithmetic, so a binding across a Real declaration stays equal. Clients send every exact Rational as rational_value under rational_values, the service reads a binary64-exact one as that Rational, and a realValue stays a Real. Co-Authored-By: jason.han --- api/proto/sysml.pb.go | 13 +- api/proto/sysml.proto | 13 +- changes/unreleased/exact-rationals.changed.md | 4 +- .../org/openmbee/opensysml/DocumentValue.java | 8 +- .../java/org/openmbee/opensysml/Model.java | 17 +- .../java/org/openmbee/opensysml/Quantity.java | 11 +- .../java/org/openmbee/opensysml/Value.java | 16 +- .../openmbee/opensysml/internal/Protos.java | 149 +++++++++++++- .../opensysml/proto/DocumentValue.java | 24 +-- .../proto/DocumentValueOrBuilder.java | 6 +- .../openmbee/opensysml/proto/Quantity.java | 24 +-- .../opensysml/proto/QuantityOrBuilder.java | 6 +- .../openmbee/opensysml/proto/Rational.java | 8 +- .../org/openmbee/opensysml/proto/Value.java | 60 +++--- .../opensysml/proto/ValueOrBuilder.java | 15 +- .../internal/RationalProtosTest.java | 53 ++++- client/julia/OpenSysML/src/document.jl | 5 +- client/julia/OpenSysML/src/model.jl | 4 +- client/julia/OpenSysML/src/values.jl | 34 +++- client/julia/OpenSysML/src/verification.jl | 3 +- client/julia/OpenSysML/test/runtests.jl | 7 +- .../+internal/bindingRationalsAsReals.m | 19 ++ .../+opensysml/+internal/rationalDouble.m | 11 ++ .../matlab/+opensysml/+internal/rationalOf.m | 11 +- client/matlab/+opensysml/encodeValue.m | 22 ++- client/matlab/+opensysml/runDocumentQuery.m | 5 +- client/matlab/README.md | 2 +- client/matlab/tests/test_encode_value.m | 7 +- client/matlab/tests/test_surface_offline.m | 19 ++ client/node/src/core/connection.ts | 4 + client/node/src/core/document.ts | 23 ++- client/node/src/core/index.ts | 1 + client/node/src/core/values.ts | 64 ++++++- client/node/src/generated/sysml_pb.ts | 13 +- client/node/test/rational.test.ts | 50 ++++- client/opensysml/analysis.go | 5 + client/opensysml/client.go | 48 ++--- client/opensysml/convert.go | 19 +- client/opensysml/documents.go | 26 ++- client/opensysml/execute.go | 3 + client/opensysml/explore.go | 3 + client/opensysml/verify.go | 3 + client/python/opensysml/connection.py | 22 ++- client/python/opensysml/document.py | 17 +- client/python/opensysml/values.py | 78 +++++++- client/python/tests/test_rational.py | 117 ++++++++++- .../rust/conformance/sysml.descriptor.binpb | Bin 152233 -> 152258 bytes client/rust/opensysml/src/document.rs | 20 +- client/rust/opensysml/src/domain.rs | 2 +- client/rust/opensysml/src/encode.rs | 115 ++++++++++- client/rust/opensysml/src/operations.rs | 9 +- .../rust/opensysml/src/proto/sysml/sysml.rs | 13 +- client/rust/opensysml/src/rational.rs | 68 +++++-- docs/project/exact-rational-evaluation.md | 109 +++++++---- docs/project/native-compilation.md | 7 +- docs/project/spec-compliance.md | 2 +- docs/reference/java-api.md | 8 + docs/reference/julia-api.md | 5 +- docs/reference/matlab-api.md | 5 +- docs/reference/node-api.md | 2 +- docs/reference/python-api.md | 2 +- docs/reference/rust-api.md | 2 +- docs/reference/wire-contract.md | 34 ++-- internal/doc/queryexec/operations.go | 4 +- internal/exec/runtime/eval.go | 6 +- ...obustness_rational_real_comparison_test.go | 181 ++++++++++++++++++ internal/exec/runtime/set_order.go | 15 +- ..._accumulation_stays_binary64.expected.json | 9 + ...ion_real_accumulation_stays_binary64.sysml | 20 ++ .../calc_library_conversions.expected.json | 2 +- .../calc_library_conversions.sysml | 4 +- ..._real_binding_meets_rational.expected.json | 17 ++ ...instance_real_binding_meets_rational.sysml | 23 +++ internal/exec/runtime/value_equality.go | 13 +- internal/exec/solve/replay.go | 8 +- internal/frontend/engine/values.go | 10 +- .../frontend/grpc/convert_rational_test.go | 33 +++- internal/frontend/protoconv/convert.go | 72 ++++++- internal/frontend/repl/compile_test.go | 2 + .../repl/testdata/compile_calcs.sysml | 15 ++ internal/semantic/semantics/eval.go | 8 +- internal/semantic/semantics/rational.go | 36 ++-- internal/semantic/semantics/rational_test.go | 12 +- internal/translate/codegen/rational.go | 36 +--- tests/grpc/conformance_test.go | 9 + tests/grpc/testdata/conformance/README.md | 7 +- .../evaluate_calc_rational_wire.expected.json | 8 + .../evaluate_calc_rational_wire.sysml | 9 + ...tional_wire_canonical_answer.expected.json | 8 + ..._calc_rational_wire_canonical_answer.sysml | 9 + ...ational_input_meets_rational.expected.json | 10 + ...action_rational_input_meets_rational.sysml | 14 ++ ...on_real_input_meets_rational.expected.json | 10 + ...ute_action_real_input_meets_rational.sysml | 14 ++ 94 files changed, 1668 insertions(+), 431 deletions(-) create mode 100644 client/matlab/+opensysml/+internal/bindingRationalsAsReals.m create mode 100644 client/matlab/+opensysml/+internal/rationalDouble.m create mode 100644 internal/exec/runtime/robustness_rational_real_comparison_test.go create mode 100644 internal/exec/runtime/testdata/conformance/action_real_accumulation_stays_binary64.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/action_real_accumulation_stays_binary64.sysml create mode 100644 internal/exec/runtime/testdata/conformance/instance_real_binding_meets_rational.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/instance_real_binding_meets_rational.sysml create mode 100644 tests/grpc/testdata/conformance/evaluate_calc_rational_wire.expected.json create mode 100644 tests/grpc/testdata/conformance/evaluate_calc_rational_wire.sysml create mode 100644 tests/grpc/testdata/conformance/evaluate_calc_rational_wire_canonical_answer.expected.json create mode 100644 tests/grpc/testdata/conformance/evaluate_calc_rational_wire_canonical_answer.sysml create mode 100644 tests/grpc/testdata/conformance/execute_action_rational_input_meets_rational.expected.json create mode 100644 tests/grpc/testdata/conformance/execute_action_rational_input_meets_rational.sysml create mode 100644 tests/grpc/testdata/conformance/execute_action_real_input_meets_rational.expected.json create mode 100644 tests/grpc/testdata/conformance/execute_action_real_input_meets_rational.sysml diff --git a/api/proto/sysml.pb.go b/api/proto/sysml.pb.go index c243faed57..ef513c5482 100644 --- a/api/proto/sysml.pb.go +++ b/api/proto/sysml.pb.go @@ -7868,8 +7868,9 @@ type Value_BigIntValue struct { } type Value_RationalValue struct { - // An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1` - // or `1 / 3` evaluates; one a double holds exactly crosses as real_value. + // An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The + // server answers one a double holds exactly as real_value; a client sends + // any exact Rational here, and an inbound real_value is always a Real. RationalValue *Rational `protobuf:"bytes,23,opt,name=rational_value,json=rationalValue,proto3,oneof"` } @@ -7920,8 +7921,8 @@ func (*Value_BigIntValue) isValue_Kind() {} func (*Value_RationalValue) isValue_Kind() {} // Rational is an exact rational number in lowest terms: numerator over a -// positive denominator, each written as big_int_value is. A decoder rejects one -// not in lowest terms or one a double holds exactly, so no number has two spellings. +// positive denominator, each written as big_int_value is. The server rejects one +// not in lowest terms; a client also rejects an answered one a double holds exactly. type Rational struct { state protoimpl.MessageState `protogen:"open.v1"` Numerator string `protobuf:"bytes,1,opt,name=numerator,proto3" json:"numerator,omitempty"` @@ -8731,7 +8732,7 @@ type Quantity_BigIntMagnitude struct { } type Quantity_RationalMagnitude struct { - // An exact Rational magnitude no double holds exactly, as Value.rational_value. + // An exact Rational magnitude, as Value.rational_value. RationalMagnitude *Rational `protobuf:"bytes,6,opt,name=rational_magnitude,json=rationalMagnitude,proto3,oneof"` } @@ -10562,7 +10563,7 @@ type DocumentValue_BigIntValue struct { } type DocumentValue_RationalValue struct { - // An exact Rational no double holds exactly, as Value.rational_value. + // An exact Rational, as Value.rational_value. RationalValue *Rational `protobuf:"bytes,14,opt,name=rational_value,json=rationalValue,proto3,oneof"` } diff --git a/api/proto/sysml.proto b/api/proto/sysml.proto index 6c1be48d75..16344c1756 100644 --- a/api/proto/sysml.proto +++ b/api/proto/sysml.proto @@ -1525,15 +1525,16 @@ message Value { // An Integer beyond int64, in decimal (`-` signed, no `+`, no leading // zeros). An Integer that fits int64 is always int_value, never this. string big_int_value = 22; - // An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1` - // or `1 / 3` evaluates; one a double holds exactly crosses as real_value. + // An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The + // server answers one a double holds exactly as real_value; a client sends + // any exact Rational here, and an inbound real_value is always a Real. Rational rational_value = 23; } } // Rational is an exact rational number in lowest terms: numerator over a -// positive denominator, each written as big_int_value is. A decoder rejects one -// not in lowest terms or one a double holds exactly, so no number has two spellings. +// positive denominator, each written as big_int_value is. The server rejects one +// not in lowest terms; a client also rejects an answered one a double holds exactly. message Rational { string numerator = 1; string denominator = 2; @@ -1673,7 +1674,7 @@ message Quantity { double real_magnitude = 2; // An Integer magnitude beyond int64, in decimal, as Value.big_int_value. string big_int_magnitude = 5; - // An exact Rational magnitude no double holds exactly, as Value.rational_value. + // An exact Rational magnitude, as Value.rational_value. Rational rational_magnitude = 6; } // Unit as written ("km/h") or as an operation composed it ("m/s"); empty for @@ -2094,7 +2095,7 @@ message DocumentValue { DocumentEvent event = 12; // a row Events answered; answered, never bound // An Integer beyond int64, in decimal, as Value.big_int_value. string big_int_value = 13; - // An exact Rational no double holds exactly, as Value.rational_value. + // An exact Rational, as Value.rational_value. Rational rational_value = 14; } // Metamodel type of element_id ("PartUsage", ...); answered, ignored when bound. diff --git a/changes/unreleased/exact-rationals.changed.md b/changes/unreleased/exact-rationals.changed.md index 27cf8a047b..262052d579 100644 --- a/changes/unreleased/exact-rationals.changed.md +++ b/changes/unreleased/exact-rationals.changed.md @@ -1,2 +1,2 @@ -- **Rational values are exact.** A KerML `Rational` is a rational number (KerML 1.0 §9.3.2.2.8), so a decimal or exponent literal is the exact Rational it spells and Integer `/`, the `RationalFunctions` operators, `rat`, `numer`, `denom`, `floor`, `round`, `abs`, `max`, `min`, `gcd`, `sum`, `product`, `ToString` and `ToRational` compute without rounding: `0.1 + 0.2 == 0.3` is `true`, `1 / 3` is `1/3`, and `numer(rat(1, 3))` is `1` where it was `6004799503160661`. A whole result of rational arithmetic stays a Rational unless the declaration returns `Integer`. A terminating Rational prints as a decimal (`0.1`), any other as `numer/denom` (`1/3`). A Rational whose numerator and denominator outgrow the evaluation's size budget is the typed `ErrRationalSizeLimit`, never a rounded double. `Real` stays IEEE 754 binary64: a value declared `Real`, an operation with no rational result (`sqrt`, trigonometry, `exp`, `ln`, a non-Integer exponent) and mixed Rational/Real arithmetic answer a binary64 Real. -- **Exact Rationals cross every boundary.** gRPC adds `Value.rational_value`, `Quantity.rational_magnitude` and `DocumentValue.rational_value` under the new `rational_values` capability, carrying a Rational no double holds in lowest terms (a service without the capability reports it unsupported and refuses one sent, as `big_int_values` does); the JSON engine adds `rationalValue`; the Go client decodes `opensysml.Rational`, the Python client `fractions.Fraction`, the Node client `{ kind: "rational", numerator, denominator }`, the Java client `Value.RationalValue` over `org.openmbee.opensysml.Rational`, the Rust client `Value::Rational` over `opensysml::Rational`, the Julia client `Rational{BigInt}` and the MATLAB client `struct('numerator', …, 'denominator', …)`; each refuses to send one to a service without `rational_values`. RDF export keeps an exponent literal no double holds as its exact `xsd:decimal`, unit-scale composition stays exact, the SMT witness replay is exact over Integers and Rationals (only queries a binary64 Real takes part in are still marked rounded), and `sysml -compile` refuses, by construct, exact Rational arithmetic binary64 cannot reproduce. +- **Rational values are exact.** A KerML `Rational` is a rational number (KerML 1.0 §9.3.2.2.8), so a decimal or exponent literal is the exact Rational it spells and Integer `/`, the `RationalFunctions` operators, `rat`, `numer`, `denom`, `floor`, `round`, `abs`, `max`, `min`, `gcd`, `sum`, `product`, `ToString` and `ToRational` compute without rounding: `0.1 + 0.2 == 0.3` is `true`, `1 / 3` is `1/3`, and `numer(rat(1, 3))` is `1` where it was `6004799503160661`. A whole result of rational arithmetic stays a Rational unless the declaration returns `Integer`. A terminating Rational prints as a decimal (`0.1`), any other as `numer/denom` (`1/3`). A Rational whose numerator and denominator outgrow the evaluation's size budget is the typed `ErrRationalSizeLimit`, never a rounded double. `Real` stays IEEE 754 binary64: a value declared `Real`, an operation with no rational result (`sqrt`, trigonometry, `exp`, `ln`, a non-Integer exponent) and mixed Rational/Real arithmetic answer a binary64 Real. A Rational compared with a binary64 Real is rounded once first, as in mixed arithmetic, so `x : Real = 0.1; x == 0.1` and `rat(1, 3) == 1.0 / 3.0` are `true`; Rationals compare with each other, and Integers with anything, exactly. +- **Exact Rationals cross every boundary.** gRPC adds `Value.rational_value`, `Quantity.rational_magnitude` and `DocumentValue.rational_value` under the new `rational_values` capability, carrying a Rational no double holds in lowest terms (a service without the capability reports it unsupported and refuses one sent, as `big_int_values` does); the JSON engine adds `rationalValue`; the Go client decodes `opensysml.Rational`, the Python client `fractions.Fraction`, the Node client `{ kind: "rational", numerator, denominator }`, the Java client `Value.RationalValue` over `org.openmbee.opensysml.Rational`, the Rust client `Value::Rational` over `opensysml::Rational`, the Julia client `Rational{BigInt}` and the MATLAB client `struct('numerator', …, 'denominator', …)`. Each sends every exact Rational, one a double holds included, as `rational_value` to a service with `rational_values`, which reads it as that Rational while an inbound `real_value` stays a Real; to a service without the capability a client sends a Rational a double holds as that `real_value` and refuses any other. RDF export keeps an exponent literal no double holds as its exact `xsd:decimal`, unit-scale composition stays exact, the SMT witness replay is exact over Integers and Rationals (only queries a binary64 Real takes part in are still marked rounded), and `sysml -compile` refuses, by construct, exact Rational arithmetic binary64 cannot reproduce. diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java index 5a7f64c422..0b5022bf6d 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java @@ -119,23 +119,19 @@ record BigIntegerValue(java.math.BigInteger value) implements DocumentValue { record RealValue(double value) implements DocumentValue {} /** - * An exact rational no {@code double} holds; one a {@code double} holds is always a {@link + * An exact rational. A service answers one a {@code double} holds exactly as a {@link * RealValue}. * - * @param value the rational, not a {@code double} + * @param value the rational */ record RationalValue(Rational value) implements DocumentValue { /** * Creates an exact rational. * * @param value the rational, never {@code null} - * @throws IllegalArgumentException if a {@code double} holds the rational exactly */ public RationalValue { Objects.requireNonNull(value, "value"); - if (value.isBinary64()) { - throw new IllegalArgumentException(value + " is a double, which RealValue carries"); - } } } diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Model.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Model.java index 3afdb24833..fc3771e2ea 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Model.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Model.java @@ -669,7 +669,7 @@ private ExecuteActionResponse executeAction( ExecuteActionRequest.newBuilder() .setModelHash(hash) .setActionSymbolId(actionSymbolId) - .putAllInputs(Protos.protos(inputs)) + .putAllInputs(Protos.protos(inputs, connection.capabilities())) .setSchedule(schedule(options, explore)); options.performer().ifPresent(request::setPerformerSymbolId); ExecuteActionResponse response = @@ -1049,7 +1049,7 @@ public Calculation evaluateCalc(String symbolId, List arguments) { EvaluateCalcRequest.newBuilder() .setModelHash(hash) .setSymbolId(symbolId) - .addAllArguments(Protos.protos(arguments)); + .addAllArguments(Protos.protos(arguments, connection.capabilities())); engine.ifPresent(request::setEngine); EvaluateCalcResponse response = connection.call("EvaluateCalc", request.build(), EvaluateCalcResponse.getDefaultInstance()); @@ -1146,8 +1146,8 @@ private RunAnalysisResponse runAnalysis( RunAnalysisRequest.newBuilder() .setModelHash(hash) .setSymbolId(symbolId) - .addAllArguments(Protos.protos(options.arguments())) - .putAllNamedArguments(Protos.protos(options.namedArguments())) + .addAllArguments(Protos.protos(options.arguments(), connection.capabilities())) + .putAllNamedArguments(Protos.protos(options.namedArguments(), connection.capabilities())) .setSchedule(schedule(options.schedule(), options.explores(), explore)); options.subject().ifPresent(request::setSubjectSymbolId); engine.ifPresent(request::setEngine); @@ -1510,8 +1510,8 @@ public Sweep runSweep(String symbolId, List ranges, SweepOptions opt RunSweepRequest.newBuilder() .setModelHash(hash) .setSymbolId(symbolId) - .addAllArguments(Protos.protos(options.arguments())) - .putAllNamedArguments(Protos.protos(options.namedArguments())) + .addAllArguments(Protos.protos(options.arguments(), connection.capabilities())) + .putAllNamedArguments(Protos.protos(options.namedArguments(), connection.capabilities())) .setSamples(options.samples()) .setSeed(options.seed()); options.subject().ifPresent(request::setSubjectSymbolId); @@ -1569,6 +1569,11 @@ public DocumentQueryResult runDocumentQuery( if (request.getBindingsList().stream().anyMatch(Protos::holdsBigInt)) { connection.capabilities().require(Capabilities.BIG_INT_VALUES); } + if (!connection.capabilities().has(Capabilities.RATIONAL_VALUES)) { + for (int i = 0; i < request.getBindingsCount(); i++) { + request.setBindings(i, Protos.rationalsAsReals(request.getBindings(i))); + } + } if (request.getBindingsList().stream().anyMatch(Protos::holdsRational)) { connection.capabilities().require(Capabilities.RATIONAL_VALUES); } diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Quantity.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Quantity.java index fb13106f0b..861c979826 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Quantity.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Quantity.java @@ -23,7 +23,7 @@ public record Quantity(Number magnitude, Optional unit, Optional unit, Optional protos(List values return values.stream().map(Protos::proto).toList(); } + /** + * Values by name, as a request to a service advertising {@code capabilities} carries them. + * + * @param values the immutable values by name + * @param capabilities what the service reads + * @return the generated values by name + */ + public static Map protos( + Map values, Capabilities capabilities) { + Map out = new LinkedHashMap<>(); + values.forEach((name, value) -> out.put(name, fitted(proto(value), capabilities))); + return out; + } + + /** + * Values in order, as a request to a service advertising {@code capabilities} carries them. + * + * @param values the immutable values + * @param capabilities what the service reads + * @return the generated values, in order + */ + public static List protos( + List values, Capabilities capabilities) { + return values.stream().map(value -> fitted(proto(value), capabilities)).toList(); + } + + private static org.openmbee.opensysml.proto.Value fitted( + org.openmbee.opensysml.proto.Value value, Capabilities capabilities) { + return capabilities.has(Capabilities.RATIONAL_VALUES) ? value : rationalsAsReals(value); + } + + /** + * The value with each Rational a {@code double} holds exactly as that {@code double}: the form a + * service without {@code rational_values} reads. + * + * @param value the generated value + * @return the value, rewritten + */ + public static org.openmbee.opensysml.proto.Value rationalsAsReals( + org.openmbee.opensysml.proto.Value value) { + org.openmbee.opensysml.proto.Value.Builder builder = value.toBuilder(); + switch (value.getKindCase()) { + case RATIONAL_VALUE -> { + Rational rational = sent(value.getRationalValue()); + if (rational.isBinary64()) { + builder.setRealValue(rational.doubleValue()); + } + } + case QUANTITY -> builder.setQuantity(quantityRationalAsReal(value.getQuantity())); + case SEQUENCE -> { + ValueSequence.Builder sequence = value.getSequence().toBuilder(); + for (int i = 0; i < sequence.getElementsCount(); i++) { + sequence.setElements(i, rationalsAsReals(sequence.getElements(i))); + } + builder.setSequence(sequence); + } + case SET -> { + ValueSet.Builder set = value.getSet().toBuilder(); + for (int i = 0; i < set.getElementsCount(); i++) { + set.setElements(i, rationalsAsReals(set.getElements(i))); + } + builder.setSet(set); + } + case ARRAY -> { + org.openmbee.opensysml.proto.Array.Builder array = value.getArray().toBuilder(); + for (int i = 0; i < array.getElementsCount(); i++) { + array.setElements(i, rationalsAsReals(array.getElements(i))); + } + builder.setArray(array); + } + case VECTOR -> { + org.openmbee.opensysml.proto.Vector.Builder vector = value.getVector().toBuilder(); + for (int i = 0; i < vector.getComponentsCount(); i++) { + vector.setComponents(i, rationalsAsReals(vector.getComponents(i))); + } + builder.setVector(vector); + } + case VECTOR_QUANTITY -> { + org.openmbee.opensysml.proto.VectorQuantity.Builder vector = + value.getVectorQuantity().toBuilder(); + for (int i = 0; i < vector.getComponentsCount(); i++) { + vector.setComponents(i, quantityRationalAsReal(vector.getComponents(i))); + } + builder.setVectorQuantity(vector); + } + case TENSOR_QUANTITY -> { + TensorQuantity.Builder tensor = value.getTensorQuantity().toBuilder(); + for (int i = 0; i < tensor.getComponentsCount(); i++) { + tensor.setComponents(i, quantityRationalAsReal(tensor.getComponents(i))); + } + builder.setTensorQuantity(tensor); + } + case ENUM_LITERAL -> { + if (value.getEnumLiteral().hasValue()) { + builder.setEnumLiteral( + value.getEnumLiteral().toBuilder() + .setValue(rationalsAsReals(value.getEnumLiteral().getValue()))); + } + } + default -> {} + } + return builder.build(); + } + + /** + * The binding with each Rational a {@code double} holds exactly as that {@code double}, for a + * service without {@code rational_values}. + * + * @param binding the wire binding + * @return the binding, rewritten + */ + public static org.openmbee.opensysml.proto.DocumentQueryBinding rationalsAsReals( + org.openmbee.opensysml.proto.DocumentQueryBinding binding) { + org.openmbee.opensysml.proto.DocumentQueryBinding.Builder builder = binding.toBuilder(); + for (int i = 0; i < builder.getValuesCount(); i++) { + org.openmbee.opensysml.proto.DocumentValue value = builder.getValues(i); + if (value.hasRationalValue() && sent(value.getRationalValue()).isBinary64()) { + builder.setValues( + i, value.toBuilder().setRealValue(sent(value.getRationalValue()).doubleValue())); + } else if (value.hasQuantity()) { + builder.setValues( + i, value.toBuilder().setQuantity(quantityRationalAsReal(value.getQuantity()))); + } + } + return builder.build(); + } + + private static org.openmbee.opensysml.proto.Quantity quantityRationalAsReal( + org.openmbee.opensysml.proto.Quantity quantity) { + if (quantity.hasRationalMagnitude() && sent(quantity.getRationalMagnitude()).isBinary64()) { + return quantity.toBuilder() + .setRealMagnitude(sent(quantity.getRationalMagnitude()).doubleValue()) + .build(); + } + return quantity; + } + + // The rational a request's own rational_value spells, which this client wrote in lowest terms. + private static Rational sent(org.openmbee.opensysml.proto.Rational wire) { + return Rational.of( + new java.math.BigInteger(wire.getNumerator()), + new java.math.BigInteger(wire.getDenominator())); + } + private static org.openmbee.opensysml.proto.Quantity proto(Quantity quantity) { org.openmbee.opensysml.proto.Quantity.Builder builder = org.openmbee.opensysml.proto.Quantity.newBuilder(); @@ -1631,8 +1776,8 @@ public static boolean holdsBigInt(org.openmbee.opensysml.proto.DocumentQueryBind } /** - * Whether a document-query binding sends an exact Rational no double holds, which a service - * reads only when it advertises {@code rational_values}. + * Whether a document-query binding sends an exact Rational, which a service reads only when it + * advertises {@code rational_values}. * * @param binding the wire binding * @return whether a value or a quantity magnitude is a {@code rational_value} diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValue.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValue.java index 3fc7e5060b..c29273caaf 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValue.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValue.java @@ -597,7 +597,7 @@ public java.lang.String getBigIntValue() { public static final int RATIONAL_VALUE_FIELD_NUMBER = 14; /** *

-   * An exact Rational no double holds exactly, as Value.rational_value.
+   * An exact Rational, as Value.rational_value.
    * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -609,7 +609,7 @@ public boolean hasRationalValue() { } /** *
-   * An exact Rational no double holds exactly, as Value.rational_value.
+   * An exact Rational, as Value.rational_value.
    * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -624,7 +624,7 @@ public org.openmbee.opensysml.proto.Rational getRationalValue() { } /** *
-   * An exact Rational no double holds exactly, as Value.rational_value.
+   * An exact Rational, as Value.rational_value.
    * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -2796,7 +2796,7 @@ public Builder setBigIntValueBytes( org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder> rationalValueBuilder_; /** *
-     * An exact Rational no double holds exactly, as Value.rational_value.
+     * An exact Rational, as Value.rational_value.
      * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -2808,7 +2808,7 @@ public boolean hasRationalValue() { } /** *
-     * An exact Rational no double holds exactly, as Value.rational_value.
+     * An exact Rational, as Value.rational_value.
      * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -2830,7 +2830,7 @@ public org.openmbee.opensysml.proto.Rational getRationalValue() { } /** *
-     * An exact Rational no double holds exactly, as Value.rational_value.
+     * An exact Rational, as Value.rational_value.
      * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -2850,7 +2850,7 @@ public Builder setRationalValue(org.openmbee.opensysml.proto.Rational value) { } /** *
-     * An exact Rational no double holds exactly, as Value.rational_value.
+     * An exact Rational, as Value.rational_value.
      * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -2868,7 +2868,7 @@ public Builder setRationalValue( } /** *
-     * An exact Rational no double holds exactly, as Value.rational_value.
+     * An exact Rational, as Value.rational_value.
      * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -2895,7 +2895,7 @@ public Builder mergeRationalValue(org.openmbee.opensysml.proto.Rational value) { } /** *
-     * An exact Rational no double holds exactly, as Value.rational_value.
+     * An exact Rational, as Value.rational_value.
      * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -2918,7 +2918,7 @@ public Builder clearRationalValue() { } /** *
-     * An exact Rational no double holds exactly, as Value.rational_value.
+     * An exact Rational, as Value.rational_value.
      * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -2928,7 +2928,7 @@ public org.openmbee.opensysml.proto.Rational.Builder getRationalValueBuilder() { } /** *
-     * An exact Rational no double holds exactly, as Value.rational_value.
+     * An exact Rational, as Value.rational_value.
      * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -2946,7 +2946,7 @@ public org.openmbee.opensysml.proto.RationalOrBuilder getRationalValueOrBuilder( } /** *
-     * An exact Rational no double holds exactly, as Value.rational_value.
+     * An exact Rational, as Value.rational_value.
      * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValueOrBuilder.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValueOrBuilder.java index 0e087e6276..f6f111c0c2 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValueOrBuilder.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentValueOrBuilder.java @@ -266,7 +266,7 @@ public interface DocumentValueOrBuilder extends /** *
-   * An exact Rational no double holds exactly, as Value.rational_value.
+   * An exact Rational, as Value.rational_value.
    * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -275,7 +275,7 @@ public interface DocumentValueOrBuilder extends boolean hasRationalValue(); /** *
-   * An exact Rational no double holds exactly, as Value.rational_value.
+   * An exact Rational, as Value.rational_value.
    * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; @@ -284,7 +284,7 @@ public interface DocumentValueOrBuilder extends org.openmbee.opensysml.proto.Rational getRationalValue(); /** *
-   * An exact Rational no double holds exactly, as Value.rational_value.
+   * An exact Rational, as Value.rational_value.
    * 
* * .sysml.Rational rational_value = 14 [json_name = "rationalValue"]; diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Quantity.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Quantity.java index 2bc931c279..ad3492c9b8 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Quantity.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Quantity.java @@ -205,7 +205,7 @@ public java.lang.String getBigIntMagnitude() { public static final int RATIONAL_MAGNITUDE_FIELD_NUMBER = 6; /** *
-   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * An exact Rational magnitude, as Value.rational_value.
    * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -217,7 +217,7 @@ public boolean hasRationalMagnitude() { } /** *
-   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * An exact Rational magnitude, as Value.rational_value.
    * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -232,7 +232,7 @@ public org.openmbee.opensysml.proto.Rational getRationalMagnitude() { } /** *
-   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * An exact Rational magnitude, as Value.rational_value.
    * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -1038,7 +1038,7 @@ public Builder setBigIntMagnitudeBytes( org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder> rationalMagnitudeBuilder_; /** *
-     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * An exact Rational magnitude, as Value.rational_value.
      * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -1050,7 +1050,7 @@ public boolean hasRationalMagnitude() { } /** *
-     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * An exact Rational magnitude, as Value.rational_value.
      * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -1072,7 +1072,7 @@ public org.openmbee.opensysml.proto.Rational getRationalMagnitude() { } /** *
-     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * An exact Rational magnitude, as Value.rational_value.
      * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -1092,7 +1092,7 @@ public Builder setRationalMagnitude(org.openmbee.opensysml.proto.Rational value) } /** *
-     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * An exact Rational magnitude, as Value.rational_value.
      * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -1110,7 +1110,7 @@ public Builder setRationalMagnitude( } /** *
-     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * An exact Rational magnitude, as Value.rational_value.
      * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -1137,7 +1137,7 @@ public Builder mergeRationalMagnitude(org.openmbee.opensysml.proto.Rational valu } /** *
-     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * An exact Rational magnitude, as Value.rational_value.
      * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -1160,7 +1160,7 @@ public Builder clearRationalMagnitude() { } /** *
-     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * An exact Rational magnitude, as Value.rational_value.
      * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -1170,7 +1170,7 @@ public org.openmbee.opensysml.proto.Rational.Builder getRationalMagnitudeBuilder } /** *
-     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * An exact Rational magnitude, as Value.rational_value.
      * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -1188,7 +1188,7 @@ public org.openmbee.opensysml.proto.RationalOrBuilder getRationalMagnitudeOrBuil } /** *
-     * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+     * An exact Rational magnitude, as Value.rational_value.
      * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/QuantityOrBuilder.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/QuantityOrBuilder.java index 6e2bfcb306..b350539e23 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/QuantityOrBuilder.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/QuantityOrBuilder.java @@ -63,7 +63,7 @@ public interface QuantityOrBuilder extends /** *
-   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * An exact Rational magnitude, as Value.rational_value.
    * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -72,7 +72,7 @@ public interface QuantityOrBuilder extends boolean hasRationalMagnitude(); /** *
-   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * An exact Rational magnitude, as Value.rational_value.
    * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; @@ -81,7 +81,7 @@ public interface QuantityOrBuilder extends org.openmbee.opensysml.proto.Rational getRationalMagnitude(); /** *
-   * An exact Rational magnitude no double holds exactly, as Value.rational_value.
+   * An exact Rational magnitude, as Value.rational_value.
    * 
* * .sysml.Rational rational_magnitude = 6 [json_name = "rationalMagnitude"]; diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Rational.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Rational.java index 9db9b0505a..6f439dab42 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Rational.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Rational.java @@ -8,8 +8,8 @@ /** *
  * Rational is an exact rational number in lowest terms: numerator over a
- * positive denominator, each written as big_int_value is. A decoder rejects one
- * not in lowest terms or one a double holds exactly, so no number has two spellings.
+ * positive denominator, each written as big_int_value is. The server rejects one
+ * not in lowest terms; a client also rejects an answered one a double holds exactly.
  * 
* * Protobuf type {@code sysml.Rational} @@ -298,8 +298,8 @@ protected Builder newBuilderForType( /** *
    * Rational is an exact rational number in lowest terms: numerator over a
-   * positive denominator, each written as big_int_value is. A decoder rejects one
-   * not in lowest terms or one a double holds exactly, so no number has two spellings.
+   * positive denominator, each written as big_int_value is. The server rejects one
+   * not in lowest terms; a client also rejects an answered one a double holds exactly.
    * 
* * Protobuf type {@code sysml.Rational} diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java index e7e88f3f0c..4a125b65a3 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java @@ -1011,8 +1011,9 @@ public java.lang.String getBigIntValue() { public static final int RATIONAL_VALUE_FIELD_NUMBER = 23; /** *
-   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+   * server answers one a double holds exactly as real_value; a client sends
+   * any exact Rational here, and an inbound real_value is always a Real.
    * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -1024,8 +1025,9 @@ public boolean hasRationalValue() { } /** *
-   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+   * server answers one a double holds exactly as real_value; a client sends
+   * any exact Rational here, and an inbound real_value is always a Real.
    * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -1040,8 +1042,9 @@ public org.openmbee.opensysml.proto.Rational getRationalValue() { } /** *
-   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+   * server answers one a double holds exactly as real_value; a client sends
+   * any exact Rational here, and an inbound real_value is always a Real.
    * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -4968,8 +4971,9 @@ public Builder setBigIntValueBytes( org.openmbee.opensysml.proto.Rational, org.openmbee.opensysml.proto.Rational.Builder, org.openmbee.opensysml.proto.RationalOrBuilder> rationalValueBuilder_; /** *
-     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+     * server answers one a double holds exactly as real_value; a client sends
+     * any exact Rational here, and an inbound real_value is always a Real.
      * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -4981,8 +4985,9 @@ public boolean hasRationalValue() { } /** *
-     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+     * server answers one a double holds exactly as real_value; a client sends
+     * any exact Rational here, and an inbound real_value is always a Real.
      * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -5004,8 +5009,9 @@ public org.openmbee.opensysml.proto.Rational getRationalValue() { } /** *
-     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+     * server answers one a double holds exactly as real_value; a client sends
+     * any exact Rational here, and an inbound real_value is always a Real.
      * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -5025,8 +5031,9 @@ public Builder setRationalValue(org.openmbee.opensysml.proto.Rational value) { } /** *
-     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+     * server answers one a double holds exactly as real_value; a client sends
+     * any exact Rational here, and an inbound real_value is always a Real.
      * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -5044,8 +5051,9 @@ public Builder setRationalValue( } /** *
-     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+     * server answers one a double holds exactly as real_value; a client sends
+     * any exact Rational here, and an inbound real_value is always a Real.
      * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -5072,8 +5080,9 @@ public Builder mergeRationalValue(org.openmbee.opensysml.proto.Rational value) { } /** *
-     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+     * server answers one a double holds exactly as real_value; a client sends
+     * any exact Rational here, and an inbound real_value is always a Real.
      * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -5096,8 +5105,9 @@ public Builder clearRationalValue() { } /** *
-     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+     * server answers one a double holds exactly as real_value; a client sends
+     * any exact Rational here, and an inbound real_value is always a Real.
      * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -5107,8 +5117,9 @@ public org.openmbee.opensysml.proto.Rational.Builder getRationalValueBuilder() { } /** *
-     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+     * server answers one a double holds exactly as real_value; a client sends
+     * any exact Rational here, and an inbound real_value is always a Real.
      * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -5126,8 +5137,9 @@ public org.openmbee.opensysml.proto.RationalOrBuilder getRationalValueOrBuilder( } /** *
-     * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-     * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+     * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+     * server answers one a double holds exactly as real_value; a client sends
+     * any exact Rational here, and an inbound real_value is always a Real.
      * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java index c1a703f2be..fa52daba0b 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java @@ -516,8 +516,9 @@ public interface ValueOrBuilder extends /** *
-   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+   * server answers one a double holds exactly as real_value; a client sends
+   * any exact Rational here, and an inbound real_value is always a Real.
    * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -526,8 +527,9 @@ public interface ValueOrBuilder extends boolean hasRationalValue(); /** *
-   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+   * server answers one a double holds exactly as real_value; a client sends
+   * any exact Rational here, and an inbound real_value is always a Real.
    * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; @@ -536,8 +538,9 @@ public interface ValueOrBuilder extends org.openmbee.opensysml.proto.Rational getRationalValue(); /** *
-   * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1`
-   * or `1 / 3` evaluates; one a double holds exactly crosses as real_value.
+   * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The
+   * server answers one a double holds exactly as real_value; a client sends
+   * any exact Rational here, and an inbound real_value is always a Real.
    * 
* * .sysml.Rational rational_value = 23 [json_name = "rationalValue"]; diff --git a/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java b/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java index c238de771c..d71ac44c9b 100644 --- a/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java +++ b/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java @@ -72,8 +72,46 @@ void aRationalIsReadAndWrittenExactly() { assertEquals( new Value.RationalValue(Rational.of(BigInteger.TEN.pow(400), BigInteger.ONE)), Protos.value(whole).orElseThrow()); - assertThrows( - IllegalArgumentException.class, () -> new Value.RationalValue(Rational.of(1, 2))); + Value half = new Value.RationalValue(Rational.of(1, 2)); + assertEquals(wire("1", "2"), Protos.proto(half).getRationalValue()); + assertTrue(half.sameValue(new Value.RealValue(0.5))); + assertTrue(new Value.RationalValue(Rational.of(3, 1)).sameValue(new Value.IntegerValue(3))); + } + + @Test + void aRationalADoubleHoldsIsThatDoubleOnlyForAServiceWithoutRationalValues() { + var quarter = Protos.proto(new Value.RationalValue(Rational.of(1, 4))); + assertEquals(0.25, Protos.rationalsAsReals(quarter).getRealValue()); + var third = Protos.proto(new Value.RationalValue(Rational.of(1, 3))); + assertEquals(third, Protos.rationalsAsReals(third)); + var nested = + Protos.proto( + new Value.Sequence( + List.of( + new Value.RationalValue(Rational.of(1, 4)), + new Value.QuantityValue( + new Quantity( + Rational.of(1, 2), + java.util.Optional.empty(), + java.util.Optional.empty()))))); + var rewritten = Protos.rationalsAsReals(nested).getSequence(); + assertEquals(0.25, rewritten.getElements(0).getRealValue()); + assertEquals(0.5, rewritten.getElements(1).getQuantity().getRealMagnitude()); + var binding = + DocumentQueryBinding.newBuilder() + .setParameter("x") + .addValues(Protos.proto(new DocumentValue.RationalValue(Rational.of(1, 2)))) + .addValues(Protos.proto(new DocumentValue.RationalValue(Rational.of(1, 3)))) + .build(); + assertTrue(Protos.holdsRational(binding)); + var older = Protos.rationalsAsReals(binding); + assertEquals(0.5, older.getValues(0).getRealValue()); + assertEquals(wire("1", "3"), older.getValues(1).getRationalValue()); + assertEquals( + 0.25, + Protos.value(org.openmbee.opensysml.proto.Value.newBuilder().setRealValue(0.25).build()) + .orElseThrow() + .asDouble()); } @Test @@ -133,9 +171,14 @@ void aRationalMagnitudeAndComponentCrossExactly() { .build(); assertTrue( new Value.QuantityValue(third).sameValue(new Value.QuantityValue(Protos.quantity(metres)))); - assertThrows( - IllegalArgumentException.class, - () -> new Quantity(Rational.of(1, 2), java.util.Optional.empty(), java.util.Optional.empty())); + assertEquals( + wire("1", "2"), + Protos.proto( + new Value.QuantityValue( + new Quantity( + Rational.of(1, 2), java.util.Optional.empty(), java.util.Optional.empty()))) + .getQuantity() + .getRationalMagnitude()); var vector = org.openmbee.opensysml.proto.Value.newBuilder() diff --git a/client/julia/OpenSysML/src/document.jl b/client/julia/OpenSysML/src/document.jl index 66c0341b14..30fc45fbcd 100644 --- a/client/julia/OpenSysML/src/document.jl +++ b/client/julia/OpenSysML/src/document.jl @@ -182,7 +182,7 @@ function build_document_bindings(bindings=Dict()) elseif value isa Integer Dict{String,Any}(integer_arm(value, "intValue", "bigIntValue")) elseif value isa Rational - Dict{String,Any}(rational_arm(value, "realValue", "rationalValue")) + Dict{String,Any}(rational_arm(value, "rationalValue")) elseif value isa AbstractFloat Dict{String,Any}("realValue" => Float64(value)) elseif value isa Quantity @@ -204,7 +204,7 @@ function build_document_bindings(bindings=Dict()) result end -# Whether a wire binding sends an exact Rational no Float64 holds, which needs rational_values. +# Whether a wire binding sends an exact Rational, which needs rational_values. _binding_holds_rational(binding) = any(binding["values"]) do value haskey(value, "rationalValue") || (haskey(value, "quantity") && haskey(value["quantity"], "rationalMagnitude")) @@ -222,6 +222,7 @@ function run_document_query(model::Model, query_id::AbstractString; bindings=Dic require_capability(conn, CAPABILITY_DOCUMENT_QUERY) wire = build_document_bindings(bindings) any(_binding_holds_big_int, wire) && require_capability(conn, CAPABILITY_BIG_INT_VALUES) + has_capability(conn, CAPABILITY_RATIONAL_VALUES) || rationals_as_reals!(wire) any(_binding_holds_rational, wire) && require_capability(conn, CAPABILITY_RATIONAL_VALUES) request = Dict{String,Any}("modelHash" => model.hash, "queryId" => String(query_id), "bindings" => wire) diff --git a/client/julia/OpenSysML/src/model.jl b/client/julia/OpenSysML/src/model.jl index a4818cf2d7..bd36edeaed 100644 --- a/client/julia/OpenSysML/src/model.jl +++ b/client/julia/OpenSysML/src/model.jl @@ -365,7 +365,9 @@ function _encoded_inputs(model::Model, inputs) for capability in sort!(collect(value_capabilities(value))) require_capability(model.connection, capability) end - encoded[String(key)] = encode_value(value) + wire = encode_value(value) + has_capability(model.connection, CAPABILITY_RATIONAL_VALUES) || rationals_as_reals!(wire) + encoded[String(key)] = wire end return encoded end diff --git a/client/julia/OpenSysML/src/values.jl b/client/julia/OpenSysML/src/values.jl index a4e359055d..19895a4ae9 100644 --- a/client/julia/OpenSysML/src/values.jl +++ b/client/julia/OpenSysML/src/values.jl @@ -381,11 +381,31 @@ _binary64(q::Rational) = (x = Float64(q); isfinite(x) && Rational{BigInt}(x) == # A Rational as KerML holds it: lowest terms over BigInt. _exact_rational(q::Rational) = Rational{BigInt}(q) -# A Rational on the wire: realValue when a Float64 holds it exactly, rationalValue otherwise. -rational_arm(q::Rational, real::String, rational::String) = - _binary64(q) ? (real => _json_real(Float64(q))) : - (rational => Dict{String,Any}("numerator" => string(numerator(q)), - "denominator" => string(denominator(q)))) +# A Rational on the wire: rationalValue, one a Float64 holds exactly included, since +# the service reads an inbound realValue as a Real. +rational_arm(q::Rational, rational::String) = + rational => Dict{String,Any}("numerator" => string(numerator(q)), + "denominator" => string(denominator(q))) + +# Rewrite in place each Rational arm of an encoded value a Float64 holds exactly as +# that Float64: the form a service without rational_values reads. +function rationals_as_reals!(wire) + if wire isa AbstractDict + for (rational, real) in (("rationalValue", "realValue"), ("rationalMagnitude", "realMagnitude")) + terms = get(wire, rational, nothing) + terms isa AbstractDict || continue + q = parse(BigInt, terms["numerator"]) // parse(BigInt, terms["denominator"]) + if _binary64(q) + delete!(wire, rational) + wire[real] = _json_real(Float64(q)) + end + end + foreach(rationals_as_reals!, values(wire)) + elseif wire isa AbstractVector + foreach(rationals_as_reals!, wire) + end + wire +end # An Integer on the wire: intValue within Int64, bigIntValue beyond it. integer_arm(x::Integer, small::String, big::String) = @@ -519,7 +539,7 @@ function encode_quantity(q::Quantity) if q.magnitude isa Integer push!(body, integer_arm(q.magnitude, "intMagnitude", "bigIntMagnitude")) elseif q.magnitude isa Rational - push!(body, rational_arm(q.magnitude, "realMagnitude", "rationalMagnitude")) + push!(body, rational_arm(q.magnitude, "rationalMagnitude")) else body["realMagnitude"] = _json_real(Float64(q.magnitude)) end @@ -539,7 +559,7 @@ function encode_value(x::Bool) Dict{String,Any}("boolValue" => x) end encode_value(x::Integer) = Dict{String,Any}(integer_arm(x, "intValue", "bigIntValue")) -encode_value(x::Rational) = Dict{String,Any}(rational_arm(x, "realValue", "rationalValue")) +encode_value(x::Rational) = Dict{String,Any}(rational_arm(x, "rationalValue")) encode_value(x::AbstractFloat) = Dict{String,Any}("realValue" => _json_real(x)) encode_value(x::AbstractString) = Dict{String,Any}("stringValue" => String(x)) encode_value(::Nothing) = Dict{String,Any}("null" => "") diff --git a/client/julia/OpenSysML/src/verification.jl b/client/julia/OpenSysML/src/verification.jl index 5e65b1ea87..7c29a7cdb8 100644 --- a/client/julia/OpenSysML/src/verification.jl +++ b/client/julia/OpenSysML/src/verification.jl @@ -29,7 +29,8 @@ function _encoded(conn::Connection, value) for capability in sort!(collect(value_capabilities(value))) require_capability(conn, capability) end - encode_value(value) + wire = encode_value(value) + has_capability(conn, CAPABILITY_RATIONAL_VALUES) ? wire : rationals_as_reals!(wire) end function _encoded_arguments(conn::Connection, arguments) diff --git a/client/julia/OpenSysML/test/runtests.jl b/client/julia/OpenSysML/test/runtests.jl index 49342a0004..555ca969ae 100644 --- a/client/julia/OpenSysML/test/runtests.jl +++ b/client/julia/OpenSysML/test/runtests.jl @@ -23,7 +23,12 @@ const FIXTURES = normpath(joinpath(@__DIR__, "..", "..", "..", "..", "conformanc third = decode_value(JSON.parse("""{"rationalValue":{"numerator":"-1","denominator":"3"}}""")) @test third isa Rational{BigInt} && third == -1//3 @test encode_value(third) == Dict("rationalValue" => Dict("numerator" => "-1", "denominator" => "3")) - @test encode_value(1//4) == Dict("realValue" => 0.25) + @test encode_value(1//4) == Dict("rationalValue" => Dict("numerator" => "1", "denominator" => "4")) + @test OpenSysML.rationals_as_reals!(encode_value(1//4)) == Dict("realValue" => 0.25) + @test OpenSysML.rationals_as_reals!(encode_value(Any[1//3, 1//2])) == + Dict("sequence" => Dict("elements" => Any[ + Dict("rationalValue" => Dict("numerator" => "1", "denominator" => "3")), + Dict("realValue" => 0.5)])) @test encode_value(big(10)^400 // 1)["rationalValue"]["denominator"] == "1" for (n, d) in (("2", "6"), ("1", "-3"), ("1", "0"), ("1", "2"), ("3", "1"), ("0", "1"), ("-0", "3"), ("01", "3"), ("+1", "3"), ("1.5", "7"), ("", "3")) diff --git a/client/matlab/+opensysml/+internal/bindingRationalsAsReals.m b/client/matlab/+opensysml/+internal/bindingRationalsAsReals.m new file mode 100644 index 0000000000..ad0469037a --- /dev/null +++ b/client/matlab/+opensysml/+internal/bindingRationalsAsReals.m @@ -0,0 +1,19 @@ +function binding = bindingRationalsAsReals(binding) +%BINDINGRATIONALSASREALS A wire binding with each Rational a double holds sent as that double. +% The form a service without rational_values reads; other Rationals are kept. + + for i = 1:numel(binding.values) + value = binding.values{i}; + if isfield(value, 'rationalValue') + exact = opensysml.internal.rationalDouble(value.rationalValue); + if ~isempty(exact), binding.values{i} = struct('realValue', exact); end + elseif isfield(value, 'quantity') && isfield(value.quantity, 'rationalMagnitude') + exact = opensysml.internal.rationalDouble(value.quantity.rationalMagnitude); + if ~isempty(exact) + quantity = rmfield(value.quantity, 'rationalMagnitude'); + quantity.realMagnitude = exact; + binding.values{i}.quantity = quantity; + end + end + end +end diff --git a/client/matlab/+opensysml/+internal/rationalDouble.m b/client/matlab/+opensysml/+internal/rationalDouble.m new file mode 100644 index 0000000000..28e4228366 --- /dev/null +++ b/client/matlab/+opensysml/+internal/rationalDouble.m @@ -0,0 +1,11 @@ +function x = rationalDouble(terms) +%RATIONALDOUBLE The double a Rational's decimal terms are exactly; empty when none is. +% Read where both terms are exact doubles; larger terms are taken as no double. + + x = []; + n = str2double(terms.numerator); + d = str2double(terms.denominator); + if abs(n) <= flintmax && d <= flintmax && bitand(uint64(d), uint64(d) - 1) == 0 + x = n / d; + end +end diff --git a/client/matlab/+opensysml/+internal/rationalOf.m b/client/matlab/+opensysml/+internal/rationalOf.m index ba7c16655c..8ab331cc7a 100644 --- a/client/matlab/+opensysml/+internal/rationalOf.m +++ b/client/matlab/+opensysml/+internal/rationalOf.m @@ -1,9 +1,10 @@ -function r = rationalOf(raw) +function r = rationalOf(raw, canonical) %RATIONALOF The exact Rational a rationalValue spells, kept as its decimal terms. -% Terms are canonical: lowest terms over a positive denominator, of a value no -% double holds. Lowest terms and double exactness are checked where both terms -% are exact doubles; larger terms are taken as the service sent them. +% Terms are lowest terms over a positive denominator; a canonical one, as a +% service answers it, is also of a value no double holds. Both are checked where +% both terms are exact doubles; larger terms are taken as they were spelt. + if nargin < 2, canonical = true; end if ~isstruct(raw) || ~isscalar(raw) || ~isfield(raw, 'numerator') || ~isfield(raw, 'denominator') error('opensysml:decode', 'a rational carries a numerator and a denominator'); end @@ -18,7 +19,7 @@ if gcd(nd, dd) ~= 1 error('opensysml:decode', '%s/%s is not in lowest terms', n, d); end - if bitand(uint64(dd), uint64(dd) - 1) == 0 + if canonical && bitand(uint64(dd), uint64(dd) - 1) == 0 error('opensysml:decode', '%s/%s is a double, which realValue carries', n, d); end end diff --git a/client/matlab/+opensysml/encodeValue.m b/client/matlab/+opensysml/encodeValue.m index 39fcdc4d3d..35f52d1784 100644 --- a/client/matlab/+opensysml/encodeValue.m +++ b/client/matlab/+opensysml/encodeValue.m @@ -53,8 +53,14 @@ requireCapability(conn, 'big_int_values'); v = struct('bigIntValue', char(x.bigInteger)); elseif isRational(x) - requireCapability(conn, 'rational_values'); - v = struct('rationalValue', rationalTerms(x)); + terms = rationalTerms(x); + exact = opensysml.internal.rationalDouble(terms); + if isempty(exact) || isempty(conn) || conn.hasCapability('rational_values') + requireCapability(conn, 'rational_values'); + v = struct('rationalValue', terms); + else + v = struct('realValue', exact); + end elseif isfield(x, 'instanceRef') id = integerValue(x.instanceRef, 'instance reference id'); v = struct('instanceId', sprintf('%d', id)); @@ -170,8 +176,14 @@ requireCapability(conn, 'big_int_values'); body.bigIntMagnitude = char(magnitude.bigInteger); elseif isRational(magnitude) - requireCapability(conn, 'rational_values'); - body.rationalMagnitude = rationalTerms(magnitude); + terms = rationalTerms(magnitude); + exact = opensysml.internal.rationalDouble(terms); + if isempty(exact) || isempty(conn) || conn.hasCapability('rational_values') + requireCapability(conn, 'rational_values'); + body.rationalMagnitude = terms; + else + body.realMagnitude = exact; + end elseif ~isnumeric(magnitude) || ~isscalar(magnitude) || ~isreal(magnitude) opensysml.internal.raise('opensysml:encode', 'quantity magnitude must be a real scalar'); elseif isinteger(magnitude) @@ -262,7 +274,7 @@ function validateShape(dimensions, count, kind) function terms = rationalTerms(value) try - terms = opensysml.internal.rationalOf(value); + terms = opensysml.internal.rationalOf(value, false); catch e opensysml.internal.raise('opensysml:encode', e.message); end diff --git a/client/matlab/+opensysml/runDocumentQuery.m b/client/matlab/+opensysml/runDocumentQuery.m index 759d50834d..c320c1b4d7 100644 --- a/client/matlab/+opensysml/runDocumentQuery.m +++ b/client/matlab/+opensysml/runDocumentQuery.m @@ -15,6 +15,9 @@ end model.connection.require('document_query'); wire = opensysml.buildDocumentBindings(bindings); + if ~model.connection.hasCapability('rational_values') + wire = cellfun(@opensysml.internal.bindingRationalsAsReals, wire, 'UniformOutput', false); + end if any(cellfun(@bindingHoldsBigInt, wire)) model.connection.require('big_int_values'); end @@ -29,7 +32,7 @@ end function exact = bindingHoldsRational(binding) -%BINDINGHOLDSRATIONAL Whether a wire binding sends an exact Rational no double holds. +%BINDINGHOLDSRATIONAL Whether a wire binding sends an exact Rational. exact = any(cellfun(@(value) isfield(value, 'rationalValue') || ... (isfield(value, 'quantity') && isfield(value.quantity, 'rationalMagnitude')), ... binding.values)); diff --git a/client/matlab/README.md b/client/matlab/README.md index 2bb84c121a..7ba831ce61 100644 --- a/client/matlab/README.md +++ b/client/matlab/README.md @@ -100,7 +100,7 @@ A `Model` exposes `hash`, `connection`, `diagnostics`, `documents`, | --- | --- | | `intValue` | Exact `int64`; JSON decimal digits are never routed through a double. | | `bigIntValue` | `struct('bigInteger', char)`: an Integer beyond `int64`, kept as its decimal digits since no MATLAB number holds it, and sent back as written. A quantity's `bigIntMagnitude` and a vector component decode the same way. | -| `rationalValue` | `struct('numerator', char, 'denominator', char)`: an exact Rational no `double` holds (such as `1/3`), kept as its lowest-terms decimal numerator and positive denominator, and sent back as written; sending one needs `rational_values`. A Rational a `double` holds exactly arrives as `realValue`. A quantity's `rationalMagnitude` and a vector component decode the same way. | +| `rationalValue` | `struct('numerator', char, 'denominator', char)`: an exact Rational no `double` holds (such as `1/3`), kept as its lowest-terms decimal numerator and positive denominator, and sent back as written. Any such struct, one a `double` holds included, is sent as `rationalValue` to a service with `rational_values`; to one without it, a Rational a `double` holds is sent as that `realValue` and any other is refused. A Rational a `double` holds exactly arrives as `realValue`, and a `realValue` sent is always a Real. A quantity's `rationalMagnitude` and a vector component decode the same way. | | `realValue` | `double`, including `"NaN"`, `"Infinity"`, and `"-Infinity"`. | | `boolValue` / `stringValue` | `logical` / `char`. | | `instanceId` | `struct('instanceRef', int64)`. | diff --git a/client/matlab/tests/test_encode_value.m b/client/matlab/tests/test_encode_value.m index 292ffd975c..688387e6c9 100644 --- a/client/matlab/tests/test_encode_value.m +++ b/client/matlab/tests/test_encode_value.m @@ -13,8 +13,11 @@ function test_encode_value() assert_equal(opensysml.decodeValue(opensysml.encodeValue(third)), third, 'rational round trip'); q = opensysml.encodeValue(struct('magnitude', third, 'unit', 'kg', 'unitTerm', [])); assert_equal(q.quantity.rationalMagnitude, third, 'quantity rationalMagnitude'); - assert_error(@() opensysml.encodeValue(struct('numerator', '1', 'denominator', '4')), ... - 'opensysml:encode', 'a double-exact rational is a Real'); + quarter = struct('numerator', '1', 'denominator', '4'); + assert_equal(opensysml.encodeValue(quarter), struct('rationalValue', quarter), ... + 'a double-exact rational is sent exactly'); + assert_error(@() opensysml.decodeValue(struct('rationalValue', quarter)), ... + 'opensysml:decode', 'a service answers a double-exact rational as a Real'); assert_equal(opensysml.encodeValue(1.5), struct('realValue', 1.5), 'real'); assert_equal(opensysml.encodeValue('x'), struct('stringValue', 'x'), 'string'); assert_equal(opensysml.encodeValue([]), struct('null', ''), 'null'); diff --git a/client/matlab/tests/test_surface_offline.m b/client/matlab/tests/test_surface_offline.m index 4d4e3237f4..2e3a48e582 100644 --- a/client/matlab/tests/test_surface_offline.m +++ b/client/matlab/tests/test_surface_offline.m @@ -84,6 +84,15 @@ function test_surface_offline() wire = opensysml.encodeValue({int64(1), {third}}, rationalConn); assert_equal(wire.sequence.elements{2}.sequence.elements{1}.rationalValue, third, ... 'nested exact Rational with rational_values'); + half = struct('numerator', '1', 'denominator', '2'); + assert_equal(opensysml.encodeValue(half, rationalConn), struct('rationalValue', half), ... + 'double-exact Rational with rational_values'); + wire = opensysml.encodeValue({half}, conn); + assert_equal(wire.sequence.elements{1}, struct('realValue', 0.5), ... + 'double-exact Rational without rational_values'); + halfQuantity = opensysml.encodeValue(struct('magnitude', half, 'unit', 'kg'), conn); + assert_equal(halfQuantity.quantity.realMagnitude, 0.5, ... + 'double-exact Rational magnitude without rational_values'); assert_equal(opensysml.parseUint64('18446744073709551615'), ... intmax('uint64'), 'exact uint64 parsing'); @@ -232,6 +241,16 @@ function test_surface_offline() 'bindings', struct('n', {bound})), ... 'opensysml:missingCapability', 'document exact Rational without rational_values'); end + halfBound = struct('numerator', '1', 'denominator', '2'); + older = opensysml.buildDocumentBindings( ... + struct('n', {{halfBound, thirdBound, struct('magnitude', halfBound, 'unit', 'kg')}})); + older = cellfun(@opensysml.internal.bindingRationalsAsReals, older, 'UniformOutput', false); + assert_equal(older{1}.values{1}, struct('realValue', 0.5), ... + 'document double-exact Rational as a Real without rational_values'); + assert_equal(older{1}.values{2}.rationalValue, thirdBound, ... + 'document Rational no double holds kept without rational_values'); + assert_equal(older{1}.values{3}.quantity.realMagnitude, 0.5, ... + 'document double-exact Rational magnitude as a Real without rational_values'); assert_error(@() opensysml.buildDocumentBindings(struct('bad', {struct('type', 'verdict')})), ... 'opensysml:argument', 'unsupported document binding'); diff --git a/client/node/src/core/connection.ts b/client/node/src/core/connection.ts index b34c22306b..88056120d1 100644 --- a/client/node/src/core/connection.ts +++ b/client/node/src/core/connection.ts @@ -103,6 +103,7 @@ import { import { bindingHoldsBigInt, bindingHoldsRational, + bindingRationalsAsReals, buildBindings, documentResult, type BindingValues, @@ -570,6 +571,9 @@ export class Connection { if (wire.some(bindingHoldsBigInt)) { requireCapability(this.info, CAPABILITY_BIG_INT_VALUES, upgradeRemedy(CAPABILITY_BIG_INT_VALUES)); } + if (!this.info.has(CAPABILITY_RATIONAL_VALUES)) { + wire.forEach(bindingRationalsAsReals); + } if (wire.some(bindingHoldsRational)) { requireCapability(this.info, CAPABILITY_RATIONAL_VALUES, upgradeRemedy(CAPABILITY_RATIONAL_VALUES)); } diff --git a/client/node/src/core/document.ts b/client/node/src/core/document.ts index 47ece82669..a5a5749e64 100644 --- a/client/node/src/core/document.ts +++ b/client/node/src/core/document.ts @@ -26,6 +26,7 @@ import { encodeRational, fitsInt64, formatValue, + quantityRationalAsReal, rationalAsDouble, type SysMLValue, } from "./values.js"; @@ -39,7 +40,7 @@ export function bindingHoldsBigInt(binding: DocumentQueryBinding): boolean { ); } -/** Whether a wire binding sends an exact Rational no double holds, which needs `rational_values`. */ +/** Whether a wire binding sends an exact Rational, which needs `rational_values`. */ export function bindingHoldsRational(binding: DocumentQueryBinding): boolean { return binding.values.some( (value) => @@ -48,6 +49,21 @@ export function bindingHoldsRational(binding: DocumentQueryBinding): boolean { ); } +/** Rewrites each Rational a binding sends that a double holds exactly as that double, for a service without `rational_values`. */ +export function bindingRationalsAsReals(binding: DocumentQueryBinding): void { + for (const value of binding.values) { + if (value.kind.case === "rationalValue") { + const { numerator, denominator } = value.kind.value; + const double = rationalAsDouble({ numerator: BigInt(numerator), denominator: BigInt(denominator) }); + if (double !== undefined) { + value.kind = { case: "realValue", value: double }; + } + } else if (value.kind.case === "quantity") { + quantityRationalAsReal(value.kind.value); + } + } +} + /** A model element, named by qualified name. */ export class ElementRef { /** Qualified name of the element. */ @@ -389,10 +405,7 @@ function boundValue(parameter: string, value: DocumentValue): PbDocumentValue { ); } if (typeof value === "object" && "kind" in value && value.kind === "rational") { - const double = rationalAsDouble(value); - return double === undefined - ? create(DocumentValueSchema, { kind: { case: "rationalValue", value: encodeRational(value) } }) - : create(DocumentValueSchema, { kind: { case: "realValue", value: double } }); + return create(DocumentValueSchema, { kind: { case: "rationalValue", value: encodeRational(value) } }); } if (typeof value === "object" && "kind" in value && value.kind === "quantity") { return create(DocumentValueSchema, { diff --git a/client/node/src/core/index.ts b/client/node/src/core/index.ts index 4de17e07ac..030648351b 100644 --- a/client/node/src/core/index.ts +++ b/client/node/src/core/index.ts @@ -268,6 +268,7 @@ export { rational, rationalOfDouble, rationalToNumber, + rationalsAsReals, requireInput, toValue, valuesEqual, diff --git a/client/node/src/core/values.ts b/client/node/src/core/values.ts index 2f9d56d2a6..4aea5c56c6 100644 --- a/client/node/src/core/values.ts +++ b/client/node/src/core/values.ts @@ -809,10 +809,7 @@ export function encodeQuantityMagnitude(magnitude: Magnitude): Quantity["magnitu case "real": return { case: "realMagnitude", value: magnitude.value }; case "rational": { - const double = rationalAsDouble(magnitude); - return double === undefined - ? { case: "rationalMagnitude", value: encodeRational(magnitude) } - : { case: "realMagnitude", value: double }; + return { case: "rationalMagnitude", value: encodeRational(magnitude) }; } case "int": return fitsInt64(magnitude.value) @@ -826,10 +823,7 @@ function encodeMagnitude(magnitude: Magnitude): Value { return create(ValueSchema, { kind: { case: "realValue", value: magnitude.value } }); } if (magnitude.kind === "rational") { - const double = rationalAsDouble(magnitude); - return double === undefined - ? create(ValueSchema, { kind: { case: "rationalValue", value: encodeRational(magnitude) } }) - : create(ValueSchema, { kind: { case: "realValue", value: double } }); + return create(ValueSchema, { kind: { case: "rationalValue", value: encodeRational(magnitude) } }); } return fitsInt64(magnitude.value) ? create(ValueSchema, { kind: { case: "intValue", value: magnitude.value } }) @@ -852,6 +846,51 @@ export function decodeBigInteger(text: string): bigint { return BigInt(text); } +/** + * Rewrites in place each Rational arm of a wire value a double holds exactly as + * that double: the form a service without `rational_values` reads. A Rational + * no double holds is left as it is; a collection's elements are values of their own. + */ +export function rationalsAsReals(value: Value): void { + switch (value.kind.case) { + case "rationalValue": { + const double = wireRationalAsDouble(value.kind.value); + if (double !== undefined) { + value.kind = { case: "realValue", value: double }; + } + return; + } + case "quantity": + quantityRationalAsReal(value.kind.value); + return; + case "vector": + value.kind.value.components.forEach(rationalsAsReals); + return; + case "vectorQuantity": + value.kind.value.components.forEach(quantityRationalAsReal); + return; + case "tensorQuantity": + value.kind.value.components.forEach(quantityRationalAsReal); + return; + default: + return; + } +} + +/** Rewrites a quantity's Rational magnitude a double holds exactly as `real_magnitude`. */ +export function quantityRationalAsReal(quantity: Quantity): void { + if (quantity.magnitude.case === "rationalMagnitude") { + const double = wireRationalAsDouble(quantity.magnitude.value); + if (double !== undefined) { + quantity.magnitude = { case: "realMagnitude", value: double }; + } + } +} + +function wireRationalAsDouble(wire: Rational): number | undefined { + return rationalAsDouble({ numerator: BigInt(wire.numerator), denominator: BigInt(wire.denominator) }); +} + /** The `Rational` message of an exact Rational, numerator and denominator in full. */ export function encodeRational(value: RationalValue): Rational { return create(RationalSchema, { @@ -1479,8 +1518,13 @@ function encodeInput(value: SysMLValue, info: ServerInfo): Value { }), }, }); - default: - return encodeValue(value); + default: { + const wire = encodeValue(value); + if (!info.has(CAPABILITY_RATIONAL_VALUES)) { + rationalsAsReals(wire); + } + return wire; + } } } diff --git a/client/node/src/generated/sysml_pb.ts b/client/node/src/generated/sysml_pb.ts index a802b46a5a..fafa6c32a9 100644 --- a/client/node/src/generated/sysml_pb.ts +++ b/client/node/src/generated/sysml_pb.ts @@ -4409,8 +4409,9 @@ export type Value = Message<"sysml.Value"> & { case: "bigIntValue"; } | { /** - * An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1` - * or `1 / 3` evaluates; one a double holds exactly crosses as real_value. + * An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The + * server answers one a double holds exactly as real_value; a client sends + * any exact Rational here, and an inbound real_value is always a Real. * * @generated from field: sysml.Rational rational_value = 23; */ @@ -4428,8 +4429,8 @@ export const ValueSchema: GenMessage = /*@__PURE__*/ /** * Rational is an exact rational number in lowest terms: numerator over a - * positive denominator, each written as big_int_value is. A decoder rejects one - * not in lowest terms or one a double holds exactly, so no number has two spellings. + * positive denominator, each written as big_int_value is. The server rejects one + * not in lowest terms; a client also rejects an answered one a double holds exactly. * * @generated from message sysml.Rational */ @@ -4816,7 +4817,7 @@ export type Quantity = Message<"sysml.Quantity"> & { case: "bigIntMagnitude"; } | { /** - * An exact Rational magnitude no double holds exactly, as Value.rational_value. + * An exact Rational magnitude, as Value.rational_value. * * @generated from field: sysml.Rational rational_magnitude = 6; */ @@ -5875,7 +5876,7 @@ export type DocumentValue = Message<"sysml.DocumentValue"> & { case: "bigIntValue"; } | { /** - * An exact Rational no double holds exactly, as Value.rational_value. + * An exact Rational, as Value.rational_value. * * @generated from field: sysml.Rational rational_value = 14; */ diff --git a/client/node/test/rational.test.ts b/client/node/test/rational.test.ts index 03f8844675..6824cdc9b7 100644 --- a/client/node/test/rational.test.ts +++ b/client/node/test/rational.test.ts @@ -22,10 +22,12 @@ import { formatValue, rational, rationalToNumber, + rationalsAsReals, toValue, valuesEqual, type SysMLValue, } from "../src/node/index.js"; +import { bindingRationalsAsReals } from "../src/core/document.js"; import { fakeConnection } from "./support/fake.js"; const third = rational(1n, 3n); @@ -43,7 +45,21 @@ test("a Rational no double holds travels as rational_value and reads back exactl } }); -test("a Rational a double holds travels as real_value", () => { +test("a Rational a double holds is sent as rational_value", () => { + for (const value of [rational(1n, 2n), rational(-3n, 4n), rational(5n), rational(0n)]) { + assert.deepEqual(encodeValue(value).kind, { + case: "rationalValue", + value: create(RationalSchema, { numerator: value.numerator.toString(), denominator: value.denominator.toString() }), + }); + } +}); + +test("a Rational a double holds is that double for a service without rational_values", () => { + const asReal = (value: SysMLValue) => { + const wire = encodeValue(value); + rationalsAsReals(wire); + return wire; + }; for (const [value, double] of [ [rational(1n, 2n), 0.5], [rational(-3n, 4n), -0.75], @@ -51,11 +67,12 @@ test("a Rational a double holds travels as real_value", () => { [rational(1n, 2n ** 1074n), 2 ** -1074], [rational(2n ** 1023n), 2 ** 1023], ] as const) { - assert.deepEqual(encodeValue(value).kind, { case: "realValue", value: double }); + assert.deepEqual(asReal(value).kind, { case: "realValue", value: double }); } - assert.equal(encodeValue(rational(1n, 2n ** 1075n)).kind.case, "rationalValue"); - assert.equal(encodeValue(rational(2n ** 1024n)).kind.case, "rationalValue"); - assert.equal(encodeValue(rational(2n ** 53n + 1n)).kind.case, "rationalValue"); + assert.equal(asReal(rational(1n, 2n ** 1075n)).kind.case, "rationalValue"); + assert.equal(asReal(rational(2n ** 1024n)).kind.case, "rationalValue"); + assert.equal(asReal(rational(2n ** 53n + 1n)).kind.case, "rationalValue"); + assert.equal(asReal(third).kind.case, "rationalValue"); }); test("rational reduces over a positive denominator and refuses a zero one", () => { @@ -111,6 +128,8 @@ test("a quantity, a vector and a document value keep a Rational exactly", () => assert.deepEqual(roundTrip(quantity), quantity); const half: SysMLValue = { kind: "quantity", magnitude: rational(1n, 2n), unit: "kg" }; const halfWire = encodeValue(half); + assert.equal(halfWire.kind.case === "quantity" ? halfWire.kind.value.magnitude.case : "", "rationalMagnitude"); + rationalsAsReals(halfWire); assert.equal(halfWire.kind.case === "quantity" ? halfWire.kind.value.magnitude.case : "", "realMagnitude"); const vector: SysMLValue = { kind: "vector", components: [third, { kind: "int", value: 2n }] }; @@ -137,7 +156,7 @@ test("a quantity, a vector and a document value keep a Rational exactly", () => const [bound] = buildBindings({ x: third }); assert.equal(bound.values[0].kind.case, "rationalValue"); const [boundHalf] = buildBindings({ x: rational(1n, 2n) }); - assert.deepEqual(boundHalf.values[0].kind, { case: "realValue", value: 0.5 }); + assert.equal(boundHalf.values[0].kind.case, "rationalValue"); const [boundQuantity] = buildBindings({ m: { kind: "quantity", magnitude: tenth, unit: "kg" } }); const magnitude = boundQuantity.values[0].kind; assert.equal(magnitude.case === "quantity" ? magnitude.value.magnitude.case : "", "rationalMagnitude"); @@ -169,7 +188,12 @@ test("a Rational no double holds is refused before the call by a service without }, ); } - assert.equal(toValue(rational(1n, 2n), older).kind.case, "realValue"); + assert.deepEqual(toValue(rational(1n, 2n), older).kind, { case: "realValue", value: 0.5 }); + const nested = toValue([rational(1n, 2n)], older); + assert.deepEqual( + nested.kind.case === "sequence" ? nested.kind.value.elements[0].kind : undefined, + { case: "realValue", value: 0.5 }, + ); const current = new ServerInfo({ version: "v", capabilities: [CAPABILITY_RATIONAL_VALUES], @@ -177,6 +201,8 @@ test("a Rational no double holds is refused before the call by a service without origin: "test", }); assert.equal(toValue(third, current).kind.case, "rationalValue"); + assert.equal(toValue(rational(1n, 4n), current).kind.case, "rationalValue"); + assert.equal(toValue(0.25, current).kind.case, "realValue"); }); test("a document query bound to a Rational no double holds needs rational_values", async () => { @@ -205,3 +231,13 @@ test("a quantity magnitude reads rational_magnitude", () => { }); assert.deepEqual(decodeValue(wire), { kind: "quantity", magnitude: rational(-1n, 3n), unit: "m" }); }); + +test("a document binding a double holds reaches a service without rational_values as that double", () => { + const [bound] = buildBindings({ x: rational(1n, 2n) }); + bindingRationalsAsReals(bound); + assert.deepEqual(bound.values[0].kind, { case: "realValue", value: 0.5 }); + const [quantity] = buildBindings({ m: { kind: "quantity", magnitude: rational(1n, 4n), unit: "kg" } }); + bindingRationalsAsReals(quantity); + const magnitude = quantity.values[0].kind; + assert.deepEqual(magnitude.case === "quantity" ? magnitude.value.magnitude : undefined, { case: "realMagnitude", value: 0.25 }); +}); diff --git a/client/opensysml/analysis.go b/client/opensysml/analysis.go index cec767c855..e8d22c0470 100644 --- a/client/opensysml/analysis.go +++ b/client/opensysml/analysis.go @@ -2,6 +2,8 @@ package opensysml import ( "context" + "maps" + "slices" pb "github.com/Open-MBEE/OpenSysML/api/proto" ) @@ -284,5 +286,8 @@ func (c *client) analysisRequest(ctx context.Context, hash, symbolID string, opt req.NamedArguments[argument.name] = sent } } + if err := c.fitRationals(ctx, append(req.Arguments, slices.Collect(maps.Values(req.NamedArguments))...)...); err != nil { + return nil, err + } return c.caller.runAnalysis(ctx, req) } diff --git a/client/opensysml/client.go b/client/opensysml/client.go index f9ae0ac4a3..8a15144808 100644 --- a/client/opensysml/client.go +++ b/client/opensysml/client.go @@ -7,6 +7,7 @@ import ( "sync" pb "github.com/Open-MBEE/OpenSysML/api/proto" + "github.com/Open-MBEE/OpenSysML/internal/frontend/protoconv" ) // Client answers SysML v2 questions: parse, look up, evaluate, instantiate. @@ -610,9 +611,6 @@ func (c *client) requireValueCapabilities(ctx context.Context, values ...Value) if slices.ContainsFunc(values, carriesBigInt) { needed = append(needed, CapabilityBigIntValues) } - if slices.ContainsFunc(values, carriesRational) { - needed = append(needed, CapabilityRationalValues) - } if len(needed) == 0 { return nil } @@ -732,33 +730,29 @@ func isBigInt(n Number) bool { func quantityIsBigInt(q Quantity) bool { return isBigInt(q.Magnitude) } -// carriesRational reports whether a value, or any value nested in it, holds an -// exact Rational, as itself or as a quantity's magnitude. -func carriesRational(value Value) bool { - switch v := value.(type) { - case Rational: - return true - case Quantity: - return isRational(v.Magnitude) - case Vector: - return slices.ContainsFunc(v, isRational) - case VectorQuantity: - return slices.ContainsFunc(v, quantityIsRational) - case TensorQuantity: - return slices.ContainsFunc(v.Components, quantityIsRational) - case EnumLiteral: - return v.Value != nil && carriesRational(v.Value) +// fitRationals sends each exact Rational as rational_value to a service reading +// rational_values; to one without, a Rational a double holds crosses as that Real, +// and any other is refused rather than read as null. +func (c *client) fitRationals(ctx context.Context, values ...*pb.Value) error { + if !slices.ContainsFunc(values, protoconv.ValueCarriesRational) { + return nil } - return slices.ContainsFunc(nestedValues(value), carriesRational) -} - -func isRational(n Number) bool { - _, ok := n.(Rational) - return ok + info, err := c.serverInfo(ctx) + if err != nil { + return err + } + if info.Has(CapabilityRationalValues) { + return nil + } + for _, value := range values { + protoconv.RationalsAsReals(value) + } + if slices.ContainsFunc(values, protoconv.ValueCarriesRational) { + return c.requireCapabilities(ctx, CapabilityRationalValues) + } + return nil } -func quantityIsRational(q Quantity) bool { return isRational(q.Magnitude) } - // nestedValues are the values a value holds: a sequence's or a set's elements, // an array's. func nestedValues(value Value) []Value { diff --git a/client/opensysml/convert.go b/client/opensysml/convert.go index 162e2582e3..d4f9a75149 100644 --- a/client/opensysml/convert.go +++ b/client/opensysml/convert.go @@ -2,11 +2,11 @@ package opensysml import ( "fmt" - "math" "math/big" pb "github.com/Open-MBEE/OpenSysML/api/proto" "github.com/Open-MBEE/OpenSysML/internal/frontend/protoconv" + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" ) // The conversions from the wire types to the public ones. Every conversion @@ -246,9 +246,6 @@ func valueToProto(value Value) (*pb.Value, error) { } return &pb.Value{Kind: &pb.Value_BigIntValue{BigIntValue: n.String()}}, nil case Rational: - if f, exact := v.Rat().Float64(); exact && !math.IsInf(f, 0) { - return &pb.Value{Kind: &pb.Value_RealValue{RealValue: f}}, nil - } return &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: rationalToProto(v)}}, nil case Real: return &pb.Value{Kind: &pb.Value_RealValue{RealValue: float64(v)}}, nil @@ -399,10 +396,6 @@ func quantityToProto(quantity Quantity) (*pb.Quantity, error) { out.Magnitude = &pb.Quantity_BigIntMagnitude{BigIntMagnitude: n.String()} } case Rational: - if f, exact := magnitude.Rat().Float64(); exact && !math.IsInf(f, 0) { - out.Magnitude = &pb.Quantity_RealMagnitude{RealMagnitude: f} - break - } out.Magnitude = &pb.Quantity_RationalMagnitude{RationalMagnitude: rationalToProto(magnitude)} case Real: out.Magnitude = &pb.Quantity_RealMagnitude{RealMagnitude: float64(magnitude)} @@ -558,17 +551,17 @@ func migrationReportFromProto(report *pb.MigrationReport) *MigrationReport { return out } -// rationalToProto marshals a Rational that is no integer, in lowest terms. +// rationalToProto marshals a Rational in lowest terms. func rationalToProto(q Rational) *pb.Rational { r := q.Rat() return &pb.Rational{Numerator: r.Num().String(), Denominator: r.Denom().String()} } -// rationalFromProto is false for a Rational not in lowest terms or whole, -// which the wire never spells. +// rationalFromProto is false for a Rational not in lowest terms or one a double +// holds exactly, which a service never sends. func rationalFromProto(pr *pb.Rational) (Rational, bool) { - v, err := protoconv.ProtoToRational(pr) - if err != nil { + v, ok := semantics.CanonicalRational(pr.GetNumerator(), pr.GetDenominator()) + if !ok { return Rational{}, false } return Rational{r: v.Rat()}, true diff --git a/client/opensysml/documents.go b/client/opensysml/documents.go index 82e1c7f383..641fb2ee93 100644 --- a/client/opensysml/documents.go +++ b/client/opensysml/documents.go @@ -2,7 +2,6 @@ package opensysml import ( "context" - "math" "math/big" "slices" "strconv" @@ -234,6 +233,26 @@ func bindingHoldsRational(binding *pb.DocumentQueryBinding) bool { return slices.ContainsFunc(binding.GetValues(), protoconv.DocumentValueHoldsRational) } +// fitDocumentRationals is fitRationals for query bindings. +func (c *client) fitDocumentRationals(ctx context.Context, bindings []*pb.DocumentQueryBinding) error { + info, err := c.serverInfo(ctx) + if err != nil { + return err + } + if info.Has(CapabilityRationalValues) { + return nil + } + for _, binding := range bindings { + for _, value := range binding.GetValues() { + protoconv.DocumentRationalAsReal(value) + } + } + if slices.ContainsFunc(bindings, bindingHoldsRational) { + return c.requireCapabilities(ctx, CapabilityRationalValues) + } + return nil +} + func (c *client) RunDocumentQuery( ctx context.Context, model *Model, @@ -262,7 +281,7 @@ func (c *client) RunDocumentQuery( } } if slices.ContainsFunc(req.Bindings, bindingHoldsRational) { - if err := c.requireCapabilities(ctx, CapabilityRationalValues); err != nil { + if err := c.fitDocumentRationals(ctx, req.Bindings); err != nil { return nil, err } } @@ -345,9 +364,6 @@ func cellToProto(cell Cell) (*pb.DocumentValue, error) { } return &pb.DocumentValue{Kind: &pb.DocumentValue_BigIntValue{BigIntValue: value.String()}}, nil case Rational: - if f, exact := value.Rat().Float64(); exact && !math.IsInf(f, 0) { - return &pb.DocumentValue{Kind: &pb.DocumentValue_RealValue{RealValue: f}}, nil - } return &pb.DocumentValue{Kind: &pb.DocumentValue_RationalValue{RationalValue: rationalToProto(value)}}, nil case Real: return &pb.DocumentValue{Kind: &pb.DocumentValue_RealValue{RealValue: float64(value)}}, nil diff --git a/client/opensysml/execute.go b/client/opensysml/execute.go index 12b6595e40..3a215b45de 100644 --- a/client/opensysml/execute.go +++ b/client/opensysml/execute.go @@ -133,6 +133,9 @@ func (c *client) ExecuteAction( } req.Inputs[name] = sent } + if err := c.fitRationals(ctx, slices.Collect(maps.Values(req.Inputs))...); err != nil { + return nil, err + } } resp, err := c.caller.executeAction(ctx, req) if err != nil { diff --git a/client/opensysml/explore.go b/client/opensysml/explore.go index 37325411a3..c9f44bb3e5 100644 --- a/client/opensysml/explore.go +++ b/client/opensysml/explore.go @@ -177,6 +177,9 @@ func (c *client) ExploreAction( } req.Inputs[name] = sent } + if err := c.fitRationals(ctx, slices.Collect(maps.Values(req.Inputs))...); err != nil { + return nil, err + } } resp, err := c.caller.executeAction(ctx, req) if err != nil { diff --git a/client/opensysml/verify.go b/client/opensysml/verify.go index a8d5cc748b..4110311558 100644 --- a/client/opensysml/verify.go +++ b/client/opensysml/verify.go @@ -501,6 +501,9 @@ func (c *client) calculate( } req.Arguments = append(req.Arguments, sent) } + if err := c.fitRationals(ctx, req.Arguments...); err != nil { + return nil, err + } resp, err := c.caller.evaluateCalc(ctx, req) if err != nil { return nil, err diff --git a/client/python/opensysml/connection.py b/client/python/opensysml/connection.py index b2639a6cf8..7a17ef1161 100644 --- a/client/python/opensysml/connection.py +++ b/client/python/opensysml/connection.py @@ -78,7 +78,13 @@ path_is_v1, ) from opensysml.diagnostic import Diagnostic -from opensysml.document import binding_holds_big_int, binding_holds_rational, build_bindings, result_of as document_result +from opensysml.document import ( + binding_holds_big_int, + binding_holds_rational, + binding_rationals_as_reals, + build_bindings, + result_of as document_result, +) from opensysml.edit import error_for_failure, failure_name, referrers_of, result_of from opensysml.enumeration import EnumLiteral from opensysml.exploration import Exploration, Outcome @@ -117,6 +123,7 @@ pb_holds_big_int, pb_holds_rational, rational_value_to_pb, + rationals_as_reals, value_to_python, ) @@ -1771,6 +1778,11 @@ def run_document_query(self, model_hash, query_id, bindings=None): CAPABILITY_BIG_INT_VALUES, upgrade_remedy(CAPABILITY_BIG_INT_VALUES), ) + if any(binding_holds_rational(binding) for binding in request.bindings) and not self.server_info().has( + CAPABILITY_RATIONAL_VALUES + ): + for binding in request.bindings: + binding_rationals_as_reals(binding) if any(binding_holds_rational(binding) for binding in request.bindings): require( self.server_info(), @@ -2945,8 +2957,14 @@ def _require_big_int_values(self, value): return value def _require_exact_values(self, value): - """Refuse to send a big Integer or exact Rational a service without its capability would read as null.""" + """Refuse to send a big Integer or exact Rational a service without its capability would read as null. + + Every exact Rational travels as ``rational_value``; to a service without + ``rational_values`` one a double holds exactly travels as that double. + """ self._require_big_int_values(value) + if pb_holds_rational(value) and not self.server_info().has(CAPABILITY_RATIONAL_VALUES): + rationals_as_reals(value) if pb_holds_rational(value): require( self.server_info(), diff --git a/client/python/opensysml/document.py b/client/python/opensysml/document.py index 589cd99e19..67806d7471 100644 --- a/client/python/opensysml/document.py +++ b/client/python/opensysml/document.py @@ -31,6 +31,7 @@ holds_exactly_as_double, integer_from_decimal, integer_to_decimal, + quantity_rational_as_real, rational_from_pb, rational_to_pb, ) @@ -263,10 +264,22 @@ def binding_holds_big_int(binding: "sysml_pb2.DocumentQueryBinding") -> bool: def binding_holds_rational(binding: "sysml_pb2.DocumentQueryBinding") -> bool: - """Whether a wire binding sends an exact Rational no double holds, which needs ``rational_values``.""" + """Whether a wire binding sends an exact Rational, which needs ``rational_values``.""" return any(_document_holds_rational(value) for value in binding.values) +def binding_rationals_as_reals(binding: "sysml_pb2.DocumentQueryBinding") -> None: + """Rewrite each Rational a binding sends that a double holds exactly as that double, for a service without ``rational_values``.""" + for value in binding.values: + if value.WhichOneof("kind") == "rational_value": + exact = Fraction(integer_from_decimal(value.rational_value.numerator), + integer_from_decimal(value.rational_value.denominator)) + if holds_exactly_as_double(exact): + value.real_value = float(exact) + elif value.WhichOneof("kind") == "quantity": + quantity_rational_as_real(value.quantity) + + def _document_holds_rational(value: "sysml_pb2.DocumentValue") -> bool: kind = value.WhichOneof("kind") if kind == "rational_value": @@ -336,8 +349,6 @@ def _bound_value(parameter, value): if isinstance(value, float): return sysml_pb2.DocumentValue(real_value=value) if isinstance(value, Fraction): - if holds_exactly_as_double(value): - return sysml_pb2.DocumentValue(real_value=float(value)) return sysml_pb2.DocumentValue(rational_value=rational_to_pb(value)) if isinstance(value, Quantity): return sysml_pb2.DocumentValue(quantity=_bound_quantity(parameter, value)) diff --git a/client/python/opensysml/values.py b/client/python/opensysml/values.py index 2ea8426270..7735e005da 100644 --- a/client/python/opensysml/values.py +++ b/client/python/opensysml/values.py @@ -101,8 +101,62 @@ def _quantity_holds_rational(quantity: "sysml_pb2.Quantity") -> bool: return quantity.WhichOneof("magnitude") == "rational_magnitude" +def rationals_as_reals(value: "sysml_pb2.Value") -> None: + """Rewrite in place each Rational arm :func:`pb_holds_rational` reads that a double holds exactly as that double. + + That is the form a service without ``rational_values`` reads; a Rational no + double holds is left as it is. + """ + kind = value.WhichOneof("kind") + if kind == "rational_value": + exact = _double_of(value.rational_value) + if exact is not None: + value.real_value = exact + elif kind == "quantity": + quantity_rational_as_real(value.quantity) + elif kind == "vector": + for component in value.vector.components: + rationals_as_reals(component) + elif kind == "vector_quantity": + for quantity in value.vector_quantity.components: + quantity_rational_as_real(quantity) + elif kind == "tensor_quantity": + for quantity in value.tensor_quantity.components: + quantity_rational_as_real(quantity) + + +def quantity_rational_as_real(quantity: "sysml_pb2.Quantity") -> None: + """Rewrite a quantity's Rational magnitude a double holds exactly as ``real_magnitude``.""" + if quantity.WhichOneof("magnitude") == "rational_magnitude": + exact = _double_of(quantity.rational_magnitude) + if exact is not None: + quantity.real_magnitude = exact + + +def _double_of(pb_rational: "sysml_pb2.Rational") -> Optional[float]: + value = Fraction(integer_from_decimal(pb_rational.numerator), integer_from_decimal(pb_rational.denominator)) + return float(value) if holds_exactly_as_double(value) else None + + +def format_rational(value: Fraction) -> str: + """Spell a Rational exactly, as the service prints it: a terminating decimal as ``5.4``, any other as ``1/3``.""" + if value.denominator == 1: + return str(value.numerator) + den, twos, fives = value.denominator, 0, 0 + while den % 2 == 0: + den, twos = den // 2, twos + 1 + while den % 5 == 0: + den, fives = den // 5, fives + 1 + if den != 1: + return f"{value.numerator}/{value.denominator}" + places = max(twos, fives) + digits = str(abs(value.numerator) * 10 ** places // value.denominator).rjust(places + 1, "0") + sign = "-" if value < 0 else "" + return f"{sign}{digits[:-places]}.{digits[-places:]}".rstrip("0") + + def holds_exactly_as_double(value: Fraction) -> bool: - """Whether a double holds the Rational exactly, so that it travels as ``real_value``.""" + """Whether a double holds the Rational exactly, as the service sends it in ``real_value``.""" try: f = float(value) except OverflowError: @@ -121,6 +175,9 @@ def rational_to_pb(value: Fraction) -> "sysml_pb2.Rational": def rational_from_pb(pb_rational: "sysml_pb2.Rational") -> Fraction: """The exact Rational a ``Rational`` message spells. + A service sends a Rational a double holds exactly as ``real_value``, so one + spelled here is refused, as any other noncanonical spelling is. + Raises: UnsupportedValueError: If it is not in lowest terms over a positive denominator, or a double holds it, which travels as ``real_value``. @@ -137,9 +194,11 @@ def rational_from_pb(pb_rational: "sysml_pb2.Rational") -> Fraction: def rational_value_to_pb(value: Fraction) -> "sysml_pb2.Value": - """Encode a Rational as ``real_value`` where a double holds it, ``rational_value`` otherwise.""" - if holds_exactly_as_double(value): - return sysml_pb2.Value(real_value=float(value)) + """Encode a Rational as ``rational_value``, one a double holds exactly included. + + A service reads an inbound ``real_value`` as a Real; :func:`rationals_as_reals` + rewrites the message for a service without ``rational_values``. + """ return sysml_pb2.Value(rational_value=rational_to_pb(value)) @@ -387,7 +446,7 @@ def to_pb(self) -> "sysml_pb2.Quantity": pb_quantity.big_int_magnitude = integer_to_decimal(self.magnitude) elif isinstance(self.magnitude, int): pb_quantity.int_magnitude = self.magnitude - elif isinstance(self.magnitude, Fraction) and not holds_exactly_as_double(self.magnitude): + elif isinstance(self.magnitude, Fraction): pb_quantity.rational_magnitude.CopyFrom(rational_to_pb(self.magnitude)) else: pb_quantity.real_magnitude = float(self.magnitude) @@ -514,8 +573,7 @@ def __truediv__(self, other: Magnitude) -> "Quantity": return Quantity(self.magnitude / other, self.unit) def __str__(self) -> str: - magnitude = f"{self.magnitude:g}" if isinstance(self.magnitude, float) else str(self.magnitude) - return f"{magnitude} [{self.unit}]" + return f"{_format_number(self.magnitude)} [{self.unit}]" def __repr__(self) -> str: return f"Quantity({self.magnitude!r}, {self.unit!r})" @@ -719,7 +777,11 @@ def _number_to_pb(value: Magnitude) -> "sysml_pb2.Value": def _format_number(value: Magnitude) -> str: - return f"{value:g}" if isinstance(value, float) else str(value) + if isinstance(value, float): + return f"{value:g}" + if isinstance(value, Fraction): + return format_rational(value) + return str(value) @dataclass(frozen=True, eq=False) diff --git a/client/python/tests/test_rational.py b/client/python/tests/test_rational.py index d907d065d1..c39627f9c1 100644 --- a/client/python/tests/test_rational.py +++ b/client/python/tests/test_rational.py @@ -1,9 +1,11 @@ -"""KerML Rationals are exact: one no double holds crosses the wire in full. +"""KerML Rationals are exact, and cross the wire exactly. The service sends a Rational a double holds exactly, as ``0.5``, as ``real_value`` and any other, as ``1/3`` or ``0.1``, as the numerator and denominator of ``rational_value``; the client decodes the latter to a -``fractions.Fraction`` and encodes a ``Fraction`` the same way. +``fractions.Fraction``. The client sends every ``Fraction`` as +``rational_value`` to a service with ``rational_values``, one a double holds +included, since the service reads an inbound ``real_value`` as a Real. """ from fractions import Fraction @@ -21,9 +23,10 @@ MissingCapabilityError, ) from opensysml.connection import Connection -from opensysml.document import build_bindings, _value_of +from opensysml.document import binding_rationals_as_reals, build_bindings, _value_of from opensysml.errors import UnsupportedValueError from opensysml.proto import sysml_pb2 +from tests.service_gate import skip_or_fail_without_service from opensysml.values import ( Quantity, TensorQuantity, @@ -31,9 +34,11 @@ UnitFactor, Vector, VectorQuantity, + format_rational, holds_exactly_as_double, rational_from_pb, rational_value_to_pb, + rationals_as_reals, value_to_python, ) @@ -50,12 +55,36 @@ def test_a_rational_no_double_holds_round_trips_exactly(value): @pytest.mark.parametrize("value", [Fraction(1, 2), Fraction(-3, 4), Fraction(5), Fraction(0)]) -def test_a_rational_a_double_holds_travels_as_real_value(value): +def test_a_rational_a_double_holds_is_sent_as_rational_value(value): pb = rational_value_to_pb(value) + assert pb.WhichOneof("kind") == "rational_value" + assert (pb.rational_value.numerator, pb.rational_value.denominator) == (str(value.numerator), str(value.denominator)) + + +@pytest.mark.parametrize("value", [Fraction(1, 2), Fraction(-3, 4), Fraction(5), Fraction(0)]) +def test_a_rational_a_double_holds_is_that_double_for_an_older_service(value): + pb = rational_value_to_pb(value) + rationals_as_reals(pb) assert pb.WhichOneof("kind") == "real_value" assert pb.real_value == float(value) +def test_a_rational_no_double_holds_stays_rational_for_an_older_service(): + pb = rational_value_to_pb(Fraction(1, 3)) + rationals_as_reals(pb) + assert pb.WhichOneof("kind") == "rational_value" + + +@pytest.mark.parametrize( + "value, text", + [(Fraction(27, 5), "5.4"), (Fraction(-1, 8), "-0.125"), (Fraction(1, 1000), "0.001"), + (Fraction(5), "5"), (Fraction(1, 3), "1/3"), (Fraction(-7, 6), "-7/6")], +) +def test_a_rational_prints_as_the_service_prints_it(value, text): + assert format_rational(value) == text + assert str(Quantity(value, KG)) == f"{text} [kg]" + + def test_a_rational_beyond_the_double_range_is_not_a_double(): assert not holds_exactly_as_double(Fraction(10 ** 400)) assert rational_value_to_pb(Fraction(10 ** 400)).WhichOneof("kind") == "rational_value" @@ -80,7 +109,8 @@ def test_a_rational_quantity_magnitude_round_trips(): pb = Quantity(Fraction(1, 3), KG).to_pb() assert pb.WhichOneof("magnitude") == "rational_magnitude" assert Quantity.from_pb(pb).magnitude == Fraction(1, 3) - assert Quantity(Fraction(1, 2), KG).to_pb().real_magnitude == 0.5 + half = Quantity(Fraction(1, 2), KG).to_pb() + assert (half.rational_magnitude.numerator, half.rational_magnitude.denominator) == ("1", "2") def test_rational_quantities_compare_exactly(): @@ -99,6 +129,8 @@ def test_a_rational_document_value_round_trips(): assert binding.values[0].WhichOneof("kind") == "rational_value" assert _value_of(binding.values[0]) == Fraction(1, 3) (half,) = build_bindings({"x": Fraction(1, 2)}) + assert half.values[0].WhichOneof("kind") == "rational_value" + binding_rationals_as_reals(half) assert half.values[0].real_value == 0.5 @@ -140,12 +172,17 @@ def test_a_rational_is_not_sent_to_a_service_without_the_capability(): assert excinfo.value.capability == CAPABILITY_RATIONAL_VALUES stub.EvaluateCalc.assert_not_called() assert conn._python_to_value(Fraction(1, 2)).real_value == 0.5 + assert conn._python_to_value([Fraction(1, 2)]).sequence.elements[0].real_value == 0.5 + assert conn._python_to_value(Quantity(Fraction(1, 2), KG)).quantity.real_magnitude == 0.5 def test_a_rational_is_sent_to_a_service_with_the_capability(): conn = make_connection(Mock(), OLD + (CAPABILITY_RATIONAL_VALUES,)) pb = conn._python_to_value([1, Fraction(1, 3)]).sequence.elements[1] assert (pb.rational_value.numerator, pb.rational_value.denominator) == ("1", "3") + half = conn._python_to_value(Fraction(1, 2)) + assert (half.rational_value.numerator, half.rational_value.denominator) == ("1", "2") + assert conn._python_to_value(0.5).WhichOneof("kind") == "real_value" def test_a_rational_document_binding_needs_the_capability(): @@ -156,3 +193,73 @@ def test_a_rational_document_binding_needs_the_capability(): conn.run_document_query("hash", "Q::q", bindings={"x": value}) assert excinfo.value.capability == CAPABILITY_RATIONAL_VALUES stub.RunDocumentQuery.assert_not_called() + + +def test_a_document_binding_a_double_holds_reaches_an_older_service_as_that_double(): + stub = Mock() + stub.RunDocumentQuery.return_value = sysml_pb2.RunDocumentQueryResponse() + conn = make_connection(stub, OLD + (CAPABILITY_DOCUMENT_QUERY,)) + conn.run_document_query("hash", "Q::q", bindings={"x": Fraction(1, 2)}) + (request,), _ = stub.RunDocumentQuery.call_args + assert request.bindings[0].values[0].real_value == 0.5 + + +RATIONAL_MODEL = """ +package R { + private import ScalarValues::*; + calc def Third { in x : Rational; return : Rational = x / 3; } + calc third : Third; + action addThird { + in x; + out y; + first start; + action inner { assign y := x + 1 / 3; } + then done; + succession first start then inner; + } +} +""" + + +@pytest.mark.integration +class TestRationalsAgainstTheService: + """What the real service makes of each wire spelling of a number.""" + + def setup_method(self): + import grpc + + from opensysml import Connection + + try: + self.conn = Connection(auto_start=False) + self.conn._stub.GetDiagnostics(sysml_pb2.DiagnosticsRequest(model_hash="")) + except grpc.RpcError as exc: + if exc.code() != grpc.StatusCode.NOT_FOUND: + self.conn = None + skip_or_fail_without_service( + f"the sysml-grpc service on localhost:50051 answered {exc.code()}" + ) + except Exception as exc: + self.conn = None + skip_or_fail_without_service( + f"no sysml-grpc service could be reached on localhost:50051 ({exc})" + ) + self.model = self.conn.load_from_content(RATIONAL_MODEL) + + def teardown_method(self): + if getattr(self, "conn", None) is not None: + self.conn.close() + + def test_a_fraction_a_double_holds_stays_exact(self): + assert self.conn.calc("R::third", self.model.hash, arguments=[Fraction(1, 4)]).value == Fraction(1, 12) + assert self.conn.calc("R::third", self.model.hash, arguments=[Fraction(1, 10)]).value == Fraction(1, 30) + + def test_a_canonical_answer_a_double_holds_is_a_float(self): + value = self.conn.calc("R::third", self.model.hash, arguments=[Fraction(3, 2)]).value + assert value == 0.5 and isinstance(value, float) + + def test_a_float_input_is_a_real(self): + exact = self.conn.execute_action("R::addThird", self.model.hash, inputs={"x": Fraction(1, 4)}) + real = self.conn.execute_action("R::addThird", self.model.hash, inputs={"x": 0.25}) + assert exact["y"] == Fraction(7, 12) + assert real["y"] == 0.25 + 1 / 3 and isinstance(real["y"], float) diff --git a/client/rust/conformance/sysml.descriptor.binpb b/client/rust/conformance/sysml.descriptor.binpb index 81d91db4c299f93c5b28c52edd02ddb0e5586e08..a60a87841a7f90ca903866c6deb710a0aa9e3040 100644 GIT binary patch delta 11061 zcmZ8n33OCNy6#&vP3QK#eV2p)VQa#wkVZwv(Fsx9#$jC8l}(8d1q=zBKvsJcL{J`U1i~f^5b!ZLI*59ZWmw+heSg(0@0@qec!sa4zVH9*ufMAP zTb&ilQodZ4!t|}{Sl&o|_rV5w1Gj!4aA;!D$Yjw-ZSF(*t%#P{4OXqD@ zY8|zk$(}2<9x3)eG(8x6mZiJF8ERGGiJJvbY zWk@V<3^Z~}^Gf2eKEw0$SV>;-n7raxZb8Y|yy6mWZPc12z$?zn&3}=G=KUd-8ylKG zBCntnV+)3r=omXLmiJoj(9&41+|m(+1-bdL;d#Y*uvah)|HVcWyi{0*fBG7+5hbzQ z{IR*?@PDyhkdEuRsn;i;L&9l8K@G5V#B8c?J?d@|({E}z3-)kBZyJF~hL-UoAHPwX z5XKC|1bE6Ot##{v^E5ZVq!9K;<_#@{$pZY3rk6L2=GQa<>k++Pum_C2X@qUvK=^vF zfE%1(utvf%P<;Wz3S#U8BgIt^eoZhMrh8O$PBd5(uTujRP*F9}o06t_>!nh6qsIEK zdYDsBH#E%%R3vY0(spXrG$ZWlec(<{a%TpwO=|t(Gn5t@p*6!uNflavaASs%-bg>e z1P8rylYRfH4b(c%^-1KwTqC8SN76u=Ycy`6|DC~pFE_N%2vjA{e5wuAtc6B6O&y_Nj$o3%5YD-9-Ng+^*MQX6|5 zGGA#lZKiLb>GyU+)ka`b^3oP%7rm4ZKlgf1?qWK4TC_8_DN=LD=u(hQ2TY+mmazY2Det zFH|=4A^Y15)#8-}WrWl94=7rF$;4iR#WH5_nw{D+{K^jPscbYzBj`pN?NvqqVPvo2$ejTQBYTY& zE%oPw7CLMAU;az$pHt&2XmGQ}h{$Z|D;A>0NRx8{ghGwcDpRkbz17bR)f$0sc-l^_ z(ylearoMo@>_=W6;k$NfA7md<%4mddN0e`XFn>h(1_Nh5HE|8BQdYM(U1QGE%y_8hrZ$3NSxRp!(wVKhRx zP6-1-xK6nS2;n+%Z81&yc{g;<2>gRD*rP47&e78R2ZWy|;qUmL_GcPg5I5dMy$xsZhWyP*q4;3r;xpSB=zfs&8TnEoXEquLojh$5goC;*}Oqmg2Y zoCAd7kB04dRCHcc$5VesXAGdCQur>a;|aZ>UZmsc8k`9S2e_eMjllo#_BGm7>sKS3 zs`rF91IU{zywlfO@2o3I3XPDuqK+6qNL`^L<|#oz=QUpawf0QbHJ?J(uNf)w=o}!d zUo$eA>fHo|xxwqa{eJDy_;sH`NpW4xbpT_qa-HTXw#ER0xA^<}wMB_rY9pf&oLiKc zbaepIAQ>MP9O#B_8-X`XO`PllG4i$%ZYV-Gkmfnw^!73k_A;Q{W`J1M@us(z0YM#a zdV6^wYe@4SZz2;{(Yg+DL*-`RJ-+XNw$d#(aa#4m90!F1Zs={3&ERhz)cPjgHp6n! z29f2-X0Vy?7zm37n4%y|O*VBIHuS>MWD{YdR2f78nqmg!W(5KjP&y4LdL#100R(jl zIZ57x1~G(uD$lLep1)_R&mqOBW<)tdF`jC+jOmXHb3oJ0V4IBl`Gs1o8E<(=YZpg@ zyqE`x*iSdjmY(Yr`{`z8YkfBD#6b)poyAKIX>;SVOfM*%0#VE0f7^0Kqdr=^3Et^%fZr)UVW}hR}}NVKP||03on| zAu)@#z~JsM8$<*K1b2rSwe*#gi9-nP;?quR)$v`vJv!}nsqDoV8bsN9HzJYihW43( zTK;mKc2{Db88*ZOa%s*rrdN0YVFG}10)P;#QG$RFtRX>~doFqSwaP6ZPyuBL1t@wE zS~__FLH$~#Wg?d*zhA8=5V(MfOUnD{M3h{b?0yyUTtWv_$bmowR8$JV0opN=N+CF) zLY_-!&Owu<-S71V!39=aI(!bAUe$-e;2tzl^_A0}e8~+S<@?TPZ`()Ba1-29EI8B+ z9XA69_)BNChwbBLI72@~i!+pBdWx?fk40JIjE7>+_I~QoSkJP&;&CzFL1Q_c(Jqr~n8@&BzIj^LV`RR9&h0!m zbUGS1ldRBKnl|us)VrATXg+mO)={h=5LOgW-f@8NvMw5t#}OdBtc$j2rv}49UG$DS z^(0w%*$thK27XEg>#^Ty_W39#^F1x!%VgpQ-aW|vnf-&3Mk8YPgIWndNdFLZ$8E`7kmZUAQyZE3gLyQE5C7G79qS4#W&9LLIEMX$Qy*%fX)}A zOx~z3i-=u}nsQG8!sx{)&TCPPzv6~2MFZFPyCGJYxD>_Ci$duYsxy9#va8X0A_~B; z`zv+RGQUETy&Mh75CI{8fO1-Z7<)O|xRs=eSzeB|Y3otZdBvwvh5!m`M+npWuJ~L^ zkt@V~9fa7x(Qd{LOK7;#%A85*fI>Yd(z;M|USr}uC=g~nSW8fS``I(q!=i2cRSqrapxinyghD{2Tl!7K_o0sSEy2v0As$Q^iPh zzQcz^Si{6S7R&I)Xc+U3)x3o_252f~m{~uTnUg8D33O;?XcaRfCQWj25vseqF zWdBB?nV?lvkD$~8WtxTR@l84#8;o#6v#h{eelxbzCegCpo}jd zBxkGm0>bWWim#-ibB@K@inSiWH0*Ogxz7QiG{^F)5I`|FU4$u^4h*R|6eo&vJvYRy zzyfZYh%;Alu1CJTPjPOacwa>rjhNT_#B7KN*P}Sivsm-Y2C+;$RC&Qbvuqbzmqvj! z&%)MC6C|Md2ySL5mRVT*8;7*eEVc(Y6!ZC%3YRq<5gh4;mRNyhyk(UAYAvyFm2^V@ zN0O6Ec`J)OnzhvO4o);eYN?eXO9Md6cBzHZfXcR!WT%qPvRL;-rB5M`D=n|C#u!j4 zE$<>9NxoF6SOX!rfU>3m1hvZYf(r<06-DSQ9ip$gp=v9z2^+bV#d?p;%NyM*x8RjL zdb}%44zO8w&91g^*1SV|=2e=`$9$v1ro}&2zM&BYKDN9n6c7eJws5M=l^pc0=LIf% zFR|Y8?w?o13aq!h`v(x5^=dw^(tI{pUS$P@`2fnR3lM@Etgwt8AgCKCdX#pr6541n zdEWwo3Mi@VfWh5pp?0F`0#J;$Z2eP;MZO!_Yy}P_SERDvYuz_nVR@70)2z2xOdj+= zxCQ{_EeHt7EtYo)1443(gtvM1wPeF`n+RuwRe0c9%%Y&8WepJu^l z@;z#6V+^eBvC`!s3wEvSsCoG^^Jvsl%8luQ7)X|B~4ks>=SCvXv8#5s5t{d_=K7>AcRlQoC|~k zI#2QX&DiMdQ@#Qnh^Kr7%8*l*_gOYdWXLHCpJg50DRQYrW2p{S!yn)>>6oAh1N1C$-;%8OZM~MVEYvBW%HU}ULvYKS*izw`)sP;a` zS2Sl!;^$Nh(Fil=tQ2{r1A=qT!j*oXpddlcTXP0%z3bjljPl~V#pIJLAc*HJTUNq= zAfC5S30FvC82mjy)q?Sy@BJx;sMYdbKt_o)`QFNqR}3H|zPCEuq3@&hD{w8b-c`{wqo70E?Zuagho&5W31)ChH%+sulr_pj=hC4@iS3_m@&E3uxi4^C_9EDskQC(866;3x_e_ zTqiShiWbn$zF{$0R02T(P(_JW@`lP)^g`)|l`e}gKq%d?P$yCf6i`;)vRLbk_Oa)N z&&l0-$GvEhZ88%8!M~+40TBFKlnE21_t@>Xc}g2r-u1TcLIX(L_7{pa|7|NxttxH) z+q7N&DJ8J`C-Qx5*pG>cws&q9FxveSZEyDjf-}*^?w0{bL7``R&n^Wl1@QosM?N5^ zlWeah1q5}HohE-6Dqy-BEVqBlFdeZ2l(Bmdz3{x;_6}u0NR``;tk(e{Rc>c^Ka>=@ zp&54IJzL9IU5~eVilrtewquo=J;M&S6kmshl+H74`Z~N9|Av<{m6w2!o@sj*6Ck8# z+PIiRRuod2&F0fOu&VfMpF{aNTg?n(z?p5M+MY>)ETn|3;4j?4W_7JlQ$izVRAEQt zGa(=(D(qMXvBLl%QDJxNgrkcEN4ueUc3>gzbSLYTm}iHZiD-|eSdvaVWusf{mI`T#4U53ov`ZLsAB7%*fiZ4?posZ&JG<4v&>>yoIly~kQK zVoa6o6}W(~RAuA$H*&g&oL**oPsBi&FQ9Z9Q1nJ*N&teo%=Vs$i)aa!+d(-aAW#A2 z>v94Z+~qdD1Lz0`1a~>Dxiky(p&gV3K#};=0aRw`5c$yem=2K-eUA~>k9@QA?gA)R zs7PD_AK5AWlvz4VKJv|C@CrL9uazQJUqZ#D(_@9oIP`+Lf-`K)8uVfB&vf|Hg3aif`ap* znjej$NUyeo@&YXq=R>ufDxcc{VWrw`)r%uFL=|IX5ex{u^|tr(J0SGd+xYqY1)*oU z!A-pJ-E2tbO=?RPF%y(cw)g8YASjz?1@=-IUF?QFw*xiF5ASAw(yY&I6cl41RZJUY z8^3)Ido8}r_DXUzLTa0hpQk8e0U@=GQis#ydoktnuH>>VY@Qb1rR<}Xnr-iE2oR)Q zHZHsqbW9b~9@)q9?_(!B@3Xyv1C8M9vu(Mp0KwTu+X}}a3zoQ{gLdFMKIV69iG9#U zjWM8{w(+h>wPpz&FlTTyXFNXV;G}!_ss~w%EMU2N z(F^Vwbv6Tndxp;D9)gQJ`<7pQkoAgx>r?2u|5n|*C1Uq}OZP6SHnNB9d!CQ^1N%DX zygJ9x2rtgt-m45CB+lF3D=HwYp0`mk?4hMB#m}&I;1+)~o9(tP&?!zoe3sHmUgXbr zWxca5+TN!n8liMiUB7@(x=2ZpFDU5zg&*(A3bKAt{+EiC{6&Qe5R_jiTXb^3SGC{#QUgOU` z#KvY_Q(0LmPT^~|EssS&P_EIYl+#B9uk&9Y!t}5ER%rRItF0ZCd zOCDweb8f1oKqG{1s-*yg!cE(i2P+_q-lT)|En12)H#FV}e87!vtlS#!U?~PdxQv!! z0$<#X4USK6yi`IXlqNXdo(6={1V?3i8HKQ%cYK5`ikCayo<<|JI=1{=1q7$u!7q2> z<*RS54;sdW3xtpX_)`jz(}MJ6^>F2+m{&Oa30^Um3xv{HsUV`&mT9~B%4k>3{l(&l8|j_{OP{uJETyH)RD) zb$~FAo09XoC)l8D?s$b+nJB2a<85d_C~(J>2Pz;GxP!Y^8pXvqkB|8y8=RQuE9eNo zJYRwC(Rq${B$SCddY;p&wHG_wqw^i_RilhWKm}B>qx*Ee?>^n9^BpHue?Yj8`*fkh zQZqUtcD&blMb6B{sKnSSfPs*WfU>Fs zMA|HOyb2f)`pX@!0tSTsat8%)G4x+^L#v#?dLHb~itJSmzL*!%pIE&{2G%CayR#Nr zVy)VHXob{T$GcAfA+?qgg`9egHrYCdwHLQK5S#*(P65KHb&mH}34mg7^Gr1uO6#1q z?exV|O^kCxe{}+z9W7%PizPFkVokKfU!AagWgbVqed>6>CIP_?ptJ)BJD)lc`NjYU zJD)l&lxLh3UIZ>oQ% zWAAl0w9N_Z7$y zb@FY8!HM*0*;{cm$gDxP==c8G5D@GgbPVE$xM1*gItKS7|Me_uq;=lo zlSp)rN;M1uX%D5^f72A+a6<>2z#*Q~lYN^w;DlR>C?0gY z&tgC*9(25)YTh7owJPs{Km}A(nnA5{2ECxx!kL#sql*h8rG;G%U9A@k2O0y>= Aw*UYD delta 10999 zcmZu%d3+T`w!U4tG&h~@Tis0v7`6)n>Od|yjH8Z#pyQ0gsO%`4l7T2-NXX_ozZY2) zMRpi4k3t4c=8Z+5M%<(58ijGy6k`6fBCBG`_8FzPF0=m zo6nY~ey}{1SV{TBzd*+5Oh_G$XTPVo-K&i7=RvA=<4l zIvCd~`>rthq`KvX6^-_08GiI?TC{cMEU|XI(Jm#CmoMB6M&q=OiR_}ph|bA7M_A(Y zdZV#%pRhI<%?FMw%r7d+DWa!^IoY{SJqPuiq-8l)15z`koUNfj&}pV&3c;2M>zZM) zXdmBv#g0s}jm+=F?hVGohCLIbaz^Hl68$zBx27cW3&~)!(vFFue0Y_}D;f8foWjIt z7%qN3KT$M3CpULY-ZMq8oo1y(`@qyIcFeVGEUOQT_|XYAYa$H;HIOi-A?8l7Q+*BL z)&#q8MnJ{Gi8i|>c-X}N7g%!*`8YK_cx-?>(QeYz`XU8O>g$_t+L5x#hyHHtGWeTz z%(n)>oxbGGbdkBq7?hl@r7#Go>2_)(DFq0r>2^jF>mN*V@N{nFqD{t7qto1wME2*} zsf_~?*`I4Sy~V0yu;0%&7uu1E%G*CSMjCvf9ZR=HLaQIyUnJh%V&o(jX(XGrxY^Q%OI?Z%_YRoXlTA+^d*b)^&_q*mFD(yiUJ{QhKqedR0Ljb28l z^)_o5%p&{i?U?czlR#QeK37Fye}Hd(Vn=pVUfp5zVnaXC)i8kUZ?{=XsR0y~P}M9T zYIeJwru+beh3&Sd@(&0L+wJBptWPMw1ATLs9obj8Xs6N5aCh0U4C`+ct$}1>kNEDt zjq*1}mB6*fee;AJIVDPW z8^!JkI~KQ=kZX^VYqcU}k5SRRRtsYg!nIl$5W=VGlaakEK2qp{j)A>DGWmDvOZV;bd;|~}QChJ2A6-B*X>k!6Z zrJmL*H^dNv*TpvnjPla!di!D!oaI?0D}6u11HHFa~MO&r-~Jaj3@d|4LPJZ)k)LNP>iQKtrFI7 zX%1+b6K$K>JJB;Qk@G_K$l^r5?BX%`dD*#%aoNx0jVUe}m7_0s(dV$yK8gA2t{x^+ zGtF^Y1uLV}Omo`YYR#dII*cLWv&6E)Mp<%}6VxmQ;m<55D0e_`W;rNqu8UmDwz725uA8TSfp9}t`hvj0Phw1c>>tXz29 z_?OXXWoU>3ztRcrs^PNMS30<>rcgya>6>eu$lt}s|1e(TYpA07LF!3zd7bEe!Wb}m zomRpil-4;x?*Rz&>zttXc#_PlcY?MB2mu6CCj_8;`U{mjUr*GBr8PLdQKZ!vy}E7G zi+z$Y?2L_0n(7t+p|H`(Xd!b92!)MKqP6t^R`HZ?ZgwKu#GD$VC*SO#b)mcaDO$xA zai+$2v~-Jh1%nXUqT>Mwp)C{-s{5zN%2tPENGm{C1)y35pnRI@>8GG>bEHyeJZ{`X>=_8RLf%! z@}D|^JRsyhC3*5}1bOzE!&Eqc;2EIu3{XA|k_-syXL?a1XhZIFnCcRM5LiG{uA&_< zxI3MOX%Yj1yVHqtYXw!}2!gxBwp!!E)N9DIHC-i#mRx!E8{^k4yX}t$ zXWK{_)cx@^6$wC^75CKz00@o!ar74h;ms)DJQR<7`PKk7zqkbE7N0Bdw<8u$( zXj6*6vEIY+AiIE2I~)&Q7XYDlIF4JLvO9`$a3mfS1`x^)r|Ds<=kU3O^Q_r@S5`^Ka3)o#UG){?EXZLDeceDVlTy`Dr7(iD4<#vAm(0*H*Kw`a+R0jZQBJ@JiHuIsbT;nwF87{ zeV0QnmCR-0K7m|}_RXvD$YgG0_DamXvv(uWI>zqjSL3l}IJ=_J(d5;2kso7SlGo$G z8HqtCUXM47OK$+7cs=g<)_at;(Nw@UM0JdPP&!jpJVJAhINwbR#P4VZ6E;gMVU92o5Kr5#ltzAwF{^QP6I#{<}vcj zn!|%$1yD}TkYS2u07GgH#fjpa;+yk$>ySdGgKO|w9n&#|G?B?)K&g4@PSw8=00g+p2xmYW6~iunSL zQn+kbh~QY?T+AcOM4vdj#20g1I6V=-vE<|u(T}qSvzGASc*P*3mhe>7KmcO3OE?+` zYVO98ou%S4&U%$D4JqXDQXX{Qm;=gE9$fch$(ITpYapZvP<1|lpjPl8xPYKmP=rp? zQTwcKuH=yo*xrX&qW|+bIph0f=RK2?7*mwUuYA*Gy$p9H#|bo*cG9!7oR7p=k4;N{ zqQ)8<6;M&z1B1Jsqr;*e1W?ZAuCD*++7g$GwPASAbN+?9WTJGtcRHZiO* zdpNl*q);-q>40GlDBCDtt0-W(wE7+5aAUSQYe&c-r5!v~o)R<%oE>yZJTECgJHA8w>MXD8lrI&f4zmhm31JDPeX7H=nay~8iI3x3aY;}6Z50h zqI)JA*r__C#6YR$!8g%d76YZ4%p9d7ahz{{$s^~)XPN8>|B|C^rLVi=s208w%bKx4 zUBA+6#vqpQm0mL-gul{j288fewB|gifQL2Wx6Rr3t~H?oUCA|}0#!&24_@BJ$qK3A zczL7MjH3dn6=#~Wfn94u1qx*?Pg5NS=0KsAH&Wjp0pUX}$A`4Lr2^2`qNoMCH~F;= z`#4!3Uvs>M)8+uAN&J>fOa6K=jym)+;zSF!BzZ>15Q8vthNr5F9uS-}92fmv(hLfu zj?W$PN$1~;V^kM)oT;z5fFRa!SM|$)Al7m8%WqMR$I)WG6;3NAx_=uk(WFrxd`7_> zNPNpP)fEE>iEsJscUV=FzdYako=094XIinngTLport*Bqqx}7V8$qrS2#W$#*AgHM z{J?`Ruz)b|15Z~Q9}ot9pljkqxR^%)z9<^BW-oWW7%EWbcv1Hcm_w7euc{vq3Kyvo z=%ml1JYEu8TeDtSmw3=RVGxu{JZPN&LAgW`r4Pq>gnkxX+OWaNpLG&22+Gg8_5o=U z)&5e7WgaE2UVPk!Ei0`LIh441ojA+^r=HBvDVj$+`zmLu)dYe9ppFt{@~W;?JcZI# zo}roqKqy`1=pLyA@+gtlIlDFUw#4J3=VpI$$8RvC##AK$f`46C0wDO;sS+kB@3Gr& zh~KqkuXei;y3i03H^M~G=D)$ybyjKf-=OVsUP)m0PZa-Y%Py2obc1s{kJ0X*=mxtV z5S)oFcE1WhD*8S*_&AftQV|b8b>strI>`+>RX|WDx#{XRr95W&(aG*F6=orJfGT!( z;VC?y>;{K2AfzU{p6cQOAvM{}41QzD_s!{UFQ3Y^ zOw7E4&F)sF7lJ{oqs&cHpAZ2dQRXIYm-`A35@l|OjySMbbi8lQcOwhM@L#chrSsib za~b3Dw2B2f#z0sHpo%dd1Q)nLQvnFU1upJLO3!$*_O{E^y#$091F9GU!nL>EG_`F3 z<>XtGr%iw%^|p(zo$^$4v0{MaisLj&R z@owlb9Ubq69wV;rg=XoS44}$Tfjs)(b5jRtvvh>K7n;T7_uZ(vObS>71vQt>jQ4fL z;VG!^Q^j?WfkOU15O*ipU0ENv!8hvyc}#rZI;yJ$1my$rw7aC>lvp7aBw3HF6>d~} zM90DkH(lL<1+pht;o=VLDJeJ=RtmEdTJx1|R9%||@>E#qHd0^e0bymO+q|VkUxEr4 zPK6J}bDdcCtPkC&>M=10D<8VSJ0c(`AJP_96r2XD#5Y7)6;f!|uL>!&>sJxw5osmO zkFFMvbY_qJadnueG*DLSqAp-*psaSAwU8Y_0khFQyZB}Qq5tRSfZ$ht`Vk-@Zggg5 z7N)8W7(~dfb%S5+0U^8A#jo~HN>3a=xyadF! z>Bl4m50ARc&+IYxjrGk%mwQ;JB(N&`BI){3?K&X1N6B^BVHJ_<$HlCB&|w`9Ipq5B zkVCE?CyxB-t%%?W@zlMnJnKZrp*ubn)A%!;aDSbp^4k)MSi0&gP*uX?4eW^`{sE! za$UULmF?!|=`g3CPKzm%7eqlf)<5fl8@yLy5K0&H-3th%3se-jl7fdnii_P?Ue=G= z|6-ZIA9a)fLHUuQ)Lv4Mzl&m8cXoT$#gIbzyQp(fEKBbq<$^vQ7E}5ziEp~IAz7C~ z3Z?IozQZwxCegMikrbrwiWq%Adp_%muF7J07+-N+bvgoqa)mafT0SCJFYF#zetl?# z(pRr@Q7qF}Pr0D)qQ!K*UlY@Mu%X?r=~Q44Lf3RE0HJWr_0{R49u=3<& zFWA!aT?0 zRB`e_Ha}}>=o@8Ysu%1G%%Mpha!&7qCA68Qi6y^hgR-WD6xvMFyx^?}b3mEqp}D5J zs)RPv4AJ%>)?@UHkV4s*5k`PE(+r9L-QgvajhW#pC=Gz>RKY_~XL`ZsBtTGSdiWqx zD5=;}bHu)fSaH^zP=PkloG=cwiRMroRElsjmWgqHV8gS@LMzlSmxXJho3V`6G(=j# z%{Wi|`wwhb*Lhyhh?U5;d7c;STtFzy^L%x30zzS)hr3l7#YH(^%={x8UOGQiu&~nk zp#t5T^Sxm2m&k5;zSsKJAV|137kI(9ixQRwDxeM$-Jc6W_v!vz;CYR#d!_rhKNote zQD%q4fYIXFhw(8RP*drSTo|T??#P9n>&xEuIo~YzB1^=+hgrWrmwUm7)90wl7kj}^ zG(hkLPzeG;aIqEygy3Sz^-ye^=Y4aT7kO8V`4by5WSNKGv#0`|C&A@j(D4GH>;P34 z2Z;Ju?gbq$AoQ1eLB|US{pB7S-a_cV;F~MF$Xd~&7b|dAcz7>*$bs$v3xqkv-z-qwJsVb}!aKzM#ECzV7ho1x>_zGc&nE=N?ZWx5EqG&j2B} z!^7DwTY#6ywVmRD-e>`Kh8%KXrx*MmBIbazlTJGP92bqgL?_+uO0N%VV%)VmB$3o^ zU1XR9(rzlUGvxQnzInil974R07>T)^pUM#Rt(YTT>B}~i9`Ir01&8vno0|**1q8>sMYZ8FXs5d{IufyE{CtA KL}QVrk^ci@-?gp) diff --git a/client/rust/opensysml/src/document.rs b/client/rust/opensysml/src/document.rs index fa080ebe76..8c57d4d1f5 100644 --- a/client/rust/opensysml/src/document.rs +++ b/client/rust/opensysml/src/document.rs @@ -5,7 +5,7 @@ use std::fmt; use crate::domain::{ quantity_from_wire, rational_from_wire, rational_to_wire, BigInteger, Quantity, }; -use crate::encode::quantity_to_wire; +use crate::encode::{quantity_rational_as_real, quantity_to_wire, wire_exact_f64}; use crate::error::Error; use crate::rational::Rational; use crate::wire; @@ -176,7 +176,7 @@ pub enum DocumentValue { Integer(i64), /// An Integer beyond `i64`. BigInteger(BigInteger), - /// An exact Rational no `f64` holds. + /// An exact Rational; a service answers one an `f64` holds as a `Real`. Rational(Rational), /// A real number. Real(f64), @@ -289,6 +289,22 @@ pub(crate) fn binding_holds_rational(binding: &wire::DocumentQueryBinding) -> bo }) } +/// Rewrite each Rational a binding sends that an `f64` holds exactly as that `f64`, for a service without `rational_values`. +pub(crate) fn binding_rationals_as_reals(binding: &mut wire::DocumentQueryBinding) { + use wire::document_value::Kind; + for value in &mut binding.values { + match &mut value.kind { + Some(Kind::RationalValue(terms)) => { + if let Some(x) = wire_exact_f64(terms) { + value.kind = Some(Kind::RealValue(x)); + } + } + Some(Kind::Quantity(quantity)) => quantity_rational_as_real(quantity), + _ => {} + } + } +} + /// Whether a wire binding sends an Integer beyond int64, which needs `big_int_values`. pub(crate) fn binding_holds_big_int(binding: &wire::DocumentQueryBinding) -> bool { use wire::document_value::Kind; diff --git a/client/rust/opensysml/src/domain.rs b/client/rust/opensysml/src/domain.rs index 2a18e5bf25..767b5b9bba 100644 --- a/client/rust/opensysml/src/domain.rs +++ b/client/rust/opensysml/src/domain.rs @@ -1134,7 +1134,7 @@ fn unit_term_from_wire(term: wire::UnitTerm) -> UnitTerm { } pub(crate) fn rational_from_wire(v: &wire::Rational) -> Result { - Rational::parse(&v.numerator, &v.denominator) + Rational::parse_canonical(&v.numerator, &v.denominator) } pub(crate) fn rational_to_wire(q: &Rational) -> wire::Rational { diff --git a/client/rust/opensysml/src/encode.rs b/client/rust/opensysml/src/encode.rs index 0c62e6a35c..ddb1baf5a0 100644 --- a/client/rust/opensysml/src/encode.rs +++ b/client/rust/opensysml/src/encode.rs @@ -8,6 +8,7 @@ use crate::capabilities::{ }; use crate::domain::{rational_to_wire, Capabilities, Magnitude, Quantity, UnitTerm, Value}; use crate::error::Error; +use crate::rational::Rational; use crate::wire; /// Deepest nesting a sent value may have, so a cyclic-looking builder cannot exhaust the stack. @@ -29,7 +30,7 @@ fn require(capabilities: &Capabilities, capability: &str) -> Result<(), Error> { capabilities.require(capability, upgrade_remedy(capability)) } -/// Refuse an Integer beyond int64 or an exact Rational to a service that would read its arm as null. +/// Refuse an Integer beyond int64 or an exact Rational no `f64` holds to a service that would read its arm as null. fn require_magnitudes<'a>( capabilities: &Capabilities, magnitudes: impl Iterator, @@ -37,7 +38,10 @@ fn require_magnitudes<'a>( for magnitude in magnitudes { match magnitude { Magnitude::BigInteger(_) => require(capabilities, CAPABILITY_BIG_INT_VALUES)?, - Magnitude::Rational(_) => require(capabilities, CAPABILITY_RATIONAL_VALUES)?, + Magnitude::Rational(v) if v.exact_f64().is_none() => { + require(capabilities, CAPABILITY_RATIONAL_VALUES)? + } + Magnitude::Rational(_) => {} Magnitude::Integer(_) | Magnitude::Real(_) => {} } } @@ -52,14 +56,17 @@ fn encode(value: &Value, capabilities: &Capabilities, depth: usize) -> Result kind(Kind::IntValue(*v)), Value::BigInteger(v) => { require(capabilities, CAPABILITY_BIG_INT_VALUES)?; kind(Kind::BigIntValue(v.as_str().to_owned())) } Value::Rational(v) => { - require(capabilities, CAPABILITY_RATIONAL_VALUES)?; + require_magnitudes( + capabilities, + std::iter::once(&Magnitude::Rational(v.clone())), + )?; kind(Kind::RationalValue(rational_to_wire(v))) } Value::Real(v) => kind(Kind::RealValue(*v)), @@ -222,7 +229,51 @@ fn encode(value: &Value, capabilities: &Capabilities, depth: usize) -> Result { + if let Some(x) = wire_exact_f64(terms) { + value.kind = Some(Kind::RealValue(x)); + } + } + Some(Kind::Quantity(quantity)) => quantity_rational_as_real(quantity), + Some(Kind::Vector(vector)) => vector.components.iter_mut().for_each(rationals_as_reals), + Some(Kind::VectorQuantity(vector)) => vector + .components + .iter_mut() + .for_each(quantity_rational_as_real), + Some(Kind::TensorQuantity(tensor)) => tensor + .components + .iter_mut() + .for_each(quantity_rational_as_real), + _ => {} + } +} + +/// Rewrite a quantity's Rational magnitude an f64 holds exactly as `real_magnitude`. +pub(crate) fn quantity_rational_as_real(quantity: &mut wire::Quantity) { + use wire::quantity::Magnitude; + if let Some(Magnitude::RationalMagnitude(terms)) = &quantity.magnitude { + if let Some(x) = wire_exact_f64(terms) { + quantity.magnitude = Some(Magnitude::RealMagnitude(x)); + } + } +} + +pub(crate) fn wire_exact_f64(terms: &wire::Rational) -> Option { + Rational::parse(&terms.numerator, &terms.denominator) + .ok() + .and_then(|rational| rational.exact_f64()) } fn unit_label(unit: &str) -> &str { @@ -652,6 +703,60 @@ mod tests { assert!(Value::Quantity(kilometres).same_value(&Value::Quantity(metres_value))); } + #[test] + fn a_rational_an_f64_holds_is_sent_exactly_or_as_that_f64_to_an_older_service() { + use wire::value::Kind; + let quarter = crate::Rational::parse("1", "4").unwrap(); + let without: Vec<&str> = ALL + .iter() + .copied() + .filter(|name| *name != CAPABILITY_RATIONAL_VALUES) + .collect(); + let value = Value::Rational(quarter.clone()); + match value_to_wire(&value, &capabilities(ALL)).unwrap().kind { + Some(Kind::RationalValue(terms)) => { + assert_eq!( + (terms.numerator.as_str(), terms.denominator.as_str()), + ("1", "4") + ) + } + other => panic!("expected rational_value, got {other:?}"), + } + assert_eq!( + value_to_wire(&value, &capabilities(&without)).unwrap().kind, + Some(Kind::RealValue(0.25)) + ); + let nested = Value::Sequence(vec![value.clone()]); + match value_to_wire(&nested, &capabilities(&without)) + .unwrap() + .kind + { + Some(Kind::Sequence(sequence)) => { + assert_eq!(sequence.elements[0].kind, Some(Kind::RealValue(0.25))) + } + other => panic!("expected a sequence, got {other:?}"), + } + match value_to_wire( + &Value::Quantity(metres(Magnitude::Rational(quarter))), + &capabilities(&without), + ) + .unwrap() + .kind + { + Some(Kind::Quantity(quantity)) => assert_eq!( + quantity.magnitude, + Some(wire::quantity::Magnitude::RealMagnitude(0.25)) + ), + other => panic!("expected a quantity, got {other:?}"), + } + assert_eq!( + value_to_wire(&Value::Real(0.25), &capabilities(ALL)) + .unwrap() + .kind, + Some(Kind::RealValue(0.25)) + ); + } + #[test] fn an_integer_beyond_int64_needs_big_int_values_bare_or_nested() { let wide = BigInteger::parse("1180591620717411303424").unwrap(); diff --git a/client/rust/opensysml/src/operations.rs b/client/rust/opensysml/src/operations.rs index fa43c2300c..bb458e5ba3 100644 --- a/client/rust/opensysml/src/operations.rs +++ b/client/rust/opensysml/src/operations.rs @@ -5,8 +5,8 @@ use std::collections::{BTreeMap, HashMap}; use crate::capabilities::*; use crate::conversion::{conversion_of, request_of, Conversion, ConvertOptions, ConvertSource}; use crate::document::{ - binding_holds_big_int, binding_holds_rational, bindings_to_wire, result_of, DocumentForm, - DocumentQueryResult, DocumentValue, + binding_holds_big_int, binding_holds_rational, binding_rationals_as_reals, bindings_to_wire, + result_of, DocumentForm, DocumentQueryResult, DocumentValue, }; use crate::domain::{Model, Value}; use crate::encode::value_to_wire; @@ -361,8 +361,11 @@ impl Connection { query_id: &str, bindings: &[(K, Vec)], ) -> Result { - let bindings = bindings_to_wire(bindings)?; + let mut bindings = bindings_to_wire(bindings)?; self.require_all(&[CAPABILITY_DOCUMENT_QUERY])?; + if !self.capabilities().has(CAPABILITY_RATIONAL_VALUES) { + bindings.iter_mut().for_each(binding_rationals_as_reals); + } if bindings.iter().any(binding_holds_big_int) { self.require_all(&[CAPABILITY_BIG_INT_VALUES])?; } diff --git a/client/rust/opensysml/src/proto/sysml/sysml.rs b/client/rust/opensysml/src/proto/sysml/sysml.rs index 6be81a420c..a5e952805d 100644 --- a/client/rust/opensysml/src/proto/sysml/sysml.rs +++ b/client/rust/opensysml/src/proto/sysml/sysml.rs @@ -1919,15 +1919,16 @@ pub mod value { /// zeros). An Integer that fits int64 is always int_value, never this. #[prost(string, tag="22")] BigIntValue(::prost::alloc::string::String), - /// An exact Rational (KerML 9.3.2.2.8) no double holds exactly, as `0.1` - /// or `1 / 3` evaluates; one a double holds exactly crosses as real_value. + /// An exact Rational (KerML 9.3.2.2.8), as `0.1` or `1 / 3` evaluates. The + /// server answers one a double holds exactly as real_value; a client sends + /// any exact Rational here, and an inbound real_value is always a Real. #[prost(message, tag="23")] RationalValue(super::Rational), } } /// Rational is an exact rational number in lowest terms: numerator over a -/// positive denominator, each written as big_int_value is. A decoder rejects one -/// not in lowest terms or one a double holds exactly, so no number has two spellings. +/// positive denominator, each written as big_int_value is. The server rejects one +/// not in lowest terms; a client also rejects an answered one a double holds exactly. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Rational { #[prost(string, tag="1")] @@ -2109,7 +2110,7 @@ pub mod quantity { /// An Integer magnitude beyond int64, in decimal, as Value.big_int_value. #[prost(string, tag="5")] BigIntMagnitude(::prost::alloc::string::String), - /// An exact Rational magnitude no double holds exactly, as Value.rational_value. + /// An exact Rational magnitude, as Value.rational_value. #[prost(message, tag="6")] RationalMagnitude(super::Rational), } @@ -2621,7 +2622,7 @@ pub mod document_value { /// An Integer beyond int64, in decimal, as Value.big_int_value. #[prost(string, tag="13")] BigIntValue(::prost::alloc::string::String), - /// An exact Rational no double holds exactly, as Value.rational_value. + /// An exact Rational, as Value.rational_value. #[prost(message, tag="14")] RationalValue(super::Rational), } diff --git a/client/rust/opensysml/src/rational.rs b/client/rust/opensysml/src/rational.rs index 0dc575d123..59913bad55 100644 --- a/client/rust/opensysml/src/rational.rs +++ b/client/rust/opensysml/src/rational.rs @@ -5,10 +5,9 @@ use std::fmt; use crate::error::Error; -/// An exact Rational no `f64` holds, such as `1/3` or `1/10`: a numerator over a -/// positive denominator in lowest terms, each held as its decimal digits. One an -/// `f64` holds exactly is always a `Real`, never this, so two numbers are equal -/// exactly when their arms and terms are. +/// An exact Rational, such as `1/3` or `1/10`: a numerator over a positive +/// denominator in lowest terms, each held as its decimal digits. A service answers +/// one an `f64` holds exactly as a `Real`, so a decoded Rational is never one. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Rational { numerator: String, @@ -17,9 +16,32 @@ pub struct Rational { impl Rational { /// The rational `numerator/denominator` spells: decimal digits with no leading - /// zero, a `-` only on the numerator, in lowest terms, and not a number an - /// `f64` holds, which a `Real` carries. + /// zero, a `-` only on the numerator, and in lowest terms. pub fn parse(numerator: &str, denominator: &str) -> Result { + Self::terms(numerator, denominator).map(|(rational, _)| rational) + } + + /// The rational a service answers, which is also not a number an `f64` holds. + pub(crate) fn parse_canonical(numerator: &str, denominator: &str) -> Result { + match Self::terms(numerator, denominator)? { + (_, true) => Err(Error::Decode(format!( + "not a canonical rational: {numerator}/{denominator} is an f64, which real_value carries" + ))), + (rational, false) => Ok(rational), + } + } + + /// The `f64` equal to this Rational, if one holds it exactly. + pub fn exact_f64(&self) -> Option { + let digits = self.numerator.strip_prefix('-').unwrap_or(&self.numerator); + match (Natural::parse(digits), Natural::parse(&self.denominator)) { + (Some(n), Some(d)) if binary64(&n, &d) => Some(self.to_f64()), + _ => None, + } + } + + // The validated terms, and whether an f64 holds them exactly. + fn terms(numerator: &str, denominator: &str) -> Result<(Self, bool), Error> { let malformed = |why: &str| { Error::Decode(format!( "not a canonical rational: {numerator}/{denominator} {why}" @@ -38,13 +60,14 @@ impl Rational { if !Natural::gcd(&n, &d).is_one() { return Err(malformed("is not in lowest terms")); } - if binary64(&n, &d) { - return Err(malformed("is an f64, which real_value carries")); - } - Ok(Self { - numerator: numerator.to_owned(), - denominator: denominator.to_owned(), - }) + let exact = binary64(&n, &d); + Ok(( + Self { + numerator: numerator.to_owned(), + denominator: denominator.to_owned(), + }, + exact, + )) } /// The numerator's decimal digits, `-` signed. @@ -325,7 +348,7 @@ mod tests { } #[test] - fn only_a_canonical_rational_no_f64_holds_parses() { + fn only_lowest_terms_parse_and_only_one_no_f64_holds_is_canonical() { for (n, d) in [ ("2", "6"), ("1", "0"), @@ -335,14 +358,21 @@ mod tests { ("+1", "3"), ("1.5", "7"), ("", "3"), - ("1", "2"), - ("3", "1"), - ("0", "1"), - ("-5", "4"), - ("1", "4503599627370496"), ] { assert!(Rational::parse(n, d).is_err(), "{n}/{d}"); } + for (n, d, x) in [ + ("1", "2", 0.5), + ("3", "1", 3.0), + ("0", "1", 0.0), + ("-5", "4", -1.25), + ("1", "4503599627370496", 1.0 / 4503599627370496.0), + ] { + assert_eq!(q(n, d).exact_f64(), Some(x), "{n}/{d}"); + assert!(Rational::parse_canonical(n, d).is_err(), "{n}/{d}"); + } + assert_eq!(q("1", "3").exact_f64(), None); + assert!(Rational::parse_canonical("1", "3").is_ok()); let r = q("-1", "3"); assert_eq!((r.numerator(), r.denominator()), ("-1", "3")); assert_eq!(r.to_string(), "-1/3"); diff --git a/docs/project/exact-rational-evaluation.md b/docs/project/exact-rational-evaluation.md index 50a1c0ce9f..8ebcb218fa 100644 --- a/docs/project/exact-rational-evaluation.md +++ b/docs/project/exact-rational-evaluation.md @@ -74,9 +74,9 @@ nothing about KerML Rationals and were not consulted. Rational operands are exact; Integer `/` is the exact Rational quotient (`1 / 3` is `1/3`, which replaces the once-rounded `IntQuotient`); `**` and `^` with an Integer exponent are exact, a negative exponent inverting (`(2 / 3) ** -2` is `2.25`); `0 ** -1` is a domain error. -- **Comparison and equality** (`semantics.CompareRat`, `CompareExactReal`) are exact between exact - values. Between a Rational and a binary64 Real they are currently exact, as the Integer precedent - `CompareIntReal` is; see [the open choice](#comparing-a-rational-with-a-binary64-real) below. +- **Comparison and equality** (`semantics.CompareRat`, `CompareReal`) are exact between exact + values. Between a Rational and a binary64 Real they are at Real precision, the Rational rounded + once; see [the rule](#comparing-a-rational-with-a-binary64-real) below. - **Library functions** (`internal/exec/runtime/library_conversions.go`, `library_functions.go`). `rat`, `numer`, `denom`, `gcd`, `abs`, `floor`, `round`, `max`, `min`, `sum`, `product`, `ToString`, `ToRational`, `ToInteger` compute exactly over exact operands; @@ -107,17 +107,17 @@ nothing about KerML Rationals and were not consulted. ### Every boundary -- **gRPC** (`api/proto/sysml.proto`, `internal/frontend/protoconv`, `internal/frontend/grpc`). A - Rational binary64 holds exactly crosses as the existing `real_value`/`real_magnitude`, so old - clients see what they saw. Any other crosses as the additive `Rational` message (numerator and - denominator as decimal text) in `Value`, `Quantity` and `DocumentValue`, negotiated by the - `rational_values` capability exactly as `big_int_values` negotiates `big_int_value`: a service - that does not advertise it answers such a Rational as an unsupported value naming the - capability, refuses a request or document query carrying or answering one with `UNIMPLEMENTED`, - and never sends a nearest double; the bundled clients refuse to send one to it. A wire Rational - must be canonical (positive denominator, lowest terms, not a binary64), so no Rational has two - encodings, and an inbound `real_value` is a binary64 Real (see - [the open choice](#a-rational-a-double-holds-on-input)). +- **gRPC** (`api/proto/sysml.proto`, `internal/frontend/protoconv`, `internal/frontend/grpc`). The + service answers a Rational binary64 holds exactly as the existing `real_value`/`real_magnitude`, + so old clients see what they saw. Any other crosses as the additive `Rational` message + (numerator and denominator as decimal text) in `Value`, `Quantity` and `DocumentValue`, + negotiated by the `rational_values` capability exactly as `big_int_values` negotiates + `big_int_value`: a service that does not advertise it answers such a Rational as an unsupported + value naming the capability, refuses a request or document query carrying or answering one with + `UNIMPLEMENTED`, and never sends a nearest double. An answered Rational is canonical (positive + denominator, lowest terms, not a binary64); an inbound one need only be in lowest terms over a + positive denominator, and an inbound `real_value` is a binary64 Real (see + [the wire rule](#a-rational-a-double-holds-on-input)). - **JSON** (`internal/frontend/engine`, `internal/frontend/core`): `"rationalValue": {"numerator": "1", "denominator": "3"}` and `"rationalMagnitude"`, with the same canonical rule ([wire contract](../reference/wire-contract.md)). @@ -127,7 +127,9 @@ nothing about KerML Rationals and were not consulted. bigint, denominator: bigint}` with `rational()`, `formatRational()`, `rationalToNumber()` and `rationalOfDouble()`; generated classes declare `RationalValue` (`asRational`). Java and Rust carry an exact numerator/denominator pair, Julia `Rational{BigInt}`, MATLAB a struct of decimal - strings. Each declares `rational_values` and refuses a noncanonical encoding. + strings. Each sends every exact Rational as `rational_value` under `rational_values`, rewrites + one a double holds to `real_value` for a service without it and refuses any other, and refuses + an answered Rational that is not canonical. - **RDF** (`internal/translate/export/rdf_expr.go`). A literal is written as the exact Rational it denotes: a decimal token as `xsd:decimal`, an exponent token binary64 holds exactly as `xsd:double`, any other exponent token as its exact `xsd:decimal` (`1E-1` → `"0.1"^^xsd:decimal`). @@ -146,34 +148,61 @@ nothing about KerML Rationals and were not consulted. ## Comparing a Rational with a binary64 Real -With exact Rationals and binary64 Reals, `attribute x : Real = 0.1; x == 0.1` compares the double -nearest 1/10 with 1/10 itself. KerML has Rational ⊂ Real and so asks for the mathematical -comparison; but the binary64 Real is this implementation's approximation, not the text's, and -the text does not say how an approximate Real meets an exact Rational. Two readings: - -- **Exact comparison** (implemented): `x == 0.1` is `false` and `x > 0.1` is `true`, as the - Integer precedent `CompareIntReal` compares a large Integer with a Real exactly. -- **Round the Rational once**, as mixed arithmetic does: `x == 0.1` is `true`. - -The choice is recorded as open; the implementation follows the Integer precedent until it is made. +With exact Rationals and binary64 Reals, `attribute x : Real = 0.1; x == 0.1` meets the double +nearest 1/10 (held by `x`, since a `Real` declaration rounds) with 1/10 itself. KerML has Rational +⊂ Real (§9.3.2.2.8) and so asks for the mathematical comparison of two reals; but the binary64 +Real is this implementation's approximation (§9.3.2.2.9 leaves precision open), not the text's, +and the text does not say how an approximate Real meets an exact Rational. The rule is therefore +tool-defined, and chosen so the approximation does not give a wrong answer elsewhere: + +- **A Rational meeting a binary64 Real is rounded once, to the nearest double, before it is + compared** (`semantics.CompareReal`): `==`, `!=`, `<`, `<=`, `>`, `>=` between them compare + `RealOf(rational)` with the Real. So `x == 0.1` is `true`, `x : Real = 1 / 3; x == 1 / 3` is + `true`, `rat(1, 3) == 1.0 / 3.0` is `true` (`1.0 / 3.0` is exact `1/3` too) and `rat(1, 4) == + 0.25` is `true`. A Rational whose magnitude no finite double holds compares as the infinity of + its sign; a NaN compares as it does with any number (unordered, `!=` true). +- This is the rule mixed arithmetic already follows (the Rational is rounded once where a + binary64 Real takes part), and the one a binding needs: KerML §7.4.9 asserts that both ends of + a binding connector hold equal values, so a Rational bound to a `Real` feature — rounded by the + declaration — must equal the Rational it was bound from. Exact comparison would make that + binding's own equality `false`, a wrong answer rather than a choice + (`instance_real_binding_meets_rational`). +- Rational with Rational stays exact (`0.1 + 0.2 == 0.3` is `true`), and so does Integer with + anything: an Integer keeps its kind in a `Real` feature, so `CompareIntReal` compares a large + Integer with a Real exactly as before. +- Equality of values (sets, `includes`, query `==`, solver witness replay, compiled `==`) uses the + same rule, so a Rational and a Real that compare equal are one member of a set. + +A long `Real`-typed accumulation of a decimal literal (`x := x + 0.01` a thousand times, `x : +Real`) is binary64 at every step and so never reaches `ErrRationalSizeLimit` +(`action_real_accumulation_stays_binary64`, and `TestRuntimeRobustnessRationalRealComparison` +under a lowered budget); the same accumulation over exact Rationals is exact and stays within the +budget only while its denominators do. ## A Rational a double holds, on input -A Rational binary64 holds exactly (`1/4`) crosses the wire as `real_value`, so a client cannot -tell the service that a `0.25` it sends is the Rational rather than the Real, and the service -reads it as a Real: `in x : Rational; x + 1 / 3` with `x` sent as `0.25` is binary64 -`0.5833333333333333`, while `x` sent as the `rational_value` `1/3` gives the exact `2/3`. KerML -says nothing about a wire. Three readings: - -- **An inbound `real_value` is always a Real** (implemented): the encoding decides. -- **The declaration decides**: a binary64 value held by a feature or parameter declared - `Rational` is the exact Rational equal to it, as a `Real` declaration already rounds. This - changes in-model semantics too. -- **Wire only**: clients send every exact Rational as `rational_value` to a service with - `rational_values`, which then accepts a binary64-exact `rational_value` on input while - answering canonically. - -The choice is recorded as open; the implementation reads the encoding until it is made. +The service answers a Rational binary64 holds exactly (`1/4`) as `real_value`, so its answers +never spell one number two ways. KerML says nothing about a wire, so how a client tells the +service that a `0.25` it sends is the Rational rather than the Real is tool-defined: + +- **Clients send every exact Rational as `rational_value`** to a service that advertises + `rational_values`, one a double holds included (`Fraction(1, 4)`, `1//4`, `Rational.of(1, 4)`). +- **The service accepts a binary64-exact `rational_value` on input** as that exact Rational + (`protoconv.LowestTermsRational`; still refused unless in lowest terms over a positive + denominator), while every answer stays canonical (`CanonicalRational`): `in x : Rational; x + + 1 / 3` with `x` sent as the `rational_value` `1/4` is exact `7/12`. +- **An inbound `real_value` is always a Real**: the same `x` sent as `real_value` `0.25` is + binary64 `0.5833333333333333`. The declaration does not reinterpret it. +- Runtime semantics do not depend on the encoding beyond that: once read, an exact Rational is + the same value whether it arrived on the wire or was written in the model. +- To a service without `rational_values`, which would read the arm as unknown, a client sends a + Rational a double holds exactly as that `real_value` — the only form such a service reads — and + refuses any other Rational before the call. + +Both directions are covered by the gRPC conformance cases (`execute_action_rational_input_meets_ +rational`, `execute_action_real_input_meets_rational`, `evaluate_calc_rational_wire`, +`evaluate_calc_rational_wire_canonical_answer`), `internal/frontend/grpc/convert_rational_test.go` +and each client's tests (`client/python/tests/test_rational.py` against a live service). ## The pilot differs by design diff --git a/docs/project/native-compilation.md b/docs/project/native-compilation.md index aef88a3037..2c2a05f10e 100644 --- a/docs/project/native-compilation.md +++ b/docs/project/native-compilation.md @@ -133,9 +133,10 @@ arithmetic rather than the host language's: might not be held; and an Integer `/` whose quotient reaches a `Real` declaration is the exact quotient rounded once there, as the interpreter's Real declaration rounds it (`7 / 2` is `3.5`, `1 / 3` is `0.3333333333333333`; C refines with `__int128`, Go uses `math/big.Rat`). - A comparison between a Real and a Rational literal compares exactly, through the literal's - nearest double and the side the Rational lies on, and an Integer quotient against a whole - number compares exactly (`a / 3 >= 2`). Anything else is refused, naming the + A comparison between a Real and a Rational literal is at Real precision, the literal rounded + once to its nearest double as the interpreter rounds it (`x == 0.1` holds for the Real `0.1`); + a literal past the binary64 range is refused (`literal 1e400 is outside the Real range`). An + Integer quotient against a whole number compares exactly (`a / 3 >= 2`). Anything else is refused, naming the construct: `exact Rational arithmetic '*' over a value binary64 does not hold exactly`, `'**' of an exact Rational by an Integer exponent`, `'<' of an exact Rational binary64 does not hold exactly`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 39c68957fb..746d4c390d 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -1580,7 +1580,7 @@ restores library symbols without their AST). | Rational arithmetic is exact: `+`, `-`, `*`, `/`, `%` and `**`/`^` with an Integer exponent over Integers and Rationals answer the exact Rational `RationalFunctions` declares, the Integer `/` included (`1 / 3` is `1/3`, `IntegerFunctions::'/'` returning `Rational`); a whole result stays a Rational (`6 / 3` is the Rational `2.0`) unless the declaration returns `Integer` (`+`, `-`, `*`, `%`, `**` over Integers, `floor`, `round`, `numer`, `denom`, `ToInteger`). Comparison and equality are exact (`semantics.CompareRat`); `abs`, `max`, `min`, `sum`, `product` keep exactness. A `*` operand follows the existing `LiteralInfinity` rules | `semantics/rational.go` `RatArith`, `smallRatArith`, `RatPow`, `CompareRat`, `RatNeg`, `RatAbs`; `runtime/eval.go` `arithmeticValues`, `comparisonValues`; `runtime/library_functions.go`, `runtime/collections.go` (aggregation) | `semantics/rational_test.go:TestRationalArithmeticIsExact`, `:TestRationalPower`, `:TestRationalComparisonAndRounding`, `runtime/division_test.go:TestWholeNumberQuotientIsRational`, `:TestQuotientBeyondFloatExactRangeIsExact`; conformance `calc_exact_rational_arithmetic`, `action_exact_rational_accumulation` (trace), `constraint_rational_quotient`, `requirement_rational_quotient`; pilot-exec-diff `exact_rationals.cases` | ✅ Faithful | | A Rational is held in lowest terms over a positive denominator, inline (`int64` numerator, `uint32` denominator) when it fits and as an immutable `math/big.Rat` otherwise, so each Rational has one representation; a result whose numerator-plus-denominator bits exceed the evaluation's budget (`DefaultMaxIntegerBits`, shared with Integers) is `ErrRationalSizeLimit`, refused before an oversized power is computed and never rounded | `semantics/rational.go` `RatValue`, `FracValue`, `smallRat`, `sizedRat`, `RationalSizeExceeded` | `semantics/rational_test.go:TestRationalNormalization`; robustness `TestRuntimeRobustnessExactRational` (denominator growth, harmonic sum, oversized power) | ✅ Faithful | | A Rational prints exactly: a terminating decimal as the Real of that value prints (`0.1`, `2.0`, `1.5e-05`), any other as `numer/denom` (`1/3`), in the REPL, JSON, traces and `ToString`; `ToRational` reads both forms. The specification fixes no output form | `semantics/rational.go` `Value.FormatRational`, `formatDecimal`, `ParseRationalText` | `semantics/rational_test.go:TestRationalFormatting`, `:TestParseRationalIsExact` | ✅ Faithful (presentation is an OpenSysML choice) | -| `Real` stays IEEE 754 binary64 (§9.3.2.2.9 leaves precision to the implementation): a value declared `Real` is rounded once to the nearest double, operations with no rational result (`sqrt`, trigonometry, `exp`, `ln`, `**` with a non-Integer exponent, anything typed only `Real`) answer a Real, and mixed Rational/Real arithmetic rounds the Rational once to the nearest double. Mixed Rational/Real *comparison* is exact, as Integer/Real comparison already is (`x : Real = 0.1; x == 0.1` is `false`, the double nearest `1/10` not being `1/10`) — a policy the specification does not settle, recorded in [exact-rational-evaluation.md](exact-rational-evaluation.md#comparing-a-rational-with-a-binary64-real) | `semantics/quantity_eval.go` `RealOf`; `semantics/rational.go` `CompareExactReal`, `ratToFloat`; `runtime/eval.go` `arithmeticValues` | `semantics/rational_test.go:TestRationalComparisonAndRounding`, `semantics/quantity_eval_test.go`; conformance `calc_exact_rational_arithmetic` (`root`) | ⚠️ Approximate — the mixed comparison rule is open | +| `Real` stays IEEE 754 binary64 (§9.3.2.2.9 leaves precision to the implementation): a value declared `Real` is rounded once to the nearest double, operations with no rational result (`sqrt`, trigonometry, `exp`, `ln`, `**` with a non-Integer exponent, anything typed only `Real`) answer a Real, and mixed Rational/Real arithmetic rounds the Rational once to the nearest double. Mixed Rational/Real *comparison* (`==`, `!=`, `<`, `<=`, `>`, `>=`, set membership, query equality, witness replay, compiled comparisons) is at Real precision: the Rational is rounded once to the nearest double, as in mixed arithmetic, so `x : Real = 0.1; x == 0.1` and `rat(1, 3) == 1.0 / 3.0` are `true` and a binding's two ends (§7.4.9) stay equal across a `Real` declaration; a Rational past the binary64 range compares as the infinity of its sign, NaN as with any number; Rational/Rational and Integer/anything stay exact (`CompareIntReal`). A tool-defined approximation of KerML's mathematical Real where the specification does not settle it, recorded in [exact-rational-evaluation.md](exact-rational-evaluation.md#comparing-a-rational-with-a-binary64-real). On the wire an exact Rational is sent as `rational_value` (one a double holds included) and an inbound `real_value` is always a Real ([the wire rule](exact-rational-evaluation.md#a-rational-a-double-holds-on-input)) | `semantics/quantity_eval.go` `RealOf`; `semantics/rational.go` `CompareReal`, `ratToFloat`, `LowestTermsRational`, `CanonicalRational`; `semantics/eval.go`; `runtime/eval.go` `arithmeticValues`; `runtime/set_order.go` `compareExactReal`; `runtime/value_equality.go`; `runtime/write_conformance.go` `holdAsReal`; `solve/replay.go`; `codegen/rational.go`; `protoconv/convert.go` | `semantics/rational_test.go:TestRationalComparisonAndRounding`, `semantics/quantity_eval_test.go`; `runtime/robustness_rational_real_comparison_test.go:TestRuntimeRobustnessRationalRealComparison`; conformance `calc_exact_rational_arithmetic` (`root`), `instance_real_binding_meets_rational`, `action_real_accumulation_stays_binary64`; `grpc/convert_rational_test.go:TestBinaryExactRationalValueReadsAsARational`; gRPC conformance `execute_action_rational_input_meets_rational`, `execute_action_real_input_meets_rational`, `evaluate_calc_rational_wire`, `evaluate_calc_rational_wire_canonical_answer`; `client/python/tests/test_rational.py` | ⚠️ Approximate — `Real` is binary64, and a Rational meeting one is compared at its precision | | Every boundary keeps a Rational exact: gRPC carries one no double holds as `Value.rational_value`/`Quantity.rational_magnitude`/`DocumentValue.rational_value` in lowest terms under the `rational_values` capability (a service without it reports the value unsupported and refuses one sent, as `big_int_values` does; a noncanonical encoding is `ErrRationalNotCanonical`), the JSON engine as `rationalValue`, the Go, Python and Node clients as `Rational`, `fractions.Fraction` and `{numerator, denominator}` bigints, and a Rational a double holds exactly as that Real | `api/proto/sysml.proto` `Rational`; `frontend/protoconv/convert.go` `RationalToProto`, `ProtoToRational`, `ValueCarriesRational`; `frontend/grpc/service.go` `CapabilityRationalValues`; `semantics/rational.go` `CanonicalRational`; `client/opensysml/value.go` `Rational`; `client/python/opensysml/values.py`; `client/node/src/core/values.ts` | `grpc/convert_rational_test.go:TestRationalValueRoundTripsOverTheWire`, `:TestRationalQuantityMagnitudeCrossesExactly`, `:TestRationalValueReadsOnlyCanonicalTerms`, `:TestEvaluateCalcTakesAndReturnsRationals`, `:TestRationalCapability`; `client/opensysml/edges_test.go:TestRationalArithmeticIsExact`; `client/python/tests/test_rational.py`; `client/node/test/rational.test.ts` | ✅ Faithful | | RDF keeps a literal's exact value: a decimal exports as `xsd:decimal`, an exponent literal as `xsd:double` only when a double holds it exactly and as the exact `xsd:decimal` otherwise, and an imported `xsd:double`/`xsd:float` is read as the binary value it names | `translate/export/rdf_expr.go` `exactRealLiteral`; `translate/export/api_json_in.go` | `tests/export/result_expression_test.go:TestRationalLiteralValuesAreExact` | ✅ Faithful | | `sysml -compile` compiles exact arithmetic where one binary64 rounding gives the exact result (whole values within `2^53`, Rationals a double holds), folds exact constants, and refuses any other exact Rational operation with a typed error naming the construct, never emitting code that rounds a Rational the interpreter keeps exact; a declared `Real` stays binary64 | `translate/codegen/rational.go` `exactness`, `translate/codegen/ir.go` | `translate/codegen/library_test.go` | ⚠️ Approximate — exact Rationals outside binary64 are refused, not compiled | diff --git a/docs/reference/java-api.md b/docs/reference/java-api.md index 233f48ddd8..8b7bb7e120 100644 --- a/docs/reference/java-api.md +++ b/docs/reference/java-api.md @@ -457,6 +457,14 @@ connection.capabilities().require(Capabilities.FEATURE_VALUES); if (connection.capabilities().has(Capabilities.ENUM_VALUES)) { } ``` +An exact `Rational` sent as an input, argument or document binding crosses as +`rationalValue` to a service advertising `RATIONAL_VALUES`, one a double holds +(`Rational.of(1, 4)`) included, and the service reads it as that Rational; a +`RealValue` is always a binary64 Real. To a service without the capability a +Rational a double holds is sent as that `realValue` and any other is refused +before the call. An answered Rational is canonical: one a double holds arrives as +a `RealValue`. + The client checks before a gated call rather than relying on the refusal, because a capability that only describes how a response is *populated* omits its fields instead of failing, and a call that relied on failure alone would read an answer diff --git a/docs/reference/julia-api.md b/docs/reference/julia-api.md index cd48c227f3..fd92f34265 100644 --- a/docs/reference/julia-api.md +++ b/docs/reference/julia-api.md @@ -103,8 +103,9 @@ returns a `StateRun` with `states_visited`, `final_context`, and `final_time`. `decode_value` decodes the service's value oneof and `encode_value` creates request values. The mapping retains exact `Int64` values and `BigInt` ones beyond `Int64`, decodes an exact Rational no `Float64` holds (`rationalValue`, a quantity's -`rationalMagnitude`) as `Rational{BigInt}` and sends one back the same way, which needs the -service's `rational_values` capability, decodes sequences and sets recursively, and exposes +`rationalMagnitude`) as `Rational{BigInt}`, sends every `Rational` (`1//4` included) as +`rationalValue` to a service with the `rational_values` capability and, to one without it, one a +`Float64` holds as that `realValue` while refusing any other (a `Float64` sent is always a Real), decodes sequences and sets recursively, and exposes structured values through `Quantity`, `EnumLiteral`, `ArrayValue`, `VectorValue`, `VectorQuantity`, `TensorQuantity`, `MeasurementRef`, `FunctionRef`, `Metaobject`, `Undetermined`, `Unset`, and `Infinity`. diff --git a/docs/reference/matlab-api.md b/docs/reference/matlab-api.md index 0bf9a479c3..f4e6ccddb1 100644 --- a/docs/reference/matlab-api.md +++ b/docs/reference/matlab-api.md @@ -94,8 +94,9 @@ writes supported request values. `intValue` uses decimal JSON strings and exact accumulation, including `-9223372036854775808`; an Integer beyond `int64` arrives as `bigIntValue` and decodes to `struct('bigInteger', digits)`, its decimal digits kept as `char` because no MATLAB number holds it, and is sent back as written; an exact Rational no double holds arrives as `rationalValue` and -decodes to `struct('numerator', digits, 'denominator', digits)`, sent back as written to a service -with the `rational_values` capability; `realValue` supports `"NaN"`, `"Infinity"`, and +decodes to `struct('numerator', digits, 'denominator', digits)`; any such struct, one a double +holds included, is sent as `rationalValue` to a service with the `rational_values` capability, +and to one without it only as the `realValue` a double holds exactly, refused otherwise; `realValue` supports `"NaN"`, `"Infinity"`, and `"-Infinity"`. Other primitive mappings include `boolValue` to `logical`, `stringValue` to `char`, `complex` to a complex scalar, and `sequence` to a cell array. `null` becomes `[]`. diff --git a/docs/reference/node-api.md b/docs/reference/node-api.md index 84b37492d7..cd77eda356 100644 --- a/docs/reference/node-api.md +++ b/docs/reference/node-api.md @@ -98,7 +98,7 @@ than a message with optional fields: switch (value.kind) { case "int": value.value; // bigint, never lossy, beyond int64 too case "real": value.value; // number - case "rational": value.numerator; value.denominator; // bigint terms: an exact Rational no double holds + case "rational": value.numerator; value.denominator; // bigint terms: exact; answered only when no double holds it, sent as rationalValue always case "complex": value.value.real; value.value.imaginary; // one value, not two floats case "boolean": case "string": value.value; diff --git a/docs/reference/python-api.md b/docs/reference/python-api.md index c421d0f2c3..4d14ed83b1 100644 --- a/docs/reference/python-api.md +++ b/docs/reference/python-api.md @@ -156,7 +156,7 @@ collection property. | SysML | Python | | --- | --- | | `Real` | `float` | -| `Rational` | `fractions.Fraction`, exact whichever wire arm carried it (`as_rational`); a value read untyped is a `float` where a double holds it exactly (`0.5`) and a `Fraction` otherwise (`1/3`, `0.1`) | +| `Rational` | `fractions.Fraction`, exact whichever wire arm carried it (`as_rational`); a value read untyped is a `float` where a double holds it exactly (`0.5`) and a `Fraction` otherwise (`1/3`, `0.1`). A `Fraction` is sent as `rational_value` to a service with `rational_values`, and a `float` is always a Real | | `Complex` | `complex`, one number rather than two floats | | `Array` | `opensysml.Array`: `dimensions` and row-major `elements`, `nested()` unfolds them | | `CartesianVectorValue` and the other numeric vectors | `opensysml.Vector`: a tuple of `int`/`float` components, kept apart | diff --git a/docs/reference/rust-api.md b/docs/reference/rust-api.md index 13c5d8ca5c..6824430daa 100644 --- a/docs/reference/rust-api.md +++ b/docs/reference/rust-api.md @@ -153,7 +153,7 @@ the elements that still refer to it. match value { Value::Integer(v) => (), Value::Real(v) => (), - Value::Rational(r) => (), // exact, no double holds it: r.numerator(), r.denominator() as decimal text; r.to_f64() rounds once + Value::Rational(r) => (), // exact; answered only when no f64 holds it, sent as rational_value always: r.numerator(), r.denominator() as decimal text; r.to_f64() rounds once Value::Complex(z) => (), // z.real, z.imaginary; one value, Display as `1.5 - 2.0i` Value::Boolean(v) => (), Value::Text(v) => (), diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index a4c04c5471..66cf7e56fa 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -205,7 +205,7 @@ arms, each captured from `Evaluate` against the model at the end of this section | `intValue` | string | `{"result":{"intValue":"4"}}` | Integer within 64 bits; string because `int64` | | `bigIntValue` | string | `{"result":{"bigIntValue":"1180591620717411303424"}}` | Integer beyond 64 bits, in decimal; KerML Integers are unbounded | | `realValue` | number | `{"result":{"realValue":1.4142135623730951}}` | Real (IEEE-754 double), or a Rational a double holds exactly | -| `rationalValue` | object | `{"result":{"rationalValue":{"numerator":"1","denominator":"3"}}}` | Rational no double holds, in lowest terms; KerML Rationals are exact | +| `rationalValue` | object | `{"result":{"rationalValue":{"numerator":"1","denominator":"3"}}}` | Rational in lowest terms; KerML Rationals are exact. Answered only for one no double holds, accepted for any | | `boolValue` | boolean | `{"result":{"boolValue":true}}` | Boolean | | `stringValue` | string | `{"result":{"stringValue":"abc"}}` | String | | `instanceId` | string | `{"result":{"instanceId":"2"}}` | A reference to a runtime instance, by id | @@ -350,17 +350,27 @@ to it, document-query bindings included, before the call. exactly: `1 / 3` answers `{"result":{"rationalValue":{"numerator":"1","denominator":"3"}}}` and `0.1 + 0.2` answers `{"numerator":"3","denominator":"10"}`. `numerator` and `denominator` are canonical decimal strings (no `+`, no leading zero), the fraction is in lowest terms and the -denominator is positive. A Rational a double holds exactly (`0.5`, `2.0 ** 70`) is sent as -`realValue`, so the two arms never spell the same number; an Integer is never sent here, though a -whole Rational no double holds is (denominator `1`). A decoder rejects a `rationalValue` that is -not in lowest terms or that a double holds. The service accepts `rationalValue` on input under the -same rule, and reads a `realValue` sent to it as a binary64 Real. A value declared `Real` is IEEE 754 binary64 and always -answers `realValue` (`rover.mass` is `{"realValue":20}`). The arm is negotiated by the -`rational_values` capability exactly as `big_int_values` negotiates `bigIntValue`: a service -that does not advertise it sends such a Rational (bare, nested, or as a `rationalMagnitude`) as -an unsupported `null` naming it, refuses a document query bound to or answering one with -`UNIMPLEMENTED` naming the capability, and the bundled clients refuse to send one to it before -the call. +denominator is positive. The service answers a Rational a double holds exactly (`0.5`, +`2.0 ** 70`) as `realValue`, so its answers never spell one number two ways; an Integer is never +answered here, though a whole Rational no double holds is (denominator `1`). A client decoder +rejects an answered `rationalValue` that is not in lowest terms or that a double holds. + +On input the direction of the rule is reversed. The service accepts any `rationalValue` in lowest +terms, one a double holds included, as that exact Rational: `{"numerator":"1","denominator":"4"}` +bound to `in x : Rational` evaluates `x + 1/3` to `7/12`. A `realValue` sent to it is always a +binary64 Real, never a Rational: `0.25` bound to the same parameter evaluates `x + 1/3` to +`0.5833333333333333`. Evaluation does not depend on which arm carried a value beyond that: once +read, an exact Rational behaves the same whether it arrived as `rationalValue` or was written in +the model. So the bundled clients send every exact Rational (`Fraction(1, 4)`, `1//4`, +`Rational.of(1, 4)`) as `rationalValue` to a service that advertises `rational_values`. + +A value declared `Real` is IEEE 754 binary64 and always answers `realValue` (`rover.mass` is +`{"realValue":20}`). The arm is negotiated by the `rational_values` capability exactly as +`big_int_values` negotiates `bigIntValue`: a service that does not advertise it sends such a +Rational (bare, nested, or as a `rationalMagnitude`) as an unsupported `null` naming it, and +refuses a document query bound to or answering one with `UNIMPLEMENTED` naming the capability. +To such a service the bundled clients send a Rational a double holds exactly as that `realValue`, +the only form it reads, and refuse to send any other Rational before the call. MATLAB's `jsondecode` gives you a `char` array, which `int64(str2double(...))` corrupts and `sscanf(s, '%ld')` does not; R needs diff --git a/internal/doc/queryexec/operations.go b/internal/doc/queryexec/operations.go index 3646b14f0f..1b9de5fe7f 100644 --- a/internal/doc/queryexec/operations.go +++ b/internal/doc/queryexec/operations.go @@ -1032,11 +1032,11 @@ func compareNumeric(left, right Value) int { } if exactKind(left.Kind()) && right.Kind() == ValueReal { r, _ := right.Real() - return semantics.CompareExactReal(exactOperand(left), r) + return semantics.CompareReal(exactOperand(left), r) } if left.Kind() == ValueReal && exactKind(right.Kind()) { l, _ := left.Real() - return -semantics.CompareExactReal(exactOperand(right), l) + return -semantics.CompareReal(exactOperand(right), l) } switch left.Kind() { case ValueReal: diff --git a/internal/exec/runtime/eval.go b/internal/exec/runtime/eval.go index d55049f9f5..233f1d6324 100644 --- a/internal/exec/runtime/eval.go +++ b/internal/exec/runtime/eval.go @@ -2554,13 +2554,13 @@ func constComparison(op ast.OperatorKind, left, right semantics.Value) (bool, er res, _ := semantics.OrderSatisfies(op, semantics.CompareRat(left, right)) return res, nil } - // An exact number orders against a Real exactly, neither rounded to the other. + // A Rational meets a Real at Real precision, an Integer exactly. if left.IsExact() && right.Kind == semantics.ValReal && !math.IsNaN(right.Real) { - res, _ := semantics.OrderSatisfies(op, semantics.CompareExactReal(left, right.Real)) + res, _ := semantics.OrderSatisfies(op, semantics.CompareReal(left, right.Real)) return res, nil } if left.Kind == semantics.ValReal && right.IsExact() && !math.IsNaN(left.Real) { - res, _ := semantics.OrderSatisfies(op, -semantics.CompareExactReal(right, left.Real)) + res, _ := semantics.OrderSatisfies(op, -semantics.CompareReal(right, left.Real)) return res, nil } diff --git a/internal/exec/runtime/robustness_rational_real_comparison_test.go b/internal/exec/runtime/robustness_rational_real_comparison_test.go new file mode 100644 index 0000000000..4dbb0a4057 --- /dev/null +++ b/internal/exec/runtime/robustness_rational_real_comparison_test.go @@ -0,0 +1,181 @@ +package runtime + +import ( + "errors" + "math" + "testing" + "time" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// TestRuntimeRobustnessRationalRealComparison covers the edges of a Rational +// compared at Real precision with a binary64 Real: NaN, the infinities, a +// magnitude past the binary64 range, set membership, and a Real-typed +// accumulation that stays a double rather than growing an exact denominator. +func TestRuntimeRobustnessRationalRealComparison(t *testing.T) { + t.Run("against_nan", testRationalAgainstNaN) + t.Run("against_the_infinities", testRationalAgainstTheInfinities) + t.Run("beyond_the_binary64_range", testRationalBeyondTheBinary64Range) + t.Run("set_membership", testRationalRealSetMembership) + t.Run("real_accumulation_under_a_lowered_budget", testRealAccumulationUnderALoweredBudget) +} + +const rationalRealComparisonSrc = ` + package test { + private import ScalarValues::*; + calc tenth { in r; return : Boolean[6] ordered nonunique = (0.1 == r, 0.1 != r, 0.1 < r, 0.1 <= r, 0.1 > r, 0.1 >= r); } + calc huge { in r; return : Boolean[6] ordered nonunique = (1.0e400 == r, 1.0e400 != r, 1.0e400 < r, 1.0e400 <= r, 1.0e400 > r, 1.0e400 >= r); } + calc tiny { in r; return : Boolean[6] ordered nonunique = (-1.0e400 == r, -1.0e400 != r, -1.0e400 < r, -1.0e400 <= r, -1.0e400 > r, -1.0e400 >= r); } + } +` + +// compareRationalWithReal invokes the named calc of rationalRealComparisonSrc on r +// and returns its six comparisons: ==, !=, <, <=, >, >=. +func compareRationalWithReal(t *testing.T, calc string, r float64) [6]bool { + t.Helper() + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, rationalRealComparisonSrc)) + root := idx.DocumentRoot("") + sym := findSymbolByName(root, calc, ast.DefCalc) + if sym == nil { + t.Fatalf("%s calc not found", calc) + } + got, err := ctx.InvokeCalc(sym, []Value{realConst(r)}, root) + if err != nil { + t.Fatalf("%s(%v): %v", calc, r, err) + } + elements := elementsOf(got) + if len(elements) != 6 { + t.Fatalf("%s(%v) = %s; want six Booleans", calc, r, FormatValue(got)) + } + var out [6]bool + for i, e := range elements { + if e.Kind != ValConst || e.Const.Kind != semantics.ValBool { + t.Fatalf("%s(%v)[%d] = %s; want a Boolean", calc, r, i, FormatValue(e)) + } + out[i] = e.Const.Bool + } + return out +} + +// A Real feature admits no NaN, but an untyped parameter holds one: a Rational +// meets it as a Real does, unequal and neither above nor below it. +func testRationalAgainstNaN(t *testing.T) { + for _, calc := range []string{"tenth", "huge", "tiny"} { + if got, want := compareRationalWithReal(t, calc, math.NaN()), [6]bool{false, true, false, false, false, false}; got != want { + t.Errorf("%s against NaN = %v; want %v", calc, got, want) + } + } +} + +// A finite Rational lies strictly between the infinities. +func testRationalAgainstTheInfinities(t *testing.T) { + if got, want := compareRationalWithReal(t, "tenth", math.Inf(1)), [6]bool{false, true, true, true, false, false}; got != want { + t.Errorf("0.1 against +Inf = %v; want %v", got, want) + } + if got, want := compareRationalWithReal(t, "tenth", math.Inf(-1)), [6]bool{false, true, false, false, true, true}; got != want { + t.Errorf("0.1 against -Inf = %v; want %v", got, want) + } +} + +// A Rational no binary64 holds compares as the infinity of its sign: above the +// largest double, and equal to the infinity itself. +func testRationalBeyondTheBinary64Range(t *testing.T) { + cases := []struct { + calc string + r float64 + want [6]bool + }{ + {"huge", math.MaxFloat64, [6]bool{false, true, false, false, true, true}}, + {"huge", math.Inf(1), [6]bool{true, false, false, true, false, true}}, + {"huge", math.Inf(-1), [6]bool{false, true, false, false, true, true}}, + {"tiny", -math.MaxFloat64, [6]bool{false, true, true, true, false, false}}, + {"tiny", math.Inf(-1), [6]bool{true, false, false, true, false, true}}, + } + for _, c := range cases { + if got := compareRationalWithReal(t, c.calc, c.r); got != c.want { + t.Errorf("%s against %v = %v; want %v", c.calc, c.r, got, c.want) + } + } +} + +// A Rational and the Real it rounds to are one member of a set, and two +// Rationals stay distinct however close. +func testRationalRealSetMembership(t *testing.T) { + third := rationalConst(t, 1, 3) + nearest := rationalConst(t, 6004799503160661, 18014398509481984) + tenth := rationalConst(t, 1, 10) + s := NewSet() + s.Add(tenth) + s.Add(realConst(0.1)) + if s.Size() != 1 || !s.Contains(realConst(0.1)) || !s.Contains(tenth) { + t.Errorf("{1/10, 0.1} has %d members; want one", s.Size()) + } + s = NewSet() + s.Add(third) + s.Add(nearest) + if s.Size() != 2 { + t.Errorf("{1/3, %s} has %d members; want two", FormatValue(nearest), s.Size()) + } + if !s.Contains(realConst(1.0 / 3.0)) { + t.Error("a Real 1/3 is not a member of a set holding the Rational 1/3") + } +} + +// A Real feature holds each step's sum as a double, so ten thousand steps of +// an exact 0.01 stay fast under the smallest budget and never reach it. +func testRealAccumulationUnderALoweredBudget(t *testing.T) { + src := ` + package test { + private import ScalarValues::*; + action accumulate { + out attribute total : Real = 0.0; + first start; + then action sum { + for i in 1..10000 { + assign total := total + 0.01; + } + } + then done; + } + } + ` + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, src)) + budgets := DefaultBudgets() + budgets.MaxIntegerBits = MinMaxIntegerBits + if err := ctx.SetBudgets(budgets); err != nil { + t.Fatal(err) + } + sym := findSymbolByName(idx.DocumentRoot(""), "accumulate", ast.DefAction) + if sym == nil { + t.Fatal("accumulate action not found") + } + start := time.Now() + out, err := ctx.ExecuteAction(sym) + if errors.Is(err, semantics.ErrRationalSizeLimit) { + t.Fatalf("a Real accumulation reached the Rational size budget: %v", err) + } + if err != nil { + t.Fatal(err) + } + total := out["total"] + if total.Kind != ValConst || total.Const.Kind != semantics.ValReal { + t.Fatalf("total = %s; want a Real", FormatValue(total)) + } + if math.Abs(total.Const.Real-100) > 1e-9 || total.Const.Real == 100 { + t.Errorf("total = %v; want the binary64 sum near 100", total.Const.Real) + } + if elapsed := time.Since(start); elapsed > 10*time.Second { + t.Errorf("ten thousand Real steps took %v", elapsed) + } +} + +func rationalConst(t *testing.T, num, den int64) Value { + t.Helper() + r, ok := semantics.FracValue(num, den) + if !ok { + t.Fatalf("%d/%d is not a Rational", num, den) + } + return Value{Kind: ValConst, Const: r} +} diff --git a/internal/exec/runtime/set_order.go b/internal/exec/runtime/set_order.go index 2982fa504d..1d563a6af6 100644 --- a/internal/exec/runtime/set_order.go +++ b/internal/exec/runtime/set_order.go @@ -273,26 +273,27 @@ func canonicalClass(v Value) int { return classOther } -// compareNumbers orders the numeric constants exactly, infinity above every finite number. +// compareNumbers orders the numeric constants as the comparison operators do, +// infinity above every finite number. func compareNumbers(a, b semantics.Value) int { switch { case a.IsExact() && b.IsExact(): return semantics.CompareRat(a, b) case a.IsExact() && b.Kind == semantics.ValReal: - return compareIntReal(a, b.Real) + return compareExactReal(a, b.Real) case a.Kind == semantics.ValReal && b.IsExact(): - return -compareIntReal(b, a.Real) + return -compareExactReal(b, a.Real) } return cmp.Compare(numberOf(a), numberOf(b)) } -// compareIntReal orders an exact number against a Real without rounding either, -// a NaN below every number as cmp.Compare puts it. -func compareIntReal(i semantics.Value, r float64) int { +// compareExactReal orders an exact number against a Real as semantics.CompareReal +// does, a NaN below every number as cmp.Compare puts it. +func compareExactReal(i semantics.Value, r float64) int { if math.IsNaN(r) { return cmp.Compare(0.0, r) } - return semantics.CompareExactReal(i, r) + return semantics.CompareReal(i, r) } func numberOf(v semantics.Value) float64 { diff --git a/internal/exec/runtime/testdata/conformance/action_real_accumulation_stays_binary64.expected.json b/internal/exec/runtime/testdata/conformance/action_real_accumulation_stays_binary64.expected.json new file mode 100644 index 0000000000..75c06b44ba --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_real_accumulation_stays_binary64.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::accumulate", + "outputs": { + "total": {"type": "Real", "value": 9.999999999999831}, + "reachesTen": {"type": "Boolean", "value": false} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_real_accumulation_stays_binary64.sysml b/internal/exec/runtime/testdata/conformance/action_real_accumulation_stays_binary64.sysml new file mode 100644 index 0000000000..ef4518f070 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_real_accumulation_stays_binary64.sysml @@ -0,0 +1,20 @@ +package test { + private import ScalarValues::*; + + // A Real feature holds binary64 values: each step rounds the exact 0.01 into + // the Real once, so a thousand steps stay a double rather than an exact + // Rational whose denominator would grow, and reach 9.999999999999831, not 10. + action accumulate { + out attribute total : Real = 0.0; + out attribute reachesTen : Boolean = false; + + first start; + then action sum { + for i in 1..1000 { + assign total := total + 0.01; + } + assign reachesTen := total == 10; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_library_conversions.expected.json b/internal/exec/runtime/testdata/conformance/calc_library_conversions.expected.json index 2fe3713c4e..5a64edc74b 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_conversions.expected.json +++ b/internal/exec/runtime/testdata/conformance/calc_library_conversions.expected.json @@ -19,7 +19,7 @@ "stringFromNull": {"type": "String", "value": "null"}, "stringFromLiteral": {"type": "String", "value": "Color::red"}, "roundTrip": {"type": "Boolean", "value": true}, - "realTenthIsNotTenth": {"type": "Boolean", "value": false}, + "realTenthIsTenth": {"type": "Boolean", "value": true}, "naturalFromInteger": {"type": "Integer", "value": 9}, "integerFromReal": {"type": "Integer", "value": 2}, "integerFromNegativeReal": {"type": "Integer", "value": -2}, diff --git a/internal/exec/runtime/testdata/conformance/calc_library_conversions.sysml b/internal/exec/runtime/testdata/conformance/calc_library_conversions.sysml index 276695105c..b9ce5de4e6 100644 --- a/internal/exec/runtime/testdata/conformance/calc_library_conversions.sysml +++ b/internal/exec/runtime/testdata/conformance/calc_library_conversions.sysml @@ -21,8 +21,8 @@ package test { out stringFromAnything = BaseFunctions::ToString(3); out stringFromNull = BaseFunctions::ToString(null); out stringFromLiteral = BaseFunctions::ToString(Color::red); - out roundTrip = RealFunctions::ToReal(RealFunctions::ToString(RealFunctions::ToReal("0.1"))) == RealFunctions::ToReal("0.1"); - out realTenthIsNotTenth = RealFunctions::ToReal("0.1") == 0.1; + out roundTrip = RealFunctions::ToReal(RealFunctions::ToString(0.1)) == 0.1; + out realTenthIsTenth = RealFunctions::ToReal("0.1") == 0.1; out naturalFromInteger = IntegerFunctions::ToNatural(9); out integerFromReal = RealFunctions::ToInteger(2.7); out integerFromNegativeReal = RealFunctions::ToInteger(-2.7); diff --git a/internal/exec/runtime/testdata/conformance/instance_real_binding_meets_rational.expected.json b/internal/exec/runtime/testdata/conformance/instance_real_binding_meets_rational.expected.json new file mode 100644 index 0000000000..8a1feb2ed8 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/instance_real_binding_meets_rational.expected.json @@ -0,0 +1,17 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::sample", + "validation": { + "verdicts": [ + {"kind": "constraint", "assertion": "assert constraint boundTenth", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint boundThird", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint tenthNeitherAboveNorBelow", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint thirdAtRealPrecision", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint rationalsExact", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint beyondRange", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint integerExact", "status": "holds"} + ], + "valid": true + } +} diff --git a/internal/exec/runtime/testdata/conformance/instance_real_binding_meets_rational.sysml b/internal/exec/runtime/testdata/conformance/instance_real_binding_meets_rational.sysml new file mode 100644 index 0000000000..284ab05682 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/instance_real_binding_meets_rational.sysml @@ -0,0 +1,23 @@ +// A Real feature bound to a Rational holds its nearest binary64, and a Rational +// meets a binary64 Real at Real precision, as mixed arithmetic does, so the +// binding's equality holds (KerML 1.0 §7.4.9). Rationals compare with each other +// exactly, an Integer with a Real exactly, and a Rational past the binary64 +// range compares as an infinity. +package test { + private import ScalarValues::*; + + part def Sample { + attribute x : Real = 0.1; + attribute y : Real = 1 / 3; + attribute big : Real = 1.0e308; + assert constraint boundTenth { x == 0.1 } + assert constraint boundThird { y == 1 / 3 } + assert constraint tenthNeitherAboveNorBelow { not (x > 0.1) and not (x < 0.1) and x <= 0.1 and x >= 0.1 and not (x != 0.1) } + assert constraint thirdAtRealPrecision { RationalFunctions::rat(1, 3) == 1.0 / 3.0 and RationalFunctions::rat(1, 3) == RealFunctions::ToReal("1") / 3.0 } + assert constraint rationalsExact { RationalFunctions::rat(1, 3) != RationalFunctions::rat(6004799503160661, 18014398509481984) } + assert constraint beyondRange { 1.0e400 > big and 1.0e400 != big and -1.0e400 < -big } + assert constraint integerExact { 9007199254740993 > RealFunctions::ToReal("9007199254740992") } + } + + part sample : Sample; +} diff --git a/internal/exec/runtime/value_equality.go b/internal/exec/runtime/value_equality.go index 68e110901d..68473e41d9 100644 --- a/internal/exec/runtime/value_equality.go +++ b/internal/exec/runtime/value_equality.go @@ -44,17 +44,14 @@ func (ctx *Context) valueKey(v Value) valueKey { case ValConst: switch v.Const.Kind { case semantics.ValInt, semantics.ValRational, semantics.ValReal: - if n, ok := v.Const.WholeNumber(); ok { + // Every number keys as its nearest binary64, where a Rational equals a Real. + r := semantics.Value{Kind: semantics.ValReal, Real: v.Const.AsReal()} + if n, ok := r.WholeNumber(); ok { key.intVal = n - } else if n, ok := wholeBeyondInt64(v.Const); ok { + } else if n, ok := wholeBeyondInt64(r); ok { key.bigVal = n.FormatInt() - } else if v.Const.Kind != semantics.ValRational { - key.realVal = v.Const.Real - } else if f := v.Const.AsReal(); semantics.CompareExactReal(v.Const, f) == 0 { - // A Rational a binary64 holds exactly equals that Real and shares its key. - key.realVal = f } else { - key.bigVal = v.Const.FormatRational() + key.realVal = r.Real } case semantics.ValBool: key.boolVal = v.Const.Bool diff --git a/internal/exec/solve/replay.go b/internal/exec/solve/replay.go index b2a39d3e0b..05e74b6377 100644 --- a/internal/exec/solve/replay.go +++ b/internal/exec/solve/replay.go @@ -423,16 +423,16 @@ func (v replayValue) asReal() float64 { return v.f } -// compareNumbers orders two numbers by exact value, a binary64 by the exact -// value it holds, as the evaluator's mixed comparison does. +// compareNumbers orders two numbers as the evaluator's comparison does: exact +// values exactly, a Rational against a binary64 at Real precision. func compareNumbers(a, b replayValue) int { switch { case a.isExact() && b.isExact(): return semantics.CompareRat(a.i, b.i) case a.isExact(): - return semantics.CompareExactReal(a.i, b.f) + return semantics.CompareReal(a.i, b.f) case b.isExact(): - return -semantics.CompareExactReal(b.i, a.f) + return -semantics.CompareReal(b.i, a.f) case a.f < b.f: return -1 case a.f > b.f: diff --git a/internal/frontend/engine/values.go b/internal/frontend/engine/values.go index dfbbd0a3ec..052785ade9 100644 --- a/internal/frontend/engine/values.go +++ b/internal/frontend/engine/values.go @@ -411,7 +411,7 @@ var ( errBigIntegerNotDecimal = errors.New("big Integer is not decimal") - errRationalNotCanonical = errors.New("rational is not canonical: not in lowest terms, or exactly a double") + errRationalNotLowestTerms = errors.New("rational is not in lowest terms over a positive denominator") errVectorQuantityEmpty = errors.New("vector quantity has no components") @@ -516,12 +516,12 @@ func rationalToProto(v semantics.Value) *JRational { return &JRational{Numerator: v.RatNumer().FormatInt(), Denominator: v.RatDenom().FormatInt()} } -// protoToRational reads a rationalValue or rationalMagnitude, refusing one not -// in lowest terms or one a double holds exactly, which crosses as realValue. +// protoToRational reads a rationalValue or rationalMagnitude in lowest terms over +// a positive denominator, one a double holds exactly included. func protoToRational(pr *JRational) (semantics.Value, error) { - v, ok := semantics.CanonicalRational(pr.Numerator, pr.Denominator) + v, ok := semantics.LowestTermsRational(pr.Numerator, pr.Denominator) if !ok { - return semantics.Value{}, fmt.Errorf("%w: %q/%q", errRationalNotCanonical, pr.Numerator, pr.Denominator) + return semantics.Value{}, fmt.Errorf("%w: %q/%q", errRationalNotLowestTerms, pr.Numerator, pr.Denominator) } return v, nil } diff --git a/internal/frontend/grpc/convert_rational_test.go b/internal/frontend/grpc/convert_rational_test.go index b3477f9135..0f2b9671e1 100644 --- a/internal/frontend/grpc/convert_rational_test.go +++ b/internal/frontend/grpc/convert_rational_test.go @@ -85,18 +85,38 @@ func TestRationalQuantityMagnitudeCrossesExactly(t *testing.T) { } } -// Each Rational has one encoding: a fraction not in lowest terms, over a -// non-positive denominator, or one a double holds exactly is refused. -func TestRationalValueReadsOnlyCanonicalTerms(t *testing.T) { +// An inbound rational_value is in lowest terms over a positive denominator: a +// fraction that is not is refused. +func TestRationalValueReadsOnlyLowestTerms(t *testing.T) { idx := symbols.NewIndex() - for _, terms := range [][2]string{{"2", "6"}, {"1", "-3"}, {"-1", "-3"}, {"1", "0"}, {"1", "2"}, {"3", "1"}, {"0", "1"}, {"1.5", "7"}, {"", "3"}, {"1", ""}} { + for _, terms := range [][2]string{{"2", "6"}, {"1", "-3"}, {"-1", "-3"}, {"1", "0"}, {"0", "2"}, {"4", "2"}, {"1.5", "7"}, {"", "3"}, {"1", ""}} { pv := &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: &pb.Rational{Numerator: terms[0], Denominator: terms[1]}}} - if _, err := protoconv.ProtoToValueIn(pv, idx, nil); !errors.Is(err, protoconv.ErrRationalNotCanonical) { - t.Errorf("rational_value %s/%s: err = %v, want %v", terms[0], terms[1], err, protoconv.ErrRationalNotCanonical) + if _, err := protoconv.ProtoToValueIn(pv, idx, nil); !errors.Is(err, protoconv.ErrRationalNotLowestTerms) { + t.Errorf("rational_value %s/%s: err = %v, want %v", terms[0], terms[1], err, protoconv.ErrRationalNotLowestTerms) } } } +// A client sends every exact Rational as rational_value, so one a double holds +// exactly reads as that Rational, not as a Real; real_value always reads as a Real. +func TestBinaryExactRationalValueReadsAsARational(t *testing.T) { + idx := symbols.NewIndex() + for _, terms := range [][2]string{{"1", "2"}, {"3", "1"}, {"0", "1"}, {"-3", "8"}} { + pv := &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: &pb.Rational{Numerator: terms[0], Denominator: terms[1]}}} + got, err := protoconv.ProtoToValueIn(pv, idx, nil) + if err != nil { + t.Fatalf("rational_value %s/%s: %v", terms[0], terms[1], err) + } + if want := ratConst(t, terms[0]+"/"+terms[1]); got.Const.Kind != semantics.ValRational || semantics.CompareRat(got.Const, want.Const) != 0 { + t.Errorf("rational_value %s/%s read as %s", terms[0], terms[1], runtime.FormatValue(got)) + } + } + got, err := protoconv.ProtoToValueIn(&pb.Value{Kind: &pb.Value_RealValue{RealValue: 0.5}}, idx, nil) + if err != nil || got.Const.Kind != semantics.ValReal || got.Const.Real != 0.5 { + t.Errorf("real_value 0.5 read as %s (err %v), want the Real 0.5", runtime.FormatValue(got), err) + } +} + const rationalWireModel = ` package R { private import ScalarValues::*; @@ -128,6 +148,7 @@ func TestEvaluateCalcTakesAndReturnsRationals(t *testing.T) { }{ {arg: &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: &pb.Rational{Numerator: "1", Denominator: "10"}}}, num: "1", den: "30"}, {arg: intValue(1), num: "1", den: "3"}, + {arg: &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: &pb.Rational{Numerator: "1", Denominator: "2"}}}, num: "1", den: "6"}, } { resp, err := srv.EvaluateCalc(context.Background(), &pb.EvaluateCalcRequest{ ModelHash: hash, diff --git a/internal/frontend/protoconv/convert.go b/internal/frontend/protoconv/convert.go index 65e4691d10..8461a5647b 100644 --- a/internal/frontend/protoconv/convert.go +++ b/internal/frontend/protoconv/convert.go @@ -361,12 +361,12 @@ func RationalToProto(v semantics.Value) *pb.Rational { return &pb.Rational{Numerator: v.RatNumer().FormatInt(), Denominator: v.RatDenom().FormatInt()} } -// ProtoToRational reads a Rational, refusing one not in lowest terms or one a -// double holds exactly, which crosses as real_value. +// ProtoToRational reads a Rational in lowest terms over a positive denominator, +// one a double holds exactly included: a client sends every exact Rational so. func ProtoToRational(pr *pb.Rational) (semantics.Value, error) { - v, ok := semantics.CanonicalRational(pr.GetNumerator(), pr.GetDenominator()) + v, ok := semantics.LowestTermsRational(pr.GetNumerator(), pr.GetDenominator()) if !ok { - return semantics.Value{}, fmt.Errorf("%w: %q/%q", ErrRationalNotCanonical, pr.GetNumerator(), pr.GetDenominator()) + return semantics.Value{}, fmt.Errorf("%w: %q/%q", ErrRationalNotLowestTerms, pr.GetNumerator(), pr.GetDenominator()) } return v, nil } @@ -443,9 +443,9 @@ var ( // that is not an optionally signed run of decimal digits. ErrBigIntegerNotDecimal = errors.New("big Integer is not decimal") - // ErrRationalNotCanonical reports a rational_value or rational_magnitude - // that is not in lowest terms, or that a double holds exactly. - ErrRationalNotCanonical = errors.New("rational is not canonical: not in lowest terms, or exactly a double") + // ErrRationalNotLowestTerms reports a rational_value or rational_magnitude + // that is not in lowest terms over a positive denominator. + ErrRationalNotLowestTerms = errors.New("rational is not in lowest terms over a positive denominator") // ErrVectorQuantityEmpty reports a vector quantity of no components, whose // num is Number[1..*]. @@ -651,6 +651,64 @@ func DocumentValueHoldsRational(dv *pb.DocumentValue) bool { return false } +// RationalsAsReals rewrites in place each rational arm of pv, nested ones +// included, that a double holds exactly as that double: the form a service +// without rational_values reads. A Rational no double holds is left as it is. +func RationalsAsReals(pv *pb.Value) { + switch k := pv.GetKind().(type) { + case *pb.Value_RationalValue: + if f, ok := binaryRational(k.RationalValue); ok { + pv.Kind = &pb.Value_RealValue{RealValue: f} + } + case *pb.Value_Quantity: + quantityRationalAsReal(k.Quantity) + case *pb.Value_VectorQuantity: + for _, q := range k.VectorQuantity.GetComponents() { + quantityRationalAsReal(q) + } + case *pb.Value_TensorQuantity: + for _, q := range k.TensorQuantity.GetComponents() { + quantityRationalAsReal(q) + } + case *pb.Value_EnumLiteral: + RationalsAsReals(k.EnumLiteral.GetValue()) + } + for _, nested := range NestedValues(pv) { + RationalsAsReals(nested) + } +} + +// DocumentRationalAsReal is RationalsAsReals for a document value. +func DocumentRationalAsReal(dv *pb.DocumentValue) { + switch k := dv.GetKind().(type) { + case *pb.DocumentValue_RationalValue: + if f, ok := binaryRational(k.RationalValue); ok { + dv.Kind = &pb.DocumentValue_RealValue{RealValue: f} + } + case *pb.DocumentValue_Quantity: + quantityRationalAsReal(k.Quantity) + case *pb.DocumentValue_Event: + DocumentRationalAsReal(k.Event.GetTime()) + } +} + +func quantityRationalAsReal(q *pb.Quantity) { + if m, ok := q.GetMagnitude().(*pb.Quantity_RationalMagnitude); ok { + if f, ok := binaryRational(m.RationalMagnitude); ok { + q.Magnitude = &pb.Quantity_RealMagnitude{RealMagnitude: f} + } + } +} + +// binaryRational is the double a lowest-terms Rational is exactly, if any. +func binaryRational(pr *pb.Rational) (float64, bool) { + v, ok := semantics.LowestTermsRational(pr.GetNumerator(), pr.GetDenominator()) + if !ok { + return 0, false + } + return v.BinaryExact() +} + func isRationalValue(pv *pb.Value) bool { _, ok := pv.GetKind().(*pb.Value_RationalValue) return ok diff --git a/internal/frontend/repl/compile_test.go b/internal/frontend/repl/compile_test.go index 880afe0cb9..dd3329ec40 100644 --- a/internal/frontend/repl/compile_test.go +++ b/internal/frontend/repl/compile_test.go @@ -38,6 +38,7 @@ var compiledCases = []compiledCase{ {"Logic", []string{"true", "true", "0"}}, {"Logic", []string{"true", "true", "5"}}, {"ExactRational", []string{"6", "0.1"}}, {"ExactRational", []string{"5", "0.0"}}, {"ExactRational", []string{"7", "0.09"}}, {"ExactRational", []string{"-6", "-1e308"}}, {"ExactRational", []string{"6", "0.1000000000000001"}}, + {"RealAgainstNearest", []string{"0.1"}}, {"RealAgainstNearest", []string{"0.1000000000000001"}}, {"RealAgainstNearest", []string{"0.09999999999999999"}}, {"RealAgainstNearest", []string{"-0.1"}}, {"RealAgainstNearest", []string{"1e308"}}, {"RealAgainstNearest", []string{"-1e308"}}, {"RealAgainstNearest", []string{"0.0"}}, {"ExactWide", []string{"3"}}, {"ExactWide", []string{"-3"}}, {"ExactWide", []string{"0"}}, {"Compare", []string{"2.0", "2"}}, {"Compare", []string{"2.5", "2"}}, {"Compare", []string{"0.1", "1"}}, {"Sign", []string{"-9"}}, {"Sign", []string{"0"}}, {"Sign", []string{"3"}}, @@ -560,6 +561,7 @@ func TestCompileRefusesWhatItCannotCompile(t *testing.T) { {"ExactProduct", "exact Rational arithmetic '*' over a value binary64 does not hold exactly"}, {"ExactPower", "'**' of an exact Rational by an Integer exponent"}, {"ExactCompare", "'<' of an exact Rational binary64 does not hold exactly"}, + {"BeyondCompare", "literal 1e400 is outside the Real range"}, {"Extent", "operator 'all'"}, {"RealIntIdentity", "'===' between Real and Integer"}, {"SelectNonBoolean", "select whose body yields Integer, not a Boolean"}, diff --git a/internal/frontend/repl/testdata/compile_calcs.sysml b/internal/frontend/repl/testdata/compile_calcs.sysml index 947b9ef7b9..4fdaf78b49 100644 --- a/internal/frontend/repl/testdata/compile_calcs.sysml +++ b/internal/frontend/repl/testdata/compile_calcs.sysml @@ -69,6 +69,20 @@ package Compiled { in x : Real; return : Boolean = 0.1 + 0.2 == 0.3 and 1 / 3 * 3 == 1 and a / 3 >= 2 and a * 1.0 + a == 2 * a and x < 0.1; } + // A Real meets a Rational literal at Real precision, the literal rounded once. + calc def RealAgainstNearest { + in x : Real; + attribute r : Integer = 0; + if x < 0.1 { assign r := r + 1; } + if x <= 0.1 { assign r := r + 2; } + if x > 0.1 { assign r := r + 4; } + if x >= 0.1 { assign r := r + 8; } + if x == 0.1 { assign r := r + 16; } + if x != 0.1 { assign r := r + 32; } + if -0.1 < x { assign r := r + 64; } + if -0.1 >= x { assign r := r + 128; } + return : Integer = r; + } calc def ExactWide { in a : Integer; return : Real = a * 2.0 + 0.5; } calc def Compare { @@ -555,6 +569,7 @@ package Refused { calc def ExactProduct { in a : Integer; return : Real = a * 0.1; } calc def ExactPower { in a : Integer; return : Real = (a * 0.5) ** 2; } calc def ExactCompare { in a : Integer; return : Boolean = a / 3 < 0.1; } + calc def BeyondCompare { in x : Real; return : Boolean = x < 1e400; } calc def EnumParam { in c : Color; return : Boolean = c == Color::red; } calc def RealIntIdentity { in x : Real; in y : Integer; return : Boolean = x === y; } calc def SelectNonBoolean { in x : Integer[0..*]; return : Integer[0..*] = x->select {in v; v}; } diff --git a/internal/semantic/semantics/eval.go b/internal/semantic/semantics/eval.go index d075d3fa9c..1631d68ec3 100644 --- a/internal/semantic/semantics/eval.go +++ b/internal/semantic/semantics/eval.go @@ -558,9 +558,9 @@ func evalEquality(op ast.OperatorKind, l, r Value) (Value, bool) { case l.IsExact() && r.IsExact(): eq = CompareRat(l, r) == 0 case l.IsExact() && r.Kind == ValReal: - eq = !math.IsNaN(r.Real) && CompareExactReal(l, r.Real) == 0 + eq = !math.IsNaN(r.Real) && CompareReal(l, r.Real) == 0 case l.Kind == ValReal && r.IsExact(): - eq = !math.IsNaN(l.Real) && CompareExactReal(r, l.Real) == 0 + eq = !math.IsNaN(l.Real) && CompareReal(r, l.Real) == 0 case l.IsNumeric() && r.IsNumeric(): eq = l.AsReal() == r.AsReal() default: @@ -596,11 +596,11 @@ func evalComparison(op ast.OperatorKind, l, r Value) (Value, bool) { return Value{Kind: ValBool, Bool: res}, true } if l.IsExact() && r.Kind == ValReal && !math.IsNaN(r.Real) { - res, _ := OrderSatisfies(op, CompareExactReal(l, r.Real)) + res, _ := OrderSatisfies(op, CompareReal(l, r.Real)) return Value{Kind: ValBool, Bool: res}, true } if l.Kind == ValReal && r.IsExact() && !math.IsNaN(l.Real) { - res, _ := OrderSatisfies(op, -CompareExactReal(r, l.Real)) + res, _ := OrderSatisfies(op, -CompareReal(r, l.Real)) return Value{Kind: ValBool, Bool: res}, true } lf, rf := l.AsReal(), r.AsReal() diff --git a/internal/semantic/semantics/rational.go b/internal/semantic/semantics/rational.go index d6f0e01a11..12a2fe8527 100644 --- a/internal/semantic/semantics/rational.go +++ b/internal/semantic/semantics/rational.go @@ -1,6 +1,7 @@ package semantics import ( + "cmp" "errors" "fmt" "math" @@ -307,20 +308,13 @@ func smallRatArith(op ast.OperatorKind, an, ad, bn, bd int64) (Value, bool) { return Value{}, false } -// CompareExactReal orders the exact number v against the non-NaN Real r -// exactly, neither rounded to the other. -func CompareExactReal(v Value, r float64) int { +// CompareReal orders the exact number v against the non-NaN Real r at Real +// precision: an Integer exactly, a Rational as its nearest binary64 (±Inf past the range). +func CompareReal(v Value, r float64) int { if v.Kind == ValInt { return CompareIntReal(v, r) } - switch { - case math.IsInf(r, 1): - return -1 - case math.IsInf(r, -1): - return 1 - } - x, _ := RationalOfReal(r) - return CompareRat(v, x) + return cmp.Compare(ratToFloat(v), r) } // RatNeg is the negation of the exact number v, of v's kind. @@ -634,11 +628,9 @@ func (v Value) BinaryExact() (float64, bool) { return f, exact && !math.IsInf(f, 0) } -// CanonicalRational reads the Rational a wire encoding spells as two decimals: -// a numerator over a positive denominator sharing no factor with it, and no -// binary64 holds exactly, which crosses as a double instead; so no Rational has -// two spellings. -func CanonicalRational(numerator, denominator string) (Value, bool) { +// LowestTermsRational reads the Rational two decimals spell: a numerator over a +// positive denominator sharing no factor with it, which every Rational has once. +func LowestTermsRational(numerator, denominator string) (Value, bool) { num, okNum := ParseInteger(numerator) den, okDen := ParseInteger(denominator) if !okNum || !okDen || CompareInt(den, IntValue(1)) < 0 { @@ -648,7 +640,17 @@ func CanonicalRational(numerator, denominator string) (Value, bool) { if new(big.Int).GCD(nil, nil, new(big.Int).Abs(n), d).Cmp(big.NewInt(1)) != 0 { return Value{}, false } - v := RatValue(new(big.Rat).SetFrac(n, d)) + return RatValue(new(big.Rat).SetFrac(n, d)), true +} + +// CanonicalRational is LowestTermsRational refusing a Rational a binary64 holds +// exactly, which the canonical wire form carries as a double; so no Rational a +// service sends has two spellings. +func CanonicalRational(numerator, denominator string) (Value, bool) { + v, ok := LowestTermsRational(numerator, denominator) + if !ok { + return Value{}, false + } if _, exact := v.BinaryExact(); exact { return Value{}, false } diff --git a/internal/semantic/semantics/rational_test.go b/internal/semantic/semantics/rational_test.go index 7b9d678de8..6fb1a084e3 100644 --- a/internal/semantic/semantics/rational_test.go +++ b/internal/semantic/semantics/rational_test.go @@ -160,9 +160,15 @@ func TestRationalComparisonAndRounding(t *testing.T) { if CompareRat(frac(t, math.MaxInt64, 3), frac(t, math.MaxInt64-1, 3)) <= 0 { t.Error("order of terms whose cross products overflow int64") } - // The double nearest 1/10 lies above it. - if CompareExactReal(tenth, 0.1) != -1 || CompareExactReal(frac(t, 1, 2), 0.5) != 0 || CompareExactReal(tenth, math.Inf(1)) != -1 || CompareExactReal(tenth, math.Inf(-1)) != 1 { - t.Error("exact order against a Real") + // A Rational meets a Real as its nearest binary64, ±Inf past the range; an Integer exactly. + huge, _ := ParseRational("1e400", 4096) + if CompareReal(tenth, 0.1) != 0 || CompareReal(frac(t, 1, 3), 1.0/3.0) != 0 || CompareReal(frac(t, 1, 2), 0.5) != 0 || + CompareReal(tenth, math.Inf(1)) != -1 || CompareReal(tenth, math.Inf(-1)) != 1 || + CompareReal(huge, math.MaxFloat64) != 1 || CompareReal(huge, math.Inf(1)) != 0 || CompareReal(RatNeg(huge), math.Inf(-1)) != 0 { + t.Error("order of a Rational against a Real") + } + if CompareReal(intVal(1<<53+1), 1<<53) != 1 { + t.Error("an Integer orders against a Real exactly") } for _, c := range []struct { v Value diff --git a/internal/translate/codegen/rational.go b/internal/translate/codegen/rational.go index 92c7be419a..357ced9f6f 100644 --- a/internal/translate/codegen/rational.go +++ b/internal/translate/codegen/rational.go @@ -267,8 +267,7 @@ func exactWiden(x Expr) Expr { } // rationalComparison checks the comparison or equality b over Real operands. -// A Real against a Rational literal binary64 does not hold compares exactly -// through the literal's nearest binary64 and the side the Rational lies on. +// A Real against a Rational literal compares with the literal's nearest binary64. func (fc *funcCompiler) rationalComparison(b Binary) (Expr, error) { if b.L.Type() != TypeReal || b.R.Type() != TypeReal { return b, nil @@ -297,10 +296,10 @@ func (fc *funcCompiler) rationalComparison(b Binary) (Expr, error) { } } if c, ok := constRat(b.R); ok && l == binary64 { - return tieBroken(b.Op, b.L, c), nil + return againstNearest(b.Op, b.L, c), nil } if c, ok := constRat(b.L); ok && r == binary64 { - return tieBroken(flipped(b.Op), b.R, c), nil + return againstNearest(flipped(b.Op), b.R, c), nil } return nil, fc.unsupported(fmt.Sprintf("'%s' of an exact Rational binary64 does not hold exactly (compiled code holds Rationals as binary64)", b.Op)) } @@ -345,33 +344,10 @@ func flipped(op ast.OperatorKind) ast.OperatorKind { return op } -// tieBroken is `x op r` for a finite binary64 x and a Rational r binary64 does -// not hold: no binary64 lies strictly between r and its nearest d, so x orders -// against r as against d except at x == d, where it falls on r's side of d. -func tieBroken(op ast.OperatorKind, x Expr, r *big.Rat) Expr { +// againstNearest is `x op r` at Real precision: x against the binary64 nearest +// r, which is finite since a literal or fold past the range is refused. +func againstNearest(op ast.OperatorKind, x Expr, r *big.Rat) Expr { d, _ := r.Float64() - if math.IsInf(d, 0) { - d = math.Copysign(math.MaxFloat64, d) - } - above := r.Cmp(new(big.Rat).SetFloat64(d)) > 0 - switch op { - case ast.OpLt, ast.OpLe: - if above { - op = ast.OpLe - } else { - op = ast.OpLt - } - case ast.OpGt, ast.OpGe: - if above { - op = ast.OpGt - } else { - op = ast.OpGe - } - case ast.OpEq: - op, d = ast.OpLt, -math.MaxFloat64 - default: - op, d = ast.OpGe, -math.MaxFloat64 - } return Binary{Op: op, L: x, R: RealLit{Value: d}, T: TypeBool} } diff --git a/tests/grpc/conformance_test.go b/tests/grpc/conformance_test.go index 9ce0e31dba..4dcaea45c8 100644 --- a/tests/grpc/conformance_test.go +++ b/tests/grpc/conformance_test.go @@ -752,6 +752,13 @@ func toProtoValue(t *testing.T, ev expectedValue) *pb.Value { return &pb.Value{Kind: &pb.Value_IntValue{IntValue: int64(mustFloat(t, ev))}} case "real_value": return &pb.Value{Kind: &pb.Value_RealValue{RealValue: mustFloat(t, ev)}} + case "rational_value": + text, _ := ev.Value.(string) + num, den, ok := strings.Cut(text, "/") + if !ok { + t.Fatalf("rational_value: %v is not \"/\"", ev.Value) + } + return &pb.Value{Kind: &pb.Value_RationalValue{RationalValue: &pb.Rational{Numerator: num, Denominator: den}}} case "bool_value": b, ok := ev.Value.(bool) if !ok { @@ -813,6 +820,8 @@ func describeValue(v *pb.Value) (string, interface{}) { return "int_value", k.IntValue case *pb.Value_RealValue: return "real_value", k.RealValue + case *pb.Value_RationalValue: + return "rational_value", k.RationalValue.GetNumerator() + "/" + k.RationalValue.GetDenominator() case *pb.Value_BoolValue: return "bool_value", k.BoolValue case *pb.Value_StringValue: diff --git a/tests/grpc/testdata/conformance/README.md b/tests/grpc/testdata/conformance/README.md index b2c5886ab6..1061532e8e 100644 --- a/tests/grpc/testdata/conformance/README.md +++ b/tests/grpc/testdata/conformance/README.md @@ -52,9 +52,10 @@ A feature value's `error` is a substring its `FeatureValue.error` must contain; error must carry none. A value is `{"kind": , "value": }`, where `kind` is one of -`int_value`, `real_value`, `bool_value`, `string_value`, `instance_id`, `quantity`, `null` or -`unset` — the last being a materialized feature value holding no value, as a valueless feature of a -value type does. The assertion checks the oneof arm as well as the payload, so a value returned +`int_value`, `real_value`, `rational_value`, `bool_value`, `string_value`, `instance_id`, +`quantity`, `null` or `unset` — the last being a materialized feature value holding no value, as a valueless feature of a +value type does. A `rational_value`'s literal is the string `"/"`, sent +as written and answered in lowest terms. The assertion checks the oneof arm as well as the payload, so a value returned with the wrong type fails; `instance_id`, `null` and `unset` assert the arm only, since instance ids are assigned at runtime. diff --git a/tests/grpc/testdata/conformance/evaluate_calc_rational_wire.expected.json b/tests/grpc/testdata/conformance/evaluate_calc_rational_wire.expected.json new file mode 100644 index 0000000000..ee5cdd17b4 --- /dev/null +++ b/tests/grpc/testdata/conformance/evaluate_calc_rational_wire.expected.json @@ -0,0 +1,8 @@ +{ + "rpc": "EvaluateCalc", + "symbol_id": "test::third", + "arguments": [ + {"kind": "rational_value", "value": "1/4"} + ], + "expected_result": {"kind": "rational_value", "value": "1/12"} +} diff --git a/tests/grpc/testdata/conformance/evaluate_calc_rational_wire.sysml b/tests/grpc/testdata/conformance/evaluate_calc_rational_wire.sysml new file mode 100644 index 0000000000..c92b668384 --- /dev/null +++ b/tests/grpc/testdata/conformance/evaluate_calc_rational_wire.sysml @@ -0,0 +1,9 @@ +package test { + private import ScalarValues::*; + + calc def Third { + in x : Rational; + return : Rational = x / 3; + } + calc third : Third; +} diff --git a/tests/grpc/testdata/conformance/evaluate_calc_rational_wire_canonical_answer.expected.json b/tests/grpc/testdata/conformance/evaluate_calc_rational_wire_canonical_answer.expected.json new file mode 100644 index 0000000000..6aef1156a5 --- /dev/null +++ b/tests/grpc/testdata/conformance/evaluate_calc_rational_wire_canonical_answer.expected.json @@ -0,0 +1,8 @@ +{ + "rpc": "EvaluateCalc", + "symbol_id": "test::twice", + "arguments": [ + {"kind": "rational_value", "value": "1/4"} + ], + "expected_result": {"kind": "real_value", "value": 0.5} +} diff --git a/tests/grpc/testdata/conformance/evaluate_calc_rational_wire_canonical_answer.sysml b/tests/grpc/testdata/conformance/evaluate_calc_rational_wire_canonical_answer.sysml new file mode 100644 index 0000000000..24f716f81c --- /dev/null +++ b/tests/grpc/testdata/conformance/evaluate_calc_rational_wire_canonical_answer.sysml @@ -0,0 +1,9 @@ +package test { + private import ScalarValues::*; + + calc def Twice { + in x : Rational; + return : Rational = x * 2; + } + calc twice : Twice; +} diff --git a/tests/grpc/testdata/conformance/execute_action_rational_input_meets_rational.expected.json b/tests/grpc/testdata/conformance/execute_action_rational_input_meets_rational.expected.json new file mode 100644 index 0000000000..b235fca158 --- /dev/null +++ b/tests/grpc/testdata/conformance/execute_action_rational_input_meets_rational.expected.json @@ -0,0 +1,10 @@ +{ + "rpc": "ExecuteAction", + "symbol_id": "test::addThird", + "inputs": { + "x": {"kind": "rational_value", "value": "1/4"} + }, + "expected_outputs": { + "y": {"kind": "rational_value", "value": "7/12"} + } +} diff --git a/tests/grpc/testdata/conformance/execute_action_rational_input_meets_rational.sysml b/tests/grpc/testdata/conformance/execute_action_rational_input_meets_rational.sysml new file mode 100644 index 0000000000..d11f3b62a4 --- /dev/null +++ b/tests/grpc/testdata/conformance/execute_action_rational_input_meets_rational.sysml @@ -0,0 +1,14 @@ +package test { + private import ScalarValues::*; + + action addThird { + in x; + out y; + first start; + action inner { + assign y := x + 1 / 3; + } + then done; + succession first start then inner; + } +} diff --git a/tests/grpc/testdata/conformance/execute_action_real_input_meets_rational.expected.json b/tests/grpc/testdata/conformance/execute_action_real_input_meets_rational.expected.json new file mode 100644 index 0000000000..146cdb4df7 --- /dev/null +++ b/tests/grpc/testdata/conformance/execute_action_real_input_meets_rational.expected.json @@ -0,0 +1,10 @@ +{ + "rpc": "ExecuteAction", + "symbol_id": "test::addThird", + "inputs": { + "x": {"kind": "real_value", "value": 0.25} + }, + "expected_outputs": { + "y": {"kind": "real_value", "value": 0.5833333333333333} + } +} diff --git a/tests/grpc/testdata/conformance/execute_action_real_input_meets_rational.sysml b/tests/grpc/testdata/conformance/execute_action_real_input_meets_rational.sysml new file mode 100644 index 0000000000..d11f3b62a4 --- /dev/null +++ b/tests/grpc/testdata/conformance/execute_action_real_input_meets_rational.sysml @@ -0,0 +1,14 @@ +package test { + private import ScalarValues::*; + + action addThird { + in x; + out y; + first start; + action inner { + assign y := x + 1 / 3; + } + then done; + succession first start then inner; + } +} From 1a9e09faaf14f9248c3ccce5bea4f0e280f7ee1c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:08:44 +0000 Subject: [PATCH 15/26] fix(client/python): keep the stubs' generated gRPC version at the pinned 1.83.0 Co-Authored-By: jason.han --- client/python/opensysml/proto/sysml_pb2_grpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/python/opensysml/proto/sysml_pb2_grpc.py b/client/python/opensysml/proto/sysml_pb2_grpc.py index b03ac7c4f4..b3ec296a3f 100644 --- a/client/python/opensysml/proto/sysml_pb2_grpc.py +++ b/client/python/opensysml/proto/sysml_pb2_grpc.py @@ -5,7 +5,7 @@ from . import sysml_pb2 as sysml__pb2 -GRPC_GENERATED_VERSION = '1.84.0' +GRPC_GENERATED_VERSION = '1.83.0' GRPC_VERSION = grpc.__version__ _version_not_supported = False From 5305f7fedd32d47209a6a7bf82b576f74c7251be Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:05:08 +0000 Subject: [PATCH 16/26] fix(semantics): budget short decimals, round Rationals met by Reals in clients, hold RealFunctions quantity magnitudes as Reals Co-Authored-By: jason.han --- .../java/org/openmbee/opensysml/Value.java | 11 +++++-- .../openmbee/opensysml/PublicTypesTest.java | 14 ++++++++ .../internal/RationalProtosTest.java | 2 +- client/julia/OpenSysML/src/values.jl | 5 +++ client/julia/OpenSysML/test/runtests.jl | 2 ++ client/node/src/core/values.ts | 7 ++++ client/node/test/rational.test.ts | 5 +-- client/node/test/values.test.ts | 9 +++++ client/rust/opensysml/src/domain.rs | 18 +++++++++- client/rust/opensysml/src/encode.rs | 2 +- internal/exec/runtime/collections.go | 3 +- internal/exec/runtime/collections_test.go | 11 +++++++ .../runtime/robustness_exact_rational_test.go | 33 +++++++++++++++++++ internal/semantic/semantics/rational.go | 12 +++++-- internal/semantic/semantics/rational_test.go | 5 +++ 15 files changed, 128 insertions(+), 11 deletions(-) diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java index 2b44e8748a..79feae4fb3 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java @@ -822,11 +822,18 @@ private static Number onRealAxis(Value value) { } private static boolean magnitudesEqual(Number a, Number b) { + if (a instanceof Rational && b instanceof Rational) { + return a.equals(b); + } + if (a instanceof Rational && b instanceof Double || a instanceof Double && b instanceof Rational) { + // A Rational meets a Real at Real precision, as the service compares them. + return a.doubleValue() == b.doubleValue(); + } a = a instanceof Rational rational && rational.isBinary64() ? rational.doubleValue() : a; b = b instanceof Rational rational && rational.isBinary64() ? rational.doubleValue() : b; if (a instanceof Rational || b instanceof Rational) { - // A rational no double holds is never whole, so it equals only the same rational. - return a.equals(b); + // A rational no double holds is never whole, so it is no Integer. + return false; } if (a instanceof BigInteger x) { return b instanceof BigInteger y ? x.equals(y) : b instanceof Double r && realIsBig(r, x); diff --git a/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java b/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java index 2acea8a620..91d7866b1f 100644 --- a/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java +++ b/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java @@ -207,6 +207,20 @@ void numbersNestedInCollectionsAreJudgedAsTheModelDoes() { assertTrue(new Value.NullValue().sameValue(new Value.NullValue())); } + @Test + void aRationalMeetsARealAtRealPrecision() { + Value third = new Value.RationalValue(Rational.of(1, 3)); + assertTrue(third.sameValue(new Value.RealValue(1.0 / 3.0))); + assertTrue(new Value.RealValue(1.0 / 3.0).sameValue(third)); + assertFalse(third.sameValue(new Value.RealValue(0.3333))); + assertFalse( + third.sameValue( + new Value.RationalValue(Rational.of(6004799503160661L, 18014398509481984L)))); + assertThrows( + IllegalArgumentException.class, + () -> new Value.SetValue(List.of(third, new Value.RealValue(1.0 / 3.0)))); + } + @Test void aNullAndTheEmptyCollectionsAreOneMember() { Value one = new Value.IntegerValue(1); diff --git a/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java b/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java index d71ac44c9b..236c66c1f5 100644 --- a/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java +++ b/client/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/RationalProtosTest.java @@ -63,7 +63,7 @@ void aRationalIsReadAndWrittenExactly() { assertEquals(proto, Protos.proto(read)); assertEquals(1.0 / 3.0, read.asDouble()); assertTrue(read.sameValue(new Value.RationalValue(Rational.of(2, 6)))); - assertFalse(read.sameValue(new Value.RealValue(1.0 / 3.0))); + assertTrue(read.sameValue(new Value.RealValue(1.0 / 3.0))); assertFalse(read.sameValue(new Value.IntegerValue(0))); var whole = org.openmbee.opensysml.proto.Value.newBuilder() diff --git a/client/julia/OpenSysML/src/values.jl b/client/julia/OpenSysML/src/values.jl index 19895a4ae9..3ce76a6518 100644 --- a/client/julia/OpenSysML/src/values.jl +++ b/client/julia/OpenSysML/src/values.jl @@ -667,6 +667,11 @@ function same_value(left, right) return length(left) == length(right) && all(same_value(a, b) for (a, b) in zip(left, right)) elseif left isa AbstractSet && right isa AbstractSet return _same_unordered(left, right) + elseif left isa Rational && right isa AbstractFloat + # A Rational meets a Real at Real precision, as the service compares them. + return isequal(Float64(left), right) + elseif left isa AbstractFloat && right isa Rational + return isequal(left, Float64(right)) end return isequal(left, right) end diff --git a/client/julia/OpenSysML/test/runtests.jl b/client/julia/OpenSysML/test/runtests.jl index 555ca969ae..4b94f53f5b 100644 --- a/client/julia/OpenSysML/test/runtests.jl +++ b/client/julia/OpenSysML/test/runtests.jl @@ -25,6 +25,8 @@ const FIXTURES = normpath(joinpath(@__DIR__, "..", "..", "..", "..", "conformanc @test encode_value(third) == Dict("rationalValue" => Dict("numerator" => "-1", "denominator" => "3")) @test encode_value(1//4) == Dict("rationalValue" => Dict("numerator" => "1", "denominator" => "4")) @test OpenSysML.rationals_as_reals!(encode_value(1//4)) == Dict("realValue" => 0.25) + @test same_value(third, -1/3) && same_value(-1/3, third) && !same_value(third, -0.3333) + @test !same_value(1//3, 6004799503160661//18014398509481984) @test OpenSysML.rationals_as_reals!(encode_value(Any[1//3, 1//2])) == Dict("sequence" => Dict("elements" => Any[ Dict("rationalValue" => Dict("numerator" => "1", "denominator" => "3")), diff --git a/client/node/src/core/values.ts b/client/node/src/core/values.ts index 463b727ba3..308f199283 100644 --- a/client/node/src/core/values.ts +++ b/client/node/src/core/values.ts @@ -1179,6 +1179,13 @@ function numbersEqual(a: NumberValue, b: SysMLValue): boolean { if (a.kind === "real" && b.kind === "real") { return a.value === b.value; } + // A Rational meets a Real at Real precision, as the service compares them. + if (a.kind === "rational" && b.kind === "real") { + return rationalToNumber(a) === b.value; + } + if (a.kind === "real" && b.kind === "rational") { + return a.value === rationalToNumber(b); + } const x = exactOf(a); const y = exactOf(b); return x !== undefined && y !== undefined && x.numerator * y.denominator === y.numerator * x.denominator; diff --git a/client/node/test/rational.test.ts b/client/node/test/rational.test.ts index 6824cdc9b7..9cbba2d1d7 100644 --- a/client/node/test/rational.test.ts +++ b/client/node/test/rational.test.ts @@ -103,10 +103,11 @@ test("a terminating Rational prints as its decimal, any other as numerator/denom assert.equal(formatRational(rational(-7n, 3n)), "-7/3"); }); -test("a Rational equals the number it is, Integer and Real alike, never rounding", () => { +test("a Rational equals an Integer exactly and a Real at Real precision", () => { assert.ok(valuesEqual(rational(4n, 2n), { kind: "int", value: 2n })); assert.ok(valuesEqual(rational(1n, 2n), { kind: "real", value: 0.5 })); - assert.ok(!valuesEqual(tenth, { kind: "real", value: 0.1 })); + assert.ok(valuesEqual(tenth, { kind: "real", value: 0.1 })); + assert.ok(!valuesEqual(tenth, { kind: "real", value: 0.1 + 2 ** -56 })); assert.ok(!valuesEqual(third, rational(1n, 4n))); assert.ok(valuesEqual(third, rational(2n, 6n))); assert.ok(!valuesEqual(third, { kind: "real", value: Infinity })); diff --git a/client/node/test/values.test.ts b/client/node/test/values.test.ts index 029f5e8901..8ef0c65288 100644 --- a/client/node/test/values.test.ts +++ b/client/node/test/values.test.ts @@ -93,6 +93,15 @@ test("an integer beyond int64 travels as its decimal and reads as one bigint", ( }); assert.ok(valuesEqual(back, { kind: "real", value: 2 ** 70 })); assert.ok(!valuesEqual(back, { kind: "real", value: 2 ** 69 })); + + // A Rational meets a Real at Real precision; an Integer meets one exactly. + const third: SysMLValue = { kind: "rational", numerator: 1n, denominator: 3n }; + assert.ok(valuesEqual(third, { kind: "real", value: 1 / 3 })); + assert.ok(valuesEqual({ kind: "real", value: 1 / 3 }, third)); + assert.ok(!valuesEqual(third, { kind: "real", value: 0.3333 })); + assert.ok(valuesEqual({ kind: "rational", numerator: 1n, denominator: 4n }, { kind: "real", value: 0.25 })); + assert.ok(!valuesEqual(third, { kind: "rational", numerator: 6004799503160661n, denominator: 18014398509481984n })); + assert.ok(!valuesEqual({ kind: "int", value: 2n ** 53n + 1n }, { kind: "real", value: 2 ** 53 })); assert.throws( () => decodeValue(create(ValueSchema, { kind: { case: "bigIntValue", value: "1e30" } })), MalformedValueError, diff --git a/client/rust/opensysml/src/domain.rs b/client/rust/opensysml/src/domain.rs index 27de17620d..0201423d42 100644 --- a/client/rust/opensysml/src/domain.rs +++ b/client/rust/opensysml/src/domain.rs @@ -823,8 +823,11 @@ fn magnitudes_equal(a: &Magnitude, b: &Magnitude) -> bool { | (Magnitude::Real(r), Magnitude::BigInteger(n)) => n.equals_real(*r), (Magnitude::Integer(_), Magnitude::BigInteger(_)) | (Magnitude::BigInteger(_), Magnitude::Integer(_)) => false, - // A rational is never whole and never an f64, so it is only the same rational. (Magnitude::Rational(x), Magnitude::Rational(y)) => x == y, + // A Rational meets a Real at Real precision, as the service compares them. + (Magnitude::Rational(q), Magnitude::Real(r)) + | (Magnitude::Real(r), Magnitude::Rational(q)) => q.to_f64() == *r, + // A rational is never whole, so it is no Integer. (Magnitude::Rational(_), _) | (_, Magnitude::Rational(_)) => false, } } @@ -2239,6 +2242,7 @@ mod tests { #[test] fn same_value_judges_numbers_by_value() { let z = |real, imaginary| Value::Complex(Complex { real, imaginary }); + let rational = |n, d| Value::Rational(Rational::parse(n, d).unwrap()); let metre = |magnitude| { Value::Quantity(Quantity { magnitude, @@ -2275,6 +2279,18 @@ mod tests { (Value::Integer(2), z(2.0, 0.0), true), (Value::Integer(2), z(2.0, 1.0), false), (z(2.0, 1.0), z(2.0, 1.0), true), + (rational("1", "3"), Value::Real(1.0 / 3.0), true), + (rational("1", "3"), Value::Real(0.3333), false), + ( + rational("1", "3"), + rational("6004799503160661", "18014398509481984"), + false, + ), + ( + metre(Magnitude::Rational(Rational::parse("1", "3").unwrap())), + metre(Magnitude::Real(1.0 / 3.0)), + true, + ), (Value::Integer(1), Value::Boolean(true), false), (Value::Integer(1), Value::Text("1".to_owned()), false), ( diff --git a/client/rust/opensysml/src/encode.rs b/client/rust/opensysml/src/encode.rs index ddb1baf5a0..1c09e11451 100644 --- a/client/rust/opensysml/src/encode.rs +++ b/client/rust/opensysml/src/encode.rs @@ -687,7 +687,7 @@ mod tests { "{value:?}" ); } - assert!(!Value::Rational(third.clone()).same_value(&Value::Real(third.to_f64()))); + assert!(Value::Rational(third.clone()).same_value(&Value::Real(third.to_f64()))); let kilometres = Quantity { magnitude: Magnitude::Rational(third), unit: "km".to_owned(), diff --git a/internal/exec/runtime/collections.go b/internal/exec/runtime/collections.go index ec7f270ab0..893ada4ce4 100644 --- a/internal/exec/runtime/collections.go +++ b/internal/exec/runtime/collections.go @@ -1603,8 +1603,7 @@ func (ctx *Context) aggregateQuantities(op string, elements []Value, operator as if !ok { return Value{}, fmt.Errorf("%w: %s requires numeric elements, got %s", ErrTypeMismatch, op, describeValue(elem)) } - // An Integer magnitude keeps its kind, as the quantity operators keep it. - if num := kind.element(q.Num); q.Num.Kind != semantics.ValInt && num != q.Num { + if num := kind.element(q.Num); num != q.Num { held := *q held.Num = num q = &held diff --git a/internal/exec/runtime/collections_test.go b/internal/exec/runtime/collections_test.go index 9411431150..969591e2cb 100644 --- a/internal/exec/runtime/collections_test.go +++ b/internal/exec/runtime/collections_test.go @@ -508,6 +508,17 @@ func TestAggregateQuantities(t *testing.T) { t.Errorf("sum of mixed length units = %v, want 1.5 [m]", got) } + // RealFunctions holds every magnitude as a Real, an Integer one included. + for src, want := range map[string]float64{ + "RealFunctions::sum((1 [m], 2 [m]))": 3, + "RealFunctions::product((2 [m], 3 [m]))": 6, + } { + got, err = evalIn(t, ctx, scope, src) + if err != nil || got.Kind != ValQuantity || got.Quantity().Num != (semantics.Value{Kind: semantics.ValReal, Real: want}) { + t.Errorf("%s = %s, %v; want a Real magnitude %v", src, FormatValue(got), err, want) + } + } + if _, err := evalIn(t, ctx, scope, "sum((1 [m], 2))"); err == nil { t.Error("a bare number mixed with a length aggregated without an error") } diff --git a/internal/exec/runtime/robustness_exact_rational_test.go b/internal/exec/runtime/robustness_exact_rational_test.go index 9adeef16a7..a482cc0136 100644 --- a/internal/exec/runtime/robustness_exact_rational_test.go +++ b/internal/exec/runtime/robustness_exact_rational_test.go @@ -17,6 +17,7 @@ func TestRuntimeRobustnessExactRational(t *testing.T) { t.Run("denominator_growth_beyond_a_lowered_budget", testDenominatorGrowthBeyondALoweredBudget) t.Run("power_beyond_the_size_budget", testRationalPowerBeyondTheSizeBudget) t.Run("literal_beyond_the_size_budget", testRationalLiteralBeyondTheSizeBudget) + t.Run("conversion_beyond_a_lowered_budget", testRationalConversionBeyondALoweredBudget) t.Run("division_by_zero", testRationalDivisionByZero) } @@ -82,6 +83,38 @@ func testRationalLiteralBeyondTheSizeBudget(t *testing.T) { } } +// ToRational reads its text under the run's budget, however few digits spell +// it: eighteen fractional digits need 117 bits, past a 64-bit budget. +func testRationalConversionBeyondALoweredBudget(t *testing.T) { + src := ` + package test { + private import ScalarValues::*; + calc read { + in s : String; + return : Rational = RationalFunctions::ToRational(s); + } + } + ` + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, src)) + budgets := DefaultBudgets() + budgets.MaxIntegerBits = MinMaxIntegerBits + if err := ctx.SetBudgets(budgets); err != nil { + t.Fatal(err) + } + root := idx.DocumentRoot("") + sym := findSymbolByName(root, "read", ast.DefCalc) + if sym == nil { + t.Fatal("read calc not found") + } + got, err := ctx.InvokeCalc(sym, []Value{NewStringValue("0.25")}, root) + if err != nil || got.Kind != ValConst || got.Const.FormatRational() != "0.25" { + t.Fatalf("ToRational(\"0.25\") = %s, %v; want 0.25", FormatValue(got), err) + } + if got, err := ctx.InvokeCalc(sym, []Value{NewStringValue("0.123456789012345678")}, root); !errors.Is(err, semantics.ErrRationalSizeLimit) { + t.Fatalf("ToRational of 18 digits under a 64-bit budget = %s, %v; want %v", FormatValue(got), err, semantics.ErrRationalSizeLimit) + } +} + // A zero divisor of an exact quotient is a typed error, for a Rational and an // Integer dividend alike. func testRationalDivisionByZero(t *testing.T) { diff --git a/internal/semantic/semantics/rational.go b/internal/semantic/semantics/rational.go index 12a2fe8527..f41582a7b9 100644 --- a/internal/semantic/semantics/rational.go +++ b/internal/semantic/semantics/rational.go @@ -214,6 +214,14 @@ func sizedRat(r *big.Rat, maxBits int64) (Value, error) { return RatValue(r), nil } +// sizedValue is the exact number v, refused when its terms exceed maxBits. +func sizedValue(v Value, maxBits int64) (Value, error) { + if bits := v.RatBitLen(); bits > maxBits { + return Value{}, RationalSizeExceeded(bits, maxBits) + } + return v, nil +} + // RatOf is the exact number v (Integer or Rational) as a Rational. func RatOf(v Value) Value { if v.Kind == ValRational { @@ -481,7 +489,7 @@ func ParseRational(text string, maxBits int64) (Value, error) { n = -n } v, _ := FracValue(n, pow10(int64(len(frac)))) - return v, nil + return sizedValue(v, maxBits) } digits := strings.TrimLeft(whole+frac, "0") if digits == "" { @@ -508,7 +516,7 @@ func ParseRational(text string, maxBits int64) (Value, error) { n = -n } v, _ := FracValue(n, pow10(-scale)) - return v, nil + return sizedValue(v, maxBits) } n, _ := new(big.Int).SetString(digits, 10) if neg { diff --git a/internal/semantic/semantics/rational_test.go b/internal/semantic/semantics/rational_test.go index 6fb1a084e3..4851ac1046 100644 --- a/internal/semantic/semantics/rational_test.go +++ b/internal/semantic/semantics/rational_test.go @@ -72,6 +72,11 @@ func TestParseRationalIsExact(t *testing.T) { t.Errorf("ParseRational(%.20q, 128) = %v, want ErrRationalSizeLimit", text, err) } } + for _, text := range []string{"0.123456789012345678", "-123456789.123456789", "0.123456789012345678e0"} { + if _, err := ParseRational(text, 64); !errors.Is(err, ErrRationalSizeLimit) { + t.Errorf("ParseRational(%q, 64) = %v, want ErrRationalSizeLimit", text, err) + } + } for _, text := range []string{"", "abc", "1.2.3", "NaN", "Inf", "0x1p3", "1e"} { if _, err := ParseRational(text, DefaultMaxIntegerBits); !errors.Is(err, ErrRealNotation) { t.Errorf("ParseRational(%q) = %v, want ErrRealNotation", text, err) From 6a2a09e8f16e5f6cd8876c8ad4a9c79b15a3e941 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:08:56 +0000 Subject: [PATCH 17/26] fix(client/julia): compare a Rational and a Real numerically, so zero meets negative zero Co-Authored-By: jason.han --- client/julia/OpenSysML/src/values.jl | 4 ++-- client/julia/OpenSysML/test/runtests.jl | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/julia/OpenSysML/src/values.jl b/client/julia/OpenSysML/src/values.jl index 3ce76a6518..2e22657ee6 100644 --- a/client/julia/OpenSysML/src/values.jl +++ b/client/julia/OpenSysML/src/values.jl @@ -669,9 +669,9 @@ function same_value(left, right) return _same_unordered(left, right) elseif left isa Rational && right isa AbstractFloat # A Rational meets a Real at Real precision, as the service compares them. - return isequal(Float64(left), right) + return Float64(left) == right elseif left isa AbstractFloat && right isa Rational - return isequal(left, Float64(right)) + return left == Float64(right) end return isequal(left, right) end diff --git a/client/julia/OpenSysML/test/runtests.jl b/client/julia/OpenSysML/test/runtests.jl index 4b94f53f5b..07e9fd5005 100644 --- a/client/julia/OpenSysML/test/runtests.jl +++ b/client/julia/OpenSysML/test/runtests.jl @@ -27,6 +27,7 @@ const FIXTURES = normpath(joinpath(@__DIR__, "..", "..", "..", "..", "conformanc @test OpenSysML.rationals_as_reals!(encode_value(1//4)) == Dict("realValue" => 0.25) @test same_value(third, -1/3) && same_value(-1/3, third) && !same_value(third, -0.3333) @test !same_value(1//3, 6004799503160661//18014398509481984) + @test same_value(0//1, -0.0) && same_value(-0.0, 0//1) && !same_value(1//3, NaN) @test OpenSysML.rationals_as_reals!(encode_value(Any[1//3, 1//2])) == Dict("sequence" => Dict("elements" => Any[ Dict("rationalValue" => Dict("numerator" => "1", "denominator" => "3")), From 0346c4bb6349d6323cd40009a9cce7f0c9821e5c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:47:39 +0000 Subject: [PATCH 18/26] test(doc): expect Real magnitudes from a RealFunctions::sum rollup Co-Authored-By: jason.han --- internal/doc/queryexec/derived_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/doc/queryexec/derived_test.go b/internal/doc/queryexec/derived_test.go index f7911fcae9..dbe0f58104 100644 --- a/internal/doc/queryexec/derived_test.go +++ b/internal/doc/queryexec/derived_test.go @@ -571,8 +571,8 @@ calc def Totals :> Query { children []string count int64 }{ - "depot": {"124 [kg]", []string{"10 [kg]", "10 [kg]"}, 2}, - "Stack": {"12 [kg]", []string{"1 [kg]", "1 [kg]"}, 2}, + "depot": {"124.0 [kg]", []string{"10 [kg]", "10 [kg]"}, 2}, + "Stack": {"12.0 [kg]", []string{"1 [kg]", "1 [kg]"}, 2}, "Assembly": {"1 [kg]", nil, 0}, } { result := quantityRows(t, fixture, "Totals", root) From e7fdfcfeabda17ceb029eac59c293287b77ab2aa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:51:55 +0000 Subject: [PATCH 19/26] docs(changelog): note Real magnitudes from RealFunctions quantity sums and products Co-Authored-By: jason.han --- changes/unreleased/exact-rationals.changed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/unreleased/exact-rationals.changed.md b/changes/unreleased/exact-rationals.changed.md index 262052d579..a876956d2f 100644 --- a/changes/unreleased/exact-rationals.changed.md +++ b/changes/unreleased/exact-rationals.changed.md @@ -1,2 +1,2 @@ -- **Rational values are exact.** A KerML `Rational` is a rational number (KerML 1.0 §9.3.2.2.8), so a decimal or exponent literal is the exact Rational it spells and Integer `/`, the `RationalFunctions` operators, `rat`, `numer`, `denom`, `floor`, `round`, `abs`, `max`, `min`, `gcd`, `sum`, `product`, `ToString` and `ToRational` compute without rounding: `0.1 + 0.2 == 0.3` is `true`, `1 / 3` is `1/3`, and `numer(rat(1, 3))` is `1` where it was `6004799503160661`. A whole result of rational arithmetic stays a Rational unless the declaration returns `Integer`. A terminating Rational prints as a decimal (`0.1`), any other as `numer/denom` (`1/3`). A Rational whose numerator and denominator outgrow the evaluation's size budget is the typed `ErrRationalSizeLimit`, never a rounded double. `Real` stays IEEE 754 binary64: a value declared `Real`, an operation with no rational result (`sqrt`, trigonometry, `exp`, `ln`, a non-Integer exponent) and mixed Rational/Real arithmetic answer a binary64 Real. A Rational compared with a binary64 Real is rounded once first, as in mixed arithmetic, so `x : Real = 0.1; x == 0.1` and `rat(1, 3) == 1.0 / 3.0` are `true`; Rationals compare with each other, and Integers with anything, exactly. +- **Rational values are exact.** A KerML `Rational` is a rational number (KerML 1.0 §9.3.2.2.8), so a decimal or exponent literal is the exact Rational it spells and Integer `/`, the `RationalFunctions` operators, `rat`, `numer`, `denom`, `floor`, `round`, `abs`, `max`, `min`, `gcd`, `sum`, `product`, `ToString` and `ToRational` compute without rounding: `0.1 + 0.2 == 0.3` is `true`, `1 / 3` is `1/3`, and `numer(rat(1, 3))` is `1` where it was `6004799503160661`. A whole result of rational arithmetic stays a Rational unless the declaration returns `Integer`. A terminating Rational prints as a decimal (`0.1`), any other as `numer/denom` (`1/3`). A Rational whose numerator and denominator outgrow the evaluation's size budget is the typed `ErrRationalSizeLimit`, never a rounded double. `Real` stays IEEE 754 binary64: a value declared `Real`, an operation with no rational result (`sqrt`, trigonometry, `exp`, `ln`, a non-Integer exponent) and mixed Rational/Real arithmetic answer a binary64 Real. `RealFunctions::sum` and `RealFunctions::product` over quantities with Integer magnitudes now give Real magnitudes, as they already did over plain Integers (`RealFunctions::sum((1 [m], 2 [m]))` is `3.0 [m]`). A Rational compared with a binary64 Real is rounded once first, as in mixed arithmetic, so `x : Real = 0.1; x == 0.1` and `rat(1, 3) == 1.0 / 3.0` are `true`; Rationals compare with each other, and Integers with anything, exactly. - **Exact Rationals cross every boundary.** gRPC adds `Value.rational_value`, `Quantity.rational_magnitude` and `DocumentValue.rational_value` under the new `rational_values` capability, carrying a Rational no double holds in lowest terms (a service without the capability reports it unsupported and refuses one sent, as `big_int_values` does); the JSON engine adds `rationalValue`; the Go client decodes `opensysml.Rational`, the Python client `fractions.Fraction`, the Node client `{ kind: "rational", numerator, denominator }`, the Java client `Value.RationalValue` over `org.openmbee.opensysml.Rational`, the Rust client `Value::Rational` over `opensysml::Rational`, the Julia client `Rational{BigInt}` and the MATLAB client `struct('numerator', …, 'denominator', …)`. Each sends every exact Rational, one a double holds included, as `rational_value` to a service with `rational_values`, which reads it as that Rational while an inbound `real_value` stays a Real; to a service without the capability a client sends a Rational a double holds as that `real_value` and refuses any other. RDF export keeps an exponent literal no double holds as its exact `xsd:decimal`, unit-scale composition stays exact, the SMT witness replay is exact over Integers and Rationals (only queries a binary64 Real takes part in are still marked rounded), and `sysml -compile` refuses, by construct, exact Rational arithmetic binary64 cannot reproduce. From 0aa4aca55ed91c845c466dd4a91de1c4e283d206 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:54:08 +0000 Subject: [PATCH 20/26] fix(codegen): keep negative Integer powers exact Rationals An Integer to a negative literal power is classified as the exact quotient it is, so exact arithmetic over it is refused rather than rounded, and a Go quotient below the least Real or beyond the greatest fails as the interpreter does. Co-Authored-By: jason.han --- docs/project/native-compilation.md | 4 +++- internal/frontend/repl/compile_test.go | 3 +++ internal/frontend/repl/testdata/compile_calcs.sysml | 3 +++ internal/translate/codegen/emit_go.go | 5 ++++- internal/translate/codegen/rational.go | 2 +- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/project/native-compilation.md b/docs/project/native-compilation.md index 123a0664cb..a706d75c76 100644 --- a/docs/project/native-compilation.md +++ b/docs/project/native-compilation.md @@ -151,7 +151,9 @@ arithmetic only where binary64 gives the interpreter's answer and refuses the re the interpreter does not. A Go Real parameter given an Integer meets an exact Rational only at run time, so there the refusal is a run-time failure with the same message (`unsupported: exact Rational arithmetic '*' over a value binary64 does not hold exactly ...` for `x * 0.1` given `3`), -and an Integer to a negative literal power is the exact quotient `1 / x ** n`, rounded once. +and an Integer to a negative literal power is the exact quotient `1 / x ** n`, rounded once where it +reaches a Real (a nonzero quotient below the least Real is an overflow error, as in the interpreter) +and refused where exact arithmetic would go on with it (`a ** -1 * 3`). ## Strings and enumerations diff --git a/internal/frontend/repl/compile_test.go b/internal/frontend/repl/compile_test.go index f9b72b51f7..cffb9505b9 100644 --- a/internal/frontend/repl/compile_test.go +++ b/internal/frontend/repl/compile_test.go @@ -40,6 +40,8 @@ var compiledCases = []compiledCase{ {"ExactRational", []string{"-6", "-1e308"}}, {"ExactRational", []string{"6", "0.1000000000000001"}}, {"RealAgainstNearest", []string{"0.1"}}, {"RealAgainstNearest", []string{"0.1000000000000001"}}, {"RealAgainstNearest", []string{"0.09999999999999999"}}, {"RealAgainstNearest", []string{"-0.1"}}, {"RealAgainstNearest", []string{"1e308"}}, {"RealAgainstNearest", []string{"-1e308"}}, {"RealAgainstNearest", []string{"0.0"}}, {"ExactWide", []string{"3"}}, {"ExactWide", []string{"-3"}}, {"ExactWide", []string{"0"}}, + {"NegativePower", []string{"3"}}, {"NegativePower", []string{"-2"}}, {"NegativePower", []string{"0"}}, + {"NegativePowerTiny", []string{"10"}}, {"NegativePowerTiny", []string{"1"}}, {"ExactHeldInteger", []string{"3"}}, {"ExactHeldInteger", []string{"3.0"}}, {"ExactHeldInteger", []string{"0.5"}}, {"ExactHeldCompare", []string{"3"}}, {"ExactHeldCompare", []string{"0.3"}}, {"ExactHeldCompare", []string{"2.5"}}, {"Compare", []string{"2.0", "2"}}, {"Compare", []string{"2.5", "2"}}, {"Compare", []string{"0.1", "1"}}, @@ -820,6 +822,7 @@ func TestCompileRefusesWhatItCannotCompile(t *testing.T) { {"ExactProduct", "exact Rational arithmetic '*' over a value binary64 does not hold exactly"}, {"ExactPower", "'**' of an exact Rational by an Integer exponent"}, {"ExactCompare", "'<' of an exact Rational binary64 does not hold exactly"}, + {"NegativePowerExact", "exact Rational arithmetic '*' over a value binary64 does not hold exactly"}, {"BeyondCompare", "literal 1e400 is outside the Real range"}, {"Extent", "operator 'all'"}, {"SelectNonBoolean", "select whose body yields Integer, not a Boolean"}, diff --git a/internal/frontend/repl/testdata/compile_calcs.sysml b/internal/frontend/repl/testdata/compile_calcs.sysml index d609046c1b..3c9eb1fd14 100644 --- a/internal/frontend/repl/testdata/compile_calcs.sysml +++ b/internal/frontend/repl/testdata/compile_calcs.sysml @@ -84,6 +84,8 @@ package Compiled { return : Integer = r; } calc def ExactWide { in a : Integer; return : Real = a * 2.0 + 0.5; } + calc def NegativePower { in a : Integer; return : Real = a ** -2; } + calc def NegativePowerTiny { in a : Integer; return : Real = a ** -400; } // A Real parameter given an Integer meets an exact Rational at run time. calc def ExactHeldInteger { in x : Real; return : Real = x * 0.1; } calc def ExactHeldCompare { in x : Real; return : Boolean = x / 3 < 0.1 or x != 2.5; } @@ -748,6 +750,7 @@ package Refused { calc def ExactProduct { in a : Integer; return : Real = a * 0.1; } calc def ExactPower { in a : Integer; return : Real = (a * 0.5) ** 2; } calc def ExactCompare { in a : Integer; return : Boolean = a / 3 < 0.1; } + calc def NegativePowerExact { in a : Integer; return : Real = a ** -1 * 3; } calc def BeyondCompare { in x : Real; return : Boolean = x < 1e400; } calc def SelectNonBoolean { in x : Integer[0..*]; return : Integer[0..*] = x->select {in v; v}; } calc def CollectNull { in x : Integer[0..*]; return : Integer[0..*] = x->collect {in v; null}; } diff --git a/internal/translate/codegen/emit_go.go b/internal/translate/codegen/emit_go.go index 560f3aa835..eb7da08767 100644 --- a/internal/translate/codegen/emit_go.go +++ b/internal/translate/codegen/emit_go.go @@ -223,7 +223,10 @@ func sysmlQuot(a, b sysmlInt) float64 { } else { q, _ = new(big.Rat).SetFrac(a.toBig(), b.toBig()).Float64() } - return q + if q == 0 && a.sign() != 0 { + sysmlFail("arithmetic overflow: a nonzero Rational below the least Real") + } + return sysmlFinite(q) } func sysmlICmp(a, b sysmlInt) int { diff --git a/internal/translate/codegen/rational.go b/internal/translate/codegen/rational.go index 6899a2331d..9b919ab0f6 100644 --- a/internal/translate/codegen/rational.go +++ b/internal/translate/codegen/rational.go @@ -73,7 +73,7 @@ func (fc *funcCompiler) classify(x Expr, reached bool) exactness { return exactRational } case Cond: - return join(fc.classify(x.Then, reached), fc.classify(x.Else, reached)) + return fc.classifyBranches(reached, x.Then, x.Else) case LibCall: switch x.Op { case LibAbsReal, LibMaxReal, LibMinReal: From 2197c1a185f2ad434b72479bd70b681f565b5f33 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:36:33 +0000 Subject: [PATCH 21/26] docs(python): document CAPABILITY_RATIONAL_VALUES in the regenerated API reference Co-Authored-By: jason.han --- docs/reference/python-api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/python-api.md b/docs/reference/python-api.md index c38d188c38..7087fe7cff 100644 --- a/docs/reference/python-api.md +++ b/docs/reference/python-api.md @@ -40,6 +40,10 @@ connecting to models. options: heading_level: 3 +::: opensysml.CAPABILITY_RATIONAL_VALUES + options: + heading_level: 3 + ::: opensysml.load options: heading_level: 3 From 8ced1ccc51da409d77d64242aa1b2b5ff946bcd9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:12:19 +0000 Subject: [PATCH 22/26] docs(repl): expect exact Rational quotients and Real sums in the browser walkthroughs Co-Authored-By: jason.han --- docs/assets/repl-walkthroughs.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/assets/repl-walkthroughs.json b/docs/assets/repl-walkthroughs.json index e3d08945f3..0778231cdc 100644 --- a/docs/assets/repl-walkthroughs.json +++ b/docs/assets/repl-walkthroughs.json @@ -21,7 +21,7 @@ "100 [SI::km] / 2 [SI::h]" ], "expect": [ - "= 13.88888888888889 [SI::'m/s']" + "= 125/9 [SI::'m/s']" ] }, { @@ -60,7 +60,7 @@ "%eval Garage::car.mass" ], "expect": [ - "= 1300 [kg]" + "= 1300.0 [kg]" ] }, { @@ -83,7 +83,7 @@ "%calc Speed 42.195 [SI::km] 2 [SI::h]" ], "expect": [ - "= 5.860416666666667 [SI::'m/s']" + "= 2813/480 [SI::'m/s']" ] }, { From bb0cb3ec58541a8ee4e6c3686bf98f460494344c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:41:49 +0000 Subject: [PATCH 23/26] fix(lsp): show exact Rational values as inlay hints; order the exact accumulation fixture's statements Co-Authored-By: jason.han --- .../conformance/action_exact_rational_accumulation.sysml | 4 ++-- internal/frontend/lsp/inlayhint.go | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.sysml b/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.sysml index e79e892103..85ce440f36 100644 --- a/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.sysml +++ b/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.sysml @@ -13,10 +13,10 @@ package test { for i in 1..10 { assign tenths := tenths + 0.1; } - for n in 1..4 { + then for n in 1..4 { assign harmonic := harmonic + 1 / n; } - assign reachesOne := tenths == 1; + then assign reachesOne := tenths == 1; } then done; } diff --git a/internal/frontend/lsp/inlayhint.go b/internal/frontend/lsp/inlayhint.go index 4d90cae6fb..1d2a691ceb 100644 --- a/internal/frontend/lsp/inlayhint.go +++ b/internal/frontend/lsp/inlayhint.go @@ -152,6 +152,8 @@ func constantText(v semantics.Value) (string, bool) { return v.FormatInt(), true case semantics.ValReal: return semantics.FormatReal(v.Real), true + case semantics.ValRational: + return v.FormatRational(), true case semantics.ValBool: if v.Bool { return "true", true From e3f8a9752ba98486e6c54109f6fda60cfb7d8176 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:41:50 +0000 Subject: [PATCH 24/26] test(wasm): make room in the engine size budget for exact Rationals Co-Authored-By: jason.han --- tests/wasm/engine_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wasm/engine_test.go b/tests/wasm/engine_test.go index 576d8d95b7..b234b05578 100644 --- a/tests/wasm/engine_test.go +++ b/tests/wasm/engine_test.go @@ -25,7 +25,7 @@ import ( // engineGzipBudget bounds the gzipped js build of sysml-engine: serving JSON // rather than protobuf is what keeps it small, so exceeding the budget means // the engine pulled in a dependency it must not have. -const engineGzipBudget = 7500000 +const engineGzipBudget = 7600000 // engineSubtests runs the sysml-engine half of the run gate on bins, linked // into bins["sysml-engine"] with the other commands so it is not built twice: From b3a3e03ab6ce5c8b0f021d973d309d2f470516f6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:53:30 +0000 Subject: [PATCH 25/26] test(runtime): record the exact accumulation fixture's ordered trace Co-Authored-By: jason.han --- .../action_exact_rational_accumulation.trace.golden | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.trace.golden b/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.trace.golden index 6cd17217b9..52ba980571 100644 --- a/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.trace.golden +++ b/internal/exec/runtime/testdata/conformance/action_exact_rational_accumulation.trace.golden @@ -1,4 +1,6 @@ step 1: token 1@sum +enter action node: sum +step 2: token 1@for i stmt for i eval literal 1 -> 1 eval literal 10 -> 10 @@ -53,6 +55,7 @@ stmt for i eval feature tenths -> 0.9 eval literal 0.1 -> 0.1 eval operator + -> 1.0 +step 3: token 1@for n stmt for n eval literal 1 -> 1 eval literal 4 -> 4 @@ -85,9 +88,11 @@ stmt for n eval feature n -> 4 eval operator / -> 0.25 eval operator + -> 25/12 +step 4: token 1@assign reachesOne stmt assign reachesOne eval feature tenths -> 1.0 eval literal 1 -> 1 eval operator == -> true -step 2: token 1@done -step 3: no active tokens +leave action node: sum +step 5: token 1@done +step 6: no active tokens From 014fceb7bcecd98a056d96d3ce9231d19d00bc3c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:47:39 +0000 Subject: [PATCH 26/26] feat(check): report a decimal literal rounded by a Real feature as an opt-in lint Co-Authored-By: jason.han --- .../unreleased/rounded-real-literal.added.md | 1 + cmd/sysml/lint.go | 4 +- cmd/sysml/lint_test.go | 21 +++++ cmd/sysml/main.go | 6 ++ cmd/sysml/usage.go | 4 +- docs/guide/03-command-line.md | 15 ++-- docs/project/spec-compliance.md | 1 + docs/reference/cli.md | 3 +- docs/reference/diagnostics.md | 64 ++++++++++---- docs/reference/lsp.md | 9 ++ internal/check/passes/lint_rounded_real.go | 84 +++++++++++++++++++ .../check/passes/lint_rounded_real_test.go | 69 +++++++++++++++ internal/check/passes/lints.go | 21 +++-- internal/check/passes/lints_test.go | 25 +++++- internal/check/passes/typecheck_expr.go | 4 + internal/frontend/core/core.go | 1 + internal/frontend/grpc/lint_test.go | 36 ++++++++ internal/frontend/grpc/service.go | 1 + internal/frontend/lsp/conformance.go | 27 ++++-- internal/frontend/lsp/lint_test.go | 31 +++++++ internal/frontend/repl/conformance.go | 44 ++++++++-- internal/frontend/repl/lint_test.go | 33 ++++++++ internal/frontend/repl/meta.go | 2 +- internal/workspace/model/batch.go | 2 +- internal/workspace/model/lints_test.go | 41 +++++++++ internal/workspace/model/workspace.go | 34 +++++++- packaging/man/man1/sysml.1 | 8 +- 27 files changed, 537 insertions(+), 54 deletions(-) create mode 100644 changes/unreleased/rounded-real-literal.added.md create mode 100644 internal/check/passes/lint_rounded_real.go create mode 100644 internal/check/passes/lint_rounded_real_test.go create mode 100644 internal/frontend/grpc/lint_test.go diff --git a/changes/unreleased/rounded-real-literal.added.md b/changes/unreleased/rounded-real-literal.added.md new file mode 100644 index 0000000000..3908e51c3f --- /dev/null +++ b/changes/unreleased/rounded-real-literal.added.md @@ -0,0 +1 @@ +- **An opt-in `rounded-real-literal` lint.** A decimal literal written to a feature typed by `Real` that no binary64 value equals can now be reported where it is written, naming the double the feature holds: `attribute x : Real = 0.1;` warns that `0.1` is rounded to `0.10000000000000001`, while `0.25` and `1.5e3`, which a double holds exactly, are silent. It covers a feature's value, a parameter's default, a redefinition of a `Real` feature, an `assign` and each element of a sequence written out, but not a computed value. Since most decimals written to a `Real` round, it is off by default: switch it on with `-enable-lint rounded-real-literal`, `%lint rounded-real-literal on` or the new `enabledLints` editor setting (`model.WithEnabledLints` in Go). Like every lint it is a warning in every mode, and `-disable-lint` wins over `-enable-lint`. diff --git a/cmd/sysml/lint.go b/cmd/sysml/lint.go index a77811161b..772b709605 100644 --- a/cmd/sysml/lint.go +++ b/cmd/sysml/lint.go @@ -6,8 +6,8 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/check/passes" ) -// lintList is -disable-lint as written: the codes of the lints left out of the -// model's diagnostics, comma-separated or repeated. +// lintList is -disable-lint or -enable-lint as written: the codes of the lints +// left out of, or kept in, the model's diagnostics, comma-separated or repeated. type lintList []string func (l *lintList) String() string { return strings.Join(*l, ",") } diff --git a/cmd/sysml/lint_test.go b/cmd/sysml/lint_test.go index 9779696641..b533b02f18 100644 --- a/cmd/sysml/lint_test.go +++ b/cmd/sysml/lint_test.go @@ -41,3 +41,24 @@ func TestLintListAcceptsRepeatsAndCommas(t *testing.T) { t.Fatal("an unknown code was accepted") } } + +const roundedLiteral = `package P { private import ScalarValues::*; attribute x : Real = 0.1; }` + +// An opt-in lint is silent until -enable-lint names it, and -disable-lint wins. +func TestEnableLintReportsAnOptInLint(t *testing.T) { + binary := buildCLI(t) + quiet := check(t, binary, roundedLiteral, "-validate") + wantReport(t, quiet, 0) + if strings.Contains(quiet.output(), "rounded") { + t.Fatalf("an opt-in lint reported by default:\n%s", quiet.output()) + } + wantReport(t, check(t, binary, roundedLiteral, "-enable-lint", passes.CodeRoundedRealLiteral, "-validate"), 0, + "warning: 0.1 is rounded to the nearest Real, 0.10000000000000001") + both := check(t, binary, roundedLiteral, "-enable-lint", passes.CodeRoundedRealLiteral, + "-disable-lint", passes.CodeRoundedRealLiteral, "-validate") + wantReport(t, both, 0) + if strings.Contains(both.output(), "rounded") { + t.Fatalf("a disabled lint reported:\n%s", both.output()) + } + wantReport(t, check(t, binary, roundedLiteral, "-enable-lint", "no-such-lint", "-validate"), 2, "no-such-lint") +} diff --git a/cmd/sysml/main.go b/cmd/sysml/main.go index c1153154ed..90d0c39d6d 100644 --- a/cmd/sysml/main.go +++ b/cmd/sysml/main.go @@ -140,6 +140,7 @@ var ( htmlTheme string strictMode bool disabledLints lintList + enabledLints lintList noRecordCache bool modelChecks checks compileCalc string @@ -818,6 +819,11 @@ func newSession() *repl.Session { fmt.Fprintln(os.Stderr, errPrefix, err) os.Exit(2) } + if err := sess.SetEnabledLints(enabledLints); err != nil { + // Unreachable: the codes were validated as the flag was parsed. + fmt.Fprintln(os.Stderr, errPrefix, err) + os.Exit(2) + } cache, err := libs.OpenRecordCache(noRecordCache) if err != nil && !quietMode { fmt.Fprintf(os.Stderr, "%s record cache unavailable, holding every file loaded: %v\n", errPrefix, err) diff --git a/cmd/sysml/usage.go b/cmd/sysml/usage.go index 865eb8e0f2..cf66fced4f 100644 --- a/cmd/sysml/usage.go +++ b/cmd/sysml/usage.go @@ -607,7 +607,8 @@ func registerFlags(fs *flag.FlagSet) { fs.StringVar(&queryText, "query", "", "Evaluate this OSLC Query text against the model and exit") fs.Var(&modelChecks.validate, "validate", "Report the model's diagnostics and exit, nonzero on an error; -validate= checks instead every assertion about that object (repeatable)") - fs.Var(&disabledLints, "disable-lint", "Leave this lint out of the model's diagnostics: undeclared-signal, port-type-mismatch or deferred-keeper-unmarked, comma-separated or repeated") + fs.Var(&disabledLints, "disable-lint", "Leave this lint out of the model's diagnostics: undeclared-signal, port-type-mismatch, deferred-keeper-unmarked or rounded-real-literal, comma-separated or repeated") + fs.Var(&enabledLints, "enable-lint", "Report this opt-in lint, off by default, in the model's diagnostics: rounded-real-literal, comma-separated or repeated; -disable-lint wins") fs.BoolVar(&noRecordCache, "no-record-cache", false, "Parse every file loaded and hold it loaded, reading no interface record from the record cache and writing none; default off, or OPENSYSML_RECORD_CACHE=0") fs.BoolVar(&strictMode, "strict", false, "Judge the model as conforming SysML v2: notation no pinned production admits is an error, not a warning; a SysML v1 migration writes none of it") fs.Var(&modelChecks.constraints, "constraint", "Evaluate this constraint and exit (repeatable)") @@ -710,6 +711,7 @@ func optionGroups() []usage.OptionGroup { usage.Opt("validate", "[=]"), usage.Opt("strict", ""), usage.Opt("disable-lint", ""), + usage.Opt("enable-lint", ""), usage.Opt("no-record-cache", ""), usage.Opt("constraint", nameArg), usage.Opt("self-check", ""), diff --git a/docs/guide/03-command-line.md b/docs/guide/03-command-line.md index 32e4f1bf27..855bbd9cc2 100644 --- a/docs/guide/03-command-line.md +++ b/docs/guide/03-command-line.md @@ -269,13 +269,16 @@ each extension is measured against. The same setting is available as `%strict` a ## Lints -Three further warnings, `undeclared-signal`, `port-type-mismatch` and -`deferred-keeper-unmarked`, are *lints*: the model is valid SysML v2, but a `when ` that -no declaration or `send` accounts for, a connection between ports whose definitions are -unrelated, or a deferred signal's accept loop written without the `DeferredKeeper` marker that -names it as the keeper, is almost always a slip. `-strict` leaves them warnings, since they are not about notation. +Four further warnings, `undeclared-signal`, `port-type-mismatch`, +`deferred-keeper-unmarked` and `rounded-real-literal`, are *lints*: the model is valid SysML v2, +but a `when ` that no declaration or `send` accounts for, a connection between ports whose +definitions are unrelated, a deferred signal's accept loop written without the `DeferredKeeper` +marker that names it as the keeper, or a decimal literal such as `0.1` that a `Real` feature can +only hold rounded, is almost always a slip or a surprise. `-strict` leaves them warnings, since they are not about notation. `-disable-lint ` switches one off (`%lint off` at the prompt, -`disabledLints` in an editor); [the diagnostics reference](../reference/diagnostics.md) +`disabledLints` in an editor). `rounded-real-literal` is opt-in, since most decimals written +to a `Real` round: `-enable-lint rounded-real-literal` switches it on (`%lint +rounded-real-literal on`, `enabledLints`). [The diagnostics reference](../reference/diagnostics.md) states exactly what each reports. ## Running behavior diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index a9a8d09373..0877d698db 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -1926,6 +1926,7 @@ semantics layer over the conjugation parity of the typing/specialization chain. | A port usage conforms to the definition it conjugates, and two ports match when each named feature of one has a feature of the other with a conforming type and the conjugate direction (§7.12.2) | `semantics/conjugation.go` `PortsConform`, `featuresMatchConjugate`, `featureTypesConform` | `semantics/conjugation_test.go` `TestConjugatedPortConformance` | ✅ Faithful | | The ports at the two ends of an interface must have conjugate directed features: what one end sends the other receives | `semantics/conjugation.go` `InterfaceEndPortMismatch`, `endPortFeatures`; `passes/constraint.go` `checkInterfaceEndConjugation` (code `port-conjugation`) | `passes/constraint_test.go` `TestConstraintInterfaceEndConjugation`, `TestConstraintInterfaceFlowPairsDirectedFeatures`, `semantics/conjugation_test.go` `TestInterfaceEndConjugation`, `passes/w8g_notation_residue_test.go` `TestW8GInterfaceConjugationStaysAWarning` | ⚠️ Approximate (re-adjudicated: the pinned validator is silent on the same fixture, so this is a one-sided check kept as a *warning* on the precedent set for our other deliberately wider checks, and it fires only for an interface whose two ends both declare a resolvable port type; undirected features carry no flow, so they are not required to match; a feature the interface's own `flow` pairs with a complementary-direction, type-conforming feature of the other end is paired by that declaration rather than by name, so the name match is not required of it; the ends of a `connect`/`flow` clause take their types by implicit redefinition of the interface's ends, which is checked as end *identity*, not as direction) | | A `connect`, interface usage or `flow` joining two ports whose definitions neither specialize one another nor a common definition of the model, and neither of whose directed features all match the other's with the conjugate direction and a conforming type (§7.12.2), is reported as the lint `port-type-mismatch`; a connector typed by a definition whose ends are port-typed is left to that definition's `port-conjugation` check | `semantics/conjugation.go` `ConnectedPortsMismatch`; `passes/lint_port_type.go` `PortTypeMismatchPass` | `passes/lints_test.go` `TestPortTypeMismatch*`; the false-positive scan over `examples/` and `tests/testdata/` | ⚠️ Approximate (a lint: the specification states no such constraint and the pinned validator reports none, so it is a warning in every mode and can be switched off) | +| A decimal literal (the exact Rational it spells, §8.4.4.9.2) bound or assigned to a feature typed by `Real`, which holds a binary64 value, is reported as the lint `rounded-real-literal` when no double equals it, naming the double held; a literal a double holds, a `Rational` feature, a computed value and a literal beyond the binary64 range are not reported. The feature is `Real` by its written type, else its effective type (§8.3.3.3) | `passes/lint_rounded_real.go` `holdsReal`, `lintRoundedReal`, called from `passes/typecheck_expr.go` `checkBoundValue` for declared values and assignments; `semantics/rational.go` `ParseRational`; `semantics/quantity_eval.go` `RealOf` | `passes/lint_rounded_real_test.go` `TestRoundedRealLiteralLint*`; opt-in filtering in `passes/lints.go` `WithoutLints`, tested by `passes/lints_test.go` `TestWithoutLintsLeavesOptInLintsOut`, `model/lints_test.go` `TestWorkspaceEnablesAnOptInLint`, `repl/lint_test.go` `TestLintMetaCommandSwitchesAnOptInLint`, `lsp/lint_test.go` `TestDidChangeConfigurationEnablesAnOptInLint`, `cmd/sysml/lint_test.go` `TestEnableLintReportsAnOptInLint`, `grpc/lint_test.go` `TestParseFileLeavesOptInLintsOut` | ⚠️ Approximate (a lint: the specification leaves Real's precision to the implementation and states no such check, so it is a warning in every mode, and opt-in, since most decimals written to a `Real` round) | | Only a port usage or a connector end may be typed by a conjugated port definition, and `~` must name a port definition. The rule is a SysML one — `~` there occurs only in a `ConjugatedPortTyping` (`SysML.xtext` `FeatureTyping: OwnedFeatureTyping \| ConjugatedPortTyping`) — so it is scoped to a conjugated *typing*: a KerML declaration conjugation is a `Conjugation` between any two `Type`s (KerML 1.1 §7.4, `ClassifierConjugationPart`/`FeatureConjugationPart`) and demands no port | `passes/typecheck.go` `checkConjugatedTyping`, reached from `checkRelationships` for `ast.RelTyping` alone | `passes/typecheck_test.go` `TestTypeCheckConjugatedTyping`; `passes/f90_conjugation_scope_test.go` (`TestF90KerMLConjugationIsNotAPortTyping` over `testdata/passes/f90_conjugation.kerml`, `TestF90KerMLConjugationFormsAreClean`, `TestF90SysMLConjugatedPortTypingIsClean` over `testdata/passes/f90_conjugation_ports.sysml`, `TestF90SysMLConjugatedPortTypingStillChecked`) | ✅ Faithful (matched runs: the pinned `validate-kerml` is silent on every KerML conjugation form in the fixture, and the pinned `validate-sysml` rejects `~Q` for a non-port `Q` as an unresolvable `ConjugatedPortDefinition`, so the SysML check stands) | | An interface body may declare a default end with no declaration at all: `end;` is an anonymous port usage (§8.2.2.14.1 `DefaultInterfaceEnd: isEnd ?= 'end' Usage`) | `parser/parser.go` `bodyContext`/`pushBodyContext`; `parser/defusage.go` `parseAnonymousEnd`, `parseAnonymousEndUsage`, `anonymousUsageKind` | `parse/end_usages.golden`, `resolve/analysis_test.go` `TestResolveDragonStructures` | ✅ Faithful | | Anywhere else a bare `end;` is not standard notation: only `DefaultInterfaceEnd` makes the usage declaration optional, and every other `end` form (`ReferenceUsage`, `EndUsagePrefix` + a kind keyword) requires an `Identification` or a specialization part | `parser/defusage.go` `parseAnonymousEndUsage` (typed diagnostic naming the fix, `end ref;`) | `parser/negative_test.go` (`end_outside_connector`, `end_outside_connector_package`) | ✅ Faithful (see the known limitation on `Dragon.sysml` below) | diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 1586840acd..de84ae16ac 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -232,7 +232,8 @@ the same member-path parser as `Project` and `OrderBy`. | `--debug` | | Report every diagnostic over the whole session buffer, with the pass that produced it | | `--quiet` | | Report errors only, suppressing warnings | | `--strict` | | Judge the model as conforming SysML v2: notation no pinned production admits is an error, not a warning (see [Strict conformance](../guide/03-command-line.md#strict-conformance)) | -| `--disable-lint ` | | Leave the named [lint](diagnostics.md) out of the diagnostics: `undeclared-signal`, `port-type-mismatch` or `deferred-keeper-unmarked`; comma-separated or repeated. An unknown code is a usage error | +| `--disable-lint ` | | Leave the named [lint](diagnostics.md) out of the diagnostics: `undeclared-signal`, `port-type-mismatch`, `deferred-keeper-unmarked` or `rounded-real-literal`; comma-separated or repeated. An unknown code is a usage error | +| `--enable-lint ` | | Report the named opt-in [lint](diagnostics.md), off by default, in the diagnostics: `rounded-real-literal`; comma-separated or repeated. `--disable-lint` wins over it, and an unknown code is a usage error | | `--no-record-cache` | | Parse every file loaded and hold it loaded, reading no interface record from the record cache and writing none: what a run does where `OPENSYSML_RECORD_CACHE=0`. By default a file whose bytes, library, conformance mode and record format match a record in the cache is held as that record — its scopes and symbols without its tree, and the diagnostics its analysis found — and a file analyzed by `-validate`, `-satisfy` or another load writes its record for the next run; see [Interface records](../internals/interface-records.md) | | `--trace` | | Report each execution step: expression evaluation, calc invocation, action tokens, state transitions, each `choice` the executor made among alternatives the library leaves unordered, naming the alternatives and the one taken, and each `unevaluable guard` it read only to report one and could not evaluate ([Choice points](../guide/06-behavior.md)). Under `-schedule explore` the table is printed first, then the trace of one witness run per distinct outcome, each under a `trace of outcome 's witness (run ):` heading ([Exploring every linearization](#exploring-every-linearization)) | | `--convert ` | | Convert the model instead of running it: `sysml`, `kerml`, `ttl`, `turtle`, `rdf`, `api-json` or `json`. `ttl` writes the RDF graph in Turtle, `api-json` the same graph as the API's JSON element objects; both are [experimental](rdf-mapping.md#status-experimental) and every run that converts either says so on stderr (see [the RDF mapping](rdf-mapping.md)). The model argument may be a Flexo MMS project branch URL — `http(s)://host[:port][/base]/projects/{project}/branches/{branch}` or `flexo://{project}/{branch}` — both naming the endpoint `FLEXO_SYSMLV2_URL` configures — which is read as its head commit's RDF graph; see [Reading and pushing a repository branch](#reading-and-pushing-a-repository-branch) | diff --git a/docs/reference/diagnostics.md b/docs/reference/diagnostics.md index 240c659a16..ab5b470a0f 100644 --- a/docs/reference/diagnostics.md +++ b/docs/reference/diagnostics.md @@ -3,16 +3,19 @@ A *lint* is a warning about a model the specification accepts: nothing in SysML v2 or KerML makes the written model wrong, but it is almost always a slip. Each lint has a stable code, carried as the diagnostic's `code` under `-json`, in the editor and over the wire, and each -can be switched off by that code. A lint is a warning in every mode: +can be switched off by that code. A lint marked *opt-in* below is off until it is switched on +by its code, since what it reports holds of ordinary models more often than it marks a slip. +A lint is a warning in every mode: [`-strict`](../guide/03-command-line.md#strict-conformance) promotes notation no SysML v2 production admits, and a lint is not about notation, so it stays a warning and never changes the exit status or blocks a check. -| Code | Reported on | Tier | -|------|-------------|------| -| `undeclared-signal` | a transition's `when ` whose name matches no declaration visible where it is written and no signal the model sends | name resolution | -| `port-type-mismatch` | a `connect`, an interface usage or a `flow` joining two ports whose definitions are unrelated | constraint | -| `deferred-keeper-unmarked` | an accept of a deferred signal at the root of the do action of a state annotated `MigrationMetadata::DeferredEvent` that is not marked `MigrationMetadata::DeferredKeeper` | name resolution | +| Code | Reported on | Tier | Default | +|------|-------------|------|---------| +| `undeclared-signal` | a transition's `when ` whose name matches no declaration visible where it is written and no signal the model sends | name resolution | on | +| `port-type-mismatch` | a `connect`, an interface usage or a `flow` joining two ports whose definitions are unrelated | constraint | on | +| `deferred-keeper-unmarked` | an accept of a deferred signal at the root of the do action of a state annotated `MigrationMetadata::DeferredEvent` that is not marked `MigrationMetadata::DeferredKeeper` | name resolution | on | +| `rounded-real-literal` | a decimal literal bound or assigned to a feature typed by `Real` that no binary64 value equals, so the feature holds it rounded | type | opt-in | ## Action-step multiplicity warnings @@ -27,18 +30,21 @@ source `action-step-multiplicity`; they are not lints and cannot be disabled wit | `action-step-order-unsatisfiable` | A succession end forces an endpoint count that it does not admit | | `action-step-order-open` | The declared or defaulted succession ends do not force the endpoint counts under both readings of an unwritten end | -## Switching a lint off +## Switching a lint off or on -| Surface | Setting | -|---------|---------| -| CLI | `-disable-lint [,…]`, repeatable; an unknown code is a usage error (exit 2) | -| REPL | `%lint` lists every lint, on or off; `%lint on\|off` switches one and reprints the session's diagnostics | -| Language server | `disabledLints`, a list of codes, in `initializationOptions` or `workspace/didChangeConfiguration` ([LSP extensions](lsp.md#disabled-lints-setting)) | -| Go | `model.WithDisabledLints(codes...)` when the workspace is made, or `(*model.Workspace).SetDisabledLints(codes)` | +| Surface | Off | On (opt-in lints) | +|---------|-----|-------------------| +| CLI | `-disable-lint [,…]`, repeatable; an unknown code is a usage error (exit 2) | `-enable-lint [,…]`, the same way | +| REPL | `%lint off` | `%lint on` | +| Language server | `disabledLints`, a list of codes, in `initializationOptions` or `workspace/didChangeConfiguration` ([LSP extensions](lsp.md#disabled-lints-setting)) | `enabledLints`, the same way | +| Go | `model.WithDisabledLints(codes...)` when the workspace is made, or `(*model.Workspace).SetDisabledLints(codes)` | `model.WithEnabledLints(codes...)` or `(*model.Workspace).SetEnabledLints(codes)` | -A disabled lint is left out of what the workspace reports, not out of the analysis, so -switching it back on needs no re-analysis. The gRPC service and the clients report both lints -with their codes; a client that does not want one drops the diagnostics carrying its code. +`%lint` alone lists every lint, on or off, and switching one reprints the session's +diagnostics. A lint both disabled and enabled is off. Enabling a lint that is on by default +changes nothing. A lint left out is left out of what the workspace reports, not out of the +analysis, so switching it back needs no re-analysis. The gRPC service and the clients report +every lint that is on by default, with its code, and no opt-in lint; a client that does not +want one drops the diagnostics carrying its code. ## `undeclared-signal` @@ -147,3 +153,29 @@ it. Re-migrate the model, which writes the marker on every keeping accept, or wr below the do action's root, one beside a marked keeper of the signal, or one under a state the annotation does not name, is an ordinary accept by design and is not reported, nor is an accept whose signal does not resolve, which name resolution reports. + +## `rounded-real-literal` + +A decimal literal is the exact Rational it spells (`0.1` is 1/10, KerML 1.0 §8.4.4.9.2), while +a feature typed by `Real` holds an IEEE 754 binary64 value, so a literal written to one is +rounded once to the nearest double. Where that double differs from the literal, a model reading +the feature back gets a value other than the one it wrote, and the lint names the value held: + +```text +m.sysml:3:24: warning: 0.1 is rounded to the nearest Real, 0.10000000000000001, since a feature typed by Real holds a binary64 value; type the feature by Rational to keep the literal exact +``` + +It reports a literal, signed or not, written as a feature's value (`attribute x : Real = 0.1;`, +a parameter's default, a redefinition such as `attribute :>> x = 0.2;` of a `Real` feature) or +written by an `assign`, and each element of a sequence written out (`(0.25, 0.1)` reports +`0.1`). The feature is `Real` by its declared type, or else by the type it inherits. A literal +binary64 holds exactly (`0.25`, `1.5e3`), an Integer literal, a feature typed by `Rational` +and a computed value (`1 / 3`, `0.1 + 0.2`) are not reported, and neither is a literal beyond +the binary64 range, which the run refuses. Comparison with the feature is at Real precision +([exact Rationals](../project/exact-rational-evaluation.md#comparing-a-rational-with-a-binary64-real)), +so `x == 0.1` still holds; the lint is about the value the feature holds and reports. + +The lint is opt-in: a decimal written to a `Real` feature is how ordinary models state +measured values (`efficiency = 0.92`), and nearly all of them round. Switch it on with +`-enable-lint rounded-real-literal`, `%lint rounded-real-literal on` or the `enabledLints` +editor setting to find the literals whose rounding matters. diff --git a/docs/reference/lsp.md b/docs/reference/lsp.md index de73a9e2a4..852708729b 100644 --- a/docs/reference/lsp.md +++ b/docs/reference/lsp.md @@ -94,6 +94,15 @@ and changes nothing. Changing it republishes the diagnostics of every open docum corresponds to the CLI's `-disable-lint` and the REPL's `%lint`. Strict conformance leaves a lint a warning. +The opt-in lints, off until switched on, are named by a second list setting, `enabledLints`, +read the same way and corresponding to the CLI's `-enable-lint`: + +```json +{ "sysml": { "enabledLints": ["rounded-real-literal"] } } +``` + +A lint named by both settings is off. + ## Interface records A document the server holds but no editor has open — one it read from the disk diff --git a/internal/check/passes/lint_rounded_real.go b/internal/check/passes/lint_rounded_real.go new file mode 100644 index 0000000000..af2dd51833 --- /dev/null +++ b/internal/check/passes/lint_rounded_real.go @@ -0,0 +1,84 @@ +package passes + +import ( + "fmt" + "math/big" + "strconv" + + "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/diag" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" +) + +// CodeRoundedRealLiteral marks a decimal literal bound or assigned to a Real-typed +// feature that no binary64 Real equals, so the feature holds the nearest one. +const CodeRoundedRealLiteral = "rounded-real-literal" + +// holdsReal reports whether the feature d declares is a Real one: typed Real as +// written, else by the effective types it inherits (KerML §8.3.3.3). +func (ec *exprChecker) holdsReal(declScope *symbols.Scope, d featureDecl, written semantics.PrimType) bool { + if written != semantics.PrimUnknown { + return written == semantics.PrimReal + } + for _, typ := range ec.model.FeatureTypes(declaredSymbol(declScope, d.node)) { + if prim := ec.model.PrimTypeOf(typ); prim != semantics.PrimUnknown { + return prim == semantics.PrimReal + } + } + return false +} + +// lintRoundedReal reports value, bound to a Real-typed feature, when it is a decimal +// literal, signed or not, whose exact Rational no binary64 Real equals. +func (ec *exprChecker) lintRoundedReal(value ast.Node) { + literal, sign := value, "" + if op, ok := value.(*ast.OperatorExpr); ok && len(op.Operands) == 1 && + (op.Operator == ast.OpNeg || op.Operator == ast.OpPos) { + literal = op.Operands[0] + if op.Operator == ast.OpNeg { + sign = "-" + } + } + lit, ok := literal.(*ast.LiteralReal) + if !ok { + return + } + exact, err := semantics.ParseRational(lit.Value, semantics.DefaultMaxIntegerBits) + if err != nil { + return + } + // A literal beyond the Real range is the run time's to refuse. + real, err := semantics.RealOf(exact) + if err != nil || new(big.Rat).SetFloat64(real.Real).Cmp(exact.Rat()) == 0 { + return + } + ec.lint(CodeRoundedRealLiteral, value.Span(), + "%s%s is rounded to the nearest Real, %s%s, since a feature typed by Real holds a binary64 value; type the feature by Rational to keep the literal exact", + sign, lit.Value, sign, distinctDigits(real.Real, exact)) +} + +// distinctDigits writes x with the fewest significant digits, 17 at least, that +// tell it from exact, which it rounds. +func distinctDigits(x float64, exact semantics.Value) string { + text := strconv.FormatFloat(x, 'g', 17, 64) + for digits := 18; digits <= 767; digits++ { + if parsed, err := semantics.ParseRational(text, semantics.DefaultMaxIntegerBits); err != nil || semantics.CompareRat(parsed, exact) != 0 { + break + } + text = strconv.FormatFloat(x, 'g', digits, 64) + } + return text +} + +// lint records a lint finding of code at span. +func (ec *exprChecker) lint(code string, span source.Span, format string, args ...any) { + ec.diags = append(ec.diags, diag.Diagnostic{ + Severity: diag.SeverityWarning, + Span: span, + Message: fmt.Sprintf(format, args...), + Code: code, + Source: lintSource, + }) +} diff --git a/internal/check/passes/lint_rounded_real_test.go b/internal/check/passes/lint_rounded_real_test.go new file mode 100644 index 0000000000..ba6451366c --- /dev/null +++ b/internal/check/passes/lint_rounded_real_test.go @@ -0,0 +1,69 @@ +package passes + +import ( + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/diag" +) + +func TestRoundedRealLiteralLint(t *testing.T) { + cases := []struct { + name string + src string + wants [][]string + }{ + {"declared value", `package P { private import ScalarValues::*; attribute x : Real = 0.1; }`, + [][]string{{"0.1 is rounded to the nearest Real, 0.10000000000000001", "type the feature by Rational"}}}, + {"negated", `package P { private import ScalarValues::*; attribute x : Real = -0.1; }`, + [][]string{{"-0.1 is rounded to the nearest Real, -0.10000000000000001"}}}, + {"exponent", `package P { private import ScalarValues::*; attribute x : Real = 1.1e-1; }`, + [][]string{{"1.1e-1 is rounded"}}}, + {"each element", `package P { private import ScalarValues::*; attribute xs : Real[3] = (0.25, 0.1, 0.3); }`, + [][]string{{"0.1 is rounded"}, {"0.3 is rounded"}}}, + {"inherited type", `package P { private import ScalarValues::*; part def A { attribute x : Real; } part a : A { attribute :>> x = 0.2; } }`, + [][]string{{"0.2 is rounded"}}}, + {"rounding past seventeen digits", `package P { private import ScalarValues::*; attribute x : Real = 0.01; }`, + [][]string{{"0.01 is rounded to the nearest Real, 0.0100000000000000002,"}}}, + {"calc parameter", `package P { private import ScalarValues::*; calc def F { in g : Real = 9.81; return r : Real = g; } }`, + [][]string{{"9.81 is rounded to the nearest Real, 9.8100000000000005"}}}, + {"assignment", `package P { private import ScalarValues::*; action a { attribute x : Real := 0.0; assign x := 0.7; } }`, + [][]string{{"0.7 is rounded"}}}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + wantLints(t, c.src, CodeRoundedRealLiteral, c.wants...) + }) + } +} + +func TestRoundedRealLiteralLintSilent(t *testing.T) { + cases := []struct{ name, src string }{ + {"exact binary fraction", `package P { private import ScalarValues::*; attribute x : Real = 0.25; }`}, + {"whole decimal", `package P { private import ScalarValues::*; attribute x : Real = 1.5e3; }`}, + {"Integer literal", `package P { private import ScalarValues::*; attribute x : Real = 3; }`}, + {"Rational feature", `package P { private import ScalarValues::*; attribute x : Rational = 0.1; }`}, + {"untyped feature", `package P { attribute x = 0.1; }`}, + {"computed value", `package P { private import ScalarValues::*; attribute x : Real = 1 / 3; attribute y : Real = 0.1 + 0.2; }`}, + {"beyond the Real range", `package P { private import ScalarValues::*; attribute x : Real = 1e400; }`}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + for _, mode := range []diag.ConformanceMode{diag.ConformanceDefault, diag.ConformanceStrict} { + if got := lintDiags(t, c.src, CodeRoundedRealLiteral, mode); len(got) != 0 { + t.Errorf("%s: got %+v, want no finding", mode, got) + } + } + }) + } +} + +func TestRoundedRealLiteralLintDisabled(t *testing.T) { + src := `package P { private import ScalarValues::*; attribute x : Real = 0.1; }` + got := lintDiags(t, src, CodeRoundedRealLiteral, diag.ConformanceDefault) + if len(got) != 1 { + t.Fatalf("got %d findings, want 1", len(got)) + } + if kept := WithoutLints(got, map[string]bool{CodeRoundedRealLiteral: true}, map[string]bool{CodeRoundedRealLiteral: true}); len(kept) != 0 { + t.Errorf("disabled lint kept %+v", kept) + } +} diff --git a/internal/check/passes/lints.go b/internal/check/passes/lints.go index 8333bb7496..0633a559d8 100644 --- a/internal/check/passes/lints.go +++ b/internal/check/passes/lints.go @@ -25,7 +25,11 @@ const CodePortTypeMismatch = "port-type-mismatch" const CodeDeferredKeeperUnmarked = "deferred-keeper-unmarked" // lintCodes are the codes of the lints, in the order surfaces list them. -var lintCodes = []string{CodeUndeclaredSignal, CodePortTypeMismatch, CodeDeferredKeeperUnmarked} +var lintCodes = []string{CodeUndeclaredSignal, CodePortTypeMismatch, CodeDeferredKeeperUnmarked, CodeRoundedRealLiteral} + +// optInLints are the lints left out of the diagnostics until a surface enables +// them: their findings hold of ordinary models more often than they mark a slip. +var optInLints = []string{CodeRoundedRealLiteral} // LintCodes returns the codes of the diagnostics a surface may disable. func LintCodes() []string { return slices.Clone(lintCodes) } @@ -33,13 +37,16 @@ func LintCodes() []string { return slices.Clone(lintCodes) } // IsLintCode reports whether code is the code of a lint. func IsLintCode(code string) bool { return slices.Contains(lintCodes, code) } -// WithoutLints returns diags without the lint findings whose codes disabled -// holds; diags itself is returned when it has none to drop. -func WithoutLints(diags []diag.Diagnostic, disabled map[string]bool) []diag.Diagnostic { - if len(disabled) == 0 { - return diags +// IsOptInLint reports whether code is a lint reported only once enabled. +func IsOptInLint(code string) bool { return slices.Contains(optInLints, code) } + +// WithoutLints returns diags without the lint findings a surface leaves out: +// those whose codes disabled holds, and those of the opt-in lints enabled does +// not hold; diags itself is returned when it has none to drop. +func WithoutLints(diags []diag.Diagnostic, disabled, enabled map[string]bool) []diag.Diagnostic { + drop := func(d diag.Diagnostic) bool { + return d.Source == lintSource && (disabled[d.Code] || IsOptInLint(d.Code) && !enabled[d.Code]) } - drop := func(d diag.Diagnostic) bool { return d.Source == lintSource && disabled[d.Code] } if !slices.ContainsFunc(diags, drop) { return diags } diff --git a/internal/check/passes/lints_test.go b/internal/check/passes/lints_test.go index 5ebc2c7f3f..4a4404a6b3 100644 --- a/internal/check/passes/lints_test.go +++ b/internal/check/passes/lints_test.go @@ -204,11 +204,11 @@ func TestWithoutLints(t *testing.T) { {Code: CodePortTypeMismatch, Source: lintSource}, {Code: CodeUndeclaredSignal, Source: "name-resolution"}, } - got := WithoutLints(diags, map[string]bool{CodeUndeclaredSignal: true}) + got := WithoutLints(diags, map[string]bool{CodeUndeclaredSignal: true}, nil) if len(got) != 2 || got[0].Code != CodePortTypeMismatch || got[1].Source != "name-resolution" { t.Fatalf("got %+v", got) } - if got := WithoutLints(diags, nil); len(got) != len(diags) { + if got := WithoutLints(diags, nil, nil); len(got) != len(diags) { t.Fatalf("nothing disabled dropped %+v", got) } for _, code := range LintCodes() { @@ -221,6 +221,27 @@ func TestWithoutLints(t *testing.T) { } } +// An opt-in lint is left out until enabled, and disabling it wins over enabling it. +func TestWithoutLintsLeavesOptInLintsOut(t *testing.T) { + diags := []diag.Diagnostic{ + {Code: CodeRoundedRealLiteral, Source: lintSource}, + {Code: CodeUndeclaredSignal, Source: lintSource}, + } + if !IsOptInLint(CodeRoundedRealLiteral) || IsOptInLint(CodeUndeclaredSignal) { + t.Fatal("want rounded-real-literal, and only it, opt-in") + } + if got := WithoutLints(diags, nil, nil); len(got) != 1 || got[0].Code != CodeUndeclaredSignal { + t.Fatalf("by default got %+v, want only the on-by-default lint", got) + } + enabled := map[string]bool{CodeRoundedRealLiteral: true} + if got := WithoutLints(diags, nil, enabled); len(got) != len(diags) { + t.Fatalf("enabled got %+v, want both", got) + } + if got := WithoutLints(diags, enabled, enabled); len(got) != 1 || got[0].Code != CodeUndeclaredSignal { + t.Fatalf("enabled and disabled got %+v, want it left out", got) + } +} + const portDefs = ` item def Power; item def Fuel; port def PowerOut { out item p : Power; } diff --git a/internal/check/passes/typecheck_expr.go b/internal/check/passes/typecheck_expr.go index fa6eca957e..37fccb1fb0 100644 --- a/internal/check/passes/typecheck_expr.go +++ b/internal/check/passes/typecheck_expr.go @@ -140,9 +140,13 @@ func (ec *exprChecker) checkBoundValue(valueScope, declScope *symbols.Scope, d f // The elements the lattice typed are its to judge; the rest are judged by // their static result types. latticeTyped := make(map[ast.Node]bool) + real := ec.holdsReal(declScope, d, want) // A collection literal binds elementwise, so each element is checked // against the feature's type rather than the sequence as a whole. for _, element := range valueElements(value) { + if real { + ec.lintRoundedReal(element) + } // A collection value binds the elements its body or collection produces. Inferring // the value checks and reports on them; their types are then read silently. if elements, collection := ec.model.CollectionElements(valueScope, element); collection { diff --git a/internal/frontend/core/core.go b/internal/frontend/core/core.go index 9b8e2ef63f..a25c1232a2 100644 --- a/internal/frontend/core/core.go +++ b/internal/frontend/core/core.go @@ -277,6 +277,7 @@ func (c *Core) parseModel(inputs []sourceInput, mode diag.ConformanceMode) (stri for i, document := range documents { document.diagnostics, _ = passes.AnalyzeInBatch(inputs[i].name, inputs[i].kind, document.root, document.diagnostics, index, passes.Options{Conformance: mode}, batch) + document.diagnostics = passes.WithoutLints(document.diagnostics, nil, nil) } } diff --git a/internal/frontend/grpc/lint_test.go b/internal/frontend/grpc/lint_test.go new file mode 100644 index 0000000000..33bc84237d --- /dev/null +++ b/internal/frontend/grpc/lint_test.go @@ -0,0 +1,36 @@ +package grpc + +import ( + "context" + "testing" + + pb "github.com/Open-MBEE/OpenSysML/api/proto" + "github.com/Open-MBEE/OpenSysML/internal/check/passes" +) + +// The service reports the lints on by default and no opt-in one. +func TestParseFileLeavesOptInLintsOut(t *testing.T) { + srv := mustNewService(t, 10) + parsed, err := srv.ParseFile(context.Background(), &pb.ParseFileRequest{ + Source: &pb.ParseFileRequest_Content{Content: `package P { + private import ScalarValues::*; + attribute x : Real = 0.1; + state def M { entry; then a; state a; state b; transition first a when Pnig then b; } +}`}, + }) + if err != nil { + t.Fatalf("ParseFile: %v", err) + } + var signal bool + for _, d := range parsed.Diagnostics { + switch d.GetCode() { + case passes.CodeRoundedRealLiteral: + t.Errorf("an opt-in lint was reported: %s", d.GetMessage()) + case passes.CodeUndeclaredSignal: + signal = true + } + } + if !signal { + t.Fatalf("want the on-by-default lint reported: %v", parsed.Diagnostics) + } +} diff --git a/internal/frontend/grpc/service.go b/internal/frontend/grpc/service.go index 8f9eccedcd..55bea44617 100644 --- a/internal/frontend/grpc/service.go +++ b/internal/frontend/grpc/service.go @@ -848,6 +848,7 @@ func (s *Service) parseModel(inputs []sourceInput, mode diag.ConformanceMode) (s for i, doc := range documents { doc.Diagnostics, _ = passes.AnalyzeInBatch(inputs[i].name, inputs[i].kind, doc.Root, doc.Diagnostics, idx, passes.Options{Conformance: mode}, batch) + doc.Diagnostics = passes.WithoutLints(doc.Diagnostics, nil, nil) } } diff --git a/internal/frontend/lsp/conformance.go b/internal/frontend/lsp/conformance.go index cc615e0e7c..8aa6de17ae 100644 --- a/internal/frontend/lsp/conformance.go +++ b/internal/frontend/lsp/conformance.go @@ -16,6 +16,10 @@ const strictConformanceKey = "strictConformance" // diagnostics, spelled as the CLI's -disable-lint flag. const disabledLintsKey = "disabledLints" +// enabledLintsKey is the setting naming the opt-in lints an editor keeps in the +// diagnostics, spelled as the CLI's -enable-lint flag. +const enabledLintsKey = "enabledLints" + // settingsSection is the section an editor nests this server's settings under. const settingsSection = "sysml" @@ -38,8 +42,8 @@ func (s *Server) applyConformanceSettings(ctx context.Context, payload any) bool } // DidChangeConfiguration applies the settings the client pushed. Only the -// conformance mode and the disabled lints are read; a payload that mentions -// neither changes nothing. +// conformance mode and the disabled and enabled lints are read; a payload that +// mentions none of them changes nothing. func (s *Server) DidChangeConfiguration(ctx context.Context, params *protocol.DidChangeConfigurationParams) error { if params == nil { return nil @@ -53,11 +57,20 @@ func (s *Server) DidChangeConfiguration(ctx context.Context, params *protocol.Di return nil } -// applyLintSettings replaces the lints the workspace leaves out with the ones a -// settings payload names, and reports whether it named any setting: a payload -// without it leaves the lints alone, and an unknown code is shown the client. +// applyLintSettings replaces the lints the workspace leaves out, and the opt-in +// lints it keeps, with the ones a settings payload names, and reports whether it +// named either setting: a payload without one leaves those lints alone. func (s *Server) applyLintSettings(ctx context.Context, payload any) bool { - value, ok := sysmlSetting(payload, disabledLintsKey) + disabled := s.applyLintSetting(ctx, payload, disabledLintsKey, s.ws.SetDisabledLints) + enabled := s.applyLintSetting(ctx, payload, enabledLintsKey, s.ws.SetEnabledLints) + return disabled || enabled +} + +// applyLintSetting hands set the codes the list setting key names, and reports +// whether it took them: a value that is not a list of strings is ignored, and +// an unknown code is shown the client. +func (s *Server) applyLintSetting(ctx context.Context, payload any, key string, set func([]string) error) bool { + value, ok := sysmlSetting(payload, key) if !ok { return false } @@ -73,7 +86,7 @@ func (s *Server) applyLintSettings(ctx context.Context, payload any) bool { } codes = append(codes, code) } - if err := s.ws.SetDisabledLints(codes); err != nil { + if err := set(codes); err != nil { if s.client != nil { _ = s.client.ShowMessage(ctx, &protocol.ShowMessageParams{Type: protocol.MessageTypeError, Message: err.Error()}) } diff --git a/internal/frontend/lsp/lint_test.go b/internal/frontend/lsp/lint_test.go index cd78f5f6ed..8f2644040b 100644 --- a/internal/frontend/lsp/lint_test.go +++ b/internal/frontend/lsp/lint_test.go @@ -68,3 +68,34 @@ func TestDidChangeConfigurationRepublishesWithoutTheLint(t *testing.T) { t.Fatalf("published %v after disabling the lint", last.Diagnostics) } } + +// enabledLints is read in the same shapes as disabledLints, and keeps an +// opt-in lint in what is republished. +func TestDidChangeConfigurationEnablesAnOptInLint(t *testing.T) { + ws := model.NewWorkspace() + s := NewServer(ws) + if _, err := s.Initialize(context.Background(), &protocol.InitializeParams{ + InitializationOptions: map[string]any{"sysml.enabledLints": []any{"bogus"}}, + }); err != nil { + t.Fatal(err) + } + if len(ws.EnabledLints()) != 0 { + t.Fatalf("an unknown code was taken: %v", ws.EnabledLints()) + } + fc := &fakeClient{} + s.client = fc + ws.Open("a.sysml", []byte("package P { private import ScalarValues::*; attribute x : Real = 0.1; }"), 1) + if err := s.DidChangeConfiguration(context.Background(), &protocol.DidChangeConfigurationParams{ + Settings: map[string]any{"sysml": map[string]any{"enabledLints": []any{passes.CodeRoundedRealLiteral}}}, + }); err != nil { + t.Fatal(err) + } + published := fc.all() + if len(published) == 0 { + t.Fatal("enabling a lint republished nothing") + } + last := published[len(published)-1] + if len(last.Diagnostics) != 1 || last.Diagnostics[0].Code != passes.CodeRoundedRealLiteral { + t.Fatalf("published %v after enabling the lint", last.Diagnostics) + } +} diff --git a/internal/frontend/repl/conformance.go b/internal/frontend/repl/conformance.go index c5ee74fedd..b0def1f7d3 100644 --- a/internal/frontend/repl/conformance.go +++ b/internal/frontend/repl/conformance.go @@ -59,15 +59,36 @@ func (s *Session) SetDisabledLints(codes []string) error { return s.ws.SetDisabledLints(codes) } +// EnabledLints reports the codes of the opt-in lints the session keeps in its +// diagnostics. +func (s *Session) EnabledLints() []string { + defer s.reading()() + return s.ws.EnabledLints() +} + +// SetEnabledLints replaces the opt-in lints the session keeps in its +// diagnostics; a code naming no lint is an error and changes nothing. +func (s *Session) SetEnabledLints(codes []string) error { + defer s.enter()() + return s.ws.SetEnabledLints(codes) +} + +// lintOn reports whether a lint is reported under the disabled and enabled sets. +func lintOn(code string, disabled, enabled []string) bool { + if slices.Contains(disabled, code) { + return false + } + return !passes.IsOptInLint(code) || slices.Contains(enabled, code) +} + // doLint lists the lints, each on or off, or switches one, reporting what the // buffer looks like with it switched. func (s *Session) doLint(args []string) []string { - disabled := s.ws.DisabledLints() list := func() []string { - cur := s.ws.DisabledLints() + disabled, enabled := s.ws.DisabledLints(), s.ws.EnabledLints() out := make([]string, 0, len(passes.LintCodes())) for _, code := range passes.LintCodes() { - out = append(out, fmt.Sprintf("lint %s: %s", code, onOff(!slices.Contains(cur, code)))) + out = append(out, fmt.Sprintf("lint %s: %s", code, onOff(lintOn(code, disabled, enabled)))) } return out } @@ -82,15 +103,26 @@ func (s *Session) doLint(args []string) []string { if err := passes.CheckLintCodes([]string{code}); err != nil { return []string{fmt.Sprintf("error: %v", err)} } - next := slices.DeleteFunc(slices.Clone(disabled), func(c string) bool { return c == code }) + without := func(codes []string) []string { + return slices.DeleteFunc(slices.Clone(codes), func(c string) bool { return c == code }) + } + disabled, enabled := without(s.ws.DisabledLints()), without(s.ws.EnabledLints()) switch args[1] { case "on": + if passes.IsOptInLint(code) { + enabled = append(enabled, code) + } case "off": - next = append(next, code) + if !passes.IsOptInLint(code) { + disabled = append(disabled, code) + } default: return []string{fmt.Sprintf("error: unknown lint setting %q (want on or off)", args[1])} } - if err := s.ws.SetDisabledLints(next); err != nil { + if err := s.ws.SetDisabledLints(disabled); err != nil { + return []string{fmt.Sprintf("error: %v", err)} + } + if err := s.ws.SetEnabledLints(enabled); err != nil { return []string{fmt.Sprintf("error: %v", err)} } return append(list(), s.diagnosticLines()...) diff --git a/internal/frontend/repl/lint_test.go b/internal/frontend/repl/lint_test.go index 0c76ae4672..367822c030 100644 --- a/internal/frontend/repl/lint_test.go +++ b/internal/frontend/repl/lint_test.go @@ -59,3 +59,36 @@ func TestStrictModeLeavesLintsWarnings(t *testing.T) { t.Fatalf("strict mode changed the lint: %v", s.Diagnostics()) } } + +// An opt-in lint lists as off and reports nothing until %lint switches it on. +func TestLintMetaCommandSwitchesAnOptInLint(t *testing.T) { + s := NewSession() + s.Submit("private import ScalarValues::*; attribute x : Real = 0.1;") + rounded := func() int { + n := 0 + for _, d := range s.Diagnostics() { + if d.Code == passes.CodeRoundedRealLiteral { + n++ + } + } + return n + } + if rounded() != 0 { + t.Fatalf("want the opt-in lint off by default: %v", s.Diagnostics()) + } + if out := strings.Join(meta(t, s, "%lint"), "\n"); !strings.Contains(out, "lint rounded-real-literal: off") || + !strings.Contains(out, "lint undeclared-signal: on") { + t.Fatalf("%%lint = %q", out) + } + out := strings.Join(meta(t, s, "%lint rounded-real-literal on"), "\n") + if rounded() != 1 || !strings.Contains(out, "lint rounded-real-literal: on") { + t.Fatalf("%%lint on = %q, diagnostics %v", out, s.Diagnostics()) + } + if got := s.EnabledLints(); len(got) != 1 || got[0] != passes.CodeRoundedRealLiteral { + t.Fatalf("EnabledLints = %v", got) + } + meta(t, s, "%lint rounded-real-literal off") + if rounded() != 0 || len(s.EnabledLints()) != 0 || len(s.DisabledLints()) != 0 { + t.Fatalf("%%lint off left enabled %v, disabled %v, diagnostics %v", s.EnabledLints(), s.DisabledLints(), s.Diagnostics()) + } +} diff --git a/internal/frontend/repl/meta.go b/internal/frontend/repl/meta.go index 11aebd9511..ec0570e6d2 100644 --- a/internal/frontend/repl/meta.go +++ b/internal/frontend/repl/meta.go @@ -191,7 +191,7 @@ var metaCommandTable = []metaCommand{ {name: "%verbosity", group: groupSettings, args: "[level]", desc: "show or set output level: quiet, normal or debug"}, {name: "%trace", group: groupSettings, args: "[on|off]", desc: "show or set execution tracing (evaluation, calc, action and state steps)"}, {name: "%strict", group: groupSettings, args: "[on|off]", desc: "show or set strict conformance: report notation no SysML v2 production admits as an error"}, - {name: "%lint", group: groupSettings, args: "[ on|off]", desc: "list the lints — warnings about models the specification accepts — each on or off, or switch one by its code: undeclared-signal, port-type-mismatch"}, + {name: "%lint", group: groupSettings, args: "[ on|off]", desc: "list the lints — warnings about models the specification accepts — each on or off, or switch one by its code: undeclared-signal, port-type-mismatch, deferred-keeper-unmarked, rounded-real-literal (opt-in, off by default)"}, {name: "%schedule", group: groupSettings, args: "[]", desc: "show or set the scheduling policy runs started from here on resolve choice points under: declared, reverse or seed:"}, {name: "%seed", group: groupSettings, args: "[|off]", desc: "show or set the seed runs started from here on draw their modeled randomness from — Probability-weighted decisions, RandomFunctions — whatever the schedule; off leaves it to the schedule's seed:"}, {name: "%draws", group: groupSettings, args: "[]", desc: "show or set how runs started from here on resolve RandomFunctions draws: random (from the seed), min, max or average of each call's distribution; min, max and average need no seed"}, diff --git a/internal/workspace/model/batch.go b/internal/workspace/model/batch.go index f5c7fea184..73901b101d 100644 --- a/internal/workspace/model/batch.go +++ b/internal/workspace/model/batch.go @@ -244,7 +244,7 @@ func (w *Workspace) DiagnosticsAll(names []string) [][]diag.Diagnostic { if doc := w.docs[name]; out[i] == nil && doc != nil && !doc.Recorded() { out[i] = w.diagCache[name] } - out[i] = passes.WithoutLints(out[i], w.disabledLints) + out[i] = passes.WithoutLints(out[i], w.disabledLints, w.enabledLints) } return out } diff --git a/internal/workspace/model/lints_test.go b/internal/workspace/model/lints_test.go index cabff87380..0ecfe6aac7 100644 --- a/internal/workspace/model/lints_test.go +++ b/internal/workspace/model/lints_test.go @@ -64,6 +64,47 @@ func TestWorkspaceRejectsAnUnknownLint(t *testing.T) { } } +// An opt-in lint is left out until enabled, on every read of the diagnostics, +// and a disabled lint stays out even when enabled. +func TestWorkspaceEnablesAnOptInLint(t *testing.T) { + const src = `package P { private import ScalarValues::*; attribute x : Real = 0.1; }` + ws := NewWorkspace() + ws.Open("a.sysml", []byte(src), 1) + if n := lintCount(ws.Diagnostics("a.sysml"), passes.CodeRoundedRealLiteral); n != 0 { + t.Fatalf("want the opt-in lint off by default, got %d finding(s)", n) + } + if err := ws.SetEnabledLints([]string{passes.CodeRoundedRealLiteral}); err != nil { + t.Fatal(err) + } + if lintCount(ws.Diagnostics("a.sysml"), passes.CodeRoundedRealLiteral) != 1 { + t.Fatalf("Diagnostics left out an enabled lint: %v", ws.Diagnostics("a.sysml")) + } + if _, diags, _ := ws.AnalyzedContent("a.sysml"); lintCount(diags, passes.CodeRoundedRealLiteral) != 1 { + t.Fatal("AnalyzedContent left out an enabled lint") + } + if all := ws.DiagnosticsAll([]string{"a.sysml"}); lintCount(all[0], passes.CodeRoundedRealLiteral) != 1 { + t.Fatal("DiagnosticsAll left out an enabled lint") + } + if got := ws.EnabledLints(); len(got) != 1 || got[0] != passes.CodeRoundedRealLiteral { + t.Fatalf("EnabledLints = %v", got) + } + if err := ws.SetDisabledLints([]string{passes.CodeRoundedRealLiteral}); err != nil { + t.Fatal(err) + } + if n := lintCount(ws.Diagnostics("a.sysml"), passes.CodeRoundedRealLiteral); n != 0 { + t.Fatalf("a disabled lint reported %d finding(s) though enabled", n) + } + if err := ws.SetEnabledLints([]string{"no-such-lint"}); err == nil { + t.Fatal("an unknown code was accepted") + } + if got := ws.EnabledLints(); len(got) != 1 { + t.Fatalf("a rejected setting changed the enabled lints: %v", got) + } + if got := NewWorkspace(WithEnabledLints(passes.CodeRoundedRealLiteral)).EnabledLints(); len(got) != 1 { + t.Fatalf("WithEnabledLints = %v", got) + } +} + // A signal another open document sends silences the lint, and closing that // document brings it back: the sent names are a workspace-wide union. func TestWorkspaceSignalLintFollowsOtherDocuments(t *testing.T) { diff --git a/internal/workspace/model/workspace.go b/internal/workspace/model/workspace.go index 1a4d9936e3..912c68d636 100644 --- a/internal/workspace/model/workspace.go +++ b/internal/workspace/model/workspace.go @@ -65,6 +65,8 @@ type Workspace struct { // disabledLints holds the codes of the lints this workspace's diagnostics // leave out; they are computed and recorded whatever it holds. disabledLints map[string]bool + // enabledLints holds the codes of the opt-in lints its diagnostics keep. + enabledLints map[string]bool // libSource yields the text of the library files the index was built from, // nil when the index came without one; libDocs caches them parsed. libSource libs.Source @@ -109,6 +111,12 @@ func WithDisabledLints(codes ...string) Option { return func(w *Workspace) { w.disabledLints = lintSet(codes) } } +// WithEnabledLints keeps the opt-in lints with these codes (see +// passes.IsOptInLint) in this workspace's diagnostics; a disabled lint stays out. +func WithEnabledLints(codes ...string) Option { + return func(w *Workspace) { w.enabledLints = lintSet(codes) } +} + // WithLibrarySource names the source the index's library documents were read // from, so LibraryDocument can serve their text. It must serve the bytes the // index was built from, or the index's spans address the wrong text. @@ -303,6 +311,28 @@ func (w *Workspace) SetDisabledLints(codes []string) error { return nil } +// EnabledLints reports the codes of the opt-in lints this workspace keeps in +// its diagnostics, sorted. +func (w *Workspace) EnabledLints() []string { + w.mu.RLock() + defer w.mu.RUnlock() + return slices.Sorted(maps.Keys(w.enabledLints)) +} + +// SetEnabledLints replaces the opt-in lints this workspace keeps in its +// diagnostics with codes; none leaves every opt-in lint out. A code that names +// no lint is an error, and leaves the setting as it was; one naming a lint on by +// default changes nothing, and a disabled lint stays out either way. +func (w *Workspace) SetEnabledLints(codes []string) error { + if err := passes.CheckLintCodes(codes); err != nil { + return err + } + w.mu.Lock() + defer w.mu.Unlock() + w.enabledLints = lintSet(codes) + return nil +} + // lintSet is codes as a set, nil for none. func lintSet(codes []string) map[string]bool { if len(codes) == 0 { @@ -577,7 +607,7 @@ func (w *Workspace) Diagnostics(name string) []diag.Diagnostic { if doc == nil { return nil } - return passes.WithoutLints(w.diagnosticsLocked(name, doc), w.disabledLints) + return passes.WithoutLints(w.diagnosticsLocked(name, doc), w.disabledLints, w.enabledLints) } // AnalyzedContent returns a document's diagnostics together with the content @@ -590,7 +620,7 @@ func (w *Workspace) AnalyzedContent(name string) ([]byte, []diag.Diagnostic, boo if doc == nil { return nil, nil, false } - return doc.Content, passes.WithoutLints(w.diagnosticsLocked(name, doc), w.disabledLints), true + return doc.Content, passes.WithoutLints(w.diagnosticsLocked(name, doc), w.disabledLints, w.enabledLints), true } // diagnosticsLocked analyzes doc, caching the result. Caller holds the lock. diff --git a/packaging/man/man1/sysml.1 b/packaging/man/man1/sysml.1 index 0ceb2552d8..96181d6a88 100644 --- a/packaging/man/man1/sysml.1 +++ b/packaging/man/man1/sysml.1 @@ -43,8 +43,12 @@ is an error, not a warning; a SysML v1 migration writes none of it .TP .BR \-disable\-lint " \fIcode\fP" Leave this lint out of the model's diagnostics: undeclared\-signal, -port\-type\-mismatch or deferred\-keeper\-unmarked, comma\-separated or -repeated +port\-type\-mismatch, deferred\-keeper\-unmarked or rounded\-real\-literal, +comma\-separated or repeated +.TP +.BR \-enable\-lint " \fIcode\fP" +Report this opt\-in lint, off by default, in the model's diagnostics: +rounded\-real\-literal, comma\-separated or repeated; \-disable\-lint wins .TP .B \-no\-record\-cache Parse every file loaded and hold it loaded, reading no interface record from