diff --git a/contracts/chainlink/LibChainlink.sol b/contracts/chainlink/LibChainlink.sol index 3cca4884b..03f1c9bce 100644 --- a/contracts/chainlink/LibChainlink.sol +++ b/contracts/chainlink/LibChainlink.sol @@ -48,10 +48,13 @@ library LibChainlink { } // Safely cast the answer to uint256 and scale it to 18 decimal FP. + // We round up because reporting a non-zero price as zero can cause + // issues downstream. This rounding up only happens if the values are + // being scaled down. return answer_.toUint256().scale18( AggregatorV3Interface(feed_).decimals(), - Math.Rounding.Down + Math.Rounding.Up ); } } diff --git a/contracts/interpreter/deploy/IExpressionDeployerV1.sol b/contracts/interpreter/deploy/IExpressionDeployerV1.sol index 76a1e8650..8a46a84f1 100644 --- a/contracts/interpreter/deploy/IExpressionDeployerV1.sol +++ b/contracts/interpreter/deploy/IExpressionDeployerV1.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: CAL -pragma solidity =0.8.17; +pragma solidity ^0.8.17; /// Config required to build a new `State`. /// @param sources Sources verbatim. These sources MUST be provided in their diff --git a/contracts/interpreter/extern/IInterpreterExternV1.sol b/contracts/interpreter/extern/IInterpreterExternV1.sol new file mode 100644 index 000000000..69957842a --- /dev/null +++ b/contracts/interpreter/extern/IInterpreterExternV1.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: CAL +pragma solidity ^0.8.15; + +type EncodedExternDispatch is uint256; +type ExternDispatch is uint256; + +/// @title IInterpreterExternV1 +/// Handle a single dispatch from some calling contract with an array of +/// inputs and array of outputs. Ostensibly useful to build "word packs" for +/// `IInterpreterV1` so that less frequently used words can be provided in +/// a less efficient format, but without bloating the base interpreter in +/// terms of code size. Effectively allows unlimited words to exist as externs +/// alongside interpreters. +interface IInterpreterExternV1 { + /// Handles a single dispatch. + /// @param dispatch_ Encoded information about the extern to dispatch. + /// Analogous to the opcode/operand in the interpreter. + /// @param inputs_ The array of inputs for the dispatched logic. + /// @return outputs_ The result of the dispatched logic. + function extern( + ExternDispatch dispatch_, + uint256[] memory inputs_ + ) external view returns (uint256[] memory outputs_); +} diff --git a/contracts/interpreter/extern/LibExtern.sol b/contracts/interpreter/extern/LibExtern.sol new file mode 100644 index 000000000..a5a07ae55 --- /dev/null +++ b/contracts/interpreter/extern/LibExtern.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: CAL +pragma solidity ^0.8.15; + +import "./IInterpreterExternV1.sol"; + +library LibExtern { + function decode( + EncodedExternDispatch dispatch_ + ) internal pure returns (IInterpreterExternV1, ExternDispatch) { + return ( + IInterpreterExternV1( + address(uint160(EncodedExternDispatch.unwrap(dispatch_))) + ), + ExternDispatch.wrap(EncodedExternDispatch.unwrap(dispatch_) >> 160) + ); + } +} diff --git a/contracts/interpreter/ops/AllStandardOps.sol b/contracts/interpreter/ops/AllStandardOps.sol index 4553f1482..e2b50b50a 100644 --- a/contracts/interpreter/ops/AllStandardOps.sol +++ b/contracts/interpreter/ops/AllStandardOps.sol @@ -11,6 +11,7 @@ import "./core/OpContext.sol"; import "./core/OpContextRow.sol"; import "./core/OpDebug.sol"; import "./core/OpDoWhile.sol"; +import "./core/OpExtern.sol"; import "./core/OpFoldContext.sol"; import "./core/OpGet.sol"; import "./core/OpLoopN.sol"; @@ -71,7 +72,7 @@ import "./tier/OpUpdateTimesForTierRange.sol"; error BadDynamicLength(uint256 dynamicLength, uint256 standardOpsLength); /// @dev Number of ops currently provided by `AllStandardOps`. -uint256 constant ALL_STANDARD_OPS_LENGTH = 61; +uint256 constant ALL_STANDARD_OPS_LENGTH = 62; /// @title AllStandardOps /// @notice Every opcode available from the core repository laid out as a single @@ -203,6 +204,7 @@ library AllStandardOps { OpContextRow.integrity, OpDebug.integrity, OpDoWhile.integrity, + OpExtern.integrity, OpFoldContext.integrity, OpGet.integrity, OpLoopN.integrity, @@ -292,6 +294,7 @@ library AllStandardOps { OpContextRow.run, OpDebug.run, OpDoWhile.run, + OpExtern.intern, OpFoldContext.run, OpGet.run, OpLoopN.run, diff --git a/contracts/interpreter/ops/core/OpExtern.sol b/contracts/interpreter/ops/core/OpExtern.sol new file mode 100644 index 000000000..0c1ecfd46 --- /dev/null +++ b/contracts/interpreter/ops/core/OpExtern.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: CAL +pragma solidity ^0.8.15; + +import "../../../math/Binary.sol"; +import "../../deploy/LibIntegrityCheck.sol"; +import "./OpReadMemory.sol"; +import "../../extern/LibExtern.sol"; +import "../../run/LibStackPointer.sol"; + +/// Thrown when the length of results from an extern don't match what the operand +/// defines. This is bad because it implies our integrity check miscalculated the +/// stack which is undefined behaviour. +/// @param expected The length we expected based on the operand. +/// @param actual The length that was returned from the extern. +error BadExternResultsLength(uint256 expected, uint256 actual); + +library OpExtern { + using LibIntegrityCheck for IntegrityCheckState; + using LibStackPointer for StackPointer; + + function integrity( + IntegrityCheckState memory integrityCheckState_, + Operand operand_, + StackPointer stackTop_ + ) internal pure returns (StackPointer) { + uint256 inputs_ = Operand.unwrap(operand_) & MASK_5BIT; + uint256 outputs_ = (Operand.unwrap(operand_) >> 5) & MASK_5BIT; + uint256 offset_ = Operand.unwrap(operand_) >> 10; + + if (offset_ >= integrityCheckState_.constantsLength) { + revert OutOfBoundsConstantsRead( + integrityCheckState_.constantsLength, + offset_ + ); + } + + return + integrityCheckState_.push( + integrityCheckState_.pop(stackTop_, inputs_), + outputs_ + ); + } + + function intern( + InterpreterState memory interpreterState_, + Operand operand_, + StackPointer stackTop_ + ) internal view returns (StackPointer) { + IInterpreterExternV1 interpreterExtern_; + ExternDispatch externDispatch_; + uint256 head_; + uint256[] memory tail_; + { + uint256 inputs_ = Operand.unwrap(operand_) & MASK_5BIT; + uint256 offset_ = (Operand.unwrap(operand_) >> 10); + + // Mirrors constant opcode. + EncodedExternDispatch encodedDispatch_; + assembly ("memory-safe") { + encodedDispatch_ := mload(add( + mload(add(interpreterState_, 0x20)), + mul(0x20, offset_) + )) + } + + (interpreterExtern_, externDispatch_) = LibExtern.decode( + encodedDispatch_ + ); + (head_, tail_) = stackTop_.list(inputs_); + stackTop_ = stackTop_.down(inputs_).down().push(head_); + } + + { + uint256 outputs_ = (Operand.unwrap(operand_) >> 5) & MASK_5BIT; + + uint256[] memory results_ = interpreterExtern_.extern( + externDispatch_, + tail_ + ); + + if (results_.length != outputs_) { + revert BadExternResultsLength(outputs_, results_.length); + } + + stackTop_ = stackTop_.push(results_); + } + + return stackTop_; + } +} diff --git a/contracts/interpreter/shared/Rainterpreter.sol b/contracts/interpreter/shared/Rainterpreter.sol index 245d13bfc..9d7d6dd39 100644 --- a/contracts/interpreter/shared/Rainterpreter.sol +++ b/contracts/interpreter/shared/Rainterpreter.sol @@ -17,7 +17,7 @@ error UnexpectedOpMetaHash(bytes32 actualOpMeta); /// @dev Hash of the known store bytecode. bytes32 constant STORE_BYTECODE_HASH = bytes32( - 0xaa747007d5351b774ad5b5fcea64d2d3b0f43b4dcf5d366bf1a61455a3ecef7c + 0xbc52db76e944bf5245c516990668f268c9d2f24dc3aa1b06b4f9d128914df383 ); /// @dev Hash of the known op meta. diff --git a/contracts/interpreter/shared/RainterpreterExpressionDeployer.sol b/contracts/interpreter/shared/RainterpreterExpressionDeployer.sol index 0cc53612b..f668f55b9 100644 --- a/contracts/interpreter/shared/RainterpreterExpressionDeployer.sol +++ b/contracts/interpreter/shared/RainterpreterExpressionDeployer.sol @@ -21,7 +21,7 @@ error MissingEntrypoint(uint256 expectedEntrypoints, uint256 actualEntrypoints); /// immutable for any given interpreter so once the expression deployer is /// constructed and has verified that this matches what the interpreter reports, /// it can use this constant value to compile and serialize expressions. -bytes constant OPCODE_FUNCTION_POINTERS = hex"09ce09dc0a320a840b020b2e0bc70c910dc60dfb0e190ea10eb00ebe0ecc0eda0eb00ee80ef60f040f130f210f300f3e0f4c0fc40fd30fe20ff11000100f101e10671079108710b910c710d510e310f211011110111f112e113d114c115b116a11791188119611a411b211c011ce11dc11ea11f912081216128d"; +bytes constant OPCODE_FUNCTION_POINTERS = hex"09d709e50a3b0a8d0b0b0b370bd00d5b0e250f5a0f8f0fad1035104410521060106e1044107c108a109810a710b510c410d210e01158116711761185119411a311b211fb120d121b124d125b126912771286129512a412b312c212d112e012ef12fe130d131c132a13381346135413621370137e138d139c13aa141c"; /// @title RainterpreterExpressionDeployer /// @notice Minimal binding of the `IExpressionDeployerV1` interface to the diff --git a/contracts/interpreter/shared/RainterpreterExtern.sol b/contracts/interpreter/shared/RainterpreterExtern.sol new file mode 100644 index 000000000..96b8dcfa8 --- /dev/null +++ b/contracts/interpreter/shared/RainterpreterExtern.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: CAL +pragma solidity ^0.8.15; + +import "../extern/IInterpreterExternV1.sol"; +import "../ops/chainlink/OpChainlinkOraclePrice.sol"; +import "../run/LibStackPointer.sol"; +import "../../array/LibUint256Array.sol"; + +/// Thrown when the inputs don't match the expected inputs. +error BadInputs(uint256 expected, uint256 actual); + +/// EXPERIMENTAL implementation of `IInterpreterExternV1`. +/// Currently only implements the Chainlink oracle price opcode as a starting +/// point to test and flesh out externs generally. +/// Hopefully one day the idea of there being only a single extern contract seems +/// quaint. +contract RainterpreterExtern is IInterpreterExternV1 { + using LibStackPointer for uint256[]; + using LibStackPointer for StackPointer; + using LibUint256Array for uint256; + + /// @inheritdoc IInterpreterExternV1 + function extern( + ExternDispatch, + uint256[] memory inputs_ + ) external view returns (uint256[] memory) { + if (inputs_.length != 2) { + revert BadInputs(2, inputs_.length); + } + return + inputs_ + .asStackPointerAfter() + .applyFn(OpChainlinkOraclePrice.f) + .peek() + .arrayFrom(); + } +} diff --git a/test/Interpreter/Ops/Chainlink/chainlinkPrice.ts b/test/Interpreter/Ops/Chainlink/chainlinkPrice.ts index 2eb8ebb17..87e6e62a7 100644 --- a/test/Interpreter/Ops/Chainlink/chainlinkPrice.ts +++ b/test/Interpreter/Ops/Chainlink/chainlinkPrice.ts @@ -1,8 +1,14 @@ -import type { AggregatorV3Interface } from "../../../../typechain"; +import type { + AggregatorV3Interface, + RainterpreterExtern, + Rainterpreter, + IInterpreterV1Consumer, +} from "../../../../typechain"; import { AllStandardOps, assertError, eighteenZeros, + externOperand, getBlockTimestamp, memoryOperand, MemoryType, @@ -13,18 +19,38 @@ import { import { FakeContract, smock } from "@defi-wonderland/smock"; import { concat } from "ethers/lib/utils"; import { assert } from "chai"; -import { iinterpreterV1ConsumerDeploy } from "../../../../utils/deploy/test/iinterpreterV1Consumer/deploy"; +import { + expressionConsumerDeploy, + iinterpreterV1ConsumerDeploy, +} from "../../../../utils/deploy/test/iinterpreterV1Consumer/deploy"; +import { + rainterpreterDeploy, + rainterpreterExtern, +} from "../../../../utils/deploy/interpreter/shared/rainterpreter/deploy"; +import { ethers } from "hardhat"; const Opcode = AllStandardOps; describe("CHAINLINK_PRICE Opcode tests", async function () { - let fakeChainlinkOracle: FakeContract; + let rainInterpreter: Rainterpreter; + let logic: IInterpreterV1Consumer; + let rainInterpreterExtern: RainterpreterExtern; + beforeEach(async () => { - fakeChainlinkOracle = await smock.fake("AggregatorV3Interface"); + rainInterpreter = await rainterpreterDeploy(); + + rainInterpreterExtern = await rainterpreterExtern(); + const consumerFactory = await ethers.getContractFactory( + "IInterpreterV1Consumer" + ); + logic = (await consumerFactory.deploy()) as IInterpreterV1Consumer; + await logic.deployed(); }); - it("should revert if price is stale", async () => { + it("should revert if price is stale", async () => { + + const fakeChainlinkOracle = await smock.fake("AggregatorV3Interface"); const chainlinkPriceData = { roundId: 1, answer: 123 + eighteenZeros, @@ -81,7 +107,9 @@ describe("CHAINLINK_PRICE Opcode tests", async function () { ); }); - it("should revert if price is 0", async () => { + it("should revert if price is 0", async () => { + const fakeChainlinkOracle = await smock.fake("AggregatorV3Interface"); + const chainlinkPriceData = { roundId: 1, answer: 0 + eighteenZeros, @@ -127,6 +155,8 @@ describe("CHAINLINK_PRICE Opcode tests", async function () { }); it("should correctly scale answer from 6 decimal to 18 decimal FP", async () => { + const fakeChainlinkOracle = await smock.fake("AggregatorV3Interface"); + const chainlinkPriceData = { roundId: 1, answer: 123 + sixZeros, @@ -169,6 +199,8 @@ describe("CHAINLINK_PRICE Opcode tests", async function () { }); it("should get price from chainlink oracle", async () => { + const fakeChainlinkOracle = await smock.fake("AggregatorV3Interface"); + const chainlinkPriceData = { roundId: 1, answer: 123 + eighteenZeros, @@ -210,4 +242,6 @@ describe("CHAINLINK_PRICE Opcode tests", async function () { assert(price_.eq(123 + eighteenZeros)); }); + + }); diff --git a/test/Interpreter/Shared/RainterpreterExtern/extern.ts b/test/Interpreter/Shared/RainterpreterExtern/extern.ts new file mode 100644 index 000000000..62a52bc7e --- /dev/null +++ b/test/Interpreter/Shared/RainterpreterExtern/extern.ts @@ -0,0 +1,258 @@ +import type { + AggregatorV3Interface, + RainterpreterExtern, + Rainterpreter, + IInterpreterV1Consumer + } from "../../../../typechain"; + import { + AllStandardOps, + assertError, + eighteenZeros, + externOperand, + getBlockTimestamp, + memoryOperand, + MemoryType, + op, + sixZeros, + timewarp, + } from "../../../../utils"; + import { FakeContract, smock } from "@defi-wonderland/smock"; + import { concat } from "ethers/lib/utils"; + import { assert } from "chai"; + import { expressionConsumerDeploy, iinterpreterV1ConsumerDeploy } from "../../../../utils/deploy/test/iinterpreterV1Consumer/deploy"; + import { rainterpreterDeploy, rainterpreterExtern } from "../../../../utils/deploy/interpreter/shared/rainterpreter/deploy"; + import { ethers } from "hardhat"; + + const Opcode = AllStandardOps; + + describe("CHAINLINK_PRICE Opcode tests", async function () { + let rainInterpreter: Rainterpreter; + let logic: IInterpreterV1Consumer; + let rainInterpreterExtern: RainterpreterExtern; + + beforeEach(async () => { + rainInterpreter = await rainterpreterDeploy(); + rainInterpreterExtern = await rainterpreterExtern(); + const consumerFactory = await ethers.getContractFactory( + "IInterpreterV1Consumer" + ); + logic = (await consumerFactory.deploy()) as IInterpreterV1Consumer; + await logic.deployed(); + + + }); + + it("extern op should revert if price is 0", async () => { + const fakeChainlinkOracle = await smock.fake("AggregatorV3Interface"); + + const chainlinkPriceData = { + roundId: 1, + answer: 0 + eighteenZeros, + startedAt: 2, + updatedAt: 3, + answeredInRound: 4, + }; + + fakeChainlinkOracle.latestRoundData.returns(chainlinkPriceData); + fakeChainlinkOracle.decimals.returns(18); + + const feed = fakeChainlinkOracle.address; + const staleAfter = (await getBlockTimestamp()) + 10000; + + const constants = [rainInterpreterExtern.address,feed,staleAfter] + + const v0 = op(Opcode.READ_MEMORY, memoryOperand(MemoryType.Constant, 1)); + const v1 = op(Opcode.READ_MEMORY, memoryOperand(MemoryType.Constant, 2)); + + // prettier-ignore + const source0 = concat([ + v0, + v1, + op(Opcode.EXTERN, externOperand(0, 2 ,1)), + ]); + + const expression0 = await expressionConsumerDeploy( + { + sources: [source0], + constants, + }, + rainInterpreter, + 1 + ); + + + await assertError( + async () => + await logic["eval(address,uint256,uint256[][])"]( + rainInterpreter.address, + expression0.dispatch, + [] + ), + "NotPosIntPrice(0)", + "did not revert when chainlink price was 0" + ); + + + }); + + it("extern op should correctly scale answer from 6 decimal to 18 decimal FP", async () => { + const fakeChainlinkOracle = await smock.fake("AggregatorV3Interface"); + + const chainlinkPriceData = { + roundId: 1, + answer: 123 + sixZeros, + startedAt: 2, + updatedAt: 3, + answeredInRound: 4, + }; + + fakeChainlinkOracle.latestRoundData.returns(chainlinkPriceData); + fakeChainlinkOracle.decimals.returns(6); + + const feed = fakeChainlinkOracle.address; + const staleAfter = (await getBlockTimestamp()) + 10000; + + const constants = [rainInterpreterExtern.address,feed,staleAfter] + + const v0 = op(Opcode.READ_MEMORY, memoryOperand(MemoryType.Constant, 1)); + const v1 = op(Opcode.READ_MEMORY, memoryOperand(MemoryType.Constant, 2)); + + // prettier-ignore + const source0 = concat([ + v0, + v1, + op(Opcode.EXTERN, externOperand(0, 2 ,1)), + ]); + + const expression0 = await expressionConsumerDeploy( + { + sources: [source0], + constants, + }, + rainInterpreter, + 1 + ); + await logic["eval(address,uint256,uint256[][])"]( + rainInterpreter.address, + expression0.dispatch, + [] + ); + const result0 = await logic.stackTop(); + + assert(result0.eq(123 + eighteenZeros)); + + }); + + it("extern op should return expected value", async () => { + const fakeChainlinkOracle2 = await smock.fake("AggregatorV3Interface"); + + const timestamp = await getBlockTimestamp(); + + const chainlinkPriceData = { + roundId: 4, + answer: 123 + eighteenZeros, + startedAt: timestamp, + updatedAt: timestamp, + answeredInRound: 4, + }; + + fakeChainlinkOracle2.latestRoundData.returns(chainlinkPriceData); + fakeChainlinkOracle2.decimals.returns(18); + + const feed = fakeChainlinkOracle2.address; + const staleAfter = 10000; + + const constants = [rainInterpreterExtern.address,feed,staleAfter] + + const v0 = op(Opcode.READ_MEMORY, memoryOperand(MemoryType.Constant, 1)); + const v1 = op(Opcode.READ_MEMORY, memoryOperand(MemoryType.Constant, 2)); + + // prettier-ignore + const source0 = concat([ + v0, + v1, + op(Opcode.EXTERN, externOperand(0, 2 ,1)), + ]); + + const expression0 = await expressionConsumerDeploy( + { + sources: [source0], + constants, + }, + rainInterpreter, + 1 + ); + await logic["eval(address,uint256,uint256[][])"]( + rainInterpreter.address, + expression0.dispatch, + [] + ); + const result0 = await logic.stackTop(); + + assert(result0.eq(123 + eighteenZeros)); + + }); + + it("rainInterpreterExtern should revert with BadInputs", async () => { + const fakeChainlinkOracle2 = await smock.fake("AggregatorV3Interface"); + + const timestamp = (await getBlockTimestamp()) - 1; + const chainlinkPriceData = { + roundId: 4, + answer: "123" + eighteenZeros, + startedAt: timestamp, + updatedAt: timestamp, + answeredInRound: 4, + }; + + fakeChainlinkOracle2.latestRoundData.returns(chainlinkPriceData); + fakeChainlinkOracle2.decimals.returns(18); + + const feed = fakeChainlinkOracle2.address; + + const inputs = [feed]; + + await assertError( + async () => + await rainInterpreterExtern.extern(0, inputs), + "BadInputs", + "did not revert when incorrect inputs" + ); + + + }); + + it("rainInterpreterExtern should get price from oracle", async () => { + const fakeChainlinkOracle2 = await smock.fake("AggregatorV3Interface"); + + const timestamp = (await getBlockTimestamp()) - 1; + const chainlinkPriceData = { + roundId: 4, + answer: "123" + eighteenZeros, + startedAt: timestamp, + updatedAt: timestamp, + answeredInRound: 4, + }; + + fakeChainlinkOracle2.latestRoundData.returns(chainlinkPriceData); + fakeChainlinkOracle2.decimals.returns(18); + + const feed = fakeChainlinkOracle2.address; + const staleAfter = 10000; + + const inputs = [feed , staleAfter ]; + + const priceData = await rainInterpreterExtern.extern(0, inputs); + assert(priceData[0].eq(123 + eighteenZeros)); + + + }); + + + + + + + + }); + \ No newline at end of file diff --git a/utils/deploy/interpreter/shared/rainterpreter/deploy.ts b/utils/deploy/interpreter/shared/rainterpreter/deploy.ts index 528a1b9ca..f24f3b9a2 100644 --- a/utils/deploy/interpreter/shared/rainterpreter/deploy.ts +++ b/utils/deploy/interpreter/shared/rainterpreter/deploy.ts @@ -1,4 +1,4 @@ -import { Rainterpreter, RainterpreterStore } from "../../../../../typechain"; +import { Rainterpreter, RainterpreterExtern, RainterpreterStore } from "../../../../../typechain"; import { getRainterpreterOpmetaBytes } from "../../../../interpreter/ops/allStandardOpmeta"; import { basicDeploy } from "../../../basicDeploy"; @@ -18,3 +18,7 @@ export const rainterpreterDeploy = async () => { export const rainterpreterStoreDeploy = async () => { return (await basicDeploy("RainterpreterStore", {})) as RainterpreterStore; }; + +export const rainterpreterExtern = async () => { + return (await basicDeploy("RainterpreterExtern", {})) as RainterpreterExtern; +}; diff --git a/utils/interpreter/interpreter.ts b/utils/interpreter/interpreter.ts index cebd87b8e..624405305 100644 --- a/utils/interpreter/interpreter.ts +++ b/utils/interpreter/interpreter.ts @@ -87,6 +87,15 @@ export function callOperand( return operand; } +export function externOperand( + offset: number, + inputs: number, + outputs: number +): number { + const operand = (offset << 10) + (outputs << 5) + inputs; + return operand; +} + /** * Builds the operand for RainInterpreter's `LOOP_N` opcode by packing 4 numbers into a single byte. * diff --git a/utils/interpreter/ops/allStandardOps.ts b/utils/interpreter/ops/allStandardOps.ts index 5ff1eb2f6..c9d1b48ac 100644 --- a/utils/interpreter/ops/allStandardOps.ts +++ b/utils/interpreter/ops/allStandardOps.ts @@ -5,6 +5,7 @@ export enum AllStandardOps { CONTEXT_ROW, DEBUG, DO_WHILE, + EXTERN, FOLD_CONTEXT, GET, LOOP_N,