nomoixyz / vulcan

Development framework for Foundry projects
https://nomoixyz.github.io/vulcan/
MIT License
286 stars 18 forks source link

Explore Option type #199

Open vdrg opened 1 year ago

vdrg commented 1 year ago

Similar to Results, but has some additional considerations. For results, we have the Ok functions for each type, and we define "error functions", and a toXResult for errors. For Options, we have Some (similar to Ok), but we don't have errors. So we probably need a None function that returns a Pointer (or some other special None type just to be able to define functions for it) and the corresponding toXOption functions for it.

Preliminary implementation:

enum OptionType {
    None,
    Some
}

type Bytes32Option is bytes32;

type AddressOption is bytes32;

type BoolOption is bytes32;

library LibOptionPointer {
    function decode(Pointer self) internal pure returns (OptionType resultType, Pointer ptr) {
        bytes memory data;
        assembly {
            data := self
        }
        (resultType, ptr) = abi.decode(data, (OptionType, Pointer));
    }

    function isNone(Pointer self) internal pure returns (bool) {
        (OptionType _type,) = decode(self);
        return _type == OptionType.None;
    }

    function isSome(Pointer self) internal pure returns (bool) {
        (OptionType _type,) = decode(self);
        return _type == OptionType.Some;
    }

    function toValue(Pointer self) internal pure returns (bytes32) {
        (, Pointer ptr) = decode(self);
        return ptr.asBytes32();
    }

    function unwrap(Pointer self) internal pure returns (Pointer ptr) {
        if (isNone(self)) {
            revert("called `Option::unwrap()` on a `None` value");
        }

        (, ptr) = decode(self);
    }

    function expect(Pointer self, string memory err) internal pure returns (Pointer ptr) {
        if (isNone(self)) {
            revert(err);
        }

        (, ptr) = decode(self);
    }

    function asBoolOption(Pointer self) internal pure returns (BoolOption ptr) {
        assembly {
            ptr := self
        }
    }
}

library LibBytes32Option {
    function isNone(Bytes32Option self) internal pure returns (bool) {
        return LibOptionPointer.isNone(self.toPointer());
    }

    function isSome(Bytes32Option self) internal pure returns (bool) {
        return LibOptionPointer.isSome(self.toPointer());
    }

    function toValue(Bytes32Option self) internal pure returns (bytes32) {
        (, Pointer ptr) = LibOptionPointer.decode(self.toPointer());

        return ptr.asBytes32();
    }

    function unwrap(Bytes32Option self) internal pure returns (bytes32) {
        return LibOptionPointer.unwrap(self.toPointer()).asBytes32();
    }

    function expect(Bytes32Option self, string memory err) internal pure returns (bytes32) {
        return LibOptionPointer.expect(self.toPointer(), err).asBytes32();
    }

    function toPointer(Bytes32Option self) internal pure returns (Pointer) {
        return Pointer.wrap(Bytes32Option.unwrap(self));
    }
}

library LibAddressOption {
    function isNone(AddressOption self) internal pure returns (bool) {
        return LibOptionPointer.isNone(self.toPointer());
    }

    function isSome(AddressOption self) internal pure returns (bool) {
        return LibOptionPointer.isSome(self.toPointer());
    }

    function toValue(AddressOption self) internal pure returns (address) {
        (, Pointer ptr) = LibOptionPointer.decode(self.toPointer());

        return ptr.asAddress();
    }

    function unwrap(AddressOption self) internal pure returns (address) {
        return LibOptionPointer.unwrap(self.toPointer()).asAddress();
    }

    function expect(AddressOption self, string memory err) internal pure returns (address) {
        return LibOptionPointer.expect(self.toPointer(), err).asAddress();
    }

    function toPointer(AddressOption self) internal pure returns (Pointer) {
        return Pointer.wrap(AddressOption.unwrap(self));
    }
}

library LibBoolOption {
    function isNone(BoolOption self) internal pure returns (bool) {
        return LibOptionPointer.isNone(self.toPointer());
    }

    function isSome(BoolOption self) internal pure returns (bool) {
        return LibOptionPointer.isSome(self.toPointer());
    }

    function toValue(BoolOption self) internal pure returns (bool) {
        (, Pointer ptr) = LibOptionPointer.decode(self.toPointer());

        return ptr.asBool();
    }

    function unwrap(BoolOption self) internal pure returns (bool) {
        return LibOptionPointer.unwrap(self.toPointer()).asBool();
    }

    function expect(BoolOption self, string memory err) internal pure returns (bool) {
        return LibOptionPointer.expect(self.toPointer(), err).asBool();
    }

    function toPointer(BoolOption self) internal pure returns (Pointer) {
        return Pointer.wrap(BoolOption.unwrap(self));
    }
}

function encode(OptionType _type, Pointer _data) pure returns (Pointer result) {
    bytes memory data = abi.encode(_type, _data);
    assembly {
        result := data
    }
}

function None() pure returns (Pointer) {
    return encode(OptionType.None, Pointer.wrap(0));
}

function Some(bool value) pure returns (BoolOption opt) {
    Pointer _value;
    assembly {
        _value := value
    }
    return BoolOption.wrap(Pointer.unwrap(encode(OptionType.Some, _value)));
}

using LibBoolOption for BoolOption global;
using LibAddressOption for AddressOption global;
using LibBytes32Option for Bytes32Option global;
vdrg commented 1 year ago

An improvement to this: the OptionType doesn't really need to be encoded, a Pointer(0) could represent None, and everything else is Some.