├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── bin └── huffc ├── dist ├── bin.js ├── compiler │ ├── compiler.js │ └── processor.js ├── evm │ └── opcodes.js ├── index.js ├── output │ └── index.js ├── parser │ ├── high-level.js │ ├── macros.js │ ├── syntax │ │ └── defintions.js │ ├── tables.js │ └── utils │ │ ├── contents.js │ │ ├── parsing.js │ │ ├── regex.js │ │ └── types.js └── utils │ ├── bytes.js │ └── files.js ├── docs └── README.md ├── logo.png ├── package.json ├── src ├── bin.ts ├── compiler │ ├── compiler.ts │ └── processor.ts ├── evm │ └── opcodes.ts ├── index.ts ├── output │ └── index.ts ├── parser │ ├── high-level.ts │ ├── macros.ts │ ├── syntax │ │ ├── defintions.ts │ │ └── reserved.ts │ ├── tables.ts │ └── utils │ │ ├── contents.ts │ │ ├── parsing.ts │ │ ├── regex.ts │ │ └── types.ts └── utils │ ├── bytes.ts │ └── files.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .huff -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "mocha": true, 5 | "node": true, 6 | "browser": true 7 | }, 8 | "rules": { 9 | "strict": 0, 10 | "arrow-body-style": 0, 11 | "comma-dangle": [ 12 | "error", 13 | { 14 | "arrays": "always-multiline", 15 | "objects": "always-multiline", 16 | "imports": "always-multiline", 17 | "exports": "always-multiline", 18 | "functions": "never" 19 | } 20 | ], 21 | "import/no-extraneous-dependencies": 0, 22 | "indent": [ 23 | "error", 24 | 4, 25 | { 26 | "SwitchCase": 1 27 | } 28 | ], 29 | "linebreak-style": 0, 30 | "no-console": 0, 31 | "no-underscore-dangle": [ 32 | "error", 33 | { 34 | "allow": [ 35 | "_id" 36 | ] 37 | } 38 | ], 39 | "prefer-template": 0, 40 | "max-len": [ 41 | "warn", 42 | 130, 43 | { 44 | "ignoreComments": true 45 | }, 46 | { 47 | "ignoreTrailingComments": true 48 | } 49 | ] 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | huff_modules 4 | tests -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 100 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Huff Programming Language 2 | 3 | ## 📌 DEPRECATION WARNING 4 | 5 | This repository is considered deprecated and will be archived. For the new version of this tool please go to [huff-rs](https://github.com/huff-language/huff-rs). 6 | 7 | You can continue to use this and it should work as-is but any future issues will not be fixed by the community. 8 | 9 | ------- 10 | 11 | ![Huff logo.](logo.png) 12 | 13 | Huff is a low-level programming language designed for developing highly optimized smart contracts that run on the Ethereum Virtual Machine (EVM). Huff does not hide the inner workings of the EVM. Instead, Huff exposes its programming stack to the developer for manual manipulation. 14 | 15 | Rather than having functions, Huff has macros - individual blocks of bytecode that can be rigorously tested and evaluated using the Huff runtime testing suite. 16 | 17 | Initially developed by the Aztec Protocol team, Huff was created to write [Weierstrudel](https://github.com/aztecprotocol/weierstrudel/tree/master/huff_modules). Weierstrudel is an on-chain elliptical curve arithmetic library that requires incredibly optimized code that neither [Solidity](https://docs.soliditylang.org/en/v0.8.14/) nor [Yul](https://docs.soliditylang.org/en/v0.8.9/yul.html) could provide. 18 | 19 | While EVM experts can use Huff to write highly-efficient smart contracts for use in production, it can also serve as a way for beginners to learn more about the EVM. 20 | 21 | ## Examples 22 | 23 | For usage examples, see the [huff-examples](https://github.com/huff-language/huff-examples) repository. 24 | 25 | ## Installation 26 | 27 | ### Prerequisities 28 | 29 | Make sure you have the following programs installed: 30 | 31 | - [yarn](https://www.npmjs.com/package/yarn) 32 | - [Typescript](https://www.npmjs.com/package/typescript) 33 | - [ts-node](https://www.npmjs.com/package/ts-node#overview) 34 | 35 | ### Steps 36 | 37 | This is how to create the contract bytecode to output _Hello, World!_ in Huff. 38 | 39 | 1. Install Huff globally: 40 | 41 | ```shell 42 | yarn global add huffc 43 | ``` 44 | 45 | **Note:** You may need to add yarn to your system's path to access globally installed packages. See [the yarn docs on global](https://classic.yarnpkg.com/en/docs/cli/global) for more details. 46 | 47 | ## Hello World 48 | 49 | 1. Create a file called `hello-world.huff` and enter the following content: 50 | 51 | ```javascript 52 | #define macro MAIN() = takes (0) returns (0) { 53 | 0x48656c6c6f2c20776f726c6421 0x00 mstore // Store "Hello, World!" in memory. 54 | 0x1a 0x00 return // Return 26 bytes starting from memory pointer 0. 55 | } 56 | ``` 57 | 58 | 2. Use `huffc` to compile the contract and output bytecode: 59 | 60 | ```shell 61 | huffc hello-world.huff --bytecode 62 | ``` 63 | 64 | This will output something like: 65 | 66 | ```plaintext 67 | 6100168061000d6000396000f36c48656c6c6f2c20776f726c6421600052601a6000f3 68 | ``` 69 | 70 | ## More help 71 | 72 | Run `huffc --help` to view a full list of arguments: 73 | 74 | ```shell 75 | huffc --help 76 | 77 | > Usage: huffc [options] 78 | > 79 | > Options: 80 | > -V, --version output the version number 81 | > -V, --version Show the version and exit 82 | > --base-path The base path to the contracts (default: "./") 83 | > --output-directory The output directory (default: "./") 84 | > --bytecode Generate and log bytecode (default: false) 85 | > -o, output The output file 86 | > -p, --paste Paste the output to the terminal 87 | > -h, --help display help for command 88 | ``` 89 | -------------------------------------------------------------------------------- /bin/huffc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node 2 | 3 | require("../src/bin"); 4 | -------------------------------------------------------------------------------- /dist/bin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var commander_1 = require("commander"); 4 | var _1 = require("./"); 5 | var fs = require("fs"); 6 | // Compiler Version 7 | var version = "2.0.0"; 8 | // Define terminal commands. 9 | commander_1.program.name("huffc"); 10 | commander_1.program.version(version); 11 | commander_1.program 12 | .option("-V, --version", "Show the version and exit") 13 | .option("--base-path ", "The base path to the contracts", "./") 14 | .option("--output-directory ", "The output directory", "./") 15 | .option("--bytecode", "Generate and log bytecode", false) 16 | .option("-o, output", "The output file") 17 | .option("-p, --paste", "Paste the output to the terminal"); 18 | // Parse the terminal arguments. 19 | commander_1.program.parse(process.argv); 20 | var options = commander_1.program.opts(); 21 | var files = commander_1.program.args; 22 | var destination = options.outputDir || "."; 23 | // Abort the program. 24 | var abort = function (msg) { 25 | console.error(msg || "Error occured"); 26 | process.exit(1); 27 | }; 28 | // Iterate the imported files. 29 | files.forEach(function (file) { 30 | // Abort if the file extension is not .huff. 31 | if (!file.endsWith(".huff")) 32 | abort("File extension must be .huff"); 33 | // Compile the file. 34 | var result = (0, _1["default"])({ 35 | filePath: file, 36 | generateAbi: true 37 | }); 38 | // If the user has specified an output file, write the output to the file. 39 | var outputPath = "".concat(options.outputDirectory).concat(files[0].replace(".huff", ".json")); 40 | if (options.output) 41 | fs.writeFileSync(outputPath, result.abi); 42 | // If the user has specified for us to log the bytecode, log it. 43 | if (options.bytecode && !options.paste) 44 | console.log(result.bytecode); 45 | if (options.paste) 46 | console.log(result); 47 | }); 48 | -------------------------------------------------------------------------------- /dist/compiler/compiler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.compileMacro = void 0; 4 | var bytes_1 = require("../utils/bytes"); 5 | var processor_1 = require("./processor"); 6 | /** 7 | * Compile a macro into raw EVM bytecode. 8 | * @param name The name of the macro. 9 | * @param args An array of arguments passed into the macro. 10 | * @param macros Maps all macros to their raw text and arguments. 11 | * @param constants Maps all constants to their values. 12 | * @param jumptables Maps all jump tables to their jump values. 13 | */ 14 | var compileMacro = function (name, args, macros, constants, jumptables) { 15 | // Process the macro and generate semi-complete EVM bytecode. 16 | var _a = (0, processor_1.processMacro)(name, 0, args, macros, constants, jumptables), bytecode = _a.bytecode, unmatchedJumps = _a.unmatchedJumps, tableInstances = _a.tableInstances, jumptable = _a.jumptable, jumpindices = _a.jumpindices; 17 | // Ensure that there are no unmatched jumps. 18 | if (unmatchedJumps.length > 0) { 19 | throw new Error("Compiler: Macro ".concat(name, " contains unmatched jump labels ").concat(unmatchedJumps 20 | .map(function (jump) { return jump.label; }) 21 | .join(" "), ", cannot compile")); 22 | } 23 | // Instantiate variables 24 | var tableBytecodeOffset = bytecode.length / 2; 25 | var tableOffsets = {}; 26 | // Iterate over the jumptables. 27 | Object.keys(jumptables).forEach(function (jumpkey) { 28 | // Get the jump table value. 29 | var table = jumptables[jumpkey]; 30 | // Store the table code. 31 | var tableCode; 32 | // Store the jumps and size. 33 | var _a = table.args, jumps = _a[0], size = _a[1]; 34 | // tableOffsets[name] = tableBytecodeOffset; 35 | tableOffsets[table.data[0]] = tableBytecodeOffset; 36 | // Incerment the table offset. 37 | tableBytecodeOffset += size; 38 | // Iterate through the jumplabels. 39 | tableCode = table.args[0] 40 | .map(function (jumplabel) { 41 | // If the label has not been defined, return "". 42 | if (!jumpindices[jumplabel]) 43 | return ""; 44 | // Otherwise, set the offset to the jumpindice. 45 | var offset = jumpindices[jumplabel]; 46 | var hex = (0, bytes_1.formatEvenBytes)((0, bytes_1.toHex)(offset)); 47 | // If the table has been compressed. 48 | if (!table.data[1]) { 49 | return (0, bytes_1.padNBytes)(hex, 0x20); 50 | } 51 | else { 52 | return (0, bytes_1.padNBytes)(hex, 0x02); 53 | } 54 | }) 55 | .join(""); 56 | bytecode += tableCode; 57 | }); 58 | // Remove table instance placeholders. 59 | tableInstances.forEach(function (instance) { 60 | // Store the name and offset of the instance. 61 | var name = instance.label, offset = instance.bytecodeIndex; 62 | // Ensure the table has been defined. 63 | if (!tableOffsets[name]) { 64 | throw new Error("Expected to find ".concat(instance.label, " in ").concat(JSON.stringify(tableOffsets))); 65 | } 66 | // Slice the bytecode to get the code before and after the offset. 67 | var before = bytecode.slice(0, offset * 2 + 2); 68 | var after = bytecode.slice(offset * 2 + 6); 69 | // Insert the offset value. 70 | bytecode = "".concat(before).concat((0, bytes_1.padNBytes)((0, bytes_1.toHex)(tableOffsets[name]), 2)).concat(after); 71 | }); 72 | return bytecode; 73 | }; 74 | exports.compileMacro = compileMacro; 75 | -------------------------------------------------------------------------------- /dist/compiler/processor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { 14 | if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { 15 | if (ar || !(i in from)) { 16 | if (!ar) ar = Array.prototype.slice.call(from, 0, i); 17 | ar[i] = from[i]; 18 | } 19 | } 20 | return to.concat(ar || Array.prototype.slice.call(from)); 21 | }; 22 | exports.__esModule = true; 23 | exports.processMacro = void 0; 24 | var opcodes_1 = require("../evm/opcodes"); 25 | var macros_1 = require("../parser/macros"); 26 | var types_1 = require("../parser/utils/types"); 27 | var bytes_1 = require("../utils/bytes"); 28 | /** 29 | * Process a macro, generating semi-complete EVM bytecode. 30 | * @param name The name of the macro. 31 | * @param bytecodeOffset The offset of the macro's bytecode. 32 | * @param args The arguments passed into the array. 33 | * @param macros Maps all macros to their raw text and arguments. 34 | * @param constants Maps all constants to their values. 35 | * @param jumptables Maps all jump tables to their jump values. 36 | * @returns Semi-complete EVM bytecode. 37 | */ 38 | var processMacro = function (name, bytecodeOffset, args, macros, constants, jumptables) { 39 | // Check if the macro exists. 40 | var macro = macros[name]; 41 | if (!macro) 42 | throw new Error("Processor: Failed to find ".concat(macro, ".")); 43 | // Map argument names to values. 44 | var params = {}; 45 | macro.args.forEach(function (arg, index) { 46 | params[arg] = args[index]; 47 | }); 48 | // Instantiate variables. 49 | var jumptable = []; 50 | var jumpindices = {}; 51 | var tableInstances = []; 52 | var offset = bytecodeOffset; 53 | // Store a copy of the body and args. 54 | var codes = macro.data.map(function (operation, index) { 55 | switch (operation.type) { 56 | // We're parsing a macro call. 57 | case types_1.OperationType.MACRO_CALL: { 58 | // Replace macro arguments with their values. 59 | operation.args.map(function (arg, index) { 60 | if (arg.startsWith("<") && arg.endsWith(">")) 61 | operation.args[index] = args[index]; 62 | }); 63 | // Process the macro call. 64 | var result = (0, exports.processMacro)(operation.value, offset, operation.args, macros, constants, jumptables); 65 | // Add the result to local variables. 66 | tableInstances = __spreadArray(__spreadArray([], tableInstances, true), result.tableInstances, true); 67 | jumptable[index] = result.jumptable = result.unmatchedJumps; 68 | jumpindices = __assign(__assign({}, jumpindices), result.jumpindices); 69 | // Add to the offset. 70 | offset += result.bytecode.length / 2; 71 | // Return the result. 72 | return result.bytecode; 73 | } 74 | // We're parsing a constant call. 75 | case types_1.OperationType.CONSTANT_CALL: { 76 | // Get the value of the constant. 77 | var value = constants[operation.value].value; 78 | // Get the push value 79 | var push = "".concat((0, bytes_1.toHex)(95 + value.length / 2)).concat(value); 80 | // Add to the offset. 81 | offset += push.length / 2; 82 | // Return the bytecode 83 | return push; 84 | } 85 | // We're parsing an argument call. 86 | case types_1.OperationType.ARG_CALL: { 87 | // Get the value of the argument. 88 | var arg = params[operation.value]; 89 | // Parse the arguments. 90 | var result = (0, exports.processMacro)(arg, offset, operation.args, (0, macros_1.parseArgument)(arg, macros, constants), constants, jumptables); 91 | // Set the jumplabels to the macro's unmatched jumps. 92 | jumptable[index] = result.unmatchedJumps; 93 | // Add to the offset. 94 | offset += result.bytecode.length / 2; 95 | // Return the bytecode 96 | return result.bytecode; 97 | } 98 | // We're parsing an opcodes. 99 | case types_1.OperationType.OPCODE: { 100 | // An opcode is a single byte of data. 101 | offset += 1; 102 | // Return the opcode value. 103 | return opcodes_1["default"][operation.value]; 104 | } 105 | // We're parsing a push operation. 106 | case types_1.OperationType.PUSH: { 107 | // Get the push value. 108 | var push = "".concat(operation.value).concat(operation.args[0]); 109 | // Add to the offset. 110 | offset += push.length / 2; 111 | // Return the bytecode. 112 | return "".concat(operation.value).concat(operation.args[0]); 113 | } 114 | // We're parsing a codesize call. 115 | case types_1.OperationType.CODESIZE: { 116 | // Calculate the code of the macro. 117 | var code = (0, exports.processMacro)(operation.value, offset, operation.args, macros, constants, jumptables).bytecode; 118 | // Calculate the length. 119 | var length_1 = (0, bytes_1.formatEvenBytes)((code.length / 2).toString(16)); 120 | // Get the push value. 121 | var push = "".concat((0, bytes_1.toHex)(95 + length_1.length / 2)).concat(length_1); 122 | // Add to the offset. 123 | offset += push.length / 2; 124 | // Return a push operation, that pushes the length to the stack. 125 | return push; 126 | } 127 | // We're parsing a jump label push. 128 | case types_1.OperationType.PUSH_JUMP_LABEL: { 129 | // Add to the jumptables 130 | jumptable[index] = [{ label: operation.value, bytecodeIndex: 0 }]; 131 | // Add to the offset. 132 | offset += 3; 133 | // Return the bytecode 134 | return "".concat(opcodes_1["default"].push2, "xxxx"); 135 | } 136 | // We're parsing a table start position call. 137 | case types_1.OperationType.TABLE_START_POSITION: { 138 | // Add to the tableInstances. 139 | tableInstances.push({ label: operation.value, bytecodeIndex: offset }); 140 | // Add to the offset. 141 | offset += 3; 142 | // Return the bytecode. 143 | return "".concat(opcodes_1["default"].push2, "xxxx"); 144 | } 145 | // We're parsing a jumpdest. 146 | case types_1.OperationType.JUMPDEST: { 147 | // Add to the jumpindices array. 148 | jumpindices[operation.value] = offset; 149 | // Add to the offset. 150 | offset += 1; 151 | // Return the bytecode. 152 | return opcodes_1["default"].jumpdest; 153 | } 154 | // Default 155 | default: { 156 | throw new Error("Processor: Cannot understand operation ".concat(operation.type, " in ").concat(name, ".")); 157 | } 158 | } 159 | }); 160 | // Store the current index. 161 | var currentIndex = bytecodeOffset; 162 | // Loop through the code definitions. 163 | var indices = codes.map(function (bytecode) { 164 | // Update the current index. 165 | currentIndex += bytecode.length / 2; 166 | // Return the index. 167 | return currentIndex; 168 | }); 169 | // Add the initial index to the start of the array. 170 | indices.unshift(bytecodeOffset); 171 | // Store an array of unmatched jumps. 172 | var unmatchedJumps = []; 173 | // Get the absolute bytecode index for each jump label. 174 | var newBytecode = codes.reduce(function (accumulator, bytecode, index) { 175 | // Store a formatted version of the bytecode. 176 | var formattedBytecode = bytecode; 177 | // Check a jump table exists at this index. 178 | if (jumptable[index]) { 179 | // Store the jumps. 180 | var jumps = jumptable[index]; 181 | // Iterate over the jumps at this index. 182 | for (var _i = 0, jumps_1 = jumps; _i < jumps_1.length; _i++) { 183 | var _a = jumps_1[_i], jumplabel = _a.label, bytecodeIndex = _a.bytecodeIndex; 184 | // If the jumplabel is defined: 185 | if (jumpindices.hasOwnProperty(jumplabel)) { 186 | // Get the absolute bytecode index and pad the value (1 byte). 187 | var jumpvalue = (0, bytes_1.padNBytes)((0, bytes_1.toHex)(jumpindices[jumplabel]), 2); 188 | // Slice the bytecode to get the code before and after the jump. 189 | var before = formattedBytecode.slice(0, bytecodeIndex + 2); 190 | var after = formattedBytecode.slice(bytecodeIndex + 6); 191 | // Ensure that the jump value is set with a placeholder. 192 | if (formattedBytecode.slice(bytecodeIndex + 2, bytecodeIndex + 6) !== "xxxx") 193 | throw new Error("Processor: Expected indicies ".concat(bytecodeIndex + 2, " to ").concat(bytecodeIndex + 6, " to be jump location, of ").concat(formattedBytecode)); 194 | // Insert the jump value. 195 | formattedBytecode = "".concat(before).concat(jumpvalue).concat(after); 196 | } 197 | // If the jumplabel has not been definied: 198 | else { 199 | // Store the offset. 200 | var jumpOffset = (indices[index] - bytecodeOffset) * 2; 201 | // Push the jump value to the unmatched jumps array. 202 | unmatchedJumps.push({ label: jumplabel, bytecodeIndex: jumpOffset + bytecodeIndex }); 203 | } 204 | } 205 | } 206 | // Return the new bytecode. 207 | return accumulator + formattedBytecode; 208 | }, ""); 209 | // Return the result. 210 | return { bytecode: newBytecode, unmatchedJumps: unmatchedJumps, tableInstances: tableInstances, jumptable: jumptable, jumpindices: jumpindices }; 211 | }; 212 | exports.processMacro = processMacro; 213 | -------------------------------------------------------------------------------- /dist/evm/opcodes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Maps all opcodes to their hex equivalents */ 3 | exports.__esModule = true; 4 | exports["default"] = { 5 | stop: "00", 6 | add: "01", 7 | mul: "02", 8 | sub: "03", 9 | div: "04", 10 | sdiv: "05", 11 | mod: "06", 12 | smod: "07", 13 | addmod: "08", 14 | mulmod: "09", 15 | exp: "0a", 16 | signextend: "0b", 17 | lt: "10", 18 | gt: "11", 19 | slt: "12", 20 | sgt: "13", 21 | eq: "14", 22 | iszero: "15", 23 | and: "16", 24 | or: "17", 25 | xor: "18", 26 | not: "19", 27 | byte: "1a", 28 | sha3: "20", 29 | keccak: "20", 30 | address: "30", 31 | balance: "31", 32 | origin: "32", 33 | caller: "33", 34 | callvalue: "34", 35 | calldataload: "35", 36 | calldatasize: "36", 37 | calldatacopy: "37", 38 | codesize: "38", 39 | codecopy: "39", 40 | gasprice: "3a", 41 | extcodesize: "3b", 42 | extcodecopy: "3c", 43 | returndatasize: "3d", 44 | returndatacopy: "3e", 45 | blockhash: "40", 46 | coinbase: "41", 47 | timestamp: "42", 48 | number: "43", 49 | difficulty: "44", 50 | gaslimit: "45", 51 | chainid: "46", 52 | pop: "50", 53 | mload: "51", 54 | mstore: "52", 55 | mstore8: "53", 56 | sload: "54", 57 | sstore: "55", 58 | jump: "56", 59 | jumpi: "57", 60 | getpc: "58", 61 | msize: "59", 62 | gas: "5a", 63 | jumpdest: "5b", 64 | push1: "60", 65 | push2: "61", 66 | push3: "62", 67 | push4: "63", 68 | push5: "64", 69 | push6: "65", 70 | push7: "66", 71 | push8: "67", 72 | push9: "68", 73 | push10: "69", 74 | push11: "6a", 75 | push12: "6b", 76 | push13: "6c", 77 | push14: "6d", 78 | push15: "6e", 79 | push16: "6f", 80 | push17: "70", 81 | push18: "71", 82 | push19: "72", 83 | push20: "73", 84 | push21: "74", 85 | push22: "75", 86 | push23: "76", 87 | push24: "77", 88 | push25: "78", 89 | push26: "79", 90 | push27: "7a", 91 | push28: "7b", 92 | push29: "7c", 93 | push30: "7d", 94 | push31: "7e", 95 | push32: "7f", 96 | log0: "a0", 97 | log1: "a1", 98 | log2: "a2", 99 | log3: "a3", 100 | log4: "a4", 101 | create: "f0", 102 | call: "f1", 103 | callcode: "f2", 104 | "return": "f3", 105 | delegatecall: "f4", 106 | staticcall: "fa", 107 | create2: "fb", 108 | revert: "fd", 109 | invalid: "fe", 110 | selfdestruct: "ff", 111 | dup1: "80", 112 | dup2: "81", 113 | dup3: "82", 114 | dup4: "83", 115 | dup5: "84", 116 | dup6: "85", 117 | dup7: "86", 118 | dup8: "87", 119 | dup9: "88", 120 | dup10: "89", 121 | dup11: "8a", 122 | dup12: "8b", 123 | dup13: "8c", 124 | dup14: "8d", 125 | dup15: "8e", 126 | dup16: "8f", 127 | swap1: "90", 128 | swap2: "91", 129 | swap3: "92", 130 | swap4: "93", 131 | swap5: "94", 132 | swap6: "95", 133 | swap7: "96", 134 | swap8: "97", 135 | swap9: "98", 136 | swap10: "99", 137 | swap11: "9a", 138 | swap12: "9b", 139 | swap13: "9c", 140 | swap14: "9d", 141 | swap15: "9e", 142 | swap16: "9f", 143 | shl: "1b", 144 | shr: "1c", 145 | sar: "1d", 146 | rol: "1e", 147 | ror: "1f" 148 | }; 149 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var bytes_1 = require("./utils/bytes"); 4 | var compiler_1 = require("./compiler/compiler"); 5 | var high_level_1 = require("./parser/high-level"); 6 | var ethers_1 = require("ethers"); 7 | var output_1 = require("./output"); 8 | /** 9 | * Compile a Huff file. 10 | * @param filePath The path to the file. 11 | * @param args An array containing the arguments to the macro. 12 | * @returns The compiled bytecode. 13 | */ 14 | var compile = function (args) { 15 | // Parse the file and generate definitions. 16 | var _a = (0, high_level_1.parseFile)(args.filePath), macros = _a.macros, constants = _a.constants, tables = _a.tables, functions = _a.functions, events = _a.events; 17 | // Generate the contract ABI. 18 | var abi = args.generateAbi ? (0, output_1.generateAbi)(functions, events) : ""; 19 | // Set storage pointer constants. 20 | constants.data = (0, high_level_1.setStoragePointerConstants)(["CONSTRUCTOR", "MAIN"], macros.data, constants); 21 | // Compile the macros. 22 | var mainBytecode = macros.data["MAIN"] 23 | ? (0, compiler_1.compileMacro)("MAIN", [], macros.data, constants.data, tables.data) 24 | : ""; 25 | var constructorBytecode = macros.data["CONSTRUCTOR"] 26 | ? (0, compiler_1.compileMacro)("CONSTRUCTOR", [], macros.data, constants.data, tables.data) 27 | : ""; 28 | // Store the sizes of the bytecode. 29 | var contractLength = mainBytecode.length / 2; 30 | var constructorLength = constructorBytecode.length / 2; 31 | // Bootstrap code variables 32 | var bootStrapCodeSize = 9; 33 | var pushContractSizeCode; 34 | var pushContractCodeOffset; 35 | // Compute pushX(contract size) 36 | if (contractLength < 256) { 37 | // Convert the size and offset to bytes. 38 | var contractSize = (0, bytes_1.padNBytes)((0, bytes_1.toHex)(contractLength), 1); 39 | // push1(contract size) 40 | pushContractSizeCode = "60".concat(contractSize); 41 | } 42 | else { 43 | // Increment bootstrap code size 44 | bootStrapCodeSize++; 45 | // Convert the size and offset to bytes. 46 | var contractSize = (0, bytes_1.padNBytes)((0, bytes_1.toHex)(contractLength), 2); 47 | // push2(contract size) 48 | pushContractSizeCode = "61".concat(contractSize); 49 | } 50 | // Compute pushX(offset to code) 51 | if ((bootStrapCodeSize + constructorLength) < 256) { 52 | // Convert the size and offset to bytes. 53 | var contractCodeOffset = (0, bytes_1.padNBytes)((0, bytes_1.toHex)(bootStrapCodeSize + constructorLength), 1); 54 | // push1(offset to code) 55 | pushContractCodeOffset = "60".concat(contractCodeOffset); 56 | } 57 | else { 58 | // Increment bootstrap code size 59 | bootStrapCodeSize++; 60 | // Convert the size and offset to bytes. 61 | var contractCodeOffset = (0, bytes_1.padNBytes)((0, bytes_1.toHex)(bootStrapCodeSize + constructorLength), 2); 62 | // push2(offset to code) 63 | pushContractCodeOffset = "61".concat(contractCodeOffset); 64 | } 65 | // pushX(contract size) dup1 pushX(offset to code) returndatsize codecopy returndatasize return 66 | var bootstrapCode = "".concat(pushContractSizeCode, "80").concat(pushContractCodeOffset, "3d393df3"); 67 | var constructorCode = "".concat(constructorBytecode).concat(bootstrapCode); 68 | var deployedBytecode = "".concat(constructorCode).concat(mainBytecode).concat(args.constructorArgs ? encodeArgs(args.constructorArgs) : ""); 69 | // Return the bytecode. 70 | return { bytecode: deployedBytecode, runtimeBytecode: mainBytecode, abi: abi }; 71 | }; 72 | /** 73 | * Encode arguments. 74 | * @param args The arguments to encode. 75 | * @returns The encoded arguments. 76 | */ 77 | function encodeArgs(args) { 78 | // Instantiate two arrays. 79 | var types = []; 80 | var values = []; 81 | // Split the array of arguments into arrays of types and values. 82 | args.forEach(function (arg) { 83 | types.push(arg.type); 84 | values.push(arg.value); 85 | }); 86 | // Encode and array the types and values. 87 | var abiCoder = new ethers_1.ethers.utils.AbiCoder(); 88 | return abiCoder.encode(types, values).replace(/^(0x)/, ""); 89 | } 90 | // Export compiler function as default. 91 | exports["default"] = compile; 92 | -------------------------------------------------------------------------------- /dist/output/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.generateAbi = void 0; 4 | /** 5 | * Generate a contract ABI 6 | * @param path The path to write the ABI to. 7 | * @param functions Function definitions map. 8 | * @param events Event definitions map. 9 | * @returns Contract ABI. 10 | */ 11 | var generateAbi = function (functions, events) { 12 | // The ABI array. 13 | var abi = []; 14 | // Add the functions to the ABI. 15 | Object.keys(functions).forEach(function (name) { 16 | // Get the function definition 17 | var _a = functions[name].data, inputs = _a.inputs, outputs = _a.outputs, type = _a.type; 18 | // Push the definition to the ABI. 19 | abi.push({ 20 | name: name, 21 | type: "function", 22 | stateMutability: type, 23 | payable: type === "payable" ? true : false, 24 | inputs: inputs.map(function (type) { 25 | return { name: "", type: type }; 26 | }), 27 | outputs: outputs.map(function (type) { 28 | return { name: "", type: type }; 29 | }) 30 | }); 31 | }); 32 | // Add the events to the ABI. 33 | Object.keys(events).forEach(function (name) { 34 | // Get the event definition. 35 | var inputs = events[name].args; 36 | abi.push({ 37 | name: name, 38 | type: "event", 39 | anonymous: false, 40 | inputs: inputs.map(function (type) { 41 | var indexed; 42 | if (type.endsWith(" indexed")) { 43 | indexed = true; 44 | type = type.replace(" indexed", ""); 45 | } 46 | return { name: "", type: type, indexed: indexed }; 47 | }) 48 | }); 49 | }); 50 | // Return the ABI. 51 | return JSON.stringify(abi, null, 2); 52 | }; 53 | exports.generateAbi = generateAbi; 54 | -------------------------------------------------------------------------------- /dist/parser/high-level.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.setStoragePointers = exports.setStoragePointerConstants = exports.parseFile = void 0; 4 | var contents_1 = require("./utils/contents"); 5 | var regex_1 = require("./utils/regex"); 6 | var defintions_1 = require("./syntax/defintions"); 7 | var parsing_1 = require("./utils/parsing"); 8 | var tables_1 = require("./tables"); 9 | var macros_1 = require("./macros"); 10 | var bytes_1 = require("../utils/bytes"); 11 | /** 12 | * Parse a file, storing the definitions of all constants, macros, and tables. 13 | * @param filePath The path to the file to parse. 14 | */ 15 | var parseFile = function (filePath) { 16 | // Get an array of file contents. 17 | var fileContents = (0, contents_1["default"])(filePath); 18 | // Extract information. 19 | var contents = fileContents.contents; 20 | var imports = fileContents.imports; 21 | // Set defintion variables. 22 | var macros = { data: {}, defintions: [] }; 23 | var constants = { data: {}, defintions: [] }; 24 | var tables = { data: {}, defintions: [] }; 25 | // Set output variables. 26 | var functions = {}; 27 | var events = {}; 28 | // Parse the file contents. 29 | contents.forEach(function (content, contentIndex) { 30 | var input = content; 31 | while (!(0, regex_1.isEndOfData)(input)) { 32 | // Check if we are parsing a macro definition. 33 | if (defintions_1.HIGH_LEVEL.MACRO.test(input)) { 34 | // Parse macro definition 35 | var macro = input.match(defintions_1.HIGH_LEVEL.MACRO); 36 | // Add the macro to the macros array. 37 | macros.defintions.push(macro[2]); 38 | // macros[name] = body, args. 39 | macros.data[macro[2]] = { value: macro[7], args: (0, parsing_1.parseArgs)(macro[3]) }; 40 | // Parse the macro. 41 | macros.data[macro[2]].data = (0, macros_1["default"])(macro[2], macros.data, constants.data, tables.data); 42 | // Slice the input 43 | input = input.slice(macro[0].length); 44 | } 45 | // Check if we are parsing an import. 46 | else if (defintions_1.HIGH_LEVEL.IMPORT.test(input)) { 47 | input = input.slice(input.match(defintions_1.HIGH_LEVEL.IMPORT)[0].length); 48 | } 49 | // Check if we are parsing a constant definition. 50 | else if (defintions_1.HIGH_LEVEL.CONSTANT.test(input)) { 51 | // Parse constant definition. 52 | var constant = input.match(defintions_1.HIGH_LEVEL.CONSTANT); 53 | var name_1 = constant[2]; 54 | var value = constant[3].replace("0x", ""); 55 | // Ensure that the constant name is all uppercase. 56 | if (name_1.toUpperCase() !== name_1) 57 | throw new SyntaxError("ParserError at ".concat(imports[contentIndex], " (Line ").concat((0, parsing_1.getLineNumber)(input, content), "): Constant ").concat(name_1, " must be uppercase.")); 58 | // Store the constant. 59 | constants.defintions.push(name_1); 60 | constants.data[name_1] = { value: value, args: [] }; 61 | // Slice the input. 62 | input = input.slice(constant[0].length); 63 | } 64 | // Check if we are parsing a function definition. 65 | else if (defintions_1.HIGH_LEVEL.FUNCTION.test(input)) { 66 | // Parse the function definition. 67 | var functionDef = input.match(defintions_1.HIGH_LEVEL.FUNCTION); 68 | // Calculate the hash of the function definition and store the first 4 bytes. 69 | // This is the signature of the function. 70 | var name_2 = functionDef[2]; 71 | // Store the input and output strings. 72 | var inputs = functionDef[3]; 73 | var outputs = functionDef[5]; 74 | // Store the function values. 75 | var definition = { 76 | inputs: inputs ? (0, parsing_1.parseArgs)(inputs) : [], 77 | outputs: outputs ? (0, parsing_1.parseArgs)(functionDef[5]) : [], 78 | type: functionDef[4] 79 | }; 80 | // Store the function definition. 81 | functions[name_2] = { value: name_2, args: [], data: definition }; 82 | // Slice the input. 83 | input = input.slice(functionDef[0].length); 84 | } 85 | // Check if we're parsing an event defintion. 86 | else if (defintions_1.HIGH_LEVEL.EVENT.test(input)) { 87 | // Parse the event definition. 88 | var eventDef = input.match(defintions_1.HIGH_LEVEL.EVENT); 89 | // Calculate the hash of the event definition and store the first 4 bytes. 90 | // This is the signature of the event. 91 | var name_3 = eventDef[2]; 92 | // Store the args. 93 | var args = (0, parsing_1.parseArgs)(eventDef[3]).map(function (arg) { return arg.replace("indexed", " indexed"); }); 94 | // Store the event definition. 95 | events[name_3] = { value: name_3, args: args }; 96 | // Slice the input. 97 | input = input.slice(eventDef[0].length); 98 | } 99 | // Check if we're parsing a code table definition. 100 | else if (defintions_1.HIGH_LEVEL.CODE_TABLE.test(input)) { 101 | // Parse the table definition. 102 | var table = input.match(defintions_1.HIGH_LEVEL.CODE_TABLE); 103 | var body = table[3]; 104 | // Parse the table. 105 | var parsed = (0, tables_1.parseCodeTable)(body); 106 | // Store the table definition. 107 | tables.defintions.push(table[2]); 108 | tables.data[table[2]] = { value: body, args: [parsed.table, parsed.size] }; 109 | // Slice the input 110 | input = input.slice(table[0].length); 111 | } 112 | // Check if we're parsing a packed table definition. 113 | else if (defintions_1.HIGH_LEVEL.JUMP_TABLE_PACKED.test(input)) { 114 | // Parse the table definition. 115 | var table = input.match(defintions_1.HIGH_LEVEL.JUMP_TABLE_PACKED); 116 | var type = table[1]; 117 | // Ensure the type is valid. 118 | if (type !== "jumptable__packed") 119 | throw new SyntaxError("ParserError at ".concat(imports[contentIndex], " (Line ").concat((0, parsing_1.getLineNumber)(input, content), "): Table ").concat(table[0], " has invalid type: ").concat(type)); 120 | // Parse the table. 121 | var body = table[3]; 122 | var parsed = (0, tables_1.parseJumpTable)(body, true); 123 | // Store the table definition. 124 | tables.defintions.push(table[2]); 125 | tables.data[table[2]] = { 126 | value: body, 127 | args: [parsed.jumps, parsed.size], 128 | data: [table[2], true] 129 | }; 130 | // Slice the input. 131 | input = input.slice(table[0].length); 132 | } 133 | // Check if we're parsing a jump table definition. 134 | else if (defintions_1.HIGH_LEVEL.JUMP_TABLE.test(input)) { 135 | // Parse the table definition. 136 | var table = input.match(defintions_1.HIGH_LEVEL.JUMP_TABLE); 137 | var type = table[1]; 138 | // Ensure the type is valid. 139 | if (type !== "jumptable") 140 | throw new SyntaxError("ParserError at ".concat(imports[contentIndex], " (Line ").concat((0, parsing_1.getLineNumber)(input, content), "): Table ").concat(table[0], " has invalid type: ").concat(type)); 141 | // Parse the table. 142 | var body = table[3]; 143 | var parsed = (0, tables_1.parseJumpTable)(body, false); 144 | // Store the table definition. 145 | tables.defintions.push(table[2]); 146 | tables.data[table[2]] = { 147 | value: body, 148 | args: [parsed.jumps, parsed.size], 149 | data: [table[2], false] 150 | }; 151 | // Slice the input. 152 | input = input.slice(table[0].length); 153 | } 154 | else { 155 | // Get the index of the current input. 156 | var index = content.indexOf(input); 157 | // Get the line number of the file. 158 | var lineNumber = content.substring(0, index).split("\n").length; 159 | // Raise error. 160 | throw new SyntaxError("ParserError at ".concat(imports[contentIndex], "(Line ").concat(lineNumber, "): Invalid Syntax\n \n ").concat(input.slice(0, input.indexOf("\n")), "\n ^\n ")); 161 | } 162 | } 163 | }); 164 | // Return all values 165 | return { macros: macros, constants: constants, functions: functions, events: events, tables: tables }; 166 | }; 167 | exports.parseFile = parseFile; 168 | var setStoragePointerConstants = function (macrosToSearch, macros, constants) { 169 | // Array of used storage pointer constants. 170 | var usedStoragePointerConstants = []; 171 | // Define a functinon that iterates over all macros and adds the storage pointer constants. 172 | var getUsedStoragePointerConstants = function (name, revertIfNonExistant) { 173 | // Store macro. 174 | var macro = macros[name]; 175 | // Check if the macro exists. 176 | if (!macro) { 177 | // Check if we should revert (and revert). 178 | if (revertIfNonExistant) 179 | throw new Error("Macro ".concat(name, " does not exist")); 180 | // Otherwise just return. 181 | return; 182 | } 183 | // Store the macro body. 184 | var body = macros[name].value; 185 | while (!(0, regex_1.isEndOfData)(body)) { 186 | // If the next call is a constant call. 187 | if (body.match(defintions_1.MACRO_CODE.CONSTANT_CALL)) { 188 | // Store the constant definition. 189 | var definition = body.match(defintions_1.MACRO_CODE.CONSTANT_CALL); 190 | var constantName = definition[1]; 191 | // Push the array to the usedStoragePointerConstants array. 192 | if (constants.data[constantName].value === "FREE_STORAGE_POINTER()" && 193 | !usedStoragePointerConstants.includes(constantName)) { 194 | usedStoragePointerConstants.push(constantName); 195 | } 196 | // Slice the body. 197 | body = body.slice(definition[0].length); 198 | } 199 | // If the next call is a macro call. 200 | else if (body.match(defintions_1.MACRO_CODE.MACRO_CALL)) { 201 | // Store the macro definition. 202 | var definition = body.match(defintions_1.MACRO_CODE.MACRO_CALL); 203 | var macroName = definition[1]; 204 | // Get the used storage pointer constants. 205 | getUsedStoragePointerConstants(macroName, true); 206 | // Slice the body. 207 | body = body.slice(definition[0].length); 208 | } 209 | // Otherwise just slice the body by one. 210 | else { 211 | body = body.slice(1); 212 | } 213 | } 214 | }; 215 | // Loop through the given macros and generate the used storage pointer constants. 216 | macrosToSearch.forEach(function (macroName) { 217 | getUsedStoragePointerConstants(macroName, false); 218 | }); 219 | // Iterate through the ordered pointers and generate 220 | // an array (ordered by the defined order) of all storage pointer constants. 221 | var orderedStoragePointerConstants = constants.defintions.filter(function (constant) { 222 | return usedStoragePointerConstants.includes(constant); 223 | }); 224 | // Update and return the constants map. 225 | return (0, exports.setStoragePointers)(constants.data, orderedStoragePointerConstants); 226 | }; 227 | exports.setStoragePointerConstants = setStoragePointerConstants; 228 | /** 229 | * Assign constants that use the builtin FREE_STORAGE_POINTER( 230 | * @param constants Maps the name of constants to their values 231 | * @param order The order that the constants were declared in 232 | */ 233 | var setStoragePointers = function (constants, order) { 234 | var usedPointers = []; 235 | // Iterate over the array of constants. 236 | order.forEach(function (name) { 237 | var value = constants[name].value; 238 | // If the value is a hex literal. 239 | if (!value.startsWith("FREE_")) { 240 | /* 241 | If the pointer is already used, throw an error. 242 | In order to safely circumvent this, all constant-defined pointers must be defined before 243 | pointers that use FREE_STORAGE_POINTER. 244 | */ 245 | if (usedPointers.includes((0, bytes_1.convertBytesToNumber)(value))) { 246 | throw "Constant ".concat(name, " uses already existing pointer"); 247 | } 248 | // Add the pointer to the list of used pointers. 249 | usedPointers.push((0, bytes_1.convertBytesToNumber)(value)); 250 | } 251 | // The value calls FREE_STORAGE_POINTER. 252 | else if (value == "FREE_STORAGE_POINTER()") { 253 | // Find the lowest available pointer value. 254 | var pointer = (0, bytes_1.findLowest)(0, usedPointers); 255 | // Add the pointer to the list of used pointers. 256 | usedPointers.push(pointer); 257 | // Set the constant to the pointer value. 258 | constants[name].value = (0, bytes_1.convertNumberToBytes)(pointer).replace("0x", ""); 259 | } 260 | }); 261 | // Return the new constants value. 262 | return constants; 263 | }; 264 | exports.setStoragePointers = setStoragePointers; 265 | -------------------------------------------------------------------------------- /dist/parser/macros.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.parseArgument = void 0; 4 | var opcodes_1 = require("../evm/opcodes"); 5 | var bytes_1 = require("../utils/bytes"); 6 | var defintions_1 = require("./syntax/defintions"); 7 | var parsing_1 = require("./utils/parsing"); 8 | var regex_1 = require("./utils/regex"); 9 | var types_1 = require("./utils/types"); 10 | /** 11 | * Parse a macro definition. 12 | * @param args The arguments passed into the macro. 13 | */ 14 | var parseMacro = function (macro, macros, constants, jumptables) { 15 | // Instantiate variables. 16 | var operations = []; 17 | var jumpdests = {}; 18 | // Store a copy of the body and args. 19 | var input = macros[macro].value; 20 | var args = macros[macro].args; 21 | // Loop through the body. 22 | while (!(0, regex_1.isEndOfData)(input)) { 23 | var token = void 0; 24 | // Check if we're parsing a macro call. 25 | if (input.match(defintions_1.MACRO_CODE.MACRO_CALL) && 26 | !(input.match(defintions_1.MACRO_CODE.MACRO_CALL) ? input.match(defintions_1.MACRO_CODE.MACRO_CALL)[1] : "").startsWith("__")) { 27 | // Parse the macro call. 28 | token = input.match(defintions_1.MACRO_CODE.MACRO_CALL); 29 | var name_1 = token[1]; 30 | var args_1 = token[2] ? (0, parsing_1.parseArgs)(token[2]) : []; 31 | // Ensure the macro exists. 32 | if (!macros[name_1]) 33 | throw new Error("Macro ".concat(name_1, " does not exist.")); 34 | // Add the macro's operations to the macro operations. 35 | operations.push({ 36 | type: types_1.OperationType.MACRO_CALL, 37 | value: name_1, 38 | args: args_1 39 | }); 40 | } 41 | // Check if we're parsing a constant call. 42 | else if (input.match(defintions_1.MACRO_CODE.CONSTANT_CALL)) { 43 | // Parse the constant call. 44 | token = input.match(defintions_1.MACRO_CODE.CONSTANT_CALL); 45 | var name_2 = token[1]; 46 | // Add the constant call to the token list. 47 | operations.push({ type: types_1.OperationType.CONSTANT_CALL, value: name_2, args: [] }); 48 | } 49 | // Check if we're parsing an argument call 50 | else if (input.match(defintions_1.MACRO_CODE.ARG_CALL)) { 51 | // Parse a template call 52 | token = input.match(defintions_1.MACRO_CODE.ARG_CALL); 53 | var name_3 = token[1]; 54 | // Verify that template has been defined 55 | if (!args.includes(name_3)) 56 | throw new Error("Arg ".concat(name_3, " is not defined")); 57 | // Add the template call to the token list 58 | operations.push({ type: types_1.OperationType.ARG_CALL, value: name_3, args: [] }); 59 | } 60 | // Check if we're parsing a code_size call 61 | else if (input.match(defintions_1.MACRO_CODE.CODE_SIZE)) { 62 | // Parse the code_size call. 63 | token = input.match(defintions_1.MACRO_CODE.CODE_SIZE); 64 | var templateParams = token[2] ? [token[2]] : []; 65 | // Add the code_size call to the token list. 66 | operations.push({ type: types_1.OperationType.CODESIZE, value: token[1], args: templateParams }); 67 | } 68 | // Check if we're parsing a table_size call 69 | else if (input.match(defintions_1.MACRO_CODE.TABLE_SIZE)) { 70 | // Parse the table_size call. 71 | token = input.match(defintions_1.MACRO_CODE.TABLE_SIZE); 72 | var name_4 = token[1]; 73 | // Verify that the table has been defined. 74 | if (!jumptables[name_4]) 75 | throw new Error("Table ".concat(name_4, " is not defined")); 76 | // Get the size of the table. 77 | var hex = (0, bytes_1.formatEvenBytes)((0, bytes_1.toHex)(jumptables[name_4].value.length)); 78 | // Add the table_size call to the token list. 79 | operations.push({ type: types_1.OperationType.PUSH, value: (0, bytes_1.toHex)(95 + hex.length / 2), args: [hex] }); 80 | } 81 | // Check if we're parsing a table_start call. 82 | else if (input.match(defintions_1.MACRO_CODE.TABLE_START)) { 83 | // Parse the table start call. 84 | token = input.match(defintions_1.MACRO_CODE.TABLE_START); 85 | // Add the table start call to the token list. 86 | operations.push({ type: types_1.OperationType.TABLE_START_POSITION, value: token[1], args: [] }); 87 | } 88 | // Check if we're parsing a jumplabel. 89 | else if (input.match(defintions_1.MACRO_CODE.JUMP_LABEL) && 90 | !constants[input.match(defintions_1.MACRO_CODE.JUMP_LABEL)[1]]) { 91 | // Parse the jump label. 92 | token = input.match(defintions_1.MACRO_CODE.JUMP_LABEL); 93 | // Ensure the label has not been defined. 94 | if (jumpdests[token[1]]) 95 | throw new Error("Jump label ".concat(token[1], " has already been defined")); 96 | // Define the jump label. 97 | jumpdests[token[1]] = true; 98 | // Add the jump label to the token list. 99 | operations.push({ type: types_1.OperationType.JUMPDEST, value: token[1], args: [] }); 100 | } 101 | // Check if we're parsing a literal. 102 | else if (input.match(defintions_1.MACRO_CODE.LITERAL_HEX)) { 103 | // Parse the value. 104 | token = input.match(defintions_1.MACRO_CODE.LITERAL_HEX); 105 | // Format the value. 106 | var hex = (0, bytes_1.formatEvenBytes)(token[1]); 107 | // Add the literal to the token list. 108 | operations.push({ type: types_1.OperationType.PUSH, value: (0, bytes_1.toHex)(95 + hex.length / 2), args: [hex] }); 109 | } 110 | // Check if we're parsing an opcode. 111 | else if (input.match(defintions_1.MACRO_CODE.TOKEN) && !constants[input.match(defintions_1.MACRO_CODE.TOKEN)[1]]) { 112 | // Parse the macro. 113 | token = input.match(defintions_1.MACRO_CODE.TOKEN); 114 | // Add the opcode to the token list. 115 | // The value pushed is dependent on whether it's a jump label 116 | // or an opcode. 117 | if (opcodes_1["default"][token[1]]) 118 | operations.push({ type: types_1.OperationType.OPCODE, value: token[1], args: [] }); 119 | else 120 | operations.push({ type: types_1.OperationType.PUSH_JUMP_LABEL, value: token[1], args: [] }); 121 | } 122 | // Throw if the value is not parsable. 123 | else 124 | throw new Error("Could not parse input"); 125 | // Slice the input 126 | input = input.slice(token[0].length); 127 | } 128 | return operations; 129 | }; 130 | /** Parse argument */ 131 | var parseArgument = function (input, macros, constants) { 132 | var _a, _b, _c, _d; 133 | // If the input is a hex literal: 134 | if ((0, regex_1.isLiteral)(input)) { 135 | // Get the bytes value of the operation. 136 | var value = input.substring(2); 137 | // Get the push value 138 | var push = (0, bytes_1.toHex)(95 + value.length / 2); 139 | // Return a macro map with a single macro containing a push operation. 140 | return _a = {}, 141 | _a[input] = { 142 | value: "", 143 | args: [], 144 | data: [{ type: types_1.OperationType.PUSH, value: push, args: [value] }] 145 | }, 146 | _a; 147 | } 148 | // If the input is an opcode: 149 | else if (opcodes_1["default"][input]) { 150 | // Return a macro map with a single macro contraining an opcode. 151 | return _b = {}, 152 | _b[input] = { 153 | value: "", 154 | args: [], 155 | data: [{ type: types_1.OperationType.OPCODE, value: input, args: [] }] 156 | }, 157 | _b; 158 | } 159 | // If the input is a macro, return the macros map. 160 | if (macros[input]) 161 | return macros; 162 | // If the input is a constant: 163 | if (constants[input]) { 164 | // Return a macro map with a single macro containing a constant call. 165 | return _c = {}, 166 | _c[input] = { 167 | value: "", 168 | args: [], 169 | data: [{ type: types_1.OperationType.CONSTANT_CALL, value: input, args: [] }] 170 | }, 171 | _c; 172 | } 173 | // If the input is a jump label: 174 | else { 175 | return _d = {}, 176 | _d[input] = { 177 | value: "", 178 | args: [], 179 | data: [{ type: types_1.OperationType.PUSH_JUMP_LABEL, value: input, args: [] }] 180 | }, 181 | _d; 182 | } 183 | }; 184 | exports.parseArgument = parseArgument; 185 | exports["default"] = parseMacro; 186 | -------------------------------------------------------------------------------- /dist/parser/syntax/defintions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.MACRO_CODE = exports.JUMP_TABLES = exports.HIGH_LEVEL = void 0; 4 | /* Regex Imports */ 5 | var regex_1 = require("../utils/regex"); 6 | /* High Level Syntax */ 7 | exports.HIGH_LEVEL = { 8 | IMPORT: (0, regex_1.combineRegexElements)([ 9 | /* "#" At the start of a line */ 10 | "^(?:[\\s\\n]*)#", 11 | /* The word "include" */ 12 | "(?:include)", 13 | /* Quotation marks */ 14 | "(?:\\\"|\\')", 15 | /* All alphanumeric characters, representing the filename */ 16 | "(.*)", 17 | /* Quotation marks */ 18 | "(?:\\\"|\\')", 19 | ]), 20 | /* Syntax for macros */ 21 | MACRO: (0, regex_1.combineRegexElements)([ 22 | /* "#define" at the start of the line */ 23 | "^(?:[\\s\\n]*#[\\s\\n]*define)", 24 | /* The whole word "macro" */ 25 | "\\b(macro)\\b", 26 | /* The name of the macro, which can be anything */ 27 | "([A-Za-z0-9_]\\w*)", 28 | /* Parenthesis that can contain anything (macro arguments) */ 29 | "\\(((.*))\\)", 30 | /* Equals sign */ 31 | "=", 32 | /* The word "takes" */ 33 | "takes", 34 | /** 35 | * A sequence of digits (a number) surrounded by parenthesis 36 | * This number represents the number of "arguments" that the macro takes 37 | */ 38 | "\\((\\d+)\\)", 39 | /* The word "returns" */ 40 | "returns", 41 | /* A sequence of digits (a number) surrounded by parenthesis */ 42 | "\\((\\d+)\\)", 43 | /** 44 | * Curly brackets that can contain all characters. 45 | * In this case, these are the contents of the macro. 46 | */ 47 | "\\{((?:[^\\}])*)\\}", 48 | ]), 49 | FUNCTION: (0, regex_1.combineRegexElements)([ 50 | /* #define event at the start of the line */ 51 | "^(?:[\\s\\n]*[\\s\\n]*#define function)", 52 | /** 53 | * The function name and parameter types 54 | * which is then turned into the function signature. 55 | * For example "example(uint, bool)" 56 | */ 57 | "((?:[\\s\\n]*)([a-zA-Z0-9_]+)(?:\\(([a-zA-Z0-9_,\\s\\n]+)?\\)))", 58 | /** 59 | * The function type (payable, nonpayable, view, pure). 60 | */ 61 | "(payable|nonpayable|view|pure)", 62 | /** 63 | * The word "returns" 64 | */ 65 | "(?:[\\s\\n]* returns)", 66 | /** 67 | * The return type of the function within parenthesis. 68 | */ 69 | "(?:\\(([a-zA-Z0-9_,\\s\\n]+)?\\))", 70 | ]), 71 | EVENT: (0, regex_1.combineRegexElements)([ 72 | /* #define event at the start of the line */ 73 | "^(?:[\\s\\n]*[\\s\\n]*#define event)", 74 | /** 75 | * The event name and parameter types 76 | * which is then turned into the function signature. 77 | * For example "example(uint, bool)" 78 | */ 79 | "((?:[\\s\\n]*)([a-zA-Z0-9_]+)(?:\\(([a-zA-Z0-9_,\\s\\n]+)?\\)))", 80 | ]), 81 | /* Syntax for constants */ 82 | CONSTANT: (0, regex_1.combineRegexElements)([ 83 | /* "#define" at the start of the line */ 84 | "^(?:[\\s\\n]*#define)", 85 | /* The whole word "constant" */ 86 | "\\b(constant)\\b", 87 | /* The name of the constant, which can be everything */ 88 | "([A-Za-z0-9_]\\w*)", 89 | /* Equals sign */ 90 | "=", 91 | /* Hex literal */ 92 | "(((?:[\\s\\n]*)0x([0-9a-fA-F]+)\\b)|(FREE_STORAGE_POINTER\\(\\)))", 93 | ]), 94 | /* Syntax for code tables */ 95 | CODE_TABLE: (0, regex_1.combineRegexElements)([ 96 | /* "#define" at the start of the line */ 97 | "^(?:[\\s\\n]*#[\\s\\n]*define)", 98 | /* The whole word "table" */ 99 | "\\b(table)\\b", 100 | /* The name of the table, which can be anything */ 101 | "([A-Za-z0-9_]\\w*)", 102 | /** 103 | * Curly brackets that can contain all characters. 104 | * In this case, these are the contents of the table. 105 | */ 106 | "\\{((?:[^\\}])*)\\}", 107 | ]), 108 | /* Syntax for jump tables */ 109 | JUMP_TABLE: (0, regex_1.combineRegexElements)([ 110 | /* "#define" at the start of the line */ 111 | "^(?:[\\s\\n]*#[\\s\\n]*define)", 112 | /* The whole word "jumptable" */ 113 | "\\b(jumptable)\\b", 114 | /* The name of the table, which can be anything */ 115 | "([A-Za-z0-9_]\\w*)", 116 | /** 117 | * Curly brackets that can contain all characters. 118 | * In this case, these are the contents of the jumptable. 119 | */ 120 | "\\{((?:[^\\}])*)\\}", 121 | ]), 122 | /* Syntax for packed jump tables */ 123 | JUMP_TABLE_PACKED: (0, regex_1.combineRegexElements)([ 124 | /* "#define" at the start of the line */ 125 | "^(?:[\\s\\n]*#[\\s\\n]*define)", 126 | /* The whole word "jumptable__packed" */ 127 | "\\b(jumptable__packed)\\b", 128 | /* The name of the table, which can be anything */ 129 | "([A-Za-z0-9_]\\w*)", 130 | /** 131 | * Curly brackets that can contain all characters. 132 | * In this case, these are the contents of the jumptable. 133 | */ 134 | "\\{((?:[^\\}])*)\\}", 135 | ]) 136 | }; 137 | /* Jump Table Syntax */ 138 | exports.JUMP_TABLES = { 139 | /* All characters, with any number of whitespace before and after */ 140 | JUMPS: new RegExp("(?:[\\s\\n]*)[a-zA-Z_0-9\\-]+(?:$|\\s+)", "g") 141 | }; 142 | /* Code Syntax, found within macros */ 143 | exports.MACRO_CODE = { 144 | /* Any text before spaces and newlines */ 145 | TOKEN: (0, regex_1.combineRegexElements)(["\\s*\\n*([^\\s]*)\\s*\\n*"]), 146 | /* Any alphanumeric combination followed by 0x */ 147 | LITERAL_HEX: (0, regex_1.combineRegexElements)(["^(?:[\\s\\n]*)0x([0-9a-fA-F]+)\\b"]), 148 | /* Syntax for macro calls */ 149 | MACRO_CALL: (0, regex_1.combineRegexElements)([ 150 | /* Any number of alphanumeric characters + underscores */ 151 | "^(?:[\\s\\n]*)([a-zA-Z0-9_]+)", 152 | /* Open Parenthesis */ 153 | "\\(", 154 | /* Any alphanumeric combination */ 155 | "([a-zA-Z0-9_\\-<>]*)", 156 | /* Closing parenthesis */ 157 | "\\)\\s*\\n*", 158 | ]), 159 | /* Syntax for constant calls */ 160 | CONSTANT_CALL: (0, regex_1.combineRegexElements)([ 161 | /* Any number of alphanumeric characters + underscores */ 162 | "^(?:[\\s\\n]*)\\[([A-Z0-9_]+)\\]", 163 | ]), 164 | /* Syntax for the builtin codesize function */ 165 | CODE_SIZE: (0, regex_1.combineRegexElements)([ 166 | /* The string "__codesize" */ 167 | "^(?:[\\s\\n]*)__codesize", 168 | /* Open Parenthesis */ 169 | "\\(", 170 | /* Any alphanumeric combination */ 171 | "([a-zA-Z0-9_\\-]+)", 172 | /* Template Arguments */ 173 | "(?:<([a-zA-Z0-9_,\\s\\n]+)>)?", 174 | /* Closing parenthesis */ 175 | "\\)\\s*\\n*", 176 | ]), 177 | /* Syntax for the builtin table size function */ 178 | TABLE_SIZE: (0, regex_1.combineRegexElements)([ 179 | /* The string "__tablesize" */ 180 | "^(?:[\\s\\n]*)__tablesize", 181 | /* Open Parenthesis */ 182 | "\\(", 183 | /* Any alphanumeric combination */ 184 | "([a-zA-Z0-9_\\-]+)", 185 | /* Template Arguments */ 186 | "(?:<([a-zA-Z0-9_,\\s\\n]+)>)?", 187 | /* Closing parenthesis */ 188 | "\\)\\s*\\n*", 189 | ]), 190 | /* Syntax for the builtin table start function */ 191 | TABLE_START: (0, regex_1.combineRegexElements)([ 192 | /* The string "__tablestart" */ 193 | "^(?:[\\s\\n]*)__tablestart", 194 | /* Open Parenthesis */ 195 | "\\(", 196 | /* Any alphanumeric combination */ 197 | "([a-zA-Z0-9_\\-]+)", 198 | /* Template Arguments */ 199 | "(?:<([a-zA-Z0-9_,\\s\\n]+)>)?", 200 | /* Closing parenthesis */ 201 | "\\)\\s*\\n*", 202 | ]), 203 | /* Syntax for template calls */ 204 | ARG_CALL: (0, regex_1.combineRegexElements)(["^(?:[\\s\\n]*)", "<([a-zA-Z0-9_\\-\\+\\*]+)>", "\\s*\\n*"]), 205 | /* Syntax for jumptables */ 206 | JUMP_LABEL: (0, regex_1.combineRegexElements)(["^(?:[\\s\\n]*)([a-zA-Z0-9_\\-]+):\\s*\\n*"]) 207 | }; 208 | -------------------------------------------------------------------------------- /dist/parser/tables.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.parseJumpTable = exports.parseCodeTable = void 0; 4 | var defintions_1 = require("./syntax/defintions"); 5 | var regex_1 = require("./utils/regex"); 6 | /** 7 | * Parse a code table definition 8 | * @param body The raw string representing the body of the table. 9 | */ 10 | var parseCodeTable = function (body) { 11 | // Parse the body of the table. 12 | var table = body 13 | .match(defintions_1.JUMP_TABLES.JUMPS) 14 | .map(function (jump) { 15 | return (0, regex_1.removeSpacesAndLines)(jump); 16 | }) 17 | .join(""); 18 | // Return the table data. 19 | return { table: table, size: table.length / 2 }; 20 | }; 21 | exports.parseCodeTable = parseCodeTable; 22 | /** 23 | * Parse a jumptable definition 24 | * @param body The raw string representing the body of the table. 25 | */ 26 | var parseJumpTable = function (body, compressed) { 27 | if (compressed === void 0) { compressed = false; } 28 | // Parse the body of the table 29 | var jumps = body.match(defintions_1.JUMP_TABLES.JUMPS).map(function (jump) { 30 | return (0, regex_1.removeSpacesAndLines)(jump); 31 | }); 32 | // Calculate the size of the table. 33 | var size; 34 | if (compressed) 35 | size = jumps.length * 0x02; 36 | else 37 | size = jumps.length * 0x20; 38 | // Return the array of jumps and the size of the table. 39 | return { 40 | jumps: jumps, 41 | size: size 42 | }; 43 | }; 44 | exports.parseJumpTable = parseJumpTable; 45 | -------------------------------------------------------------------------------- /dist/parser/utils/contents.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Imports */ 3 | var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { 4 | if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { 5 | if (ar || !(i in from)) { 6 | if (!ar) ar = Array.prototype.slice.call(from, 0, i); 7 | ar[i] = from[i]; 8 | } 9 | } 10 | return to.concat(ar || Array.prototype.slice.call(from)); 11 | }; 12 | exports.__esModule = true; 13 | var defintions_1 = require("../syntax/defintions"); 14 | var parsing_1 = require("./parsing"); 15 | var files_1 = require("../../utils/files"); 16 | /** 17 | * Given a file path, return a string containing the raw 18 | * file contents of all imported files (including files imported in imported files). 19 | * @param filepath The path to the original file 20 | */ 21 | var getAllFileContents = function (filepath) { 22 | // Map indicating whether the filepath has been included. 23 | var imported = { filepath: true }; 24 | // An array of all imported files. 25 | var files = [filepath]; 26 | // Function that reads a file and adds it to the contents array. 27 | var getContents = function (filepath, imported) { 28 | // Read the data from the file and remove all comments. 29 | var contents = (0, parsing_1.removeComments)((0, files_1.readFile)(filepath)); 30 | var includes = [contents]; 31 | var imports = [filepath]; 32 | // Read the file data from each of the imported files. 33 | getImports(contents).forEach(function (importPath) { 34 | // If the file has been imported, skip. 35 | if (imported[importPath]) 36 | return; 37 | var _a = getContents(importPath, imported), newIncludes = _a[0], newImports = _a[1]; 38 | // Add the file contents to the includes array. 39 | includes = __spreadArray(__spreadArray([], newIncludes, true), includes, true); 40 | // Add the file to the imports array. 41 | imports = __spreadArray(__spreadArray([], files, true), newImports, true); 42 | // Mark the file as imported. 43 | imported[importPath] = true; 44 | }); 45 | return [includes, imports]; 46 | }; 47 | // Get the file contents. 48 | return { contents: getContents(filepath, imported)[0], imports: files }; 49 | }; 50 | /** 51 | * Given the contents of the file, return an array 52 | * of filepaths that are imported by the file. 53 | * @param contents The raw contents of the file. 54 | */ 55 | var getImports = function (contents) { 56 | var imports = []; 57 | var nextImport = contents.match(defintions_1.HIGH_LEVEL.IMPORT); 58 | // While there are still imports to find. 59 | while (nextImport) { 60 | // Add the path of the imported file if it hasn't been added already. 61 | if (!imports.includes(nextImport[1])) 62 | imports.push(nextImport[1]); 63 | // Remove the import statement from the file contents 64 | // so the parser can find the next import. 65 | contents = contents.slice(nextImport[0].length); 66 | // Find the next import statement (this is null if not found). 67 | nextImport = contents.match(defintions_1.HIGH_LEVEL.IMPORT); 68 | } 69 | return imports; 70 | }; 71 | exports["default"] = getAllFileContents; 72 | -------------------------------------------------------------------------------- /dist/parser/utils/parsing.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.throwErrors = exports.parseArgs = exports.getLineNumber = exports.removeComments = void 0; 4 | var regex_1 = require("./regex"); 5 | /** 6 | * Given a string, generate a new string without inline comments 7 | * @param data A string of data to parse 8 | */ 9 | var removeComments = function (string) { 10 | // The formatted version of our string. 11 | var formattedData = ""; 12 | var data = string; 13 | var formatted = ""; 14 | while (!(0, regex_1.isEndOfData)(data)) { 15 | var multiIndex = data.indexOf("/*"); 16 | var singleIndex = data.indexOf("//"); 17 | if (multiIndex !== -1 && (multiIndex < singleIndex || singleIndex === -1)) { 18 | formatted += data.slice(0, multiIndex); 19 | var endBlock = data.indexOf("*/"); 20 | if (endBlock === -1) 21 | throw new Error("Could not find closing comment block."); 22 | formatted += " ".repeat(endBlock - multiIndex + 2); 23 | data = data.slice(endBlock + 2); 24 | } 25 | else if (singleIndex !== -1) { 26 | formatted += data.slice(0, singleIndex); 27 | data = data.slice(singleIndex); 28 | var endBlock = data.indexOf("\n"); 29 | if (endBlock === -1) { 30 | formatted += " ".repeat(data.length); 31 | data = ""; 32 | } 33 | else { 34 | formatted += " ".repeat(endBlock + 1); 35 | data = data.slice(endBlock + 1); 36 | } 37 | } 38 | else { 39 | formatted += data; 40 | break; 41 | } 42 | } 43 | return formatted; 44 | }; 45 | exports.removeComments = removeComments; 46 | /** 47 | * Given a string and an index, get the line number that the index is located on 48 | */ 49 | var getLineNumber = function (str, org) { 50 | // Get the index of the current input. 51 | var index = org.indexOf(str); 52 | // Get the line number of the file. 53 | return str.substring(0, index).split("\n").length; 54 | }; 55 | exports.getLineNumber = getLineNumber; 56 | /** 57 | * Parse an arguments list and convert it to an array 58 | */ 59 | var parseArgs = function (argString) { 60 | return argString.replace(" ", "").replace(" ", "").split(","); 61 | }; 62 | exports.parseArgs = parseArgs; 63 | /** 64 | * Throw errors 65 | */ 66 | var throwErrors = function (errors) { 67 | if (errors.length > 0) { 68 | errors.map(function (error) { 69 | console.log(error); 70 | }); 71 | throw new Error(""); 72 | } 73 | }; 74 | exports.throwErrors = throwErrors; 75 | -------------------------------------------------------------------------------- /dist/parser/utils/regex.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Constants */ 3 | exports.__esModule = true; 4 | exports.isLiteral = exports.containsOperators = exports.countSpaces = exports.isEndOfData = exports.removeSpacesAndLines = exports.removeSpacesAndCommas = exports.combineRegexElements = exports.operator = exports.space = exports.comma = void 0; 5 | exports.comma = new RegExp("[^,\\s\\n]*", "g"); 6 | exports.space = new RegExp("^[\\s\\n]*"); 7 | exports.operator = new RegExp("[\\+\\-\\*]"); 8 | /** 9 | * Combine an array of regex strings into one, seperated by "\\s*\\n*" 10 | */ 11 | var combineRegexElements = function (regexElements) { 12 | return new RegExp(regexElements.join("\\s*\\n*")); 13 | }; 14 | exports.combineRegexElements = combineRegexElements; 15 | /** 16 | * @param input A string containing words split by commas and spaces 17 | * @returns An array of of string, each element is one word 18 | */ 19 | var removeSpacesAndCommas = function (input) { 20 | return (input.match(exports.comma) || []).filter(function (r) { return r !== ""; }); 21 | }; 22 | exports.removeSpacesAndCommas = removeSpacesAndCommas; 23 | /** 24 | * Removes all spaces and newlines from a string 25 | */ 26 | var removeSpacesAndLines = function (input) { 27 | return input.replace(/(\r\n\t|\n|\r\t|\s)/gm, ""); 28 | }; 29 | exports.removeSpacesAndLines = removeSpacesAndLines; 30 | /** 31 | * @returns A boolean indicating whether the input is the end of the data. 32 | */ 33 | var isEndOfData = function (data) { 34 | return !RegExp("\\S").test(data); 35 | }; 36 | exports.isEndOfData = isEndOfData; 37 | /** 38 | * Count the number of times an empty character appears in a string. 39 | */ 40 | var countSpaces = function (data) { 41 | var match = data.match(exports.space); 42 | if (match) { 43 | return match[0].length; 44 | } 45 | // There are no empty spaces in the string 46 | return 0; 47 | }; 48 | exports.countSpaces = countSpaces; 49 | /** 50 | * @returns A boolean indicating whether the string contains operators. 51 | */ 52 | var containsOperators = function (input) { 53 | return exports.operator.test(input); 54 | }; 55 | exports.containsOperators = containsOperators; 56 | /** 57 | * @returns A boolean indicating whether the input is a valid literal. 58 | */ 59 | var isLiteral = function (input) { 60 | if (input.match(new RegExp("^(?:\\s*\\n*)*0x([0-9a-fA-F]+)\\b"))) { 61 | return true; 62 | } 63 | return false; 64 | }; 65 | exports.isLiteral = isLiteral; 66 | -------------------------------------------------------------------------------- /dist/parser/utils/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.Context = exports.OperationType = void 0; 4 | /* Operation Type */ 5 | var OperationType; 6 | (function (OperationType) { 7 | OperationType["OPCODE"] = "OPCODE"; 8 | OperationType["PUSH"] = "PUSH"; 9 | OperationType["JUMPDEST"] = "JUMPDEST"; 10 | OperationType["PUSH_JUMP_LABEL"] = "PUSH_JUMP_LABEL"; 11 | OperationType["MACRO_CALL"] = "MACRO_CALL"; 12 | OperationType["CONSTANT_CALL"] = "CONSTANT_CALL"; 13 | OperationType["ARG_CALL"] = "ARG_CALL"; 14 | OperationType["CODESIZE"] = "CODESIZE"; 15 | OperationType["TABLE_START_POSITION"] = "TABLE_START_POSITION"; 16 | })(OperationType = exports.OperationType || (exports.OperationType = {})); 17 | /* Top Level Context */ 18 | var Context; 19 | (function (Context) { 20 | Context[Context["NONE"] = 0] = "NONE"; 21 | Context[Context["MACRO"] = 1] = "MACRO"; 22 | })(Context = exports.Context || (exports.Context = {})); 23 | -------------------------------------------------------------------------------- /dist/utils/bytes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.padNBytes = exports.toHex = exports.formatEvenBytes = exports.removeNonMatching = exports.findLowest = exports.convertNumberToBytes = exports.convertBytesToNumber = void 0; 4 | var BN = require("bn.js"); 5 | /** 6 | * Convert a hex literal to a number 7 | * @param bytes A string representing a hex literal. 8 | */ 9 | var convertBytesToNumber = function (bytes) { 10 | return parseInt(bytes.slice(2), 10); 11 | }; 12 | exports.convertBytesToNumber = convertBytesToNumber; 13 | /** 14 | * Convert a number to a hex literal 15 | */ 16 | var convertNumberToBytes = function (number) { 17 | var value = parseInt(number.toString(), 10).toString(16); 18 | return "0x".concat(value.length % 2 == 0 ? value : "0" + value); 19 | }; 20 | exports.convertNumberToBytes = convertNumberToBytes; 21 | /** 22 | * Given an array, find the lowest missing (non-negative) number 23 | */ 24 | var findLowest = function (value, arr) { 25 | return arr.indexOf(value) < 0 ? value : (0, exports.findLowest)(value + 1, arr); 26 | }; 27 | exports.findLowest = findLowest; 28 | /** 29 | * Given two arrays, remove all elements from the first array that aren't in the second 30 | */ 31 | var removeNonMatching = function (arr1, arr2) { 32 | return arr1.filter(function (val) { return arr2.includes(val); }); 33 | }; 34 | exports.removeNonMatching = removeNonMatching; 35 | /** 36 | * Format a hex literal to make its length even 37 | */ 38 | var formatEvenBytes = function (bytes) { 39 | if (Math.floor(bytes.length / 2) * 2 !== bytes.length) { 40 | return "0".concat(bytes); 41 | } 42 | return bytes; 43 | }; 44 | exports.formatEvenBytes = formatEvenBytes; 45 | /** 46 | * Convert a hex literal to a BigNumber 47 | */ 48 | var toHex = function (number) { 49 | return new BN(number, 10).toString(16); 50 | }; 51 | exports.toHex = toHex; 52 | /** 53 | * Pad a hex value with zeroes. 54 | */ 55 | var padNBytes = function (hex, numBytes) { 56 | if (hex.length > numBytes * 2) { 57 | throw new Error("value ".concat(hex, " has more than ").concat(numBytes, " bytes!")); 58 | } 59 | var result = hex; 60 | while (result.length < numBytes * 2) { 61 | result = "0".concat(result); 62 | } 63 | return result; 64 | }; 65 | exports.padNBytes = padNBytes; 66 | -------------------------------------------------------------------------------- /dist/utils/files.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.readFile = void 0; 4 | var path = require("path"); 5 | var fs = require("fs"); 6 | /** 7 | * Read a file and return its contents 8 | * @param filePath The path to the file 9 | */ 10 | var readFile = function (filePath) { 11 | return fs.readFileSync(path.posix.resolve(filePath), "utf8"); 12 | }; 13 | exports.readFile = readFile; 14 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Huff documentation 2 | 3 | This directory contains the documentation for Huff and the build scripts for the [VuePress](https://vuepress.vuejs.org/) static site generator. 4 | 5 | ## Run locally 6 | 7 | To run the Huff docs site locally by cloning this repo, installing this site's dependencies, and running `npm run dev`. 8 | 9 | 1. Run the following commands: 10 | 11 | ```shell 12 | git clone https://github.com/huff-language/huffc 13 | cd huffc/docs 14 | npm install 15 | npm run dev 16 | ``` 17 | 18 | 1. Open [localhost:8080](http://localhost:8080) in your browser. 19 | 20 | ### OpenSSL error 21 | 22 | If you get an OpenSSL error from NPM when running `npm run dev`, you may need to export a `NODE_OPTIONS` variable for this session. More information on this error is available in the [webpack/webpack Github repo](https://github.com/webpack/webpack/issues/14532). 23 | 24 | 1. If you see something like this: 25 | 26 | ```plaintext 27 | opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ], 28 | library: 'digital envelope routines', 29 | reason: 'unsupported', 30 | code: 'ERR_OSSL_EVP_UNSUPPORTED' 31 | ``` 32 | 33 | 1. Run: 34 | 35 | ```shell 36 | export NODE_OPTIONS=--openssl-legacy-provider 37 | ``` 38 | 39 | This variable will only stay available for _this_ terminal window. You will have to recreate it for new terminal windows. 40 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huff-language/huffc/f59701694aaf42569b939043610c6832a0bfb7af/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "huffc", 3 | "description": "A low level programming language for the Ethereum Virtual Machine", 4 | "version": "0.0.25", 5 | "author": "JetJadeja", 6 | "bin": { 7 | "huffc": "bin/huffc" 8 | }, 9 | "dependencies": { 10 | "@ethersproject/abi": "^5.6.3", 11 | "bn.js": "^4.11.8", 12 | "commander": "^8.1.0", 13 | "ganache": "^7.2.0", 14 | "ts-node": "^10.1.0", 15 | "typescript": "^4.5.4" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^18.0.0", 19 | "chai": "^4.2.0", 20 | "eslint": "^5.15.3", 21 | "eslint-config-airbnb-base": "^13.1.0", 22 | "eslint-config-prettier": "^4.1.0", 23 | "eslint-plugin-import": "^2.16.0", 24 | "mocha": "^6.0.2", 25 | "prettier": "2.3.2", 26 | "sinon": "^7.2.3" 27 | }, 28 | "engines": { 29 | "node": ">=8.3" 30 | }, 31 | "homepage": "https://github.com/JetJadeja/huff", 32 | "keywords": [ 33 | "blockchain", 34 | "ethereum", 35 | "evm", 36 | "huff", 37 | "programming-language", 38 | "smart-contracts" 39 | ], 40 | "license": "LGPL-3.0", 41 | "main": "./dist/index.js", 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/JetJadeja/huff.git" 45 | }, 46 | "scripts": { 47 | "build": "tsc", 48 | "lint": "eslint --ignore-path ./.eslintignore .", 49 | "test": "mocha ./src/ --bail --colors --exit --recursive --reporter spec --trace-warnings", 50 | "exampletest": "mocha ./examples/erc20 --bail --colors --exit --recursive --reporter spec --trace-warnings" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/JetJadeja/huff/issues" 54 | }, 55 | "directories": { 56 | "example": "example", 57 | "test": "tests" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | import { program } from "commander"; 2 | import compile from "./"; 3 | import fs = require("fs"); 4 | 5 | // Compiler Version 6 | const version = "2.0.0"; 7 | 8 | // Define terminal commands. 9 | program.name("huffc"); 10 | program.version(version); 11 | program 12 | .option("-V, --version", "Show the version and exit") 13 | .option("--base-path ", "The base path to the contracts", "./") 14 | .option("--output-directory ", "The output directory", "./") 15 | .option("--bytecode", "Generate and log bytecode", false) 16 | .option("-o, output", "The output file") 17 | .option("-p, --paste", "Paste the output to the terminal") 18 | .option("-n, --no-linebreak", "Omit newline charater"); 19 | 20 | // Parse the terminal arguments. 21 | program.parse(process.argv); 22 | const options = program.opts(); 23 | 24 | var files = program.args; 25 | var destination = options.outputDir || "."; 26 | 27 | // Abort the program. 28 | const abort = (msg) => { 29 | process.stderr.write(`${msg}\n` || "Error occured\n"); 30 | process.exit(1); 31 | }; 32 | 33 | // Iterate the imported files. 34 | files.forEach((file) => { 35 | // Abort if the file extension is not .huff. 36 | if (!file.endsWith(".huff")) abort("File extension must be .huff"); 37 | 38 | // Compile the file. 39 | const result = compile({ 40 | filePath: file, 41 | generateAbi: true, 42 | }); 43 | 44 | // If the user has specified an output file, write the output to the file. 45 | const outputPath = `${options.outputDirectory}${files[0].replace(".huff", ".json")}`; 46 | if (options.output) fs.writeFileSync(outputPath, result.abi); 47 | 48 | // If the user has specified for us to log the bytecode, log it. 49 | if (options.bytecode && !options.paste) { 50 | process.stdout.write(result.bytecode); 51 | if (options.linebreak) process.stdout.write('\n'); 52 | } 53 | if (options.paste) process.stdout.write(`${JSON.stringify(result)}\n`); 54 | }); 55 | -------------------------------------------------------------------------------- /src/compiler/compiler.ts: -------------------------------------------------------------------------------- 1 | import { Definitions } from "../parser/utils/types"; 2 | import { formatEvenBytes, padNBytes, toHex } from "../utils/bytes"; 3 | import { processMacro } from "./processor"; 4 | 5 | /** 6 | * Compile a macro into raw EVM bytecode. 7 | * @param name The name of the macro. 8 | * @param args An array of arguments passed into the macro. 9 | * @param macros Maps all macros to their raw text and arguments. 10 | * @param constants Maps all constants to their values. 11 | * @param jumptables Maps all jump tables to their jump values. 12 | */ 13 | export const compileMacro = ( 14 | name: string, 15 | args: string[], 16 | macros: Definitions["data"], 17 | constants: Definitions["data"], 18 | jumptables: Definitions["data"] 19 | ) => { 20 | // Process the macro and generate semi-complete EVM bytecode. 21 | let { bytecode, unmatchedJumps, tableInstances, jumptable, jumpindices } = processMacro( 22 | name, 23 | 0, 24 | args, 25 | macros, 26 | constants, 27 | jumptables 28 | ); 29 | 30 | // Ensure that there are no unmatched jumps. 31 | if (unmatchedJumps.length > 0) { 32 | throw new Error( 33 | `Compiler: Macro ${name} contains unmatched jump labels ${unmatchedJumps 34 | .map((jump) => jump.label) 35 | .join(" ")}, cannot compile` 36 | ); 37 | } 38 | 39 | // Instantiate variables 40 | let tableBytecodeOffset = bytecode.length / 2; 41 | const tableOffsets = {}; 42 | 43 | // Iterate over the jumptables. 44 | Object.keys(jumptables).forEach((jumpkey: string) => { 45 | // Get the jump table value. 46 | const table = jumptables[jumpkey]; 47 | 48 | // Store the table code. 49 | let tableCode; 50 | 51 | // Store the jumps and size. 52 | const [jumps, size] = table.args; 53 | 54 | // tableOffsets[name] = tableBytecodeOffset; 55 | tableOffsets[table.data[0]] = tableBytecodeOffset; 56 | 57 | // Incerment the table offset. 58 | tableBytecodeOffset += size; 59 | 60 | // Iterate through the jumplabels. 61 | tableCode = table.args[0] 62 | .map((jumplabel) => { 63 | // If the label has not been defined, return "". 64 | if (!jumpindices[jumplabel]) return ""; 65 | 66 | // Otherwise, set the offset to the jumpindice. 67 | const offset = jumpindices[jumplabel]; 68 | const hex = formatEvenBytes(toHex(offset)); 69 | 70 | // If the table has been compressed. 71 | if (!table.data[1]) { 72 | return padNBytes(hex, 0x20); 73 | } else { 74 | return padNBytes(hex, 0x02); 75 | } 76 | }) 77 | .join(""); 78 | 79 | bytecode += tableCode; 80 | }); 81 | 82 | // Remove table instance placeholders. 83 | tableInstances.forEach((instance) => { 84 | // Store the name and offset of the instance. 85 | const { label: name, bytecodeIndex: offset } = instance; 86 | 87 | // Ensure the table has been defined. 88 | if (!tableOffsets[name]) { 89 | throw new Error(`Expected to find ${instance.label} in ${JSON.stringify(tableOffsets)}`); 90 | } 91 | 92 | // Slice the bytecode to get the code before and after the offset. 93 | const before = bytecode.slice(0, offset * 2 + 2); 94 | const after = bytecode.slice(offset * 2 + 6); 95 | 96 | // Insert the offset value. 97 | bytecode = `${before}${padNBytes(toHex(tableOffsets[name]), 2)}${after}`; 98 | }); 99 | 100 | return bytecode; 101 | }; 102 | -------------------------------------------------------------------------------- /src/compiler/processor.ts: -------------------------------------------------------------------------------- 1 | import opcodes from "../evm/opcodes"; 2 | import { parseArgument } from "../parser/macros"; 3 | import { Definitions, Operation, OperationType } from "../parser/utils/types"; 4 | import { formatEvenBytes, padNBytes, toHex } from "../utils/bytes"; 5 | 6 | /** 7 | * Process a macro, generating semi-complete EVM bytecode. 8 | * @param name The name of the macro. 9 | * @param bytecodeOffset The offset of the macro's bytecode. 10 | * @param args The arguments passed into the array. 11 | * @param macros Maps all macros to their raw text and arguments. 12 | * @param constants Maps all constants to their values. 13 | * @param jumptables Maps all jump tables to their jump values. 14 | * @returns Semi-complete EVM bytecode. 15 | */ 16 | export const processMacro = ( 17 | name: string, 18 | bytecodeOffset: number, 19 | args: string[], 20 | macros: Definitions["data"], 21 | constants: Definitions["data"], 22 | jumptables: Definitions["data"] 23 | ): any => { 24 | // Check if the macro exists. 25 | const macro = macros[name]; 26 | if (!macro) throw new Error(`Processor: Failed to find ${macro}.`); 27 | 28 | // Map argument names to values. 29 | const params: { [name: string]: string } = {}; 30 | macro.args.forEach((arg, index) => { 31 | params[arg] = args[index]; 32 | }); 33 | 34 | // Instantiate variables. 35 | const jumptable = []; 36 | let jumpindices: { [name: string]: number } = {}; 37 | let tableInstances: any[] = []; 38 | let offset: number = bytecodeOffset; 39 | 40 | // Store a copy of the body and args. 41 | const codes = macro.data.map((operation: Operation, index: number) => { 42 | switch (operation.type) { 43 | // We're parsing a macro call. 44 | case OperationType.MACRO_CALL: { 45 | // Replace macro arguments with their values. 46 | operation.args.map((arg: string, index: number) => { 47 | if (arg.startsWith("<") && arg.endsWith(">")) operation.args[index] = args[index]; 48 | }); 49 | 50 | // Process the macro call. 51 | const result = processMacro( 52 | operation.value, 53 | offset, 54 | operation.args, 55 | macros, 56 | constants, 57 | jumptables 58 | ); 59 | 60 | // Add the result to local variables. 61 | tableInstances = [...tableInstances, ...result.tableInstances]; 62 | jumptable[index] = result.jumptable = result.unmatchedJumps; 63 | jumpindices = { ...jumpindices, ...result.jumpindices }; 64 | 65 | // Add to the offset. 66 | offset += result.bytecode.length / 2; 67 | 68 | // Return the result. 69 | return result.bytecode; 70 | } 71 | // We're parsing a constant call. 72 | case OperationType.CONSTANT_CALL: { 73 | // Get the value of the constant. 74 | const value = constants[operation.value].value; 75 | 76 | // Get the push value 77 | const push = `${toHex(95 + value.length / 2)}${value}`; 78 | 79 | // Add to the offset. 80 | offset += push.length / 2; 81 | 82 | // Return the bytecode 83 | return push; 84 | } 85 | // We're parsing an argument call. 86 | case OperationType.ARG_CALL: { 87 | // Get the value of the argument. 88 | const arg = params[operation.value]; 89 | 90 | // Parse the arguments. 91 | const result = processMacro( 92 | arg, 93 | offset, 94 | operation.args, 95 | parseArgument(arg, macros, constants), 96 | constants, 97 | jumptables 98 | ); 99 | 100 | // Set the jumplabels to the macro's unmatched jumps. 101 | jumptable[index] = result.unmatchedJumps; 102 | 103 | // Add to the offset. 104 | offset += result.bytecode.length / 2; 105 | 106 | // Return the bytecode 107 | return result.bytecode; 108 | } 109 | // We're parsing an opcodes. 110 | case OperationType.OPCODE: { 111 | // An opcode is a single byte of data. 112 | offset += 1; 113 | 114 | // Return the opcode value. 115 | return opcodes[operation.value]; 116 | } 117 | // We're parsing a push operation. 118 | case OperationType.PUSH: { 119 | // Get the push value. 120 | const push = `${operation.value}${operation.args[0]}`; 121 | 122 | // Add to the offset. 123 | offset += push.length / 2; 124 | 125 | // Return the bytecode. 126 | return `${operation.value}${operation.args[0]}`; 127 | } 128 | // We're parsing a codesize call. 129 | case OperationType.CODESIZE: { 130 | // Calculate the code of the macro. 131 | const code = processMacro( 132 | operation.value, 133 | offset, 134 | operation.args, 135 | macros, 136 | constants, 137 | jumptables 138 | ).bytecode; 139 | 140 | // Calculate the length. 141 | const length = formatEvenBytes((code.length / 2).toString(16)); 142 | 143 | // Get the push value. 144 | const push = `${toHex(95 + length.length / 2)}${length}`; 145 | 146 | // Add to the offset. 147 | offset += push.length / 2; 148 | 149 | // Return a push operation, that pushes the length to the stack. 150 | return push; 151 | } 152 | // We're parsing a jump label push. 153 | case OperationType.PUSH_JUMP_LABEL: { 154 | // Add to the jumptables 155 | jumptable[index] = [{ label: operation.value, bytecodeIndex: 0 }]; 156 | 157 | // Add to the offset. 158 | offset += 3; 159 | 160 | // Return the bytecode 161 | return `${opcodes.push2}xxxx`; 162 | } 163 | // We're parsing a table start position call. 164 | case OperationType.TABLE_START_POSITION: { 165 | // Add to the tableInstances. 166 | tableInstances.push({ label: operation.value, bytecodeIndex: offset }); 167 | 168 | // Add to the offset. 169 | offset += 3; 170 | 171 | // Return the bytecode. 172 | return `${opcodes.push2}xxxx`; 173 | } 174 | // We're parsing a jumpdest. 175 | case OperationType.JUMPDEST: { 176 | // Add to the jumpindices array. 177 | jumpindices[operation.value] = offset; 178 | 179 | // Add to the offset. 180 | offset += 1; 181 | 182 | // Return the bytecode. 183 | return opcodes.jumpdest; 184 | } 185 | // Default 186 | default: { 187 | throw new Error(`Processor: Cannot understand operation ${operation.type} in ${name}.`); 188 | } 189 | } 190 | }); 191 | 192 | // Store the current index. 193 | let currentIndex = bytecodeOffset; 194 | 195 | // Loop through the code definitions. 196 | const indices = codes.map((bytecode) => { 197 | // Update the current index. 198 | currentIndex += bytecode.length / 2; 199 | 200 | // Return the index. 201 | return currentIndex; 202 | }); 203 | 204 | // Add the initial index to the start of the array. 205 | indices.unshift(bytecodeOffset); 206 | 207 | // Store an array of unmatched jumps. 208 | const unmatchedJumps = []; 209 | 210 | // Get the absolute bytecode index for each jump label. 211 | const newBytecode = codes.reduce((accumulator, bytecode, index) => { 212 | // Store a formatted version of the bytecode. 213 | let formattedBytecode = bytecode; 214 | 215 | // Check a jump table exists at this index. 216 | if (jumptable[index]) { 217 | // Store the jumps. 218 | const jumps = jumptable[index]; 219 | 220 | // Iterate over the jumps at this index. 221 | for (const { label: jumplabel, bytecodeIndex } of jumps) { 222 | // If the jumplabel is defined: 223 | if (jumpindices.hasOwnProperty(jumplabel)) { 224 | // Get the absolute bytecode index and pad the value (1 byte). 225 | const jumpvalue = padNBytes(toHex(jumpindices[jumplabel]), 2); 226 | 227 | // Slice the bytecode to get the code before and after the jump. 228 | const before = formattedBytecode.slice(0, bytecodeIndex + 2); 229 | const after = formattedBytecode.slice(bytecodeIndex + 6); 230 | 231 | // Ensure that the jump value is set with a placeholder. 232 | if (formattedBytecode.slice(bytecodeIndex + 2, bytecodeIndex + 6) !== "xxxx") 233 | throw new Error( 234 | `Processor: Expected indicies ${bytecodeIndex + 2} to ${ 235 | bytecodeIndex + 6 236 | } to be jump location, of ${formattedBytecode}` 237 | ); 238 | 239 | // Insert the jump value. 240 | formattedBytecode = `${before}${jumpvalue}${after}`; 241 | } 242 | // If the jumplabel has not been definied: 243 | else { 244 | // Store the offset. 245 | const jumpOffset = (indices[index] - bytecodeOffset) * 2; 246 | 247 | // Push the jump value to the unmatched jumps array. 248 | unmatchedJumps.push({ label: jumplabel, bytecodeIndex: jumpOffset + bytecodeIndex }); 249 | } 250 | } 251 | } 252 | 253 | // Return the new bytecode. 254 | return accumulator + formattedBytecode; 255 | }, ""); 256 | 257 | // Return the result. 258 | return { bytecode: newBytecode, unmatchedJumps, tableInstances, jumptable, jumpindices }; 259 | }; 260 | -------------------------------------------------------------------------------- /src/evm/opcodes.ts: -------------------------------------------------------------------------------- 1 | /* Maps all opcodes to their hex equivalents */ 2 | 3 | export default { 4 | stop: "00", 5 | add: "01", 6 | mul: "02", 7 | sub: "03", 8 | div: "04", 9 | sdiv: "05", 10 | mod: "06", 11 | smod: "07", 12 | addmod: "08", 13 | mulmod: "09", 14 | exp: "0a", 15 | signextend: "0b", 16 | lt: "10", 17 | gt: "11", 18 | slt: "12", 19 | sgt: "13", 20 | eq: "14", 21 | iszero: "15", 22 | and: "16", 23 | or: "17", 24 | xor: "18", 25 | not: "19", 26 | byte: "1a", 27 | sha3: "20", 28 | keccak: "20", 29 | address: "30", 30 | balance: "31", 31 | origin: "32", 32 | caller: "33", 33 | callvalue: "34", 34 | calldataload: "35", 35 | calldatasize: "36", 36 | calldatacopy: "37", 37 | codesize: "38", 38 | codecopy: "39", 39 | gasprice: "3a", 40 | extcodesize: "3b", 41 | extcodecopy: "3c", 42 | returndatasize: "3d", 43 | returndatacopy: "3e", 44 | blockhash: "40", 45 | coinbase: "41", 46 | timestamp: "42", 47 | number: "43", 48 | difficulty: "44", 49 | gaslimit: "45", 50 | chainid: "46", 51 | selfbalance: "47", 52 | basefee: "48", 53 | pop: "50", 54 | mload: "51", 55 | mstore: "52", 56 | mstore8: "53", 57 | sload: "54", 58 | sstore: "55", 59 | jump: "56", 60 | jumpi: "57", 61 | getpc: "58", 62 | msize: "59", 63 | gas: "5a", 64 | jumpdest: "5b", 65 | push1: "60", 66 | push2: "61", 67 | push3: "62", 68 | push4: "63", 69 | push5: "64", 70 | push6: "65", 71 | push7: "66", 72 | push8: "67", 73 | push9: "68", 74 | push10: "69", 75 | push11: "6a", 76 | push12: "6b", 77 | push13: "6c", 78 | push14: "6d", 79 | push15: "6e", 80 | push16: "6f", 81 | push17: "70", 82 | push18: "71", 83 | push19: "72", 84 | push20: "73", 85 | push21: "74", 86 | push22: "75", 87 | push23: "76", 88 | push24: "77", 89 | push25: "78", 90 | push26: "79", 91 | push27: "7a", 92 | push28: "7b", 93 | push29: "7c", 94 | push30: "7d", 95 | push31: "7e", 96 | push32: "7f", 97 | dup1: "80", 98 | dup2: "81", 99 | dup3: "82", 100 | dup4: "83", 101 | dup5: "84", 102 | dup6: "85", 103 | dup7: "86", 104 | dup8: "87", 105 | dup9: "88", 106 | dup10: "89", 107 | dup11: "8a", 108 | dup12: "8b", 109 | dup13: "8c", 110 | dup14: "8d", 111 | dup15: "8e", 112 | dup16: "8f", 113 | swap1: "90", 114 | swap2: "91", 115 | swap3: "92", 116 | swap4: "93", 117 | swap5: "94", 118 | swap6: "95", 119 | swap7: "96", 120 | swap8: "97", 121 | swap9: "98", 122 | swap10: "99", 123 | swap11: "9a", 124 | swap12: "9b", 125 | swap13: "9c", 126 | swap14: "9d", 127 | swap15: "9e", 128 | swap16: "9f", 129 | shl: "1b", 130 | shr: "1c", 131 | sar: "1d", 132 | log0: "a0", 133 | log1: "a1", 134 | log2: "a2", 135 | log3: "a3", 136 | log4: "a4", 137 | create: "f0", 138 | call: "f1", 139 | callcode: "f2", 140 | return: "f3", 141 | delegatecall: "f4", 142 | create2: "f5", 143 | staticcall: "fa", 144 | revert: "fd", 145 | invalid: "fe", 146 | selfdestruct: "ff", 147 | }; 148 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { defaultAbiCoder } from "@ethersproject/abi"; 2 | import { padNBytes, toHex } from "./utils/bytes"; 3 | import { compileMacro } from "./compiler/compiler"; 4 | import { parseFile, setStoragePointerConstants } from "./parser/high-level"; 5 | import { generateAbi } from "./output"; 6 | import { Sources } from "./parser/utils/contents"; 7 | 8 | /* Compilation Input Type */ 9 | type HuffCompilerArgs = { 10 | filePath: string; 11 | sources?: Sources; 12 | generateAbi: boolean; 13 | constructorArgs?: { type: string; value: string }[]; 14 | }; 15 | 16 | /** 17 | * Compile a Huff file. 18 | * @param filePath The path to the file. 19 | * @param args An array containing the arguments to the macro. 20 | * @returns The compiled bytecode. 21 | */ 22 | const compile = (args: HuffCompilerArgs) => { 23 | // Parse the file and generate definitions. 24 | const { macros, constants, tables, functions, events } = parseFile(args.filePath, args.sources); 25 | 26 | // Generate the contract ABI. 27 | const abi = args.generateAbi ? generateAbi(functions, events) : ""; 28 | 29 | // Set storage pointer constants. 30 | constants.data = setStoragePointerConstants(["CONSTRUCTOR", "MAIN"], macros.data, constants); 31 | 32 | // Compile the macros. 33 | const mainBytecode = macros.data["MAIN"] 34 | ? compileMacro("MAIN", [], macros.data, constants.data, tables.data) 35 | : ""; 36 | const constructorBytecode = macros.data["CONSTRUCTOR"] 37 | ? compileMacro("CONSTRUCTOR", [], macros.data, constants.data, tables.data) 38 | : ""; 39 | 40 | // Store the sizes of the bytecode. 41 | const contractLength = mainBytecode.length / 2; 42 | const constructorLength = constructorBytecode.length / 2; 43 | 44 | // Bootstrap code variables 45 | let bootStrapCodeSize = 9; 46 | let pushContractSizeCode: string; 47 | let pushContractCodeOffset: string; 48 | 49 | // Compute pushX(contract size) 50 | if (contractLength < 256) { 51 | // Convert the size and offset to bytes. 52 | const contractSize = padNBytes(toHex(contractLength), 1); 53 | 54 | // push1(contract size) 55 | pushContractSizeCode = `60${contractSize}`; 56 | } else { 57 | // Increment bootstrap code size 58 | bootStrapCodeSize++; 59 | 60 | // Convert the size and offset to bytes. 61 | const contractSize = padNBytes(toHex(contractLength), 2); 62 | 63 | // push2(contract size) 64 | pushContractSizeCode = `61${contractSize}`; 65 | } 66 | 67 | // Compute pushX(offset to code) 68 | if (bootStrapCodeSize + constructorLength < 256) { 69 | // Convert the size and offset to bytes. 70 | const contractCodeOffset = padNBytes(toHex(bootStrapCodeSize + constructorLength), 1); 71 | 72 | // push1(offset to code) 73 | pushContractCodeOffset = `60${contractCodeOffset}`; 74 | } else { 75 | // Increment bootstrap code size 76 | bootStrapCodeSize++; 77 | 78 | // Convert the size and offset to bytes. 79 | const contractCodeOffset = padNBytes(toHex(bootStrapCodeSize + constructorLength), 2); 80 | 81 | // push2(offset to code) 82 | pushContractCodeOffset = `61${contractCodeOffset}`; 83 | } 84 | 85 | // pushX(contract size) dup1 pushX(offset to code) returndatsize codecopy returndatasize return 86 | const bootstrapCode = `${pushContractSizeCode}80${pushContractCodeOffset}3d393df3`; 87 | const constructorCode = `${constructorBytecode}${bootstrapCode}`; 88 | const deployedBytecode = `${constructorCode}${mainBytecode}${ 89 | args.constructorArgs ? encodeArgs(args.constructorArgs) : "" 90 | }`; 91 | 92 | console.log(deployedBytecode); 93 | 94 | // Return the bytecode. 95 | return { bytecode: deployedBytecode, runtimeBytecode: mainBytecode, abi: abi }; 96 | }; 97 | 98 | /** 99 | * Encode arguments. 100 | * @param args The arguments to encode. 101 | * @returns The encoded arguments. 102 | */ 103 | function encodeArgs(args: { type: string; value: string }[]): string { 104 | // Instantiate two arrays. 105 | const types: string[] = []; 106 | const values: string[] = []; 107 | 108 | // Split the array of arguments into arrays of types and values. 109 | args.forEach((arg) => { 110 | types.push(arg.type); 111 | values.push(arg.value); 112 | }); 113 | 114 | // Encode and array the types and values. 115 | return defaultAbiCoder.encode(types, values).replace(/^(0x)/, ""); 116 | } 117 | 118 | // Export compiler function as default. 119 | export default compile; 120 | -------------------------------------------------------------------------------- /src/output/index.ts: -------------------------------------------------------------------------------- 1 | import fs = require("fs"); 2 | import { Definitions } from "../parser/utils/types"; 3 | 4 | /** 5 | * Generate a contract ABI 6 | * @param path The path to write the ABI to. 7 | * @param functions Function definitions map. 8 | * @param events Event definitions map. 9 | * @returns Contract ABI. 10 | */ 11 | export const generateAbi = (functions: Definitions["data"], events: Definitions["data"]) => { 12 | // The ABI array. 13 | const abi = []; 14 | 15 | // Add the functions to the ABI. 16 | Object.keys(functions).forEach((name) => { 17 | // Get the function definition 18 | const { inputs, outputs, type } = functions[name].data; 19 | 20 | // Push the definition to the ABI. 21 | abi.push({ 22 | name: name, 23 | type: "function", 24 | stateMutability: type, 25 | payable: type === "payable" ? true : false, 26 | inputs: inputs.map((type) => { 27 | return { name: "", type }; 28 | }), 29 | outputs: outputs.map((type) => { 30 | return { name: "", type }; 31 | }), 32 | }); 33 | }); 34 | 35 | // Add the events to the ABI. 36 | Object.keys(events).forEach((name) => { 37 | // Get the event definition. 38 | const inputs = events[name].args; 39 | 40 | abi.push({ 41 | name: name, 42 | type: "event", 43 | anonymous: false, 44 | inputs: inputs.map((type) => { 45 | let indexed; 46 | if (type.endsWith(" indexed")) { 47 | indexed = true; 48 | type = type.replace(" indexed", ""); 49 | } 50 | 51 | return { name: "", type, indexed }; 52 | }), 53 | }); 54 | }); 55 | 56 | // Return the ABI. 57 | return JSON.stringify(abi, null, 2); 58 | }; 59 | -------------------------------------------------------------------------------- /src/parser/high-level.ts: -------------------------------------------------------------------------------- 1 | import getAllFileContents, { Sources } from "./utils/contents"; 2 | 3 | import { isEndOfData } from "./utils/regex"; 4 | import { Definitions } from "./utils/types"; 5 | import { HIGH_LEVEL, MACRO_CODE } from "./syntax/defintions"; 6 | import { parseArgs, getLineNumber } from "./utils/parsing"; 7 | import { parseCodeTable, parseJumpTable } from "./tables"; 8 | import parseMacro from "./macros"; 9 | import { convertBytesToNumber, convertNumberToBytes, findLowest, formatEvenBytes } from "../utils/bytes"; 10 | 11 | /** 12 | * Parse a file, storing the definitions of all constants, macros, and tables. 13 | * @param filePath The path to the file to parse. 14 | */ 15 | export const parseFile = ( 16 | filePath: string, 17 | sources?: Sources 18 | ): { 19 | macros: Definitions; 20 | constants: Definitions; 21 | functions: Definitions["data"]; 22 | events: Definitions["data"]; 23 | tables: Definitions; 24 | } => { 25 | // Get file fileContents and paths. 26 | const {fileContents, filePaths} = getAllFileContents(filePath, sources); 27 | 28 | // Set defintion variables. 29 | const macros: Definitions = { data: {}, defintions: [] }; 30 | const constants: Definitions = { data: {}, defintions: [] }; 31 | const tables: Definitions = { data: {}, defintions: [] }; 32 | 33 | // Set output variables. 34 | const functions: Definitions["data"] = {}; 35 | const events: Definitions["data"] = {}; 36 | 37 | // Parse the file fileContents. 38 | fileContents.forEach((content: string, contentIndex: number) => { 39 | let input = content; 40 | 41 | while (!isEndOfData(input)) { 42 | // Check if we are parsing a macro definition. 43 | if (HIGH_LEVEL.MACRO.test(input)) { 44 | // Parse macro definition 45 | const macro = input.match(HIGH_LEVEL.MACRO); 46 | 47 | // Add the macro to the macros array. 48 | macros.defintions.push(macro[2]); 49 | 50 | // macros[name] = body, args. 51 | macros.data[macro[2]] = { value: macro[7], args: parseArgs(macro[3]) }; 52 | 53 | // Parse the macro. 54 | macros.data[macro[2]].data = parseMacro(macro[2], macros.data, constants.data, tables.data); 55 | 56 | // Slice the input 57 | input = input.slice(macro[0].length); 58 | } 59 | // Check if we are parsing an import. 60 | else if (HIGH_LEVEL.IMPORT.test(input)) { 61 | input = input.slice(input.match(HIGH_LEVEL.IMPORT)[0].length); 62 | } 63 | // Check if we are parsing a constant definition. 64 | else if (HIGH_LEVEL.CONSTANT.test(input)) { 65 | // Parse constant definition. 66 | const constant = input.match(HIGH_LEVEL.CONSTANT); 67 | const name = constant[2]; 68 | const value = formatEvenBytes(constant[3].replace("0x", "")); 69 | 70 | // Ensure that the constant name is all uppercase. 71 | if (name.toUpperCase() !== name) 72 | throw new SyntaxError( 73 | `ParserError at ${filePaths[contentIndex]} (Line ${getLineNumber( 74 | input, 75 | content 76 | )}): Constant ${name} must be uppercase.` 77 | ); 78 | 79 | // Store the constant. 80 | constants.defintions.push(name); 81 | constants.data[name] = { value, args: [] }; 82 | 83 | // Slice the input. 84 | input = input.slice(constant[0].length); 85 | } 86 | // Check if we are parsing a function definition. 87 | else if (HIGH_LEVEL.FUNCTION.test(input)) { 88 | // Parse the function definition. 89 | const functionDef = input.match(HIGH_LEVEL.FUNCTION); 90 | 91 | // Calculate the hash of the function definition and store the first 4 bytes. 92 | // This is the signature of the function. 93 | const name = functionDef[2]; 94 | 95 | // Store the input and output strings. 96 | const inputs = functionDef[3]; 97 | const outputs = functionDef[5]; 98 | 99 | // Store the function values. 100 | const definition = { 101 | inputs: inputs ? parseArgs(inputs) : [], 102 | outputs: outputs ? parseArgs(functionDef[5]) : [], 103 | type: functionDef[4], 104 | }; 105 | 106 | // Store the function definition. 107 | functions[name] = { value: name, args: [], data: definition }; 108 | 109 | // Slice the input. 110 | input = input.slice(functionDef[0].length); 111 | } 112 | // Check if we're parsing an event defintion. 113 | else if (HIGH_LEVEL.EVENT.test(input)) { 114 | // Parse the event definition. 115 | const eventDef = input.match(HIGH_LEVEL.EVENT); 116 | 117 | // Calculate the hash of the event definition and store the first 4 bytes. 118 | // This is the signature of the event. 119 | const name = eventDef[2]; 120 | 121 | // Store the args. 122 | const args = parseArgs(eventDef[3]).map((arg) => arg.replace("indexed", " indexed")); 123 | 124 | // Store the event definition. 125 | events[name] = { value: name, args }; 126 | 127 | // Slice the input. 128 | input = input.slice(eventDef[0].length); 129 | } 130 | // Check if we're parsing a code table definition. 131 | else if (HIGH_LEVEL.CODE_TABLE.test(input)) { 132 | // Parse the table definition. 133 | const table = input.match(HIGH_LEVEL.CODE_TABLE); 134 | const body = table[3]; 135 | 136 | // Parse the table. 137 | const parsed = parseCodeTable(body); 138 | 139 | // Store the table definition. 140 | tables.defintions.push(table[2]); 141 | tables.data[table[2]] = { value: body, args: [parsed.table, parsed.size] }; 142 | 143 | // Slice the input 144 | input = input.slice(table[0].length); 145 | } 146 | 147 | // Check if we're parsing a packed table definition. 148 | else if (HIGH_LEVEL.JUMP_TABLE_PACKED.test(input)) { 149 | // Parse the table definition. 150 | const table = input.match(HIGH_LEVEL.JUMP_TABLE_PACKED); 151 | const type = table[1]; 152 | 153 | // Ensure the type is valid. 154 | if (type !== "jumptable__packed") 155 | throw new SyntaxError( 156 | `ParserError at ${filePaths[contentIndex]} (Line ${getLineNumber( 157 | input, 158 | content 159 | )}): Table ${table[0]} has invalid type: ${type}` 160 | ); 161 | 162 | // Parse the table. 163 | const body = table[3]; 164 | const parsed = parseJumpTable(body, true); 165 | 166 | // Store the table definition. 167 | tables.defintions.push(table[2]); 168 | tables.data[table[2]] = { 169 | value: body, 170 | args: [parsed.jumps, parsed.size], 171 | data: [table[2], true], 172 | }; 173 | 174 | // Slice the input. 175 | input = input.slice(table[0].length); 176 | } 177 | 178 | // Check if we're parsing a jump table definition. 179 | else if (HIGH_LEVEL.JUMP_TABLE.test(input)) { 180 | // Parse the table definition. 181 | const table = input.match(HIGH_LEVEL.JUMP_TABLE); 182 | const type = table[1]; 183 | 184 | // Ensure the type is valid. 185 | if (type !== "jumptable") 186 | throw new SyntaxError( 187 | `ParserError at ${filePaths[contentIndex]} (Line ${getLineNumber( 188 | input, 189 | content 190 | )}): Table ${table[0]} has invalid type: ${type}` 191 | ); 192 | 193 | // Parse the table. 194 | const body = table[3]; 195 | const parsed = parseJumpTable(body, false); 196 | 197 | // Store the table definition. 198 | tables.defintions.push(table[2]); 199 | tables.data[table[2]] = { 200 | value: body, 201 | args: [parsed.jumps, parsed.size], 202 | data: [table[2], false], 203 | }; 204 | 205 | // Slice the input. 206 | input = input.slice(table[0].length); 207 | } else { 208 | // Get the index of the current input. 209 | const index = content.indexOf(input); 210 | 211 | // Get the line number of the file. 212 | const lineNumber = content.substring(0, index).split("\n").length; 213 | 214 | // Raise error. 215 | throw new SyntaxError( 216 | `ParserError at ${filePaths[contentIndex]}(Line ${lineNumber}): Invalid Syntax 217 | 218 | ${input.slice(0, input.indexOf("\n"))} 219 | ^ 220 | ` 221 | ); 222 | } 223 | } 224 | }); 225 | 226 | // Return all values 227 | return { macros, constants, functions, events, tables }; 228 | }; 229 | 230 | export const setStoragePointerConstants = ( 231 | macrosToSearch: string[], 232 | macros: Definitions["data"], 233 | constants: Definitions 234 | ) => { 235 | // Array of used storage pointer constants. 236 | const usedStoragePointerConstants = []; 237 | 238 | // Define a functinon that iterates over all macros and adds the storage pointer constants. 239 | const getUsedStoragePointerConstants = (name: string, revertIfNonExistant: boolean) => { 240 | // Store macro. 241 | const macro = macros[name]; 242 | 243 | // Check if the macro exists. 244 | if (!macro) { 245 | // Check if we should revert (and revert). 246 | if (revertIfNonExistant) throw new Error(`Macro ${name} does not exist`); 247 | 248 | // Otherwise just return. 249 | return; 250 | } 251 | 252 | // Store the macro body. 253 | let body = macros[name].value; 254 | 255 | while (!isEndOfData(body)) { 256 | // If the next call is a constant call. 257 | if (body.match(MACRO_CODE.CONSTANT_CALL)) { 258 | // Store the constant definition. 259 | const definition = body.match(MACRO_CODE.CONSTANT_CALL); 260 | const constantName = definition[1]; 261 | 262 | // Push the array to the usedStoragePointerConstants array. 263 | if ( 264 | constants.data[constantName].value === "FREE_STORAGE_POINTER()" && 265 | !usedStoragePointerConstants.includes(constantName) 266 | ) { 267 | usedStoragePointerConstants.push(constantName); 268 | } 269 | 270 | // Slice the body. 271 | body = body.slice(definition[0].length); 272 | } 273 | // If the next call is a macro call. 274 | else if (body.match(MACRO_CODE.MACRO_CALL)) { 275 | // Store the macro definition. 276 | const definition = body.match(MACRO_CODE.MACRO_CALL); 277 | const macroName = definition[1]; 278 | if (!macroName.startsWith("__")) { 279 | // Get the used storage pointer constants. 280 | getUsedStoragePointerConstants(macroName, true); 281 | } 282 | 283 | // Slice the body. 284 | body = body.slice(definition[0].length); 285 | } 286 | // Otherwise just slice the body by one. 287 | else { 288 | body = body.slice(1); 289 | } 290 | } 291 | }; 292 | 293 | // Loop through the given macros and generate the used storage pointer constants. 294 | macrosToSearch.forEach((macroName) => { 295 | getUsedStoragePointerConstants(macroName, false); 296 | }); 297 | 298 | // Iterate through the ordered pointers and generate 299 | // an array (ordered by the defined order) of all storage pointer constants. 300 | const orderedStoragePointerConstants = constants.defintions.filter((constant: string) => 301 | usedStoragePointerConstants.includes(constant) 302 | ); 303 | 304 | // Update and return the constants map. 305 | return setStoragePointers(constants.data, orderedStoragePointerConstants); 306 | }; 307 | 308 | /** 309 | * Assign constants that use the builtin FREE_STORAGE_POINTER( 310 | * @param constants Maps the name of constants to their values 311 | * @param order The order that the constants were declared in 312 | */ 313 | export const setStoragePointers = (constants: Definitions["data"], order: string[]) => { 314 | const usedPointers: number[] = []; 315 | 316 | // Iterate over the array of constants. 317 | order.forEach((name) => { 318 | const value = constants[name].value; 319 | 320 | // If the value is a hex literal. 321 | if (!value.startsWith("FREE_")) { 322 | /* 323 | If the pointer is already used, throw an error. 324 | In order to safely circumvent this, all constant-defined pointers must be defined before 325 | pointers that use FREE_STORAGE_POINTER. 326 | */ 327 | if (usedPointers.includes(convertBytesToNumber(value))) { 328 | throw `Constant ${name} uses already existing pointer`; 329 | } 330 | 331 | // Add the pointer to the list of used pointers. 332 | usedPointers.push(convertBytesToNumber(value)); 333 | } 334 | 335 | // The value calls FREE_STORAGE_POINTER. 336 | else if (value == "FREE_STORAGE_POINTER()") { 337 | // Find the lowest available pointer value. 338 | const pointer = findLowest(0, usedPointers); 339 | 340 | // Add the pointer to the list of used pointers. 341 | usedPointers.push(pointer); 342 | 343 | // Set the constant to the pointer value. 344 | constants[name].value = convertNumberToBytes(pointer).replace("0x", ""); 345 | } 346 | }); 347 | 348 | // Return the new constants value. 349 | return constants; 350 | }; 351 | -------------------------------------------------------------------------------- /src/parser/macros.ts: -------------------------------------------------------------------------------- 1 | import opcodes from "../evm/opcodes"; 2 | import { formatEvenBytes, toHex } from "../utils/bytes"; 3 | import { HIGH_LEVEL, MACRO_CODE } from "./syntax/defintions"; 4 | import { parseArgs } from "./utils/parsing"; 5 | import { isEndOfData, isLiteral } from "./utils/regex"; 6 | import { Definitions, Operation, OperationType } from "./utils/types"; 7 | 8 | /** 9 | * Parse a macro definition. 10 | * @param args The arguments passed into the macro. 11 | */ 12 | const parseMacro = ( 13 | macro: string, 14 | macros: Definitions["data"], 15 | constants: Definitions["data"], 16 | jumptables: Definitions["data"] 17 | ): Operation[] => { 18 | // Instantiate variables. 19 | let operations: Operation[] = []; 20 | const jumpdests = {}; 21 | 22 | // Store a copy of the body and args. 23 | let input = macros[macro].value; 24 | const args = macros[macro].args; 25 | 26 | // Loop through the body. 27 | while (!isEndOfData(input)) { 28 | let token: string[]; 29 | 30 | // Check if we're parsing a macro call. 31 | if ( 32 | input.match(MACRO_CODE.MACRO_CALL) && 33 | !(input.match(MACRO_CODE.MACRO_CALL)![1] || "").startsWith("__") 34 | ) { 35 | // Parse the macro call. 36 | token = input.match(MACRO_CODE.MACRO_CALL); 37 | const name = token[1]; 38 | const args = token[2] ? parseArgs(token[2]) : []; 39 | 40 | // Ensure the macro exists. 41 | if (!macros[name]) throw new Error(`Macro ${name} does not exist.`); 42 | 43 | // Add the macro's operations to the macro operations. 44 | operations.push({ 45 | type: OperationType.MACRO_CALL, 46 | value: name, 47 | args: args, 48 | }); 49 | } 50 | 51 | // Check if we're parsing a constant call. 52 | else if (input.match(MACRO_CODE.CONSTANT_CALL)) { 53 | // Parse the constant call. 54 | token = input.match(MACRO_CODE.CONSTANT_CALL); 55 | const name = token[1]; 56 | 57 | // Add the constant call to the token list. 58 | operations.push({ type: OperationType.CONSTANT_CALL, value: name, args: [] }); 59 | } 60 | 61 | // Check if we're parsing an argument call 62 | else if (input.match(MACRO_CODE.ARG_CALL)) { 63 | // Parse a template call 64 | token = input.match(MACRO_CODE.ARG_CALL); 65 | const name = token[1]; 66 | 67 | // Verify that template has been defined 68 | if (!args.includes(name)) throw new Error(`Arg ${name} is not defined`); 69 | 70 | // Add the template call to the token list 71 | operations.push({ type: OperationType.ARG_CALL, value: name, args: [] }); 72 | } 73 | 74 | // Check if we're parsing a code_size call 75 | else if (input.match(MACRO_CODE.CODE_SIZE)) { 76 | // Parse the code_size call. 77 | token = input.match(MACRO_CODE.CODE_SIZE); 78 | const templateParams = token[2] ? [token[2]] : []; 79 | 80 | // Add the code_size call to the token list. 81 | operations.push({ type: OperationType.CODESIZE, value: token[1], args: templateParams }); 82 | } 83 | 84 | // Check if we're parsing a table_size call 85 | else if (input.match(MACRO_CODE.TABLE_SIZE)) { 86 | // Parse the table_size call. 87 | token = input.match(MACRO_CODE.TABLE_SIZE); 88 | const name = token[1]; 89 | 90 | // Verify that the table has been defined. 91 | if (!jumptables[name]) throw new Error(`Table ${name} is not defined`); 92 | 93 | // Get the size of the table. 94 | const hex = formatEvenBytes(toHex(jumptables[name].args[1])); 95 | 96 | // Add the table_size call to the token list. 97 | operations.push({ type: OperationType.PUSH, value: toHex(95 + hex.length / 2), args: [hex] }); 98 | } 99 | 100 | // Check if we're parsing a table_start call. 101 | else if (input.match(MACRO_CODE.TABLE_START)) { 102 | // Parse the table start call. 103 | token = input.match(MACRO_CODE.TABLE_START); 104 | 105 | // Add the table start call to the token list. 106 | operations.push({ type: OperationType.TABLE_START_POSITION, value: token[1], args: [] }); 107 | } 108 | 109 | // Check if we're parsing a jumplabel. 110 | else if ( 111 | input.match(MACRO_CODE.JUMP_LABEL) && 112 | !constants[input.match(MACRO_CODE.JUMP_LABEL)[1]] 113 | ) { 114 | // Parse the jump label. 115 | token = input.match(MACRO_CODE.JUMP_LABEL); 116 | 117 | // Ensure the label has not been defined. 118 | if (jumpdests[token[1]]) throw new Error(`Jump label ${token[1]} has already been defined`); 119 | 120 | // Define the jump label. 121 | jumpdests[token[1]] = true; 122 | 123 | // Add the jump label to the token list. 124 | operations.push({ type: OperationType.JUMPDEST, value: token[1], args: [] }); 125 | } 126 | 127 | // Check if we're parsing a literal. 128 | else if (input.match(MACRO_CODE.LITERAL_HEX)) { 129 | // Parse the value. 130 | token = input.match(MACRO_CODE.LITERAL_HEX); 131 | 132 | // Format the value. 133 | const hex = formatEvenBytes(token[1]); 134 | 135 | // Add the literal to the token list. 136 | operations.push({ type: OperationType.PUSH, value: toHex(95 + hex.length / 2), args: [hex] }); 137 | } 138 | 139 | // Check if we're parsing an opcode. 140 | else if (input.match(MACRO_CODE.TOKEN) && !constants[input.match(MACRO_CODE.TOKEN)[1]]) { 141 | // Parse the macro. 142 | token = input.match(MACRO_CODE.TOKEN); 143 | 144 | // Add the opcode to the token list. 145 | // The value pushed is dependent on whether it's a jump label 146 | // or an opcode. 147 | if (opcodes[token[1]]) 148 | operations.push({ type: OperationType.OPCODE, value: token[1], args: [] }); 149 | else operations.push({ type: OperationType.PUSH_JUMP_LABEL, value: token[1], args: [] }); 150 | } 151 | // Throw if the value is not parsable. 152 | else throw new Error("Could not parse input"); 153 | 154 | // Slice the input 155 | input = input.slice(token[0].length); 156 | } 157 | 158 | return operations; 159 | }; 160 | 161 | /** Parse argument */ 162 | export const parseArgument = ( 163 | input: string, 164 | macros: Definitions["data"], 165 | constants: Definitions["data"] 166 | ): Definitions["data"] => { 167 | // If the input is a hex literal: 168 | if (isLiteral(input)) { 169 | // Get the bytes value of the operation. 170 | const value = formatEvenBytes(input.substring(2)); 171 | 172 | // Get the push value 173 | const push = toHex(95 + value.length / 2); 174 | 175 | // Return a macro map with a single macro containing a push operation. 176 | return { 177 | [input]: { 178 | value: "", 179 | args: [], 180 | data: [{ type: OperationType.PUSH, value: push, args: [value] }], 181 | }, 182 | }; 183 | } 184 | // If the input is an opcode: 185 | else if (opcodes[input]) { 186 | // Return a macro map with a single macro contraining an opcode. 187 | return { 188 | [input]: { 189 | value: "", 190 | args: [], 191 | data: [{ type: OperationType.OPCODE, value: input, args: [] }], 192 | }, 193 | }; 194 | } 195 | 196 | // If the input is a macro, return the macros map. 197 | if (macros[input]) return macros; 198 | 199 | // If the input is a constant: 200 | if (constants[input]) { 201 | // Return a macro map with a single macro containing a constant call. 202 | return { 203 | [input]: { 204 | value: "", 205 | args: [], 206 | data: [{ type: OperationType.CONSTANT_CALL, value: input, args: [] }], 207 | }, 208 | }; 209 | } 210 | 211 | // If the input is a jump label: 212 | else { 213 | return { 214 | [input]: { 215 | value: "", 216 | args: [], 217 | data: [{ type: OperationType.PUSH_JUMP_LABEL, value: input, args: [] }], 218 | }, 219 | }; 220 | } 221 | }; 222 | 223 | export default parseMacro; 224 | -------------------------------------------------------------------------------- /src/parser/syntax/defintions.ts: -------------------------------------------------------------------------------- 1 | /* Regex Imports */ 2 | import { combineRegexElements } from "../utils/regex"; 3 | 4 | /* High Level Syntax */ 5 | export const HIGH_LEVEL = { 6 | IMPORT: combineRegexElements([ 7 | /* "#" At the start of a line */ 8 | "^(?:[\\s\\n]*)#", 9 | 10 | /* The word "include" */ 11 | "(?:include)", 12 | 13 | /* Quotation marks */ 14 | "(?:\\\"|\\')", 15 | 16 | /* All alphanumeric characters, representing the filename */ 17 | "(.*)", 18 | 19 | /* Quotation marks */ 20 | "(?:\\\"|\\')", 21 | ]), 22 | 23 | /* Syntax for macros */ 24 | MACRO: combineRegexElements([ 25 | /* "#define" at the start of the line */ 26 | "^(?:[\\s\\n]*#[\\s\\n]*define)", 27 | 28 | /* The whole word "macro" */ 29 | "\\b(macro)\\b", 30 | 31 | /* The name of the macro, which can be anything */ 32 | "([A-Za-z0-9_]\\w*)", 33 | 34 | /* Parenthesis that can contain anything (macro arguments) */ 35 | "\\(((.*))\\)", 36 | 37 | /* Equals sign */ 38 | "=", 39 | 40 | /* The word "takes" */ 41 | "takes", 42 | 43 | /** 44 | * A sequence of digits (a number) surrounded by parenthesis 45 | * This number represents the number of "arguments" that the macro takes 46 | */ 47 | "\\((\\d+)\\)", 48 | 49 | /* The word "returns" */ 50 | "returns", 51 | 52 | /* A sequence of digits (a number) surrounded by parenthesis */ 53 | "\\((\\d+)\\)", 54 | 55 | /** 56 | * Curly brackets that can contain all characters. 57 | * In this case, these are the contents of the macro. 58 | */ 59 | "\\{((?:[^\\}])*)\\}", 60 | ]), 61 | 62 | FUNCTION: combineRegexElements([ 63 | /* #define event at the start of the line */ 64 | "^(?:[\\s\\n]*[\\s\\n]*#define function)", 65 | 66 | /** 67 | * The function name and parameter types 68 | * which is then turned into the function signature. 69 | * For example "example(uint, bool)" 70 | */ 71 | "((?:[\\s\\n]*)([a-zA-Z0-9_]+)(?:\\(([a-zA-Z0-9_\\[\\],\\s\\n]+)?\\)))", 72 | 73 | /** 74 | * The function type (payable, nonpayable, view, pure). 75 | */ 76 | "(payable|nonpayable|view|pure)", 77 | 78 | /** 79 | * The word "returns" 80 | */ 81 | "(?:[\\s\\n]* returns)", 82 | 83 | /** 84 | * The return type of the function within parenthesis. 85 | */ 86 | "(?:\\(([a-zA-Z0-9_\\[\\],\\s\\n]+)?\\))", 87 | ]), 88 | 89 | EVENT: combineRegexElements([ 90 | /* #define event at the start of the line */ 91 | "^(?:[\\s\\n]*[\\s\\n]*#define event)", 92 | 93 | /** 94 | * The event name and parameter types 95 | * which is then turned into the function signature. 96 | * For example "example(uint, bool)" 97 | */ 98 | "((?:[\\s\\n]*)([a-zA-Z0-9_]+)(?:\\(([a-zA-Z0-9_,\\s\\n]+)?\\)))", 99 | ]), 100 | 101 | /* Syntax for constants */ 102 | CONSTANT: combineRegexElements([ 103 | /* "#define" at the start of the line */ 104 | "^(?:[\\s\\n]*#define)", 105 | 106 | /* The whole word "constant" */ 107 | "\\b(constant)\\b", 108 | 109 | /* The name of the constant, which can be everything */ 110 | "([A-Za-z0-9_]\\w*)", 111 | 112 | /* Equals sign */ 113 | "=", 114 | 115 | /* Hex literal */ 116 | "(((?:[\\s\\n]*)0x([0-9a-fA-F]+)\\b)|(FREE_STORAGE_POINTER\\(\\)))", 117 | ]), 118 | 119 | /* Syntax for code tables */ 120 | CODE_TABLE: combineRegexElements([ 121 | /* "#define" at the start of the line */ 122 | "^(?:[\\s\\n]*#[\\s\\n]*define)", 123 | 124 | /* The whole word "table" */ 125 | "\\b(table)\\b", 126 | 127 | /* The name of the table, which can be anything */ 128 | "([A-Za-z0-9_]\\w*)", 129 | 130 | /** 131 | * Curly brackets that can contain all characters. 132 | * In this case, these are the contents of the table. 133 | */ 134 | "\\{((?:[^\\}])*)\\}", 135 | ]), 136 | 137 | /* Syntax for jump tables */ 138 | JUMP_TABLE: combineRegexElements([ 139 | /* "#define" at the start of the line */ 140 | "^(?:[\\s\\n]*#[\\s\\n]*define)", 141 | 142 | /* The whole word "jumptable" */ 143 | "\\b(jumptable)\\b", 144 | 145 | /* The name of the table, which can be anything */ 146 | "([A-Za-z0-9_]\\w*)", 147 | 148 | /** 149 | * Curly brackets that can contain all characters. 150 | * In this case, these are the contents of the jumptable. 151 | */ 152 | "\\{((?:[^\\}])*)\\}", 153 | ]), 154 | 155 | /* Syntax for packed jump tables */ 156 | JUMP_TABLE_PACKED: combineRegexElements([ 157 | /* "#define" at the start of the line */ 158 | "^(?:[\\s\\n]*#[\\s\\n]*define)", 159 | 160 | /* The whole word "jumptable__packed" */ 161 | "\\b(jumptable__packed)\\b", 162 | 163 | /* The name of the table, which can be anything */ 164 | "([A-Za-z0-9_]\\w*)", 165 | 166 | /** 167 | * Curly brackets that can contain all characters. 168 | * In this case, these are the contents of the jumptable. 169 | */ 170 | "\\{((?:[^\\}])*)\\}", 171 | ]), 172 | }; 173 | 174 | /* Jump Table Syntax */ 175 | export const JUMP_TABLES = { 176 | /* All characters, with any number of whitespace before and after */ 177 | JUMPS: new RegExp("(?:[\\s\\n]*)[a-zA-Z_0-9\\-]+(?:$|\\s+)", "g"), 178 | }; 179 | 180 | /* Code Syntax, found within macros */ 181 | export const MACRO_CODE = { 182 | /* Any text before spaces and newlines */ 183 | TOKEN: combineRegexElements(["\\s*\\n*([^\\s]*)\\s*\\n*"]), 184 | 185 | /* Any alphanumeric combination followed by 0x */ 186 | LITERAL_HEX: combineRegexElements(["^(?:[\\s\\n]*)0x([0-9a-fA-F]+)\\b"]), 187 | 188 | /* Syntax for macro calls */ 189 | MACRO_CALL: combineRegexElements([ 190 | /* Any number of alphanumeric characters + underscores */ 191 | "^(?:[\\s\\n]*)([a-zA-Z0-9_]+)", 192 | 193 | /* Open Parenthesis */ 194 | "\\(", 195 | 196 | /* Any alphanumeric combination */ 197 | "([a-zA-Z0-9_, \\-<>]*)", 198 | 199 | /* Closing parenthesis */ 200 | "\\)\\s*\\n*", 201 | ]), 202 | 203 | /* Syntax for constant calls */ 204 | CONSTANT_CALL: combineRegexElements([ 205 | /* Any number of alphanumeric characters + underscores */ 206 | "^(?:[\\s\\n]*)\\[([A-Z0-9_]+)\\]", 207 | ]), 208 | 209 | /* Syntax for the builtin codesize function */ 210 | CODE_SIZE: combineRegexElements([ 211 | /* The string "__codesize" */ 212 | "^(?:[\\s\\n]*)__codesize", 213 | 214 | /* Open Parenthesis */ 215 | "\\(", 216 | 217 | /* Any alphanumeric combination */ 218 | "([a-zA-Z0-9_\\-]+)", 219 | 220 | /* Template Arguments */ 221 | "(?:<([a-zA-Z0-9_,\\s\\n]+)>)?", 222 | 223 | /* Closing parenthesis */ 224 | "\\)\\s*\\n*", 225 | ]), 226 | 227 | /* Syntax for the builtin table size function */ 228 | TABLE_SIZE: combineRegexElements([ 229 | /* The string "__tablesize" */ 230 | "^(?:[\\s\\n]*)__tablesize", 231 | 232 | /* Open Parenthesis */ 233 | "\\(", 234 | 235 | /* Any alphanumeric combination */ 236 | "([a-zA-Z0-9_\\-]+)", 237 | 238 | /* Template Arguments */ 239 | "(?:<([a-zA-Z0-9_,\\s\\n]+)>)?", 240 | 241 | /* Closing parenthesis */ 242 | "\\)\\s*\\n*", 243 | ]), 244 | 245 | /* Syntax for the builtin table start function */ 246 | TABLE_START: combineRegexElements([ 247 | /* The string "__tablestart" */ 248 | "^(?:[\\s\\n]*)__tablestart", 249 | 250 | /* Open Parenthesis */ 251 | "\\(", 252 | 253 | /* Any alphanumeric combination */ 254 | "([a-zA-Z0-9_\\-]+)", 255 | 256 | /* Template Arguments */ 257 | "(?:<([a-zA-Z0-9_,\\s\\n]+)>)?", 258 | 259 | /* Closing parenthesis */ 260 | "\\)\\s*\\n*", 261 | ]), 262 | 263 | /* Syntax for template calls */ 264 | ARG_CALL: combineRegexElements(["^(?:[\\s\\n]*)", "<([a-zA-Z0-9_\\-\\+\\*]+)>", "\\s*\\n*"]), 265 | 266 | /* Syntax for jumptables */ 267 | JUMP_LABEL: combineRegexElements(["^(?:[\\s\\n]*)([a-zA-Z0-9_\\-]+):\\s*\\n*"]), 268 | }; 269 | -------------------------------------------------------------------------------- /src/parser/syntax/reserved.ts: -------------------------------------------------------------------------------- 1 | export const RESERVED_KEYWORDS = [ 2 | "__codesize", 3 | "__tablesize", 4 | "__tablestart" 5 | ]; -------------------------------------------------------------------------------- /src/parser/tables.ts: -------------------------------------------------------------------------------- 1 | import { JUMP_TABLES } from "./syntax/defintions"; 2 | import { removeSpacesAndLines } from "./utils/regex"; 3 | 4 | /** 5 | * Parse a code table definition 6 | * @param body The raw string representing the body of the table. 7 | */ 8 | export const parseCodeTable = (body: string): { table: string; size: number } => { 9 | // Parse the body of the table. 10 | const table = body 11 | .match(JUMP_TABLES.JUMPS) 12 | .map((jump) => { 13 | return removeSpacesAndLines(jump); 14 | }) 15 | .join(""); 16 | 17 | // Return the table data. 18 | return { table, size: table.length / 2 }; 19 | }; 20 | 21 | /** 22 | * Parse a jumptable definition 23 | * @param body The raw string representing the body of the table. 24 | */ 25 | export const parseJumpTable = ( 26 | body: string, 27 | compressed = false 28 | ): { jumps: string[]; size: number } => { 29 | // Parse the body of the table 30 | const jumps = body.match(JUMP_TABLES.JUMPS).map((jump) => { 31 | return removeSpacesAndLines(jump); 32 | }); 33 | 34 | // Calculate the size of the table. 35 | let size; 36 | if (compressed) size = jumps.length * 0x02; 37 | else size = jumps.length * 0x20; 38 | 39 | // Return the array of jumps and the size of the table. 40 | return { 41 | jumps, 42 | size, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/parser/utils/contents.ts: -------------------------------------------------------------------------------- 1 | /* Imports */ 2 | 3 | import { HIGH_LEVEL } from "../syntax/defintions"; 4 | import { removeComments } from "./parsing"; 5 | import { readFile } from "../../utils/files"; 6 | import path from 'path'; 7 | 8 | export type Sources = Record; 9 | 10 | export type FileContents = { 11 | fileContents: string[]; 12 | filePaths: string[]; 13 | } 14 | 15 | /** 16 | * Given the contents of the file, return an array 17 | * of filepaths that are imported by the file. 18 | * @param contents The raw contents of the file. 19 | */ 20 | const getImports = (contents: string) => { 21 | const imports: string[] = []; 22 | let nextImport = contents.match(HIGH_LEVEL.IMPORT); 23 | 24 | // While there are still imports to find. 25 | while (nextImport) { 26 | // Add the path of the imported file if it hasn't been added already. 27 | if (!imports.includes(nextImport[1])) imports.push(nextImport[1]); 28 | 29 | // Remove the import statement from the file contents 30 | // so the parser can find the next import. 31 | contents = contents.slice(nextImport[0].length); 32 | 33 | // Find the next import statement (this is null if not found). 34 | nextImport = contents.match(HIGH_LEVEL.IMPORT); 35 | } 36 | 37 | return imports; 38 | }; 39 | 40 | /** 41 | * Retrieve the content and paths of a given file and all its 42 | * imports, including nested imports. 43 | * @param filePath Path to the file to read from. 44 | * @param imported An object indicating whether a file has already 45 | * been imported. 46 | * @param getFile Function to get file contents for a given path. 47 | * @returns Object containing the contents and path of each 48 | * file in the project. 49 | */ 50 | const getNestedFileContents = ( 51 | filePath: string, 52 | imported: Record, 53 | getFile: (filePath: string) => string 54 | ): FileContents => { 55 | const fileData = getFile(filePath); 56 | if (!fileData) throw Error(`File not found: ${filePath}`); 57 | const contents = removeComments(fileData); 58 | const { dir } = path.parse(filePath); 59 | const relativeImports = getImports(contents); 60 | // Normalize the import paths by making them relative 61 | // to the root directory. 62 | const normalizedImports = relativeImports.map( 63 | (relativeImport) => path.join(dir, relativeImport) 64 | ); 65 | const fileContents: string[] = []; 66 | const filePaths: string[] = []; 67 | imported[filePath] = true; 68 | for (const importPath of normalizedImports) { 69 | if (imported[importPath]) continue; 70 | const { 71 | fileContents: importContents, 72 | filePaths: importPaths 73 | } = getNestedFileContents(importPath, imported, getFile); 74 | fileContents.push(...importContents); 75 | filePaths.push(...importPaths); 76 | } 77 | fileContents.push(contents); 78 | filePaths.push(filePath); 79 | return { fileContents, filePaths }; 80 | } 81 | 82 | const normalizeFilePath = (filePath: string) => { 83 | const { dir, base } = path.parse(filePath); 84 | return path.join(dir, base); 85 | } 86 | 87 | /** 88 | * Normalizes the keys in a sources object to ensure the file 89 | * reader can access them. 90 | */ 91 | const normalizeSourcePaths = (sources: Sources) => { 92 | const normalizedSources: Sources = {}; 93 | const keys = Object.keys(sources); 94 | for (const key of keys) { 95 | const normalizedKey = normalizeFilePath(key); 96 | normalizedSources[normalizedKey] = sources[key]; 97 | } 98 | return normalizedSources; 99 | } 100 | 101 | /** 102 | * Given a file path, return a string containing the raw 103 | * file contents of all imported files (including files imported in imported files). 104 | * @param entryFilePath Path to the main file of the project. 105 | * @param sources Object with file paths (relative to the root directory) mapped to their 106 | * contents. If no sources object is provided, the files will be read from the file system. 107 | */ 108 | const getAllFileContents = (entryFilePath: string, sources?: Sources): FileContents => { 109 | // Normalize the keys in the sources object 110 | const normalizedSources = sources && normalizeSourcePaths(sources); 111 | 112 | // Get file from provided sources or by reading from the filesystem 113 | const getFile = (filePath: string) => { 114 | if (sources) { 115 | const content = normalizedSources[filePath]; 116 | if (!content) { 117 | throw Error(`File ${filePath} not found in provided sources!`); 118 | } 119 | return content; 120 | } else { 121 | return readFile(filePath); 122 | } 123 | }; 124 | 125 | return getNestedFileContents( 126 | normalizeFilePath(entryFilePath), 127 | {}, 128 | getFile 129 | ); 130 | }; 131 | 132 | export default getAllFileContents; 133 | -------------------------------------------------------------------------------- /src/parser/utils/parsing.ts: -------------------------------------------------------------------------------- 1 | import assert = require("assert"); 2 | import { isEndOfData } from "./regex"; 3 | 4 | /** 5 | * Given a string, generate a new string without inline comments 6 | * @param data A string of data to parse 7 | */ 8 | export const removeComments = (string: string): string => { 9 | // The formatted version of our string. 10 | let formattedData: string = ""; 11 | 12 | let data = string; 13 | let formatted = ""; 14 | 15 | while (!isEndOfData(data)) { 16 | const multiIndex = data.indexOf("/*"); 17 | const singleIndex = data.indexOf("//"); 18 | 19 | if (multiIndex !== -1 && (multiIndex < singleIndex || singleIndex === -1)) { 20 | formatted += data.slice(0, multiIndex); 21 | const endBlock = data.indexOf("*/"); 22 | 23 | if (endBlock === -1) throw new Error("Could not find closing comment block."); 24 | 25 | formatted += " ".repeat(endBlock - multiIndex + 2); 26 | data = data.slice(endBlock + 2); 27 | } else if (singleIndex !== -1) { 28 | formatted += data.slice(0, singleIndex); 29 | data = data.slice(singleIndex); 30 | const endBlock = data.indexOf("\n"); 31 | if (endBlock === -1) { 32 | formatted += " ".repeat(data.length); 33 | data = ""; 34 | } else { 35 | formatted += " ".repeat(endBlock + 1); 36 | data = data.slice(endBlock + 1); 37 | } 38 | } else { 39 | formatted += data; 40 | break; 41 | } 42 | } 43 | 44 | return formatted; 45 | }; 46 | 47 | /** 48 | * Given a string and an index, get the line number that the index is located on 49 | */ 50 | export const getLineNumber = (str: string, org: string): number => { 51 | // Get the index of the current input. 52 | const index = org.indexOf(str); 53 | 54 | // Get the line number of the file. 55 | return str.substring(0, index).split("\n").length; 56 | }; 57 | 58 | /** 59 | * Parse an arguments list and convert it to an array 60 | */ 61 | export const parseArgs = (argString: string) => { 62 | return argString.replace(" ", "").replace(" ", "").split(","); 63 | }; 64 | 65 | /** 66 | * Throw errors 67 | */ 68 | export const throwErrors = (errors: string[]) => { 69 | if (errors.length > 0) { 70 | errors.map((error) => { 71 | process.stderr.write(`${error}\n`); 72 | }); 73 | 74 | throw new Error(""); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/parser/utils/regex.ts: -------------------------------------------------------------------------------- 1 | /* Constants */ 2 | 3 | export const comma = new RegExp("[^,\\s\\n]*", "g"); 4 | export const space = new RegExp("^[\\s\\n]*"); 5 | export const operator = new RegExp("[\\+\\-\\*]"); 6 | 7 | /** 8 | * Combine an array of regex strings into one, seperated by "\\s*\\n*" 9 | */ 10 | export const combineRegexElements = (regexElements: string[]): RegExp => { 11 | return new RegExp(regexElements.join("\\s*\\n*")); 12 | }; 13 | 14 | /** 15 | * @param input A string containing words split by commas and spaces 16 | * @returns An array of of string, each element is one word 17 | */ 18 | export const removeSpacesAndCommas = (input: string): string[] => { 19 | return (input.match(comma) || []).filter((r) => r !== ""); 20 | }; 21 | 22 | /** 23 | * Removes all spaces and newlines from a string 24 | */ 25 | export const removeSpacesAndLines = (input) => { 26 | return input.replace(/(\r\n\t|\n|\r\t|\s)/gm, ""); 27 | }; 28 | 29 | /** 30 | * @returns A boolean indicating whether the input is the end of the data. 31 | */ 32 | export const isEndOfData = (data: string): boolean => { 33 | return !RegExp("\\S").test(data); 34 | }; 35 | 36 | /** 37 | * Count the number of times an empty character appears in a string. 38 | */ 39 | export const countSpaces = (data: string): number => { 40 | const match = data.match(space); 41 | if (match) { 42 | return match[0].length; 43 | } 44 | 45 | // There are no empty spaces in the string 46 | return 0; 47 | }; 48 | 49 | /** 50 | * @returns A boolean indicating whether the string contains operators. 51 | */ 52 | export const containsOperators = (input: string) => { 53 | return operator.test(input); 54 | }; 55 | 56 | /** 57 | * @returns A boolean indicating whether the input is a valid literal. 58 | */ 59 | export const isLiteral = (input) => { 60 | if (input.match(new RegExp("^(?:\\s*\\n*)*0x([0-9a-fA-F]+)\\b"))) { 61 | return true; 62 | } 63 | 64 | return false; 65 | }; 66 | -------------------------------------------------------------------------------- /src/parser/utils/types.ts: -------------------------------------------------------------------------------- 1 | /* Type Representing Defintions */ 2 | export type Definitions = { 3 | data: { [name: string]: { args: any[]; value: string; data?: any } }; 4 | defintions: string[]; 5 | }; 6 | 7 | /* Type Representing an Operation */ 8 | export type Operation = { 9 | type: OperationType; 10 | value: string; 11 | args: any[]; 12 | }; 13 | 14 | /* Operation Type */ 15 | export enum OperationType { 16 | OPCODE = "OPCODE", 17 | PUSH = "PUSH", 18 | JUMPDEST = "JUMPDEST", 19 | PUSH_JUMP_LABEL = "PUSH_JUMP_LABEL", 20 | 21 | MACRO_CALL = "MACRO_CALL", 22 | CONSTANT_CALL = "CONSTANT_CALL", 23 | ARG_CALL = "ARG_CALL", 24 | 25 | CODESIZE = "CODESIZE", 26 | TABLE_START_POSITION = "TABLE_START_POSITION", 27 | } 28 | 29 | /* Top Level Context */ 30 | export enum Context { 31 | NONE = 0, 32 | MACRO = 1, 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/bytes.ts: -------------------------------------------------------------------------------- 1 | import BN = require("bn.js"); 2 | 3 | /** 4 | * Convert a hex literal to a number 5 | * @param bytes A string representing a hex literal. 6 | */ 7 | export const convertBytesToNumber = (bytes: string): number => { 8 | return parseInt(bytes.slice(2), 10); 9 | }; 10 | 11 | /** 12 | * Convert a number to a hex literal 13 | */ 14 | export const convertNumberToBytes = (number: number): string => { 15 | const value = parseInt(number.toString(), 10).toString(16); 16 | return `0x${value.length % 2 == 0 ? value : "0" + value}`; 17 | }; 18 | 19 | /** 20 | * Given an array, find the lowest missing (non-negative) number 21 | */ 22 | export const findLowest = (value: number, arr: number[]): number => { 23 | return arr.indexOf(value) < 0 ? value : findLowest(value + 1, arr); 24 | }; 25 | 26 | /** 27 | * Given two arrays, remove all elements from the first array that aren't in the second 28 | */ 29 | export const removeNonMatching = (arr1: any[], arr2: any[]) => { 30 | return arr1.filter((val) => arr2.includes(val)); 31 | }; 32 | 33 | /** 34 | * Format a hex literal to make its length even 35 | */ 36 | export const formatEvenBytes = (bytes: string) => { 37 | if (bytes.length % 2) { 38 | return `0${bytes}`; 39 | } 40 | return bytes; 41 | }; 42 | 43 | /** 44 | * Convert a hex literal to a BigNumber 45 | */ 46 | export const toHex = (number: number): string => { 47 | return new BN(number, 10).toString(16); 48 | }; 49 | 50 | /** 51 | * Pad a hex value with zeroes. 52 | */ 53 | export const padNBytes = (hex: string, numBytes: number) => { 54 | if (hex.length > numBytes * 2) { 55 | throw new Error(`value ${hex} has more than ${numBytes} bytes!`); 56 | } 57 | return hex.padStart(numBytes * 2, '0'); 58 | }; 59 | -------------------------------------------------------------------------------- /src/utils/files.ts: -------------------------------------------------------------------------------- 1 | import path = require("path"); 2 | import fs = require("fs"); 3 | 4 | /** 5 | * Read a file and return its contents 6 | * @param filePath The path to the file 7 | */ 8 | export const readFile = (filePath: string): string => { 9 | const resolvedPath = path.resolve(filePath); 10 | if (!fs.existsSync(filePath)) { 11 | throw Error(`File ${filePath} not found!`) 12 | } 13 | return fs.readFileSync(resolvedPath, "utf8"); 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "./dist", 5 | "suppressImplicitAnyIndexErrors": true, 6 | "noImplicitAny": false, 7 | "esModuleInterop": true, 8 | "plugins": [ 9 | { 10 | "name": "typescript-tslint-plugin", 11 | "alwaysShowRuleFailuresAsWarnings": false, 12 | "ignoreDefinitionFiles": true, 13 | "configFile": "../tslint.json", 14 | "suppressWhileTypeErrorsPresent": false 15 | } 16 | ] 17 | } 18 | } 19 | --------------------------------------------------------------------------------