summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-22 12:41:00 +0300
committerPaul Buetow <paul@buetow.org>2026-05-22 12:41:00 +0300
commit75f5f402a22b302b85bd69fe9589b82e5caade4d (patch)
tree47df2ee0313ce7ccdd7c62180b2f253cde9ef3e2
parent9a68d3658e9d4f71121f90a1e0489898fea849f5 (diff)
Make hyper operators metric-aware for consistency
HyperAdd, HyperSubtract, and HyperModulo now validate that all operands share the same metric category (Cool absorbs), convert to base units for computation, and return the result with the appropriate metric. HyperMultiply, HyperDivide, HyperPower, HyperLog2, HyperLog10, and HyperLn use raw float64 values and always return Cool (unitless), since the semantic meaning of combining N arbitrary units via these operations is unclear. - Add validateSameCategory() helper for multi-operand category checks - Add resultMetricForHyperAdd() helper to find the result metric (first non-Cool, falling back to Cool) - Replace manual pop/reverse loops with popAll() for consistency - Add comprehensive tests for all 9 hyper operators with metrics
-rw-r--r--internal/rpn/operations_hyper.go347
-rw-r--r--internal/rpn/operations_hyper_test.go418
2 files changed, 591 insertions, 174 deletions
diff --git a/internal/rpn/operations_hyper.go b/internal/rpn/operations_hyper.go
index 5f4a51d..b0bb24b 100644
--- a/internal/rpn/operations_hyper.go
+++ b/internal/rpn/operations_hyper.go
@@ -11,317 +11,316 @@ import (
// Hyper operators - operate on all values on the stack
// HyperAdd pops all values from stack, adds them left-associative (with boolean-to-number coercion), 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 operand's metric.
func (o *Operations) HyperAdd(stack *Stack) error {
- values, err := popAll(stack, "hyperadd")
+ values, err := popAll(stack, "[+]")
if err != nil {
return err
}
- // Process left-associative with Number interface
- sum := 0.0
- for i := 0; i < len(values); i++ {
- val, err := values[i].Float64()
+ // Resolve metrics for all values
+ metrics := make([]*Metric, len(values))
+ for i, v := range values {
+ metrics[i] = resolveMetric(o.metricRegistry, v)
+ }
+
+ // Validate all are compatible (all same category, or Cool absorbs)
+ if err := validateSameCategory(metrics, "[+]"); err != nil {
+ return err
+ }
+
+ // 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)
if err != nil {
- return buildError("hyperadd", fmt.Errorf("failed to get float64 value: %w", err))
+ return buildError("[+]", fmt.Errorf("operand %d: %w", i, err))
}
- sum += val
+ sum += base
}
- mode := o.GetMode()
- stack.Push(NewNumber(sum, mode))
+
+ // Result metric: first non-Cool metric (Cool absorbs), or Cool
+ resultMetric := resultMetricForHyperAdd(metrics)
+ resultVal := convertFromBase(o.metricRegistry, sum, resultMetric, pm)
+
+ stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric))
return nil
}
// 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 {
- if stack.Len() < 2 {
- return fmt.Errorf("insufficient operands for hypermultiply: need at least 2 values")
+ values, err := popAll(stack, "[*]")
+ if err != nil {
+ return err
}
- product := 1.0
- for stack.Len() > 0 {
- val, err := stack.Pop()
+ var product float64 = 1
+ for i, v := range values {
+ val, err := v.Float64()
if err != nil {
- return fmt.Errorf("hypermultiply: %w", err)
+ return buildError("[*]", fmt.Errorf("operand %d: %w", i, err))
}
- floatVal, err := val.Float64()
- if err != nil {
- return fmt.Errorf("hypermultiply: failed to get float64 value: %w", err)
+ if i == 0 {
+ product = val
+ } else {
+ product *= val
}
- product *= floatVal
}
- mode := o.GetMode()
- stack.Push(NewNumber(product, mode))
+
+ cool := coolMetric(o.metricRegistry)
+ stack.Push(NewNumber(product, o.GetMode(), cool))
return nil
}
// HyperSubtract pops all values from stack, subtracts 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 operand's metric.
func (o *Operations) HyperSubtract(stack *Stack) error {
- values, err := popAll(stack, "hypersubtract")
+ values, err := popAll(stack, "[-]")
if err != nil {
return err
}
- // Process left-associative with Number interface
- firstVal, err := values[0].Float64()
+ // Resolve metrics for all values
+ metrics := make([]*Metric, len(values))
+ for i, v := range values {
+ metrics[i] = resolveMetric(o.metricRegistry, v)
+ }
+
+ // Validate all are compatible (all same category, or Cool absorbs)
+ if err := validateSameCategory(metrics, "[-]"); err != nil {
+ return err
+ }
+
+ // Convert all to base units, subtract, convert back
+ pm := o.GetPrefixMode()
+ firstBase, err := convertToBase(o.metricRegistry, values[0], pm)
if err != nil {
- return buildError("hypersubtract", fmt.Errorf("failed to get float64 value: %w", err))
+ return buildError("[-]", fmt.Errorf("operand 0: %w", err))
}
- result := firstVal
+ result := firstBase
for i := 1; i < len(values); i++ {
- val, err := values[i].Float64()
+ base, err := convertToBase(o.metricRegistry, values[i], pm)
if err != nil {
- return buildError("hypersubtract", fmt.Errorf("failed to get float64 value: %w", err))
+ return buildError("[-]", fmt.Errorf("operand %d: %w", i, err))
}
- result -= val
+ result -= base
}
- mode := o.GetMode()
- stack.Push(NewNumber(result, mode))
+
+ // Result metric: first non-Cool metric (Cool absorbs), or Cool
+ resultMetric := resultMetricForHyperAdd(metrics)
+ resultVal := convertFromBase(o.metricRegistry, result, resultMetric, pm)
+
+ stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric))
return nil
}
// HyperDivide pops all values from stack, divides them left-associative, and pushes result.
+// No metric validation; uses raw float64 values. Result is always Cool (unitless).
func (o *Operations) HyperDivide(stack *Stack) error {
- if stack.Len() < 2 {
- return fmt.Errorf("insufficient operands for hyperdivide: need at least 2 values")
- }
-
- // Pop all values into a slice (in reverse order - top first)
- var values []Number
- for stack.Len() > 0 {
- val, err := stack.Pop()
- if err != nil {
- return fmt.Errorf("hyperdivide: %w", err)
- }
- values = append(values, val)
- }
-
- // Reverse to get left-to-right order (first pushed = first in)
- for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 {
- values[i], values[j] = values[j], values[i]
+ values, err := popAll(stack, "[/]")
+ if err != nil {
+ return err
}
- // Process left-associative with Number interface
firstVal, err := values[0].Float64()
if err != nil {
- return fmt.Errorf("hyperdivide: failed to get float64 value: %w", err)
+ return buildError("[/]", fmt.Errorf("operand 0: %w", err))
}
result := firstVal
for i := 1; i < len(values); i++ {
val, err := values[i].Float64()
if err != nil {
- return fmt.Errorf("hyperdivide: failed to get float64 value: %w", err)
+ return buildError("[/]", fmt.Errorf("operand %d: %w", i, err))
}
if val == 0 {
- return fmt.Errorf("division by zero")
+ return buildError("[/]", fmt.Errorf("division by zero at operand %d", i))
}
result /= val
}
- mode := o.GetMode()
- stack.Push(NewNumber(result, mode))
+
+ cool := coolMetric(o.metricRegistry)
+ stack.Push(NewNumber(result, o.GetMode(), cool))
return nil
}
// HyperPower pops all values from stack, raises to power left-associative, and pushes result.
+// No metric validation; uses raw float64 values. Result is always Cool (unitless).
func (o *Operations) HyperPower(stack *Stack) error {
- if stack.Len() < 2 {
- return fmt.Errorf("insufficient operands for hyperpower: need at least 2 values")
- }
-
- // Pop all values into a slice (in reverse order - top first)
- var values []Number
- for stack.Len() > 0 {
- val, err := stack.Pop()
- if err != nil {
- return fmt.Errorf("hyperpower: %w", err)
- }
- values = append(values, val)
- }
-
- // Reverse to get left-to-right order (first pushed = first in)
- for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 {
- values[i], values[j] = values[j], values[i]
+ values, err := popAll(stack, "[^]")
+ if err != nil {
+ return err
}
- // Process left-associative with Number interface
firstVal, err := values[0].Float64()
if err != nil {
- return fmt.Errorf("hyperpower: failed to get float64 value: %w", err)
+ return buildError("[^]", fmt.Errorf("operand 0: %w", err))
}
result := firstVal
for i := 1; i < len(values); i++ {
val, err := values[i].Float64()
if err != nil {
- return fmt.Errorf("hyperpower: failed to get float64 value: %w", err)
+ return buildError("[^]", fmt.Errorf("operand %d: %w", i, err))
}
result = math.Pow(result, val)
}
- mode := o.GetMode()
- stack.Push(NewNumber(result, mode))
+
+ cool := coolMetric(o.metricRegistry)
+ stack.Push(NewNumber(result, o.GetMode(), cool))
return nil
}
// HyperModulo pops all values from stack, computes modulo 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 operand's metric.
func (o *Operations) HyperModulo(stack *Stack) error {
- if stack.Len() < 2 {
- return fmt.Errorf("insufficient operands for hypermodulo: need at least 2 values")
+ values, err := popAll(stack, "[%]")
+ if err != nil {
+ return err
}
- // Pop all values into a slice (in reverse order - top first)
- var values []Number
- for stack.Len() > 0 {
- val, err := stack.Pop()
- if err != nil {
- return fmt.Errorf("hypermodulo: %w", err)
- }
- values = append(values, val)
+ // Resolve metrics for all values
+ metrics := make([]*Metric, len(values))
+ for i, v := range values {
+ metrics[i] = resolveMetric(o.metricRegistry, v)
}
- // Reverse to get left-to-right order (first pushed = first in)
- for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 {
- values[i], values[j] = values[j], values[i]
+ // Validate all are compatible (all same category, or Cool absorbs)
+ if err := validateSameCategory(metrics, "[%]"); err != nil {
+ return err
}
- // Process left-associative with Number interface
- firstVal, err := values[0].Float64()
+ // Convert all to base units, compute modulo, convert back
+ pm := o.GetPrefixMode()
+ firstBase, err := convertToBase(o.metricRegistry, values[0], pm)
if err != nil {
- return fmt.Errorf("hypermodulo: failed to get float64 value: %w", err)
+ return buildError("[%]", fmt.Errorf("operand 0: %w", err))
}
- result := firstVal
+ result := firstBase
for i := 1; i < len(values); i++ {
- val, err := values[i].Float64()
+ base, err := convertToBase(o.metricRegistry, values[i], pm)
if err != nil {
- return fmt.Errorf("hypermodulo: failed to get float64 value: %w", err)
+ return buildError("[%]", fmt.Errorf("operand %d: %w", i, err))
}
- if val == 0 {
- return fmt.Errorf("modulo by zero")
+ if base == 0 {
+ return buildError("[%]", fmt.Errorf("modulo by zero at operand %d", i))
}
- result = math.Mod(result, val)
+ result = math.Mod(result, base)
}
- mode := o.GetMode()
- stack.Push(NewNumber(result, mode))
+
+ // Result metric: first non-Cool metric (Cool absorbs), or Cool
+ resultMetric := resultMetricForHyperAdd(metrics)
+ resultVal := convertFromBase(o.metricRegistry, result, resultMetric, pm)
+
+ stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric))
return nil
}
// HyperLog2 pops all values from stack, computes sum of log2 for all values, and pushes result.
-// This follows the same pattern as HyperAdd (sum) and HyperMultiply (product).
+// No metric validation; uses raw float64 values. Result is always Cool (unitless).
func (o *Operations) HyperLog2(stack *Stack) error {
- if stack.Len() < 2 {
- return fmt.Errorf("insufficient operands for hyperlog2: need at least 2 values")
- }
-
- // Pop all values into a slice (in reverse order - top first)
- var values []Number
- for stack.Len() > 0 {
- val, err := stack.Pop()
- if err != nil {
- return fmt.Errorf("hyperlog2: %w", err)
- }
- values = append(values, val)
- }
-
- // Reverse to get left-to-right order (first pushed = first in)
- for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 {
- values[i], values[j] = values[j], values[i]
+ values, err := popAll(stack, "[lg]")
+ if err != nil {
+ return err
}
- // Sum the log2 of all values using Float64() for value conversion:
- // - true → 1, false → 0
var result float64 = 0
for i := 0; i < len(values); i++ {
val, err := values[i].Float64()
if err != nil {
- return fmt.Errorf("hyperlog2: failed to get float64 value: %w", err)
+ return buildError("[lg]", fmt.Errorf("operand %d: %w", i, err))
}
if val <= 0 {
- return fmt.Errorf("hyperlog2 undefined for non-positive numbers")
+ return buildError("[lg]", fmt.Errorf("log2 undefined for non-positive numbers"))
}
result += math.Log2(val)
}
- // Push the result as a Number
- mode := o.GetMode()
- stack.Push(NewNumber(result, mode))
+ cool := coolMetric(o.metricRegistry)
+ stack.Push(NewNumber(result, o.GetMode(), cool))
return nil
}
// HyperLog10 pops all values from stack, computes sum of log10 for all values, and pushes result.
-// This follows the same pattern as HyperAdd (sum) and HyperMultiply (product).
+// No metric validation; uses raw float64 values. Result is always Cool (unitless).
func (o *Operations) HyperLog10(stack *Stack) error {
- if stack.Len() < 2 {
- return fmt.Errorf("insufficient operands for hyperlog10: need at least 2 values")
- }
-
- // Pop all values into a slice (in reverse order - top first)
- var values []Number
- for stack.Len() > 0 {
- val, err := stack.Pop()
- if err != nil {
- return fmt.Errorf("hyperlog10: %w", err)
- }
- values = append(values, val)
- }
-
- // Reverse to get left-to-right order (first pushed = first in)
- for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 {
- values[i], values[j] = values[j], values[i]
+ values, err := popAll(stack, "[log]")
+ if err != nil {
+ return err
}
- // Sum the log10 of all values using Float64() for value conversion:
- // - true → 1, false → 0
var result float64 = 0
for i := 0; i < len(values); i++ {
val, err := values[i].Float64()
if err != nil {
- return fmt.Errorf("hyperlog10: failed to get float64 value: %w", err)
+ return buildError("[log]", fmt.Errorf("operand %d: %w", i, err))
}
if val <= 0 {
- return fmt.Errorf("hyperlog10 undefined for non-positive numbers")
+ return buildError("[log]", fmt.Errorf("log10 undefined for non-positive numbers"))
}
result += math.Log10(val)
}
- // Push the result as a Number
- mode := o.GetMode()
- stack.Push(NewNumber(result, mode))
+ cool := coolMetric(o.metricRegistry)
+ stack.Push(NewNumber(result, o.GetMode(), cool))
return nil
}
// HyperLn pops all values from stack, computes sum of natural log for all values, and pushes result.
-// This follows the same pattern as HyperAdd (sum) and HyperMultiply (product).
+// No metric validation; uses raw float64 values. Result is always Cool (unitless).
func (o *Operations) HyperLn(stack *Stack) error {
- if stack.Len() < 2 {
- return fmt.Errorf("insufficient operands for hyperln: need at least 2 values")
- }
-
- // Pop all values into a slice (in reverse order - top first)
- var values []Number
- for stack.Len() > 0 {
- val, err := stack.Pop()
- if err != nil {
- return fmt.Errorf("hyperln: %w", err)
- }
- values = append(values, val)
- }
-
- // Reverse to get left-to-right order (first pushed = first in)
- for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 {
- values[i], values[j] = values[j], values[i]
+ values, err := popAll(stack, "[ln]")
+ if err != nil {
+ return err
}
- // Sum the natural log of all values using Float64() for value conversion:
- // - true → 1, false → 0
var result float64 = 0
for i := 0; i < len(values); i++ {
val, err := values[i].Float64()
if err != nil {
- return fmt.Errorf("hyperln: failed to get float64 value: %w", err)
+ return buildError("[ln]", fmt.Errorf("operand %d: %w", i, err))
}
if val <= 0 {
- return fmt.Errorf("hyperln undefined for non-positive numbers")
+ return buildError("[ln]", fmt.Errorf("ln undefined for non-positive numbers"))
}
result += math.Log(val)
}
- mode := o.GetMode()
- stack.Push(NewNumber(result, mode))
+
+ cool := coolMetric(o.metricRegistry)
+ stack.Push(NewNumber(result, o.GetMode(), cool))
+ return nil
+}
+
+// resultMetricForHyperAdd finds the appropriate result metric for add/subtract/modulo hyper operations.
+// When Cool absorbs a non-Cool category, use the first non-Cool metric.
+// When all are Cool, use Cool.
+func resultMetricForHyperAdd(metrics []*Metric) *Metric {
+ for _, m := range metrics {
+ if m != nil && m.Category != Universal {
+ return m
+ }
+ }
+ return metrics[0]
+}
+
+// validateSameCategory checks that all metrics belong to the same category.
+// Cool (Universal) absorbs — it is compatible with any single non-Universal category.
+// Returns an error if metrics span multiple non-Universal categories.
+func validateSameCategory(metrics []*Metric, opName string) error {
+ var dominantCat Category = Universal
+ for _, m := range metrics {
+ if m == nil || m.Category == Universal {
+ continue
+ }
+ if dominantCat == Universal {
+ dominantCat = m.Category
+ } else if m.Category != dominantCat {
+ return fmt.Errorf("%s: incompatible metrics: mixed %s and %s categories",
+ opName, dominantCat, m.Category)
+ }
+ }
return nil
}
diff --git a/internal/rpn/operations_hyper_test.go b/internal/rpn/operations_hyper_test.go
new file mode 100644
index 0000000..700614f
--- /dev/null
+++ b/internal/rpn/operations_hyper_test.go
@@ -0,0 +1,418 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Paul Buetow
+
+package rpn
+
+import (
+ "math"
+ "strconv"
+ "testing"
+)
+
+func TestHyperAddMetricAware(t *testing.T) {
+ reg := GetMetricRegistry()
+ tolerance := 0.001
+
+ // Same category: [100Mbps 50Mbps 25Mbps [+] ] = 175Mbps
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("100Mbps 50Mbps 25Mbps [+]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 175-tolerance || resultVal > 175+tolerance {
+ t.Errorf("result = %g, want 175", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ mbps, _ := reg.Find("Mbps")
+ if m != mbps {
+ t.Errorf("metric = %v, want Mbps", m)
+ }
+}
+
+func TestHyperAddCoolAbsorbing(t *testing.T) {
+ reg := GetMetricRegistry()
+ tolerance := 0.001
+
+ // 5 (Cool) 100Mbps 10Mbps [+] ≈ 110.000005Mbps
+ // Cool (factor 1.0) contributes 5 base units (bps) which is negligible
+ // Result metric is Mbps (first non-Cool), value ≈ 110Mbps
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("5 100Mbps 10Mbps [+]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 110-tolerance || resultVal > 110+tolerance {
+ t.Errorf("result = %g, want ~110", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ mbps, _ := reg.Find("Mbps")
+ if m != mbps {
+ t.Errorf("metric = %v, want Mbps", m)
+ }
+}
+
+func TestHyperAddIncompatible(t *testing.T) {
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ _, err := rpn.ParseAndEvaluate("100Mbps 2hr [+]")
+ if err == nil {
+ t.Error("expected error for incompatible categories")
+ }
+}
+
+func TestHyperAddMixedUnitsSameCategory(t *testing.T) {
+ reg := GetMetricRegistry()
+ tolerance := 0.001
+
+ // 1km 500m 100m [+] = 1600m converted back to km = 1.6km
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("1km 500m 100m [+]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 1.6-tolerance || resultVal > 1.6+tolerance {
+ t.Errorf("result = %g, want 1.6", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ km, _ := reg.Find("km")
+ if m != km {
+ t.Errorf("metric = %v, want km", m)
+ }
+}
+
+func TestHyperMultiplyMetricResultIsCool(t *testing.T) {
+ reg := GetMetricRegistry()
+
+ // 3 4 5 [*] = 60 with Cool metric
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("3 4 5 [*]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal != 60 {
+ t.Errorf("result = %g, want 60", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ cool, _ := reg.Find("Cool")
+ if m != cool {
+ t.Errorf("metric = %v, want Cool", m)
+ }
+}
+
+func TestHyperMultiplyMixedMetricsIsCool(t *testing.T) {
+ reg := GetMetricRegistry()
+
+ // 100Mbps 2hr [*] = 200 (raw product, Cool metric)
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("100Mbps 2hr [*]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal != 200 {
+ t.Errorf("result = %g, want 200", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ cool, _ := reg.Find("Cool")
+ if m != cool {
+ t.Errorf("metric = %v, want Cool", m)
+ }
+}
+
+func TestHyperSubtractMetricAware(t *testing.T) {
+ reg := GetMetricRegistry()
+ tolerance := 0.001
+
+ // 1000Mbps 100Mbps 50Mbps [-] = 850Mbps
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("1000Mbps 100Mbps 50Mbps [-]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 850-tolerance || resultVal > 850+tolerance {
+ t.Errorf("result = %g, want 850", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ mbps, _ := reg.Find("Mbps")
+ if m != mbps {
+ t.Errorf("metric = %v, want Mbps", m)
+ }
+}
+
+func TestHyperSubtractMixedUnits(t *testing.T) {
+ tolerance := 0.001
+
+ // 2km 500m 100m [-] = (2000 - 500 - 100)m = 1400m = 1.4km
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("2km 500m 100m [-]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 1.4-tolerance || resultVal > 1.4+tolerance {
+ t.Errorf("result = %g, want 1.4", resultVal)
+ }
+}
+
+func TestHyperSubtractIncompatible(t *testing.T) {
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ _, err := rpn.ParseAndEvaluate("100km 2hr [-]")
+ if err == nil {
+ t.Error("expected error for incompatible categories")
+ }
+}
+
+func TestHyperDivideResultIsCool(t *testing.T) {
+ reg := GetMetricRegistry()
+
+ // 100 20 2 [/] = (100 / 20) / 2 = 2.5 with Cool metric
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("100 20 2 [/]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal != 2.5 {
+ t.Errorf("result = %g, want 2.5", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ cool, _ := reg.Find("Cool")
+ if m != cool {
+ t.Errorf("metric = %v, want Cool", m)
+ }
+}
+
+func TestHyperPowerResultIsCool(t *testing.T) {
+ reg := GetMetricRegistry()
+
+ // 2 3 2 [^] = (2^3)^2 = 64 with Cool metric
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("2 3 2 [^]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal != 64 {
+ t.Errorf("result = %g, want 64", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ cool, _ := reg.Find("Cool")
+ if m != cool {
+ t.Errorf("metric = %v, want Cool", m)
+ }
+}
+
+func TestHyperModuloMetricAware(t *testing.T) {
+ reg := GetMetricRegistry()
+ tolerance := 0.001
+
+ // 10km 3km 2km [%] = ((10 % 3) % 2)km = (1 % 2)km = 1km
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("10km 3km 2km [%]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 1-tolerance || resultVal > 1+tolerance {
+ t.Errorf("result = %g, want 1", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ km, _ := reg.Find("km")
+ if m != km {
+ t.Errorf("metric = %v, want km", m)
+ }
+}
+
+func TestHyperModuloMixedUnits(t *testing.T) {
+ tolerance := 0.001
+
+ // 1000m 300m 200m [%] = ((1000 % 300) % 200)m = (100 % 200)m = 100m = 0.1km
+ // Result uses first operand's metric (m)
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("1000m 300m 200m [%]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 100-tolerance || resultVal > 100+tolerance {
+ t.Errorf("result = %g, want 100", resultVal)
+ }
+}
+
+func TestHyperModuloIncompatible(t *testing.T) {
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ _, err := rpn.ParseAndEvaluate("100km 2hr [%]")
+ if err == nil {
+ t.Error("expected error for incompatible categories")
+ }
+}
+
+func TestHyperLog2CoolResult(t *testing.T) {
+ reg := GetMetricRegistry()
+ tolerance := 0.001
+
+ // [lg] on all values → sum of log2: log2(2) + log2(4) + log2(8) = 1 + 2 + 3 = 6 → Cool
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("2 4 8 [lg]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 6-tolerance || resultVal > 6+tolerance {
+ t.Errorf("result = %g, want 6", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ cool, _ := reg.Find("Cool")
+ if m != cool {
+ t.Errorf("metric = %v, want Cool", m)
+ }
+}
+
+func TestHyperLog10CoolResult(t *testing.T) {
+ reg := GetMetricRegistry()
+ tolerance := 0.001
+
+ // [log] on all values → sum of log10: log10(10) + log10(100) = 1 + 2 = 3 → Cool
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("10 100 [log]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 3-tolerance || resultVal > 3+tolerance {
+ t.Errorf("result = %g, want 3", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ cool, _ := reg.Find("Cool")
+ if m != cool {
+ t.Errorf("metric = %v, want Cool", m)
+ }
+}
+
+func TestHyperLnCoolResult(t *testing.T) {
+ reg := GetMetricRegistry()
+ tolerance := 0.001
+
+ // [ln] on all values → sum of ln: ln(e) + ln(e*e) = 1 + 2 = 3 → Cool
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("2.718281828459045 7.38905609893065 [ln]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 3-tolerance || resultVal > 3+tolerance {
+ t.Errorf("result = %g, want 3", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ cool, _ := reg.Find("Cool")
+ if m != cool {
+ t.Errorf("metric = %v, want Cool", m)
+ }
+}
+
+func TestHyperLogWithMetrics(t *testing.T) {
+ reg := GetMetricRegistry()
+
+ // [lg] with metrics still uses raw values and returns Cool
+ // log2(100) + log2(1000) ≈ 6.64 + 9.97 ≈ 16.61
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("100Mbps 1000Mbps [lg]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ expected := math.Log2(100) + math.Log2(1000)
+ tolerance := 0.01
+ if resultVal < expected-tolerance || resultVal > expected+tolerance {
+ t.Errorf("result = %g, want ~%g", resultVal, expected)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ cool, _ := reg.Find("Cool")
+ if m != cool {
+ t.Errorf("metric = %v, want Cool", m)
+ }
+}
+
+func TestHyperAddAllCool(t *testing.T) {
+ reg := GetMetricRegistry()
+
+ // 1 2 3 [+] with no metrics = 6 Cool
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("1 2 3 [+]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal != 6 {
+ t.Errorf("result = %g, want 6", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ cool, _ := reg.Find("Cool")
+ if m != cool {
+ t.Errorf("metric = %v, want Cool", m)
+ }
+}
+
+func TestHyperModuloByZero(t *testing.T) {
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ _, err := rpn.ParseAndEvaluate("10km 0km [%]")
+ if err == nil {
+ t.Error("expected error for modulo by zero")
+ }
+}
+
+func TestHyperDivideByZero(t *testing.T) {
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ _, err := rpn.ParseAndEvaluate("10 0 [/]")
+ if err == nil {
+ t.Error("expected error for division by zero")
+ }
+}
+
+func TestHyperLogNonPositive(t *testing.T) {
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ _, err := rpn.ParseAndEvaluate("0 5 [lg]")
+ if err == nil {
+ t.Error("expected error for log2 of non-positive number")
+ }
+}