--- tags: taylor --- # From Stack Machine to Functional Machine: Step 1 ###### tags: `Taylor` [ToC] ## Environment For illustrating our journey, we will use the [Yul language](https://solidity.readthedocs.io/en/v0.6.4/yul.html) (that compiles to Ethereum 1.0 and Wasm bytecode). ## Anatomy of a Stack Runtime ```javascript object "ContractA" { code { datacopy(0, dataoffset("Runtime"), datasize("Runtime")) return(0, datasize("Runtime")) } object "Runtime" { code { let _calldata := 256 let _output_pointer := 0 // Copy calldata at the _calldata pointer calldatacopy(_calldata, 0, calldatasize()) // Read the first 4 bytes representing the signature of the called function // https://solidity.readthedocs.io/en/v0.6.4/abi-spec.html#function-selector let fn_sig := mslice(_calldata, 4) switch fn_sig case 0xffffffff { // do SOME COMPUTING let result := 10 let result_length := 32 mstore(_output_pointer, result) return(_output_pointer, result_length) } // other cases/function signatures default { // default case - revert with error code mslicestore(_output_pointer, 0xeee1, 2) revert(_output_pointer, 2) } function mslice(position, length) -> result { result := div(mload(position), exp(2, sub(256, mul(length, 8)))) } function mslicestore(_ptr, val, length) { let slot := 32 mstore(_ptr, shl(mul(sub(slot, length), 8), val)) } } } } ``` For any calldata that begins with the bytes 0xffffffff, the above contract will perform <SOME COMPUTING> We could assimilate this behavior with a function that has a signature and the output is a number of bytes. This function may have an input (the rest of `calldata` starting from byte 5 up to the end) ``` f(some_bytes) => some_other_bytes ``` If we have the ability to parse the bytes into some defined types, we could consider that a function is a transformation from a type into another. ``` parse(some_bytes) => T1 parse(some_other_bytes) => T2 f(T1) => T2 ``` T1 and T2 could be any type. They could be even the same type or the `None` type or a `Tuple` of a number of types. Even more: we may define functions as being types as well: by using the function signature. For a complete solution on type definitions and casting, one may use dType: a fully decentralized type system. But we leave that subject for another day or [conference](https://solidity-summit.ethereum.org/). ## Functions as Types. Types as Functions. We could treat functions as types. Even better: we should treat types as functions by choosing a representative function. Such a function is the `new` function: it accepts as input the minimum number of arguments for a type creation. ``` x1 := new(uint(8)) x2 := new(byte(32)) ``` For such dependent types (that need to know the "parent" type and the size), we can define the dependent signature: ``` sig1 := signature(signature(uint), signature(8)) sig2 := signature(signature(byte), signature(32)) ``` And for simplification, we could consider `signature(<number>) => <number>` and the signature function as `bytes4(keccak(<content>))` ### Calling a function from another function: `apply` Let's define the `apply` function: ``` apply(T1) => T2 T1 = Tuple(signature(f), input) T2 = f(input) apply(signature(f), input) = f(input) ``` An example would be: ```javascript object "ContractB" { code { datacopy(0, dataoffset("Runtime"), datasize("Runtime")) return(0, datasize("Runtime")) } object "Runtime" { code { let _calldata := 256 let _output_pointer := 0 calldatacopy(_calldata, 0, calldatasize()) let fn_sig := mslice(_calldata, 4) switch fn_sig case 0xffffffff { let internal_fn_sig := mslice(add(_calldata, 4), 4) let input_pointer := add(_calldata, 8) let result_length := dtapply( internal_fn_sig, input_pointer, _output_pointer ) return (_output_pointer, result_length) } // other cases/function signatures default { mslicestore(_output_pointer, 0xeee1, 2) revert(_output_pointer, 2) } function dtapply(fsig, input, output_pointer) -> result_length { switch fsig case 0xeeeeeeee { let a := mload(input) mstore(output_pointer, add(a, 2)) result_length := 32 } case 0xdddddddd { let a := mload(input) mstore(output_pointer, sub(a, 2)) result_length := 32 } // other cases/function signatures default { // default case - revert with error code mslicestore(output_pointer, 0xeee2, 2) revert(output_pointer, 2) } } function mslice(position, length) -> result { result := div(mload(position), exp(2, sub(256, mul(length, 8)))) } function mslicestore(_ptr, val, length) { let slot := 32 mstore(_ptr, shl(mul(sub(slot, length), 8), val)) } } } } ``` Using `0xffffffffeeeeeeee000000000000000000000000000000000000000000000000000000000000000a` as calldata, we get `12`. `0xffffffff` applied `0xeeeeeeee` on the input arguments, in this case the value `10`. Using `0xffffffffdddddddd000000000000000000000000000000000000000000000000000000000000000a` as calldata, we get `8`. #### Functional interaction for `apply` ```sequence external Environment->Input Pointer:calldata Input Pointer->Type Interpreter:type(calldata[4:end]) Type Interpreter->Parser:new T1 = parse(calldata[8:end], sig(T1)) Parser->Type Interpreter:T1 Type Interpreter->Type Interpreter:isFunction(T1) note over Type Interpreter:if TRUE Type Interpreter->Function Call:apply(T1) Function Call->Type Interpreter:T2 Type Interpreter->Output Pointer:set(T2) note over Type Interpreter:if FALSE Type Interpreter->Output Pointer:set(T1) Output Pointer->external Environment:return(pointer_contents) ``` In the above case, each function extracts the arguments that it expects, but this can become verbose and prone to errors. We already have a type parser that is generated by on-chain type definitions by dType. Also we have a Type Interpreter and Function Caller: it is the Pipeline intra-contract on-chain interpreter. More on this, in the next articles. ## Recursive `apply` ```javascript object "ContractB" { code { datacopy(0, dataoffset("Runtime"), datasize("Runtime")) return(0, datasize("Runtime")) } object "Runtime" { code { let _calldata := 256 let _output_pointer := 0 calldatacopy(_calldata, 0, calldatasize()) let fn_sig := mslice(_calldata, 4) switch fn_sig case 0xffffffff { let internal_fn_sig := mslice(add(_calldata, 4), 4) let input_pointer := add(_calldata, 8) let result_length := dtapplyRecursive( internal_fn_sig, input_pointer, _output_pointer ) return (_output_pointer, result_length) } // other cases/function signatures default { mslicestore(_output_pointer, 0xeee1, 2) revert(_output_pointer, 2) } function dtapplyRecursive(fsig, input, output_pointer) -> result_length { switch fsig case 0xeeeeeeee { let a := mload(input) mstore(output_pointer, add(a, 2)) result_length := 32 } case 0xdddddddd { let a := mload(input) mstore(output_pointer, sub(a, 2)) result_length := 32 } case 0xcccccccc { let internal_fsig := mslice(input, 4) // skip the 4 byte signature & the 32 byte value let count := mslice(add(input, 36), 4) let temporary_ptr := add(input, 4) for { let i := 0 } lt(i, count) { i := add(i, 1) } { result_length := dtapplyRecursive(internal_fsig, temporary_ptr, output_pointer) temporary_ptr := output_pointer } } // other cases/function signatures default { // default case - revert with error code mslicestore(output_pointer, 0xeee2, 2) revert(output_pointer, 2) } } function mslice(position, length) -> result { result := div(mload(position), exp(2, sub(256, mul(length, 8)))) } function mslicestore(_ptr, val, length) { let slot := 32 mstore(_ptr, shl(mul(sub(slot, length), 8), val)) } } } } ``` Using `0xffffffffcccccccceeeeeeee000000000000000000000000000000000000000000000000000000000000002800000004` as calldata, we instruct the `0xcccccccc` function to apply the `0xeeeeeeee` function on the value `40`, `4` times. This means `40 + 2 * 4`. We get `48`. Using `0xffffffffccccccccdddddddd000000000000000000000000000000000000000000000000000000000000002800000004` as calldata, we get `32`: `40 - 2 * 4`. ## Function Persistence We could save any type/function on-chain (for the EVM) or into a JSON file treated by Javascript (for Wasm ). A persisted function is a combination of other functions in a recursive manner until the "leaf" functions are applied. The "leaf" functions are contained in the smart contract iself or in other (external) smart contracts or Wasm libraries (in the Wasm case). Such a system of editing and deploying functional combinations is already implemented by the Pipeline Editor. ## Implementation The ideas above and the steps that will be described in future articles are even better implemented in the Taylor project. Taylor is an interpreted functional language with typing described by dType. ## Next: Step 2 Recursive application is the "engine" of functional programming. In the next step, we will show applications of this engine. In the next article: Some functional patterns implemented in the same environment.