diff --git a/package.json b/package.json index ba8e6859..7865a70c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@aztec/huff", "description": "Compiler for Huff, a DSL for low-level Ethereum smart contract programming", - "version": "0.0.1", + "version": "0.0.2", "author": "AZTEC", "bugs": { "url": "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/AztecProtocol/huff/issues" diff --git a/src/parser.js b/src/parser.js index b6fa7da5..76675968 100644 --- a/src/parser.js +++ b/src/parser.js @@ -734,6 +734,62 @@ parser.getFileContents = (originalFilename, partialPath) => { return { filedata, raw }; }; +const normalizePath = (str) => path.join('', str); +parser.getSourcesFileContents = (sources, entryFile) => { + const included = {}; + const parentPathOf = (str) => (m = /(.*)\/(?!\/)(.*)\.huff/g.exec(str)) && m[1]; + const recurse = (filename, parentPath) => { + const fileName = normalizePath(filename) + const relativePath = path.join(parentPath || '', fileName); + let fileString = sources[relativePath]; + if (!fileString) { + if (!fs.existsSync(relativePath)) { + throw new Error(`Import not found: ${fileName} ${(parentPath && parentPath !== '.') ? `in ${parentPath}` : ''}`) + } + fileString = fs.readFileSync(relativePath, 'utf8'); + console.log(`Warning: File ${relativePath} does not exist in sources, found in filesystem.`) + } + let formatted = parser.removeComments(fileString); + let imported = []; + let test = formatted.match(grammar.topLevel.IMPORT); + while (test !== null) { + const importStatement = formatted.match(grammar.topLevel.IMPORT); + const empty = ' '.repeat(importStatement[0].length); + formatted = empty + formatted.slice(importStatement[0].length); + const importedFilename = importStatement[1]; + if (!included[importedFilename]) { + imported = [...imported, ...recurse(importedFilename, parentPathOf(relativePath))]; + included[importedFilename] = true; + } + test = formatted.match(grammar.topLevel.IMPORT); + } + const result = [ + ...imported, + { + filename: fileName, + data: formatted, + }, + ]; + return result; + }; + const filedata = recurse(entryFile); + const raw = filedata.reduce((acc, { data }) => { + return acc + data; + }, ''); + return { filedata, raw }; +}; + +parser.getContents = (source, _path) => { + if (typeof source == 'string') { + return parser.getFileContents(source, _path); + } + const sources = Object.keys(source).reduce((obj, key) => ({ + ...obj, + [normalizePath(key)]: source[key], + }), {}); + return parser.getSourcesFileContents(sources, _path); +}; + parser.parseFile = (filename, partialPath) => { const { filedata, raw } = parser.getFileContents(filename, partialPath); const map = inputMaps.createInputMap(filedata); @@ -741,13 +797,23 @@ parser.parseFile = (filename, partialPath) => { return { inputMap: map, macros, jumptables }; }; -parser.compileMacro = (macroName, filename, partialPath) => { - const { filedata, raw } = parser.getFileContents(filename, partialPath); +parser.compileMacro = (macroName, source, _path) => { + const { filedata, raw } = parser.getContents(source, _path) const map = inputMaps.createInputMap(filedata); const { macros, jumptables } = parser.parseTopLevel(raw, 0, map); const { data: { bytecode, sourcemap } } = parser.processMacro(macroName, 0, [], macros, map, jumptables); // prettier-ignore - return { bytecode, sourcemap }; }; +/** + * @param source - can be either a filename or a sources object + * @param _path - if a sources object is given, path should be the entry file; otherwise, path should be the parent path +*/ +parser.parse = (source, _path) => { + const { filedata, raw } = parser.getContents(source, _path) + const map = inputMaps.createInputMap(filedata); + const { macros, jumptables } = parser.parseTopLevel(raw, 0, map); + return { inputMap: map, macros, jumptables }; +}; + module.exports = parser; diff --git a/src/parser.spec.js b/src/parser.spec.js index 8d550f9b..63a8630c 100644 --- a/src/parser.spec.js +++ b/src/parser.spec.js @@ -12,6 +12,7 @@ const { opcodes } = require('./opcodes/opcodes'); const { expect } = chai; const pathToTestData = path.posix.resolve(__dirname, '../testData'); +const sources = require(path.join(pathToTestData, 'sources.json')); describe('parser tests', () => { describe('templates', () => { @@ -306,6 +307,16 @@ describe('parser tests', () => { expect(result.sourcemap.length).to.equal(result.bytecode.length / 2); }); + it(`can compile macro in sources object`, () => { + const result = parser.compileMacro('ADD_ONE', sources, 'test.huff'); + expect(result.bytecode).to.equal('600101') + }) + + it('can load file missing from sources object', () => { + const result = parser.compileMacro('ADD_THREE', sources, 'testData/external.huff'); + expect(result.bytecode).to.equal('600301') + }) + it('frozen version of MAIN_TWO_ENDO_MOD macro is correctly compiled', () => { const result = parser.compileMacro('MAIN_TWO_ENDO_MOD', './main_loop.huff', pathToTestData); const expected = fs.readFileSync(path.posix.resolve(pathToTestData, 'compiled.txt'), 'utf8'); diff --git a/src/runtime.js b/src/runtime.js index e390fb16..780f8440 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -95,7 +95,7 @@ function runCode(vm, bytecode, calldata, sourcemapOffset = 0, sourcemap = [], ca } function Runtime(filename, path, debug = false) { - const { inputMap, macros, jumptables } = newParser.parseFile(filename, path); + const { inputMap, macros, jumptables } = newParser.parse(filename, path); return async function runMacro(vm, macroName, stack = [], memory = [], calldata = null, callvalue = 0, callerAddr = 0) { const memoryCode = encodeMemory(memory); const stackCode = encodeStack(stack); diff --git a/src/runtime.spec.js b/src/runtime.spec.js index 6d6c99d7..9e519abf 100644 --- a/src/runtime.spec.js +++ b/src/runtime.spec.js @@ -1,6 +1,7 @@ const chai = require('chai'); const BN = require('bn.js'); const path = require('path'); +const fs = require('fs'); const { Runtime, getNewVM } = require('./runtime.js'); const vm = getNewVM(); @@ -64,3 +65,49 @@ describe('runtime tests using double algorithm', () => { expect(stack[2].eq(expected[2])).to.equal(true); }); }); + +const sources = { + 'test.huff': testHelper, + 'double.huff': fs.readFileSync(path.join(pathToTestData, 'double.huff'), 'utf8'), + 'constants.huff': fs.readFileSync(path.join(pathToTestData, 'constants.huff'), 'utf8') +}; + +describe('sources object', () => { + let double; + let inputStack; + let expected; + before(() => { + double = new Runtime(sources, 'test.huff'); + inputStack = [ + new BN('30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47', 16), + new BN('bed36146b88c48f014c6b1ac1c8441ac0291e66462c432021914cc887dd0d75', 16), + new BN('14f5a63645c1cd318e0720ac3ae1f3c164462b5a96b288ce6f45b62e2fb220ab', 16), + new BN('29185780978972d99ce22b0543d90143dacc611fff75d4705b6ac867892ee2d8', 16), + ]; + }); + it('macro DOUBLE correctly calculates point doubling', async () => { + const { stack } = await double(vm, 'DOUBLE', inputStack); + expected = [ + new BN('30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47', 16), + new BN('55f8cabad8ae94c14c1482e3e20f7ce889e3143949181f404b04da8df02029ba', 16), + new BN('43662fe9d0effbc8820577509b7454a46758301a4eedd592e61e6e357f4f4667', 16), + new BN('f8caadab040dc22bcd17b3dd99820ba0f9fc34582ab692cbca1f914c48cbad8', 16), + ]; + expect(stack[0].eq(expected[0])).to.equal(true); + expect(stack[1].eq(expected[1])).to.equal(true); + expect(stack[2].eq(expected[2])).to.equal(true); + expect(stack[3].eq(expected[3])).to.equal(true); + }); + + it('macro DOUBLE__MAIN correctly calculates point doubling (inverted y)', async () => { + const { stack } = await double(vm, 'DOUBLE__MAIN_IMPL', inputStack.slice(1)); + expected = [ + new BN('55f8cabad8ae94c14c1482e3e20f7ce889e3143949181f404b04da8df02029ba', 16), + new BN('4dc6bb6ed2a4e4b4a6eb59d2e90fb4745f2c0f99ea678a14ce43360f0a27b16e', 16), + new BN('20d7a39830f0c406fb7eca78a7e937a387e1a74be5c661607f7e930213f0426f', 16), + ]; + expect(stack[0].eq(expected[0])).to.equal(true); + expect(stack[1].eq(expected[1])).to.equal(true); + expect(stack[2].eq(expected[2])).to.equal(true); + }); +}); diff --git a/testData/add-three.huff b/testData/add-three.huff new file mode 100644 index 00000000..333e70c7 --- /dev/null +++ b/testData/add-three.huff @@ -0,0 +1 @@ +#define macro ADD_THREE = takes(0) returns(1) { 0x03 add } \ No newline at end of file diff --git a/testData/sources.json b/testData/sources.json new file mode 100644 index 00000000..e923926b --- /dev/null +++ b/testData/sources.json @@ -0,0 +1,6 @@ +{ + "testData/external.huff": "#include './add-three.huff'", + "add/two.huff": "#define macro ADD_TWO = takes(0) returns(1) { 0x02 add }", + "add/one.huff": "#define macro ADD_ONE = takes(0) returns(1) { 0x01 add }", + "test.huff": "#include './add/one.huff'" +} \ No newline at end of file