diff --git a/TESTING.md b/TESTING.md index cf054c5ba..44e97db9c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -39,3 +39,7 @@ Use `opReferenceCheck` to validate that `run` output matches a pure reference im ## Boundary Tests Always test both sides: the max valid value (should succeed) and one past it (should revert). + +## One Test at a Time + +Write one test function per edit-compile-run cycle. Do not batch multiple new tests into a single edit. Writing one test at a time produces higher quality code — each test gets full attention, compilation errors are caught immediately, and failures are unambiguous. diff --git a/audit/2026-02-17-03/triage.md b/audit/2026-02-17-03/triage.md index 38bd698a6..e6d2ce7db 100644 --- a/audit/2026-02-17-03/triage.md +++ b/audit/2026-02-17-03/triage.md @@ -228,14 +228,14 @@ Tracks the disposition of every LOW+ finding from pass2 audit reports (test cove - [FIXED] A20-1: (LOW) No test verifying `erc20-allowance` handles infinite approvals without revert — added `testOpERC20AllowanceInfiniteApproval` with type(uint256).max allowance - [FIXED] A20-2: (LOW) No test for `decimals()` revert when token does not implement `IERC20Metadata` — all three ERC20 ops have `testOp*DecimalsReadFailure` tests verifying `TokenDecimalsReadFailure` revert via TOFU `safeDecimalsForTokenReadOnly` - [FIXED] A20-4: (LOW) No test for input values with upper 96 bits set (address truncation) — added NotAnAddress runtime validation with fuzz tests across all 10 address-taking opcodes -- [PENDING] A21-1: (LOW) No test for `referenceFn` `BadOutputsLength` revert path -- [PENDING] A23-1: (LOW) LibOpGreaterThanOrEqualTo missing negative number and float equality eval tests -- [PENDING] A23-2: (LOW) LibOpLessThanOrEqualTo missing negative number and float equality eval tests -- [PENDING] A23-3: (LOW) LibOpConditions no test for exactly 2 inputs (minimum case) -- [PENDING] A23-4: (LOW) LibOpConditions odd-input revert path with reason string not tested via opReferenceCheck -- [PENDING] A24-1: (LOW) LibOpE missing operand disallowed test -- [PENDING] A24-2: (LOW) LibOpExp and LibOpExp2 fuzz tests restrict inputs to non-negative small values only -- [PENDING] A24-3: (LOW) LibOpGm fuzz test restricts inputs to non-negative small values only +- [FIXED] A21-1: (LOW) No test for `referenceFn` `BadOutputsLength` revert path — added testOpExternReferenceFnBadOutputsLength and testOpExternReferenceFnBadOutputsLengthTooMany +- [FIXED] A23-1: (LOW) LibOpGreaterThanOrEqualTo missing negative number and float equality eval tests — added 4 eval tests: 1.1>=1.2, 1.0>=1, -1.1>=-1.2, -1>=0 +- [FIXED] A23-2: (LOW) LibOpLessThanOrEqualTo missing negative number and float equality eval tests — added 4 eval tests: 1.1<=1.2, 1.0<=1, -1.1<=-1.2, -1<=0 +- [DISMISSED] A23-3: (LOW) LibOpConditions no test for exactly 2 inputs (minimum case) — false positive; testOpConditionsEval1TrueInputZeroOutput and testOpConditionsEval2MixedInputs both use exactly 2 inputs +- [DISMISSED] A23-4: (LOW) LibOpConditions odd-input revert path with reason string not tested via opReferenceCheck — finding itself acknowledges "adequate coverage"; testOpConditionsRunNoConditionsMet fuzz covers odd inputs with reason strings, testOpConditionsEvalErrorCode covers it at eval level +- [FIXED] A24-1: (LOW) LibOpE missing operand disallowed test — added testOpEEvalOperandDisallowed +- [FIXED] A24-2: (LOW) LibOpExp and LibOpExp2 fuzz tests restrict inputs to non-negative small values only — added testOpExpEvalNegativeInput (exp(-1)=1/e) and testOpExp2EvalNegativeInput (exp2(-1)=0.5) +- [FIXED] A24-3: (LOW) LibOpGm fuzz test restricts inputs to non-negative small values only — fixed implementation to compute signed geometric mean: sign * sqrt(|a| * |b|). Expanded fuzz bounds to include negatives. Added eval tests for mixed signs (unit and non-unit), both negative (equal and unequal), zero with negative, and zero bytes identity. Signed GM function upstreamed to rain.math.float as GitHub issue. - [PENDING] A24-4: (LOW) LibOpFloor eval tests missing negative value coverage - [PENDING] A25-1: (LOW) LibOpInv missing test for division by zero (inv(0)) - [PENDING] A25-2: (LOW) LibOpSub missing zero outputs and two outputs tests diff --git a/src/generated/Rainterpreter.pointers.sol b/src/generated/Rainterpreter.pointers.sol index 5d2670719..a19ddf7f3 100644 --- a/src/generated/Rainterpreter.pointers.sol +++ b/src/generated/Rainterpreter.pointers.sol @@ -10,11 +10,11 @@ pragma solidity ^0.8.25; // file needs the contract to exist so that it can be compiled. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0x4c3528ad310e82eb5cab9d310acfdf8e6035d78ea54401dc926c9803503de53b); +bytes32 constant BYTECODE_HASH = bytes32(0x66a2e1c7c62ac40c6dc3c281364370e8fd18e2cce1a2819dc38b01880cbce0fa); /// @dev The function pointers known to the interpreter for dynamic dispatch. /// By setting these as a constant they can be inlined into the interpreter /// and loaded at eval time for very low gas (~100) due to the compiler /// optimising it to a single `codecopy` to build the in memory bytes array. bytes constant OPCODE_FUNCTION_POINTERS = - hex"08f70929094d0ac50b270b390b4b0b630b860bbc0bce0be00c810ca00db50e900f2c105d11530db511fc12d713911421143214431443145414a9158115d015e815fc1644165c1674169816ad16c516dd1725174c175e17bf180c185918a618f3190019b319d519e21a701aa11ae41b081b151b221b721ba61bb31c001c311c621caf1ce01cf81d861db21dd41e621f46"; + hex"08f70929094d0ac50b270b390b4b0b630b860bbc0bce0be00c810ca00db50e900f2c105d11530db511fc12d713911421143214431443145414a9158115d015e815fc1644165c1674169816ad16c516dd1725174c175e17bf180c185918a618f3190019b319d519e21a701aa11ae41b081b151b221bb41be81bf51c421c731ca41cf11d221d3a1dc81df41e161ea41f88"; diff --git a/src/lib/deploy/LibInterpreterDeploy.sol b/src/lib/deploy/LibInterpreterDeploy.sol index e5070ecad..4f80aa65e 100644 --- a/src/lib/deploy/LibInterpreterDeploy.sol +++ b/src/lib/deploy/LibInterpreterDeploy.sol @@ -33,14 +33,14 @@ library LibInterpreterDeploy { /// The address of the `Rainterpreter` contract when deployed with the rain /// standard zoltu deployer. - address constant INTERPRETER_DEPLOYED_ADDRESS = address(0xa6c66cD36596a365DE26a649BBFCC7EfF94facdF); + address constant INTERPRETER_DEPLOYED_ADDRESS = address(0xe758FACfeb8E02bfed2d8C53B731D687DDD88A68); /// The code hash of the `Rainterpreter` contract when deployed with the rain /// standard zoltu deployer. This can be used to verify that the deployed /// contract has the expected bytecode, which provides stronger guarantees /// than just checking the address. bytes32 constant INTERPRETER_DEPLOYED_CODEHASH = - bytes32(0x4c3528ad310e82eb5cab9d310acfdf8e6035d78ea54401dc926c9803503de53b); + bytes32(0x66a2e1c7c62ac40c6dc3c281364370e8fd18e2cce1a2819dc38b01880cbce0fa); /// The address of the `RainterpreterExpressionDeployer` contract when /// deployed with the rain standard zoltu deployer. @@ -55,12 +55,12 @@ library LibInterpreterDeploy { /// The address of the `RainterpreterDISPaiRegistry` contract when deployed /// with the rain standard zoltu deployer. - address constant DISPAIR_REGISTRY_DEPLOYED_ADDRESS = address(0xE1F3845aa22b3Cfbe89cFAB564F6f3D590605777); + address constant DISPAIR_REGISTRY_DEPLOYED_ADDRESS = address(0x10B6e5DF88894d20d27E23Dd62Fb6741AC760676); /// The code hash of the `RainterpreterDISPaiRegistry` contract when /// deployed with the rain standard zoltu deployer. This can be used to /// verify that the deployed contract has the expected bytecode, which /// provides stronger guarantees than just checking the address. bytes32 constant DISPAIR_REGISTRY_DEPLOYED_CODEHASH = - bytes32(0xb979c41f1d8a0911b4b4a35f05c5ba7ab4e498f06e5d313d5007fcfb9d019c69); + bytes32(0x44bdd025183eb606fb828fa64322aa81b3da6a525e5dfde49e8dbe1fbce56b4b); } diff --git a/src/lib/op/math/LibOpGm.sol b/src/lib/op/math/LibOpGm.sol index 71612bd13..ef7cc7564 100644 --- a/src/lib/op/math/LibOpGm.sol +++ b/src/lib/op/math/LibOpGm.sol @@ -9,8 +9,9 @@ import {IntegrityCheckState} from "../../integrity/LibIntegrityCheck.sol"; import {LibDecimalFloat, Float} from "rain.math.float/lib/LibDecimalFloat.sol"; /// @title LibOpGm -/// @notice Opcode for the geometric average of two decimal floating point -/// numbers. +/// @notice Opcode for the signed geometric mean of two decimal floating point +/// numbers. Computes `sign * sqrt(|a| * |b|)`, where the sign is negative when +/// an odd number of inputs are negative. library LibOpGm { using LibDecimalFloat for Float; @@ -22,8 +23,9 @@ library LibOpGm { return (2, 1); } - /// @notice gm - /// decimal floating point geometric average of two numbers. + /// @notice Signed geometric mean of two decimal floating point numbers. + /// Computes `sign * sqrt(|a| * |b|)`. The result is negative when exactly + /// one input is negative, positive otherwise. /// @param stackTop Pointer to the top of the stack. /// @return The new stack top pointer after execution. function run(InterpreterState memory, OperandV2, Pointer stackTop) internal view returns (Pointer) { @@ -34,10 +36,15 @@ library LibOpGm { stackTop := add(stackTop, 0x20) b := mload(stackTop) } - a = a.mul(b).pow(LibDecimalFloat.FLOAT_HALF, LibDecimalFloat.LOG_TABLES_ADDRESS); + bool aNeg = a.lt(LibDecimalFloat.FLOAT_ZERO); + bool bNeg = b.lt(LibDecimalFloat.FLOAT_ZERO); + Float result = a.abs().mul(b.abs()).pow(LibDecimalFloat.FLOAT_HALF, LibDecimalFloat.LOG_TABLES_ADDRESS); + if (aNeg != bNeg) { + result = result.minus(); + } assembly ("memory-safe") { - mstore(stackTop, a) + mstore(stackTop, result) } return stackTop; } @@ -50,12 +57,18 @@ library LibOpGm { view returns (StackItem[] memory) { - // The geometric mean is sqrt(a * b). + // The geometric mean is sign * sqrt(|a| * |b|), where sign is + // negative if an odd number of inputs are negative. Float a = Float.wrap(StackItem.unwrap(inputs[0])); Float b = Float.wrap(StackItem.unwrap(inputs[1])); - a = a.mul(b).pow(LibDecimalFloat.FLOAT_HALF, LibDecimalFloat.LOG_TABLES_ADDRESS); + bool aNeg = a.lt(LibDecimalFloat.FLOAT_ZERO); + bool bNeg = b.lt(LibDecimalFloat.FLOAT_ZERO); + Float result = a.abs().mul(b.abs()).pow(LibDecimalFloat.FLOAT_HALF, LibDecimalFloat.LOG_TABLES_ADDRESS); + if (aNeg != bNeg) { + result = result.minus(); + } StackItem[] memory outputs = new StackItem[](1); - outputs[0] = StackItem.wrap(Float.unwrap(a)); + outputs[0] = StackItem.wrap(Float.unwrap(result)); return outputs; } } diff --git a/test/src/lib/op/00/LibOpExtern.t.sol b/test/src/lib/op/00/LibOpExtern.t.sol index 7702518fc..f375a7127 100644 --- a/test/src/lib/op/00/LibOpExtern.t.sol +++ b/test/src/lib/op/00/LibOpExtern.t.sol @@ -294,6 +294,127 @@ contract LibOpExternTest is OpTest { LibOpExtern.run(state, operand, stackTop); } + /// Exposed externally so mocks and reverts play nice with referenceFn. + function externalReferenceFn(InterpreterState memory state, OperandV2 operand, StackItem[] memory inputs) + external + view + returns (StackItem[] memory) + { + return LibOpExtern.referenceFn(state, operand, inputs); + } + + /// Test that `referenceFn` reverts with `BadOutputsLength` when the extern + /// returns fewer outputs than the operand specifies. + function testOpExternReferenceFnBadOutputsLength( + IInterpreterExternV4 extern, + bytes32[] memory constants, + uint16 constantIndex, + StackItem[] memory inputs, + StackItem[] memory outputs + ) external { + vm.assume(constants.length > 0); + if (inputs.length > 0x0F) { + uint256[] memory inputsCopy; + assembly ("memory-safe") { + inputsCopy := inputs + } + inputsCopy.truncate(0x0F); + } + if (outputs.length > 0x0F) { + uint256[] memory outputsCopy; + assembly ("memory-safe") { + outputsCopy := outputs + } + outputsCopy.truncate(0x0F); + } + // Need at least 1 output so we can return a mismatched count. + vm.assume(outputs.length > 0); + + InterpreterState memory state = opTestDefaultInterpreterState(); + state.constants = constants; + + assumeEtchable(address(extern)); + vm.etch(address(extern), hex"fe"); + mockImplementsERC165IInterpreterExternV4(extern); + + constantIndex = uint16(bound(constantIndex, 0, state.constants.length - 1)); + + OperandV2 operand = LibOperand.build(uint8(inputs.length), uint8(outputs.length), constantIndex); + ExternDispatchV2 externDispatch = LibExtern.encodeExternDispatch(0, operand); + EncodedExternDispatchV2 encodedExternDispatch = LibExtern.encodeExternCall(extern, externDispatch); + state.constants[constantIndex] = EncodedExternDispatchV2.unwrap(encodedExternDispatch); + + // Mock extern to return one fewer output than expected. + StackItem[] memory badOutputs = new StackItem[](outputs.length - 1); + for (uint256 i = 0; i < badOutputs.length; i++) { + badOutputs[i] = outputs[i]; + } + vm.mockCall( + address(extern), + abi.encodeWithSelector(IInterpreterExternV4.extern.selector, externDispatch), + abi.encode(badOutputs) + ); + + vm.expectRevert(abi.encodeWithSelector(BadOutputsLength.selector, outputs.length, badOutputs.length)); + this.externalReferenceFn(state, operand, inputs); + } + + /// Test that `referenceFn` reverts with `BadOutputsLength` when the extern + /// returns more outputs than the operand specifies. + function testOpExternReferenceFnBadOutputsLengthTooMany( + IInterpreterExternV4 extern, + bytes32[] memory constants, + uint16 constantIndex, + StackItem[] memory inputs, + StackItem[] memory outputs + ) external { + vm.assume(constants.length > 0); + if (inputs.length > 0x0F) { + uint256[] memory inputsCopy; + assembly ("memory-safe") { + inputsCopy := inputs + } + inputsCopy.truncate(0x0F); + } + // Cap outputs to 0x0E so we can add one more. + if (outputs.length > 0x0E) { + uint256[] memory outputsCopy; + assembly ("memory-safe") { + outputsCopy := outputs + } + outputsCopy.truncate(0x0E); + } + + InterpreterState memory state = opTestDefaultInterpreterState(); + state.constants = constants; + + assumeEtchable(address(extern)); + vm.etch(address(extern), hex"fe"); + mockImplementsERC165IInterpreterExternV4(extern); + + constantIndex = uint16(bound(constantIndex, 0, state.constants.length - 1)); + + OperandV2 operand = LibOperand.build(uint8(inputs.length), uint8(outputs.length), constantIndex); + ExternDispatchV2 externDispatch = LibExtern.encodeExternDispatch(0, operand); + EncodedExternDispatchV2 encodedExternDispatch = LibExtern.encodeExternCall(extern, externDispatch); + state.constants[constantIndex] = EncodedExternDispatchV2.unwrap(encodedExternDispatch); + + // Mock extern to return one more output than expected. + StackItem[] memory extraOutputs = new StackItem[](outputs.length + 1); + for (uint256 i = 0; i < outputs.length; i++) { + extraOutputs[i] = outputs[i]; + } + + vm.mockCall( + address(extern), + abi.encodeWithSelector(IInterpreterExternV4.extern.selector, externDispatch), + abi.encode(extraOutputs) + ); + + vm.expectRevert(abi.encodeWithSelector(BadOutputsLength.selector, outputs.length, extraOutputs.length)); + this.externalReferenceFn(state, operand, inputs); + } + /// Test the eval of extern directly. function testOpExternRunHappy( IInterpreterExternV4 extern, diff --git a/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol b/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol index aa2224d66..ffee55d58 100644 --- a/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol +++ b/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol @@ -63,6 +63,26 @@ contract LibOpGreaterThanOrEqualToTest is OpTest { checkHappy("_: greater-than-or-equal-to(1 1);", bytes32(uint256(1)), ""); } + /// Test 1.1 >= 1.2, which should return 0. + function testOpGreaterThanOrEqualToEvalOnePointOneGteOnePointTwo() external view { + checkHappy("_: greater-than-or-equal-to(1.1 1.2);", 0, ""); + } + + /// Test 1.0 >= 1, which should return 1 (equal). + function testOpGreaterThanOrEqualToEvalOnePointZeroGteOne() external view { + checkHappy("_: greater-than-or-equal-to(1.0 1);", bytes32(uint256(1)), ""); + } + + /// Test -1.1 >= -1.2, which should return 1. + function testOpGreaterThanOrEqualToEvalNegOnePointOneGteNegOnePointTwo() external view { + checkHappy("_: greater-than-or-equal-to(-1.1 -1.2);", bytes32(uint256(1)), ""); + } + + /// Test -1 >= 0, which should return 0. + function testOpGreaterThanOrEqualToEvalNegOneGteZero() external view { + checkHappy("_: greater-than-or-equal-to(-1 0);", 0, ""); + } + /// Test that a greater than or equal to without inputs fails integrity check. function testOpGreaterThanOrEqualToEvalFail0Inputs() public { vm.expectRevert(abi.encodeWithSelector(BadOpInputsLength.selector, 0, 2, 0)); diff --git a/test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol b/test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol index 9a7eadb2c..bcc479e6c 100644 --- a/test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol +++ b/test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol @@ -138,6 +138,26 @@ contract LibOpLessThanOrEqualToTest is OpTest { assertEq(kvs.length, 0); } + /// Test 1.1 <= 1.2, which should return 1. + function testOpLessThanOrEqualToEvalOnePointOneLteOnePointTwo() external view { + checkHappy("_: less-than-or-equal-to(1.1 1.2);", bytes32(uint256(1)), ""); + } + + /// Test 1.0 <= 1, which should return 1 (equal). + function testOpLessThanOrEqualToEvalOnePointZeroLteOne() external view { + checkHappy("_: less-than-or-equal-to(1.0 1);", bytes32(uint256(1)), ""); + } + + /// Test -1.1 <= -1.2, which should return 0. + function testOpLessThanOrEqualToEvalNegOnePointOneLteNegOnePointTwo() external view { + checkHappy("_: less-than-or-equal-to(-1.1 -1.2);", 0, ""); + } + + /// Test -1 <= 0, which should return 1. + function testOpLessThanOrEqualToEvalNegOneLteZero() external view { + checkHappy("_: less-than-or-equal-to(-1 0);", bytes32(uint256(1)), ""); + } + /// Test that a less than or equal to without inputs fails integrity check. function testOpLessThanOrEqualToEvalFail0Inputs() public { vm.expectRevert(abi.encodeWithSelector(BadOpInputsLength.selector, 0, 2, 0)); diff --git a/test/src/lib/op/math/LibOpE.t.sol b/test/src/lib/op/math/LibOpE.t.sol index b1c042da9..d1fa5dd36 100644 --- a/test/src/lib/op/math/LibOpE.t.sol +++ b/test/src/lib/op/math/LibOpE.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {InterpreterState} from "src/lib/state/LibInterpreterState.sol"; import {LibOpE} from "src/lib/op/math/LibOpE.sol"; import {LibOperand, OperandV2} from "test/lib/operand/LibOperand.sol"; @@ -74,4 +74,9 @@ contract LibOpETest is OpTest { function testOpEEvalTwoOutputs() external { checkBadOutputs("_ _: e();", 0, 1, 2); } + + /// Test that operand is disallowed. + function testOpEEvalOperandDisallowed() external { + checkUnhappyParse("_: e<0>();", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/math/LibOpExp.t.sol b/test/src/lib/op/math/LibOpExp.t.sol index ee50cd692..07117b4c7 100644 --- a/test/src/lib/op/math/LibOpExp.t.sol +++ b/test/src/lib/op/math/LibOpExp.t.sol @@ -78,6 +78,13 @@ contract LibOpExpTest is OpTest { ); } + /// Test exp with negative input. exp(-1) = 1/e ≈ 0.3678794... + function testOpExpEvalNegativeInput() external view { + (Float expected,) = + LibDecimalFloat.packLossy(3678794411714423215955237701614608674458111310317678345078368016974, -67); + checkHappy("_: exp(-1);", Float.unwrap(expected), "e^(-1)"); + } + /// Test the eval of `exp` for bad inputs. function testOpExpEvalZeroInputs() external { checkBadInputs("_: exp();", 0, 1, 0); diff --git a/test/src/lib/op/math/LibOpExp2.t.sol b/test/src/lib/op/math/LibOpExp2.t.sol index ae9f9800d..d15a1759b 100644 --- a/test/src/lib/op/math/LibOpExp2.t.sol +++ b/test/src/lib/op/math/LibOpExp2.t.sol @@ -44,6 +44,13 @@ contract LibOpExp2Test is OpTest { checkHappy("_: exp2(3);", Float.unwrap(LibDecimalFloat.packLossless(8000, -3)), "2^3"); } + /// Test exp2 with negative input. exp2(-1) = 0.5. + function testOpExp2EvalNegativeInput() external view { + (Float expected,) = + LibDecimalFloat.packLossy(5000000000000000000000000000000000000000000000000000000000000000000, -67); + checkHappy("_: exp2(-1);", Float.unwrap(expected), "2^(-1)"); + } + /// Test the eval of `exp2` for bad inputs. function testOpExp2EvalBad() external { checkBadInputs("_: exp2();", 0, 1, 0); diff --git a/test/src/lib/op/math/LibOpGm.t.sol b/test/src/lib/op/math/LibOpGm.t.sol index 0d9fe08e0..414fd0102 100644 --- a/test/src/lib/op/math/LibOpGm.t.sol +++ b/test/src/lib/op/math/LibOpGm.t.sol @@ -21,7 +21,8 @@ contract LibOpGmTest is OpTest { assertEq(calcOutputs, 1); } - /// Directly test the runtime logic of LibOpGm. + /// Directly test the runtime logic of LibOpGm. Fuzz inputs include negative + /// values to exercise the signed geometric mean logic. function testOpGmRun( int224 signedCoefficientA, int32 exponentA, @@ -29,9 +30,9 @@ contract LibOpGmTest is OpTest { int32 exponentB, uint16 operandData ) public view { - signedCoefficientA = int224(bound(signedCoefficientA, 0, 10000)); + signedCoefficientA = int224(bound(signedCoefficientA, -10000, 10000)); exponentA = int32(bound(exponentA, -10, 5)); - signedCoefficientB = int224(bound(signedCoefficientB, 0, 10000)); + signedCoefficientB = int224(bound(signedCoefficientB, -10000, 10000)); exponentB = int32(bound(exponentB, -10, 5)); InterpreterState memory state = opTestDefaultInterpreterState(); @@ -47,7 +48,7 @@ contract LibOpGmTest is OpTest { opReferenceCheck(state, operand, LibOpGm.referenceFn, LibOpGm.integrity, LibOpGm.run, inputs); } - /// Test the eval of `gm`. + /// Test the eval of `gm` with non-negative inputs. function testOpGmEval() external view { checkHappy("_: gm(0 0);", 0, "0 0"); checkHappy("_: gm(0 1);", 0, "0 1"); @@ -60,6 +61,57 @@ contract LibOpGmTest is OpTest { checkHappy("_: gm(4 0.5);", Float.unwrap(LibDecimalFloat.packLossless(1415, -3)), "4 0.5"); } + /// Test that gm with mixed signs returns a negative result. + /// gm(-1, 1) = -sqrt(|-1| * |1|) = -1. + function testOpGmEvalMixedSignsNegativeFirst() external view { + (Float expected,) = LibDecimalFloat.packLossy(-1e3, -3); + checkHappy("_: gm(-1 1);", Float.unwrap(expected), "-1 1"); + } + + /// Test that gm with mixed signs returns a negative result regardless of + /// argument order. gm(1, -1) = -sqrt(|1| * |-1|) = -1. + function testOpGmEvalMixedSignsNegativeSecond() external view { + (Float expected,) = LibDecimalFloat.packLossy(-1e3, -3); + checkHappy("_: gm(1 -1);", Float.unwrap(expected), "1 -1"); + } + + /// Test that gm with mixed signs and non-unit magnitudes returns a negative + /// result. gm(-2, 3) = -sqrt(|-2| * |3|) = -sqrt(6) ≈ -2.450. + function testOpGmEvalMixedSignsNonUnit() external view { + (Float expected,) = LibDecimalFloat.packLossy(-2450, -3); + checkHappy("_: gm(-2 3);", Float.unwrap(expected), "-2 3"); + } + + /// Test that gm with both inputs negative returns a positive result. + /// gm(-1, -1) = sqrt(|-1| * |-1|) = 1. + function testOpGmEvalBothNegativeEqual() external view { + checkHappy("_: gm(-1 -1);", Float.unwrap(LibDecimalFloat.packLossless(1e3, -3)), "-1 -1"); + } + + /// Test that gm with both inputs negative and unequal returns a positive + /// result. gm(-2, -3) = sqrt(|-2| * |-3|) = sqrt(6) ≈ 2.450. + function testOpGmEvalBothNegativeUnequal() external view { + checkHappy("_: gm(-2 -3);", Float.unwrap(LibDecimalFloat.packLossless(2450, -3)), "-2 -3"); + } + + /// Test that gm with zero and a negative input returns zero. + function testOpGmEvalZeroWithNegative() external view { + checkHappy("_: gm(0 -1);", 0, "0 -1"); + } + + /// Test that gm with a negative input and zero returns zero. + function testOpGmEvalNegativeWithZero() external view { + checkHappy("_: gm(-1 0);", 0, "-1 0"); + } + + /// Test that gm produces identical bytes for zero regardless of whether + /// the sign logic triggers minus(0). gm(0, -1) has aNeg!=bNeg so it + /// negates the zero result; this must produce the same bytes as gm(0, 1). + function testOpGmEvalZeroBytesIdentical() external view { + checkHappy("_: gm(0 -1);", Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "0 -1 vs FLOAT_ZERO"); + checkHappy("_: gm(-1 0);", Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "-1 0 vs FLOAT_ZERO"); + } + /// Test the eval of `gm` for bad inputs. function testOpGmOneInput() external { checkBadInputs("_: gm(1);", 1, 2, 1);