//go:build enable_antithesis_sdk package assert import ( "math" "sync" "github.com/antithesishq/antithesis-sdk-go/internal" ) // -------------------------------------------------------------------------------- // IntegerGap is used for: // - int, int8, int16, int32, int64: // - uint, uint8, uint16, uint32, uint64, uintptr: // // FloatGap is used for: // - float32, float64 // -------------------------------------------------------------------------------- type numericGapType int const ( integerGapType numericGapType = iota floatGapType ) func gapTypeForOperand[T Number](num T) numericGapType { gapType := integerGapType switch any(num).(type) { case float32, float64: gapType = floatGapType } return gapType } // -------------------------------------------------------------------------------- // numericGuidanceTracker - Tracking Info for Numeric Guidance // // For GuidanceFnMaximize: // - gap is the largest value sent so far // // For GuidanceFnMinimize: // - gap is the most negative value sent so far // // -------------------------------------------------------------------------------- type numericGuidanceInfo struct { gap any descriminator numericGapType maximize bool } type numericGuidanceTracker map[string]*numericGuidanceInfo var ( numeric_guidance_tracker numericGuidanceTracker = make(numericGuidanceTracker) numeric_guidance_tracker_mutex sync.Mutex numeric_guidance_info_mutex sync.Mutex ) func (tracker numericGuidanceTracker) getTrackerEntry(messageKey string, trackerType numericGapType, maximize bool) *numericGuidanceInfo { var trackerEntry *numericGuidanceInfo var ok bool if tracker == nil { return nil } numeric_guidance_tracker_mutex.Lock() defer numeric_guidance_tracker_mutex.Unlock() if trackerEntry, ok = numeric_guidance_tracker[messageKey]; !ok { trackerEntry = newNumericGuidanceInfo(trackerType, maximize) tracker[messageKey] = trackerEntry } return trackerEntry } // Create an numeric guidance entry func newNumericGuidanceInfo(trackerType numericGapType, maximize bool) *numericGuidanceInfo { var gap any if trackerType == integerGapType { gap = newGapValue(uint64(math.MaxUint64), maximize) } else { gap = newGapValue(float64(math.MaxFloat64), maximize) } trackerInfo := numericGuidanceInfo{ maximize: maximize, descriminator: trackerType, gap: gap, } return &trackerInfo } func (tI *numericGuidanceInfo) should_maximize() bool { return tI.maximize } func (tI *numericGuidanceInfo) is_integer_gap() bool { return tI.descriminator == integerGapType } // -------------------------------------------------------------------------------- // Represents integral and floating point extremes // -------------------------------------------------------------------------------- type gapValue[T numConstraint] struct { gap_size T gap_is_negative bool } func newGapValue[T numConstraint](sz T, is_neg bool) any { switch any(sz).(type) { case uint64: return gapValue[uint64]{gap_size: uint64(sz), gap_is_negative: is_neg} case float64: return gapValue[float64]{gap_size: float64(sz), gap_is_negative: is_neg} } return nil } func is_same_sign(left_val int64, right_val int64) bool { same_sign := false if left_val < 0 { // left is negative if right_val < 0 { same_sign = true } } else { // left is non-negative if right_val >= 0 { same_sign = true } } return same_sign } func abs_int64(val int64) uint64 { if val >= 0 { return uint64(val) } return uint64(0 - val) } func is_greater_than[T numConstraint](left gapValue[T], right gapValue[T]) bool { if !left.gap_is_negative && !right.gap_is_negative { return left.gap_size > right.gap_size } if !left.gap_is_negative && right.gap_is_negative { return true // any positive is greater than a negative } if left.gap_is_negative && right.gap_is_negative { return right.gap_size > left.gap_size } if left.gap_is_negative && !right.gap_is_negative { return false // any negative is less than a positive } return false } func is_less_than[T numConstraint](left gapValue[T], right gapValue[T]) bool { if !left.gap_is_negative && !right.gap_is_negative { return left.gap_size < right.gap_size } if !left.gap_is_negative && right.gap_is_negative { return false // any positive is greater than a negative } if left.gap_is_negative && right.gap_is_negative { return right.gap_size < left.gap_size } if left.gap_is_negative && !right.gap_is_negative { return true // any negative is less than a positive } return true } func send_value_if_needed(tI *numericGuidanceInfo, gI *guidanceInfo) { if tI == nil { return } numeric_guidance_info_mutex.Lock() defer numeric_guidance_info_mutex.Unlock() // if this is a catalog entry (gI.hit is false) // do not update the reference gap in the tracker (tI *numericGuidanceInfo) if !gI.Hit { emitGuidance(gI) return } should_send := false maximize := tI.should_maximize() var gap gapValue[uint64] var float_gap gapValue[float64] // Needs to have individual case statements to assist // the compiler to infer the actual type of the var named 'operands' switch operands := (gI.Data).(type) { case numericOperands[int32]: gap = makeGap(operands) case numericOperands[int64]: gap = makeGap(operands) case numericOperands[uint64]: gap = makeGap(operands) case numericOperands[float64]: float_gap = makeFloatGap(operands) } var prev_gap gapValue[uint64] var prev_float_gap gapValue[float64] has_prev_gap := false has_prev_float_gap := false prev_gap, has_prev_gap = tI.gap.(gapValue[uint64]) if !has_prev_gap { prev_float_gap, has_prev_float_gap = tI.gap.(gapValue[float64]) } if has_prev_gap { if maximize { should_send = is_greater_than(gap, prev_gap) } else { should_send = is_less_than(gap, prev_gap) } } if has_prev_float_gap { if maximize { should_send = is_greater_than(float_gap, prev_float_gap) } else { should_send = is_less_than(float_gap, prev_float_gap) } } if should_send { if tI.is_integer_gap() { tI.gap = gap } else { tI.gap = float_gap } emitGuidance(gI) } } func emitGuidance(gI *guidanceInfo) error { return internal.Json_data(map[string]any{"antithesis_guidance": gI}) } // When left and right are the same sign (both negative, or both non-negative) // Calculate: = (left - right). The gap_size is abs() and // gap_is_negative is (right > left) func makeGap[Op operandConstraint](operand numericOperands[Op]) gapValue[uint64] { var gap_size uint64 var gap_is_negative bool switch this_op := any(operand).(type) { case numericOperands[int32]: result := int64(this_op.Left) - int64(this_op.Right) gap_size = abs_int64(result) gap_is_negative = result < 0 case numericOperands[int64]: if is_same_sign(this_op.Left, this_op.Right) { result := int64(this_op.Left) - int64(this_op.Right) gap_size = abs_int64(result) gap_is_negative = result < 0 break } // Otherwise left and right are opposite signs // gap = abs(left) + abs(right) // gap_is_negative = left < right left_gap_size := abs_int64(this_op.Left) right_gap_size := abs_int64(this_op.Right) gap_size = left_gap_size + right_gap_size gap_is_negative = this_op.Left < this_op.Right case numericOperands[uint64]: left_val := this_op.Left right_val := this_op.Right gap_is_negative = false if left_val < right_val { gap_is_negative = true gap_size = right_val - left_val } else { gap_size = left_val - right_val } default: zero_gap, _ := newGapValue(uint64(0), false).(gapValue[uint64]) return zero_gap } this_gap, _ := newGapValue(gap_size, gap_is_negative).(gapValue[uint64]) return this_gap } // MakeGap func makeFloatGap[Op operandConstraint](operand numericOperands[Op]) gapValue[float64] { switch this_op := any(operand).(type) { case numericOperands[float64]: left_val := this_op.Left right_val := this_op.Right gap_is_negative := false var gap_size float64 if left_val < right_val { gap_is_negative = true gap_size = right_val - left_val } else { gap_size = left_val - right_val } this_gap, _ := newGapValue(gap_size, gap_is_negative).(gapValue[float64]) return this_gap default: zero_gap, _ := newGapValue(float64(0.0), false).(gapValue[float64]) return zero_gap } } // MakeFloatGap