summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-24 12:25:58 +0300
committerPaul Buetow <paul@buetow.org>2026-05-24 12:25:58 +0300
commita864beae16f3a1deaf6d7e29d573899e8aedf960 (patch)
tree24f2395b4bfd525297967b5404aa262bd73e2705
parentd2b9a38a8a24b7d8627e6a8b3fb005ac42f663c5 (diff)
rpn: extract nAryMetricOp and nAryScalarOp helpers in hyper ops
Extract shared patterns from hyper operators: - nAryMetricOp: metric resolution, category validation, base unit conversion, binary fn application, result conversion (used by HyperAdd, HyperSubtract) - nAryScalarOp: applies binary fn to pre-converted float64 values and pushes with Cool metric (used by HyperDivide, HyperPower) HyperMultiply stays inline (unique init-to-1 pattern). HyperModulo stays inline (metric-aware with modulo-by-zero check). Reduces operations_hyper.go from ~300 to ~268 lines.
-rw-r--r--internal/rpn/operations_hyper.go186
1 files changed, 77 insertions, 109 deletions
diff --git a/internal/rpn/operations_hyper.go b/internal/rpn/operations_hyper.go
index 34b520b..b446bd3 100644
--- a/internal/rpn/operations_hyper.go
+++ b/internal/rpn/operations_hyper.go
@@ -9,55 +9,82 @@ import (
"math"
)
-// Hyper operators - operate on all values on the stack
-
-// HyperAdd pops all values from stack, adds them left-associative, and pushes result.
-// Metric-aware: validates all operands share the same category (Cool absorbs), converts to base units for the
-// computation, and pushes the result with the first non-Cool metric (or Cool if all are Cool).
-func (o *Operations) HyperAdd(stack *Stack) error {
- values, err := popAll(stack, "[+]")
+// nAryMetricOp handles metric-aware n-ary operations.
+// Resolves metrics, validates categories, converts to base units,
+// applies fn left-associatively, converts back, and pushes the result.
+func (o *Operations) nAryMetricOp(stack *Stack, opName string, fn func(float64, float64) float64) error {
+ values, err := popAll(stack, opName)
if err != nil {
return err
}
+ if len(values) == 0 {
+ return buildError(opName, fmt.Errorf("need at least one operand"))
+ }
- // Resolve metrics for all values
metrics := make([]*Metric, len(values))
for i, v := range values {
m, err := resolveMetric(o.metricRegistry, v)
if err != nil {
- return buildError("[+]", err)
+ return buildError(opName, err)
}
metrics[i] = m
}
- // Validate all are compatible (all same category, or Cool absorbs)
- if err := validateCategories(metrics, "[+]"); err != nil {
+ if err := validateCategories(metrics, opName); err != nil {
return err
}
- // Result metric: first non-Cool metric (Cool absorbs), or Cool
resultMetric := resultMetricForAdd(metrics)
-
- // Convert all to base units, sum, convert back
pm := o.GetPrefixMode()
- var sum float64
- for i, v := range values {
- base, err := convertToBase(o.metricRegistry, v, pm, resultMetric)
+
+ firstBase, err := convertToBase(o.metricRegistry, values[0], pm, resultMetric)
+ if err != nil {
+ return buildError(opName, fmt.Errorf("operand 0: %w", err))
+ }
+ result := firstBase
+ for i := 1; i < len(values); i++ {
+ base, err := convertToBase(o.metricRegistry, values[i], pm, resultMetric)
if err != nil {
- return buildError("[+]", fmt.Errorf("operand %d: %w", i, err))
+ return buildError(opName, fmt.Errorf("operand %d: %w", i, err))
}
- sum += base
+ result = fn(result, base)
}
- resultVal, err := convertFromBase(o.metricRegistry, sum, resultMetric, pm)
+ resultVal, err := convertFromBase(o.metricRegistry, result, resultMetric, pm)
if err != nil {
- return buildError("[+]", err)
+ return buildError(opName, err)
}
stack.Push(NewNumberWithMetric(resultVal, o.GetMode(), resultMetric))
return nil
}
+// nAryScalarOp applies a binary function to pre-converted float64 values
+// and pushes the result with Cool metric.
+func (o *Operations) nAryScalarOp(stack *Stack, opName string, floats []float64, fn func(float64, float64) float64) error {
+ if len(floats) == 0 {
+ return buildError(opName, fmt.Errorf("need at least one operand"))
+ }
+ result := floats[0]
+ for i := 1; i < len(floats); i++ {
+ result = fn(result, floats[i])
+ }
+
+ cool, err := coolMetric(o.metricRegistry)
+ if err != nil {
+ return buildError(opName, err)
+ }
+ stack.Push(NewNumberWithMetric(result, o.GetMode(), cool))
+ return nil
+}
+
+// HyperAdd pops all values from stack, adds them left-associative, and pushes result.
+// Metric-aware: validates all operands share the same category (Cool absorbs), converts to base units for the
+// computation, and pushes the result with the first non-Cool metric (or Cool if all are Cool).
+func (o *Operations) HyperAdd(stack *Stack) error {
+ return o.nAryMetricOp(stack, "[+]", func(a, b float64) float64 { return a + b })
+}
+
// HyperMultiply pops all values from stack, multiplies them left-associative, and pushes result.
// No metric validation; uses raw float64 values. Result is always Cool (unitless).
func (o *Operations) HyperMultiply(stack *Stack) error {
@@ -66,12 +93,17 @@ func (o *Operations) HyperMultiply(stack *Stack) error {
return err
}
- var product float64 = 1
+ floats := make([]float64, len(values))
for i, v := range values {
val, err := toFloat64(v, "[*]")
if err != nil {
return buildError("[*]", fmt.Errorf("operand %d: %w", i, err))
}
+ floats[i] = val
+ }
+
+ var product float64 = 1
+ for i, val := range floats {
if i == 0 {
product = val
} else {
@@ -91,51 +123,7 @@ func (o *Operations) HyperMultiply(stack *Stack) error {
// Metric-aware: validates all operands share the same category (Cool absorbs), converts to base units for the
// computation, and pushes the result with the first non-Cool metric (or Cool if all are Cool).
func (o *Operations) HyperSubtract(stack *Stack) error {
- values, err := popAll(stack, "[-]")
- if err != nil {
- return err
- }
-
- // Resolve metrics for all values
- metrics := make([]*Metric, len(values))
- for i, v := range values {
- m, err := resolveMetric(o.metricRegistry, v)
- if err != nil {
- return buildError("[-]", err)
- }
- metrics[i] = m
- }
-
- // Validate all are compatible (all same category, or Cool absorbs)
- if err := validateCategories(metrics, "[-]"); err != nil {
- return err
- }
-
- // Result metric: first non-Cool metric (Cool absorbs), or Cool
- resultMetric := resultMetricForAdd(metrics)
-
- // Convert all to base units, subtract, convert back
- pm := o.GetPrefixMode()
- firstBase, err := convertToBase(o.metricRegistry, values[0], pm, resultMetric)
- if err != nil {
- return buildError("[-]", fmt.Errorf("operand 0: %w", err))
- }
- result := firstBase
- for i := 1; i < len(values); i++ {
- base, err := convertToBase(o.metricRegistry, values[i], pm, resultMetric)
- if err != nil {
- return buildError("[-]", fmt.Errorf("operand %d: %w", i, err))
- }
- result -= base
- }
-
- resultVal, err := convertFromBase(o.metricRegistry, result, resultMetric, pm)
- if err != nil {
- return buildError("[-]", err)
- }
-
- stack.Push(NewNumberWithMetric(resultVal, o.GetMode(), resultMetric))
- return nil
+ return o.nAryMetricOp(stack, "[-]", func(a, b float64) float64 { return a - b })
}
// HyperDivide pops all values from stack, divides them left-associative, and pushes result.
@@ -146,28 +134,19 @@ func (o *Operations) HyperDivide(stack *Stack) error {
return err
}
- firstVal, err := toFloat64(values[0], "[/]")
- if err != nil {
- return buildError("[/]", fmt.Errorf("operand 0: %w", err))
- }
- result := firstVal
- for i := 1; i < len(values); i++ {
- val, err := toFloat64(values[i], "[/]")
+ floats := make([]float64, len(values))
+ for i, v := range values {
+ val, err := toFloat64(v, "[/]")
if err != nil {
return buildError("[/]", fmt.Errorf("operand %d: %w", i, err))
}
- if val == 0 {
+ if i > 0 && val == 0 {
return buildError("[/]", fmt.Errorf("division by zero at operand %d", i))
}
- result /= val
+ floats[i] = val
}
- cool, err := coolMetric(o.metricRegistry)
- if err != nil {
- return buildError("[/]", err)
- }
- stack.Push(NewNumberWithMetric(result, o.GetMode(), cool))
- return nil
+ return o.nAryScalarOp(stack, "[/]", floats, func(a, b float64) float64 { return a / b })
}
// HyperPower pops all values from stack, raises to power left-associative, and pushes result.
@@ -178,25 +157,16 @@ func (o *Operations) HyperPower(stack *Stack) error {
return err
}
- firstVal, err := toFloat64(values[0], "[^]")
- if err != nil {
- return buildError("[^]", fmt.Errorf("operand 0: %w", err))
- }
- result := firstVal
- for i := 1; i < len(values); i++ {
- val, err := toFloat64(values[i], "[^]")
+ floats := make([]float64, len(values))
+ for i, v := range values {
+ val, err := toFloat64(v, "[^]")
if err != nil {
return buildError("[^]", fmt.Errorf("operand %d: %w", i, err))
}
- result = math.Pow(result, val)
+ floats[i] = val
}
- cool, err := coolMetric(o.metricRegistry)
- if err != nil {
- return buildError("[^]", err)
- }
- stack.Push(NewNumberWithMetric(result, o.GetMode(), cool))
- return nil
+ return o.nAryScalarOp(stack, "[^]", floats, math.Pow)
}
// HyperModulo pops all values from stack, computes modulo left-associative, and pushes result.
@@ -218,30 +188,28 @@ func (o *Operations) HyperModulo(stack *Stack) error {
metrics[i] = m
}
- // Validate all are compatible (all same category, or Cool absorbs)
if err := validateCategories(metrics, "[%]"); err != nil {
return err
}
- // Result metric: first non-Cool metric (Cool absorbs), or Cool
resultMetric := resultMetricForAdd(metrics)
-
- // Convert all to base units, compute modulo, convert back
pm := o.GetPrefixMode()
- firstBase, err := convertToBase(o.metricRegistry, values[0], pm, resultMetric)
- if err != nil {
- return buildError("[%]", fmt.Errorf("operand 0: %w", err))
- }
- result := firstBase
- for i := 1; i < len(values); i++ {
- base, err := convertToBase(o.metricRegistry, values[i], pm, resultMetric)
+
+ bases := make([]float64, len(values))
+ for i, v := range values {
+ base, err := convertToBase(o.metricRegistry, v, pm, resultMetric)
if err != nil {
return buildError("[%]", fmt.Errorf("operand %d: %w", i, err))
}
- if base == 0 {
+ if i > 0 && base == 0 {
return buildError("[%]", fmt.Errorf("modulo by zero at operand %d", i))
}
- result = math.Mod(result, base)
+ bases[i] = base
+ }
+
+ result := bases[0]
+ for i := 1; i < len(bases); i++ {
+ result = math.Mod(result, bases[i])
}
resultVal, err := convertFromBase(o.metricRegistry, result, resultMetric, pm)