Module: Hecks::Runtime::CommandRules::Arithmetic
- Included in:
- Hecks::Runtime::CommandRules
- Defined in:
- lib/hecks/runtime/command_rules/arithmetic.rb
Overview
The arithmetic half of mutation: what a source resolves to, and how increment/decrement land on an Integer or a one-numeric-field value object.
Defined Under Namespace
Classes: MutationOp
Constant Summary collapse
- MUTATION_OPS =
[ MutationOp.new(name: "set", sign: nil), MutationOp.new(name: "append", sign: nil), MutationOp.new(name: "increment", sign: 1), MutationOp.new(name: "decrement", sign: -1), # Vendored addition, not (yet) upstream hecks (migration # plan task 4, i106): multiply/clamp carry no sign -- like # set/append, they do no add-or-subtract arithmetic (multiply # scales, clamp bounds). See #multiply/#clamp below. MutationOp.new(name: "multiply", sign: nil), MutationOp.new(name: "clamp", sign: nil), # Vendored addition, not (yet) upstream hecks (migration # plan task 4): remove -- carries no sign, like set/append; it # matches a list element by value rather than doing arithmetic. # Declared here so this table stays exactly what # Vocabulary::MutationOp declares (spec/vocabulary_conformance_spec # holds the two equal) -- MutationApplier's own `when :remove` # branch (mutation_applier.rb) never calls #sign_of, so this was # a declared-vocabulary gap, not a behaviour gap. MutationOp.new(name: "remove", sign: nil), # Vendored addition, not (yet) upstream hecks — # CommandBuilder#delegates_to's own comment gives the full # reasoning; carries no sign, like set/append/remove — it does # no arithmetic, only a synchronous handoff into one nested # entity command. Declared here so this table stays exactly # what Vocabulary::MutationOp declares — MutationApplier's own # `when :delegate` branch (mutation_applier.rb) never calls # #sign_of either, same as `remove`'s own note above. MutationOp.new(name: "delegate", sign: nil) ].freeze
Instance Method Summary collapse
- #arithmetic(current, amount, target, sign) ⇒ Object
- #arithmetic_value_object(current, amount, target, sign, op) ⇒ Object
-
#clamp(current, bounds, target) ⇒ Object
Vendored addition, not (yet) upstream hecks (migration plan task 4, i106): bound the CURRENT value into
[min, max]-- no "amount" to combine, so it does not go through #arithmetic/#multiply's shared-numeric-field matching at all; it clamps whichever single numeric field the wrapping value object carries (a synthesised wrapper always carries exactly one, per Part 3a's auto-synthesis). -
#multiply(current, amount, target) ⇒ Object
Vendored addition, not (yet) upstream hecks (migration plan task 4, i106):
current * amount-- the scaling counterpart to increment/decrement's add/subtract. -
#resolve_source(source, args) ⇒ Object
A mutation's source is either the NAME OF AN ARGUMENT or a LITERAL, and the two are told apart by type : a Symbol is always a name, a String or a number is always a value.
-
#sign_of(op) ⇒ Object
Not a bare
.find(...)&.sign || -1— that silently answered DECREMENT'S sign for BOTH an op this table has never heard of AND a declared, real op that simply carries no sign at all (set/append/multiply/clamp/remove — see MUTATION_OPS above).
Instance Method Details
#arithmetic(current, amount, target, sign) ⇒ Object
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/hecks/runtime/command_rules/arithmetic.rb', line 74 def arithmetic(current, amount, target, sign) op = sign.positive? ? "increment" : "decrement" current ||= 0 if current.is_a?(Value) && amount.is_a?(Value) return arithmetic_value_object(current, amount, target, sign, op) end # `current` genuinely absent (no declared default, never set) and # `amount` arrives VO-wrapped — a real command argument typed the # same as the attribute, but with nothing to combine field-by- # field against yet (that is what `arithmetic_value_object`, # above, is for once BOTH sides carry real fields). Before this, # falling straight to `unless amount.is_a?(Numeric)` below # refused with "increment needs an Integer, got 500" — true of # nothing: 500 is exactly the Integer it asked for, just still # wearing the Money wrapper the command's own declared attribute # type put it in. Unwrapped here, the same shape #clamp already # falls through to for an absent VO-typed attribute # (`current ||= 0`, then a raw scalar) — the mutation applier # re-wraps the raw result into the declared VO type on write, # the same way it already does for clamp's own result. amount = unwrap_single_numeric_field(amount) if amount.is_a?(Value) # Widened from Integer to Numeric (migration plan task 4, i106): # miette's organ math increments a Float (`increment: 0.02`) -- # the raw, non-value-object path only ever mattered for Integer # counters before this corpus existed. Integer stays the common # case; Float is now accepted the same way. unless amount.is_a?(Numeric) raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_amount", op: op, target: target, offered: Rendering.describe(amount)) end unless current.is_a?(Numeric) raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current", op: op, target: target, offered: Rendering.describe(current)) end current + (sign * amount) end |
#arithmetic_value_object(current, amount, target, sign, op) ⇒ Object
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/hecks/runtime/command_rules/arithmetic.rb', line 115 def arithmetic_value_object(current, amount, target, sign, op) current_fields = current.to_h amount_fields = amount.to_h # Widened from Integer to Numeric -- see #arithmetic's own # comment. A synthesised value-object wrapper around a bare # Float attribute (miette's Synapse#strength, auto-wrapped per # Part 3a's "bare primitives forbidden" finding) lands here as # a one-Float-field Value exactly the way a one-Integer-field # Value already did. shared_numeric = current_fields.keys.select do |field| current_fields[field].is_a?(Numeric) && amount_fields[field].is_a?(Numeric) end unless shared_numeric.size == 1 raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: op, target: target) end field = shared_numeric.first current.with(field, current[field] + (sign * amount[field])) end |
#clamp(current, bounds, target) ⇒ Object
Vendored addition, not (yet) upstream hecks (migration plan
task 4, i106): bound the CURRENT value into [min, max] -- no
"amount" to combine, so it does not go through
#arithmetic/#multiply's shared-numeric-field matching at all;
it clamps whichever single numeric field the wrapping value
object carries (a synthesised wrapper always carries exactly
one, per Part 3a's auto-synthesis).
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/hecks/runtime/command_rules/arithmetic.rb', line 186 def clamp(current, bounds, target) min, max = bounds # THE SAME `current ||= 0` #arithmetic/#multiply both give a # PHANTOM (never-set) numeric field, one line up from each — # this was the one arithmetic op that didn't, so a VO-typed # attribute with no declared `default:` (genuinely absent, # `Instance.defaults`/`#default_for`) hit TypeMismatch on the # FIRST clamp. (#arithmetic/#multiply's OWN absent-current gap # was a real, separate bug this comment used to describe wrong — # they did not "silently treat the same absent field as zero"; # they raised too, blaming a perfectly valid `amount` for not # being an Integer when it was one, just still Money-wrapped. # Fixed alongside this one — see #unwrap_single_numeric_field.) current ||= 0 if current.is_a?(Value) fields = current.to_h field = fields.keys.find { |f| fields[f].is_a?(Numeric) } or raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current", op: "clamp", target: target, offered: Rendering.describe(current)) return current.with(field, fields[field].clamp(min, max)) end unless current.is_a?(Numeric) raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current", op: "clamp", target: target, offered: Rendering.describe(current)) end current.clamp(min, max) end |
#multiply(current, amount, target) ⇒ Object
Vendored addition, not (yet) upstream hecks (migration plan
task 4, i106): current * amount -- the scaling counterpart to
increment/decrement's add/subtract. Same raw-vs-value-object
branch shape as #arithmetic/#arithmetic_value_object, reused
rather than duplicated verb-for-verb (a Proc picks the actual
arithmetic; everything else -- the Value unwrap/rewrap, the
TypeMismatch refusals -- is identical to the additive pair).
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/hecks/runtime/command_rules/arithmetic.rb', line 159 def multiply(current, amount, target) current ||= 0 if current.is_a?(Value) && amount.is_a?(Value) return combine_value_object(current, amount, target, "multiply") { |c, a| c * a } end # Same absent-`current`, VO-wrapped-`amount` gap as `#arithmetic` # — see that method's own comment. amount = unwrap_single_numeric_field(amount) if amount.is_a?(Value) unless amount.is_a?(Numeric) && current.is_a?(Numeric) raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_amount", op: "multiply", target: target, offered: Rendering.describe(current.is_a?(Numeric) ? amount : current)) end current * amount end |
#resolve_source(source, args) ⇒ Object
A mutation's source is either the NAME OF AN ARGUMENT or a LITERAL, and
the two are told apart by type : a Symbol is always a name, a String or a
number is always a value. Checked across all eight chapters — to: :name
and to: "sold", never a Symbol meant as a value.
&& args.key?(source) used to guard the lookup, and that guard is what
made an ABSENT argument fall through to source and return THE SYMBOL
ITSELF as the value. Customer.Register without its name set name to
the literal :name, coercion met a Symbol where a PersonName belonged,
and the refusal read "name is a PersonName — pass its fields as an
object, not :name" — a message describing a mistake the caller had not
made. The real mistake, an absent argument, was never the one refused,
which is what fuzz surfaced.
Absent now resolves to nil. Whether it should be REFUSED instead is a separate question — the language cannot yet say which arguments are optional, and the meta-domain has plenty that are.
68 69 70 71 72 |
# File 'lib/hecks/runtime/command_rules/arithmetic.rb', line 68 def resolve_source(source, args) return args[source] if source.is_a?(Symbol) source end |
#sign_of(op) ⇒ Object
Not a bare .find(...)&.sign || -1 — that silently answered
DECREMENT'S sign for BOTH an op this table has never heard of
AND a declared, real op that simply carries no sign at all
(set/append/multiply/clamp/remove — see MUTATION_OPS above).
Callers today only ever reach this for :increment/:decrement
(both MutationApplier#apply and EntityInterpreter#
apply_to_element gate every other op through their own case
first, each with its own loud WiringError backstop), so this
raise is not a real runtime path yet — it is the same
backstop one level down, in case a future caller reaches
#sign_of directly for an op that was never meant to have one.
147 148 149 150 |
# File 'lib/hecks/runtime/command_rules/arithmetic.rb', line 147 def sign_of(op) MUTATION_OPS.find { |candidate| candidate.name == op.to_s }&.sign || raise(WiringError, "no sign declared for mutation op #{op.inspect} — add one before calling #sign_of") end |