UNPKG

julc

Version:

yul with some solidioms

2,429 lines (1,894 loc) 210 kB
/*! * yul.js - yul parser and transformer * Copyright (c) 2022-2023, Christopher Jeffrey (MIT License). * https://github.com/chjj */ 'use strict'; const assert = require('assert'); const fs = require('fs'); const path = require('path'); const blake2b160 = require('./hashes/blake2b160'); const blake2b256 = require('./hashes/blake2b256'); const hash160 = require('./hashes/hash160'); const hash256 = require('./hashes/hash256'); const keccak256 = require('./hashes/keccak256'); const ripemd160 = require('./hashes/ripemd160'); const sha256 = require('./hashes/sha256'); /* * Constants */ const SUPPORT_FILE = path.resolve(__dirname, '..', 'etc', 'support.yul'); const BUILTINS_FILE = path.resolve(__dirname, '..', 'etc', 'builtins.yul'); const U256_MAX = (1n << 256n) - 1n; const I256_SIGN = 1n << 255n; const DEBUG_SIG = /* keccak256("Debug(bytes32, ...)") */ '0x30c3b94e65122be6598eeacd112efc45d51b6797db32bf9b1a32af0ea1c4f604'; const hardforks = { __proto__: null, homestead: 201603, // Mar 2016 tangerineWhistle: 201610, // Oct 2016 spuriousDragon: 201611, // Nov 2016 byzantium: 201710, // Oct 2017 constantinople: 201902, // Feb 2019 petersburg: 201903, // Mar 2019 istanbul: 201912, // Dec 2019 berlin: 202104, // Apr 2021 london: 202108, // Aug 2021 paris: 202209, // Sep 2022 shanghai: 202304, // Apr 2023 cancun: 202403 // Mar 2024 }; // Function name blacklist. const blacklist = new Set([ 'include', 'struct', 'method', 'event', 'error' ]); /* * Parser */ class Parser { constructor(input, filename) { [this.code, this.comments] = stripComments(input); this.input = this.code; this.filename = filename ? path.resolve(filename) : '<stdin>'; this.root = filename ? path.dirname(this.filename) : process.cwd(); this.line = 0; this.start = 0; this.pos = 0; this.eatSpace(); } assert(value, msg = 'Parse error') { if (!value) { const file = path.relative(process.cwd(), this.filename); const line = this.line + 1; const pos = this.pos - this.start; const indent = ' '.repeat(pos); throw new SyntaxError(`${msg} (${file}:${line}:${pos})\n\n` + `${this.currentLine()}\n` + `${indent}^`); } return value; } comment(line) { return this.comments.get(line) || null; } parse() { // Root = Statements const root = this.Statements(); this.assert(this.input.length === 0); return root; } Statements() { // Statements = Statement* const nodes = []; for (;;) { const node = this.Statement(); if (!node) break; nodes.push(node); } return { type: 'Root', nodes }; } Statement() { // Statement = // Block | // FunctionDefinition | // VariableDeclaration | // Assignment | // If | // Expression | // Switch | // ForLoop | // BreakContinue | // Leave // Makes more sense to put Expression last. return this.Pragma() || this.Fold() || this.IncludeCall() || this.Enum() || this.StructDefinition() || this.Interface() || this.Contract() || this.ObjectBlock() || this.CodeBlock() || this.ConstructorDefinition() || this.DataValue() || this.Block() || this.Macro() || this.FunctionDefinition() || this.MethodDefinition() || this.EventDeclaration() || this.ErrorDeclaration() || this.VariableDeclaration() || this.ConstDeclaration() || this.MemberAssignment() || this.Assignment() || this.If() || this.Switch() || this.ForLoop() || this.While() || this.DoWhile() || this.BreakContinue() || this.Leave() || this.Emit() || this.Throw() || this.Expression(); } Pragma() { // Pragma = // 'pragma' Identifier StringLiteral if (!this.keyword('pragma')) return null; const name = this.assert(this.Identifier()); const string = this.assert(this.StringLiteral()); const value = JSON.parse(string.value); switch (name.value) { case 'license': this.assert(/^[\-\.0-9A-Za-z]+$/.test(value)); break; case 'solc': case 'yulc': this.assert(semver.test(value)); break; case 'evm': this.assert(hardforks[value] != null); break; case 'optimize': case 'deoptimize': this.assert(/^[A-Za-z]+$/.test(value)); break; default: this.assert(0, `Unknown pragma: ${name.value}`); break; } return { type: 'Pragma', name: name.value, value }; } Fold() { // Fold = // '@if' Expression '{' Statements '}' // ( 'elif' '{' Statements '}' )? // ( 'else' '{' Statements '}' )? if (!this.keyword('@if')) return null; const expr = this.assert(this.Expression()); const block = this.StatementBlock(); const branches = []; while (this.keyword('elif')) { const expr = this.assert(this.Expression()); const block = this.StatementBlock(); branches.push([expr, block]); } let otherwise = null; if (this.keyword('else')) otherwise = this.StatementBlock(); return { type: 'Fold', expr, block, branches, otherwise }; } StatementBlock() { this.expect('{'); const block = this.Statements(); this.expect('}'); return block; } IncludeCall() { // IncludeCall = 'include(' StringLiteral ')' if (!this.match(/^include\s*\(/)) return null; const name = this.assert(this.StringLiteral()); this.expect(')'); return { type: 'IncludeCall', root: this.root, name: JSON.parse(name.value) }; } Enum() { // Enum = 'enum' Identifier? '{' ( Identifier ( ':=' NumberLiteral )? )* '}' if (!this.match(/^enum(?=(?:\s+[\w$.]+)?\s*\{)/)) return null; const name = this.Identifier(); const members = []; this.expect('{'); while (!this.peek('}')) { const name = this.assert(this.Identifier()); let expr = null; if (this.consume(':=')) expr = this.assert(this.Expression()); members.push([name, expr]); } this.expect('}'); return { type: 'Enum', name, members }; } StructDefinition() { // StructDefinition = 'struct' Identifier '{' StructMember+ '}' if (!this.match(/^struct\s+(?=[\w$.]+\s*\{)/)) return null; const name = this.assert(this.Identifier()); const members = []; this.expect('{'); while (!this.peek('}')) members.push(this.StructMember()); this.assert(members.length > 0); this.expect('}'); return { type: 'StructDefinition', name, members }; } StructMember() { // StructMember = TypeName ( '+' | ( Identifier ( ':=' Literal )? ) ) const kind = this.assert(this.TypeName()); let name = Identifier('+'); let value = Literal(0); if (!this.consume('+')) { name = this.assert(this.Identifier()); if (this.consume(':=')) value = this.assert(this.MaybeLiteral()); } return { type: 'StructMember', kind, name, value }; } Interface() { // Interface = // 'interface' Identifier '{' ConstructorDeclaration? MethodDeclaration* '}' const {line} = this; if (!this.match(/^interface(?=\s+[a-zA-Z_$][\w$.]*\s*{)/)) return null; const name = this.assert(this.Identifier()); const decls = []; this.assert(!name.value.includes('.')); this.expect('{'); const ctor = this.ConstructorDeclaration(); while (!this.peek('}')) { const decl = this.assert(this.MethodDeclaration()); decls.push(decl); } this.expect('}'); return { type: 'Interface', name, ctor, decls, comment: this.comment(line) }; } Contract() { // Contract = // 'contract' Identifier ( 'optimize' )? Block const {line} = this; if (!this.match(/^contract(?=(?:\s+[a-zA-Z_$][\w$.]*)+\s*{)/)) return null; const name = this.assert(this.Identifier()); this.assert(!name.value.includes('.')); let modifier = null; if (this.keyword('optimize')) modifier = 'optimize'; const block = this.assert(this.Block()); return { type: 'Contract', name, modifier, block, comment: this.comment(line) }; } ObjectBlock() { // ObjectBlock = // 'object' StringLiteral '{' CodeBlock ( ObjectBlock | DataValue )* '}' const {line} = this; if (!this.match(/^object(?=\s*")/)) return null; const name = this.assert(this.StringLiteral()); const block = this.assert(this.Block()); return { type: 'ObjectBlock', name: JSON.parse(name.value), block, comment: this.comment(line) }; } CodeBlock() { // CodeBlock = 'code' Block if (!this.match(/^code(?=\s*{)/)) return null; const block = this.assert(this.Block()); return { type: 'CodeBlock', block }; } Constructor(decl) { // ConstructorDeclaration = // 'constructor' MethodParams Mutability? // ConstructorDefinition = ConstructorDeclaration ( 'unchecked' )? Block const {line} = this; if (!this.match(/^constructor(?=\s*\()/)) return null; const params = this.MethodParams(); let mutability = null; if (this.keyword('payable')) mutability = 'payable'; if (decl) { return { type: 'ConstructorDeclaration', params, mutability, modifier: null, block: null, comment: this.comment(line) }; } let modifier = null; if (this.keyword('unchecked')) modifier = 'unchecked'; const block = this.assert(this.Block()); return { type: 'ConstructorDefinition', params, mutability, modifier, block, comment: this.comment(line) }; } ConstructorDeclaration() { return this.Constructor(true); } ConstructorDefinition() { return this.Constructor(false); } DataValue() { // DataValue = 'data' StringLiteral ( HexLiteral | StringLiteral ) if (!this.match(/^data(?=\s*")/)) return null; const name = this.assert(this.StringLiteral()); const data = this.assert(this.HexLiteral() || this.StringLiteral()); return { type: 'DataValue', name: JSON.parse(name.value), value: { type: 'Literal', subtype: data.type, kind: null, value: data.value } }; } Block() { // Block = '{' Statement* '}' if (!this.consume('{')) return null; const body = []; for (;;) { const stmt = this.Statement(); if (!stmt) break; body.push(stmt); } this.expect('}'); return { type: 'Block', body }; } Macro() { // Macro = MacroConstant | MacroDefinition // MacroConstant = // 'macro' Identifier ':=' Expression // MacroDefinition = // 'macro' Identifier '(' IdentifierList? ')' // ( ':=' Expression | Block ) if (!this.keyword('macro')) return null; const name = this.assert(this.Identifier()); if (this.consume(':=')) { const expr = this.assert(this.Expression()); return { type: 'MacroConstant', name: name.value, expr }; } this.expect('('); let params = IdentifierList(); if (!this.peek(')')) params = this.assert(this.IdentifierList()); this.expect(')'); let block = null; if (this.consume(':=')) { const expr = this.assert(this.Expression()); block = Block([expr]); } else { block = this.assert(this.Block()); } return { type: 'MacroDefinition', name: name.value, params: params.items.map(n => n.value), block }; } FunctionDefinition() { // FunctionDefinition = // 'function' Identifier '(' TypedIdentifierList? ')' // ( 'noinline' )? ( '->' TypedIdentifierList )? Block if (!this.keyword('function')) return null; const name = this.assert(this.Identifier()); this.assert(!blacklist.has(name.value)); this.expect('('); let params = TypedIdentifierList(); if (!this.peek(')')) params = this.assert(this.TypedIdentifierList()); this.expect(')'); let modifier = null; if (this.keyword('noinline')) modifier = 'noinline'; let returns = TypedIdentifierList(); if (this.consume('->')) returns = this.assert(this.TypedIdentifierList()); const block = this.assert(this.Block()); return { type: 'FunctionDefinition', name, params, modifier, returns, block, builtin: false }; } Method(decl) { // Visibility = ( 'internal' | 'external' | 'private' | 'public' ) // Mutability = ( 'pure' | 'view' | 'payable' ) // Modifier = () // Returns = 'returns' MethodParams // MethodDeclaration = // 'method' Identifier MethodParams // Visibility? Mutability? Modifier? Returns? // MethodDefinition = MethodDeclaration Block const {line} = this; if (!this.keyword('method')) return null; const name = this.assert(this.Identifier()); this.assert(!name.value.includes('.')); const params = this.MethodParams(); // Visibility. let visibility = null; if (this.keyword('internal')) this.assert(0); else if (this.keyword('external')) visibility = 'external'; else if (this.keyword('private')) this.assert(0); else if (this.keyword('public')) visibility = 'public'; // Mutability. let mutability = null; if (this.keyword('pure')) mutability = 'pure'; else if (this.keyword('view')) mutability = 'view'; else if (this.keyword('payable')) mutability = 'payable'; // Returns. let returns = MethodParams(); if (this.keyword('returns')) returns = this.MethodParams(); if (decl) { return { type: 'MethodDeclaration', name, params, visibility, mutability, modifier: null, returns, block: null, comment: this.comment(line) }; } const block = this.assert(this.Block()); return { type: 'MethodDefinition', name, params, visibility, mutability, modifier: null, returns, block, comment: this.comment(line) }; } MethodDeclaration() { return this.Method(true); } MethodDefinition() { return this.Method(false); } MethodParams() { // MethodParamsList = // ABIType ( Identifier )? ( ',' ABIType ( Identifier )? )* // MethodParams = '(' MethodParamsList? ')' const items = []; this.expect('('); if (!this.peek(')')) { do { const type = this.assert(this.ABIType()); const name = this.Identifier(); items.push([type, name]); } while (this.consume(',')); } this.expect(')'); return { type: 'MethodParams', items }; } EventDeclaration() { // EventDeclaration = // 'event' Identifier EventParams // ( 'anonymous' )? ( 'packed' )? // ( 'inline' | 'noinline' )? const {line} = this; if (!this.keyword('event')) return null; const name = this.assert(this.Identifier()); const params = this.EventParams(); const anonymous = this.keyword('anonymous'); const packed = this.keyword('packed'); // Modifier. let modifier = null; if (this.keyword('inline')) modifier = 'inline'; else if (this.keyword('noinline')) modifier = 'noinline'; return { type: 'EventDeclaration', name, params, anonymous, packed, modifier, comment: this.comment(line) }; } EventParams() { // EventParamsList = // ABIType ( 'indexed' )? ( Identifier )? // ( ',' ABIType ( 'indexed' )? ( Identifier )? )* // EventParams = '(' EventParamsList? ')' const items = []; this.expect('('); if (!this.peek(')')) { let total = 0; do { const type = this.assert(this.ABIType()); const indexed = this.keyword('indexed'); const name = this.Identifier(); items.push([type, name, indexed]); total += (indexed | 0); this.assert(total <= 4); } while (this.consume(',')); } this.expect(')'); return { type: 'EventParams', items }; } ErrorDeclaration() { // ErrorDeclaration = // 'error' Identifier MethodParams const {line} = this; if (!this.keyword('error')) return null; const name = this.assert(this.Identifier()); const params = this.MethodParams(); return { type: 'ErrorDeclaration', name, params, comment: this.comment(line) }; } ABIType() { // https://docs.soliditylang.org/en/v0.8.16/abi-spec.html#types // ABIType = [a-z]+ ( [0-9]{1,3} ( 'x' [0-9]{1,2} )? )? ( '[]' )? const parts = this.parts(/^([a-z]+)(\d{1,3})?(\[\])?/); if (!parts) return null; const [value, base, digits, brackets] = parts; let width = 0; let array = false; switch (base) { case 'uint': case 'int': { // Left-padded width = Number(digits || 256); array = Boolean(brackets); this.assert(width > 0 && (width & 7) === 0); break; } case 'address': { // Left-padded (uint160) this.assert(!digits); width = 160; array = Boolean(brackets); break; } case 'bool': { // Left-padded (uint8) this.assert(!digits); width = 1; array = Boolean(brackets); break; } case 'fixed': case 'ufixed': { this.assert(0, 'Fixed-point not supported'); break; } case 'bytes': { // Right-padded if (digits) { width = Number(digits) * 8; array = Boolean(brackets); this.assert(width > 0); } else { this.assert(!brackets); // Possible, but we don't want to handle. width = 0; array = true; } break; } case 'string': { // Right-padded this.assert(!digits); this.assert(!brackets); // Possible, but we don't want to handle. width = 0; array = true; break; } case 'function': { // Right-padded (bytes24) this.assert(!digits); width = 192; array = Boolean(brackets); break; } default: { this.assert(0, `Invalid ABI type (${value})`); break; } } this.assert(width <= 256); return { type: 'ABIType', value, base, width, array }; } VariableDeclaration() { // VariableDeclaration = // 'let' TypedIdentifierList ( ':=' Expression )? if (!this.keyword('let')) return null; const vars = this.assert(this.TypedIdentifierList()); let expr = null; if (this.consume(':=')) expr = this.assert(this.Expression()); return { type: 'VariableDeclaration', vars, expr }; } ConstDeclaration() { // ConstDeclaration = // 'const' Identifier ( '()' )? ':=' Expression if (!this.keyword('const')) return null; const name = this.assert(this.Identifier()); const wrap = this.consume('()'); this.expect(':='); const expr = this.assert(this.Expression()); return { type: 'ConstDeclaration', name, expr, wrap }; } TypeName(name = null) { // TypeName = Identifier if (name && this.consume('@')) return name; return this.Identifier(); } TypedIdentifierList() { // TypedIdentifierList = // Identifier ( ':' TypeName )? ( ',' Identifier ( ':' TypeName )? )* const items = []; do { const name = this.Identifier(); if (!name) { this.assert(items.length === 0); return null; } let type = null; if (!this.peek(':=') && this.consume(':')) type = this.assert(this.TypeName(name)); items.push([name, type]); } while (this.consume(',')); return { type: 'TypedIdentifierList', items }; } MemberAssignment() { // MemberAssignment = // MemberIdentifier ':=' Expression const saved = this.save(); const lhs = this.MemberIdentifier(); if (!lhs) return null; const match = this.match(/^[:|]=/); if (!match) { this.restore(saved); return null; } const rhs = this.assert(this.Expression()); return { type: 'MemberAssignment', or: match[0] === '|', lhs, rhs }; } Assignment() { // Assignment = // IdentifierList ':=' Expression const saved = this.save(); const lhs = this.IdentifierList(); if (!lhs) return null; if (!this.consume(':=')) { this.restore(saved); return null; } const rhs = this.assert(this.Expression()); return { type: 'Assignment', lhs, rhs }; } IdentifierList() { // IdentifierList = Identifier ( ',' Identifier)* const items = []; do { const name = this.Identifier(); if (!name) { this.assert(items.length === 0); return null; } items.push(name); } while (this.consume(',')); return { type: 'IdentifierList', items }; } If() { // If = // 'if' Expression Block ( 'elif' Expression Block )* ( 'else' Block )? if (!this.keyword('if')) return null; const expr = this.assert(this.Expression()); const block = this.assert(this.Block()); const branches = []; while (this.keyword('elif')) { const expr = this.assert(this.Expression()); const block = this.assert(this.Block()); branches.push([expr, block]); } let otherwise = null; if (this.keyword('else')) otherwise = this.assert(this.Block()); return { type: 'If', expr, block, branches, otherwise }; } Switch() { // Switch = // 'switch' Expression ( '{' )? ( Case+ Default? | Default ) ( '}' )? if (!this.keyword('switch')) return null; const expr = this.assert(this.Expression()); const cases = []; let def = null; // Extension: optional brace. const brace = this.consume('{'); for (;;) { const case_ = this.Case(); if (!case_) break; cases.push(case_); } def = this.Default(); if (brace) this.expect('}'); this.assert(cases.length > 0 || def); return { type: 'Switch', expr, cases, def }; } Case() { // Case = // 'case' Literal Block if (!this.keyword('case')) return null; const value = this.assert(this.MethodSignature() || this.MaybeLiteral()); const block = this.assert(this.Block()); return { type: 'Case', value, block }; } Default() { // Default = // 'default' Block if (!this.keyword('default')) return null; const block = this.assert(this.Block()); return { type: 'Default', block }; } MethodSignature() { // MethodSignature = 'method' '(' Identifier MethodParams? ')' if (!this.match(/^method\s*\(/)) return null; const name = this.assert(this.Identifier()); let params = null; if (this.peek('(')) params = this.MethodParams(); this.expect(')'); return { type: 'MethodSignature', name, params }; } EventSignature() { // EventSignature = 'event' '(' Identifier EventParams? ')' if (!this.match(/^event\s*\(/)) return null; const name = this.assert(this.Identifier()); let params = null; if (this.peek('(')) params = this.EventParams(); this.expect(')'); return { type: 'EventSignature', name, params }; } ErrorSignature() { // ErrorSignature = 'error' '(' Identifier MethodParams? ')' if (!this.match(/^error\s*\(/)) return null; const name = this.assert(this.Identifier()); let params = null; if (this.peek('(')) params = this.MethodParams(); this.expect(')'); return { type: 'ErrorSignature', name, params }; } MaybeLiteral() { // MaybeLiteral = Literal | Identifier // Should be a Literal, but we might have a const here. return this.Literal() || this.Identifier(true); } ForLoop() { // ForLoop = // 'for' Block Expression Block Block if (!this.keyword('for')) return null; return { type: 'ForLoop', init: this.assert(this.Block()), test: this.assert(this.Expression()), update: this.assert(this.Block()), block: this.assert(this.Block()) }; } While() { // While = // 'while' Expression Block if (!this.keyword('while')) return null; return { type: 'While', test: this.assert(this.Expression()), block: this.assert(this.Block()) }; } DoWhile() { // DoWhile = // 'do' Block 'while' Expression if (!this.match(/^do(?=\s*\{)/)) return null; const block = this.assert(this.Block()); this.assert(this.keyword('while')); const test = this.assert(this.Expression()); return { type: 'DoWhile', test, block }; } BreakContinue() { // BreakContinue = // 'break' | 'continue' if (this.keyword('break')) { return { type: 'BreakContinue', value: 'break' }; } if (this.keyword('continue')) { return { type: 'BreakContinue', value: 'continue' }; } return null; } Leave() { // Leave = 'leave' if (!this.keyword('leave')) return null; return { type: 'Leave' }; } Emit() { // Emit = // 'emit' Identifier '(' Expression ( ',' Expression )* )? ')' if (!this.keyword('emit')) return null; const name = this.assert(this.Identifier()); this.expect('('); const offset = this.assert(this.Expression()); const args = []; while (this.consume(',')) args.push(this.assert(this.Expression())); this.expect(')'); return { type: 'Emit', name, offset, args }; } Throw() { // Throw = // 'throw' Identifier '(' ( Expression ( ',' Expression )* )? ')' // if (!this.match(/^revert(?=\s+[a-zA-Z_$][\w$.]*\s*\()/)) if (!this.keyword('throw')) return null; const name = this.assert(this.Identifier()); const args = []; this.expect('('); do { args.push(this.assert(this.Expression())); } while (this.consume(',')); this.expect(')'); return { type: 'Throw', name, offset: null, args }; } Expression() { // Expression = // FunctionCall | Identifier | Literal // Makes more sense to put Identifier last. return this.StructInitializer() || this.MethodSignature() || this.EventSignature() || this.ErrorSignature() || this.InterfaceCall() || this.FunctionCall() || this.Literal() || this.AnyIdentifier(true); } StructInitializer() { // StructInitializer = // 'struct' '(' Identifier ( ',' Expression )* ')' if (!this.match(/^struct\s*\(/)) return null; const name = this.assert(this.Identifier()); const args = []; while (this.consume(',')) { let expr = this.DefaultIdentifier(); if (!expr) expr = this.assert(this.Expression()); args.push(expr); } this.expect(')'); return { type: 'StructInitializer', name, args }; } InterfaceCall() { // InterfaceCall = // ( 'create' | 'create2' | 'call' ) ( '?' )? Identifier // '(' ( Expression ( ',' Expression )* )? ')' if (!this.peek(/^(?:create2?|call)\??\s+[a-zA-Z_$][\w$.]*\s*\(/)) return null; const kind = this.assert(this.Identifier()).value; const attempt = this.consume('?'); const ident = this.assert(this.Identifier()); const args = []; let name = ident; let method = null; if (kind === 'call') { const parts = ident.value.split('.'); this.assert(parts.length === 2); this.assert(/^[a-zA-Z_$]/.test(parts[1])); name = Identifier(parts[0]); method = Identifier(parts[1]); } else { this.assert(!ident.value.includes('.')); this.assert(!attempt); } this.expect('('); do { args.push(this.assert(this.Expression())); } while (this.consume(',')); this.expect(')'); return { type: 'InterfaceCall', kind, attempt, name, method, args }; } FunctionCall() { // FunctionCall = // Identifier '(' ( Expression ( ',' Expression )* )? ')' const saved = this.save(); const name = this.Identifier(); const args = []; if (!name) return null; if (!this.consume('(')) { this.restore(saved); return null; } if (!this.peek(')')) { do { args.push(this.assert(this.Expression())); } while (this.consume(',')); } this.expect(')'); return { type: 'FunctionCall', name, args, filename: this.filename, line: saved[1] }; } Literal() { // Literal = (NumberLiteral | // StringLiteral | // TrueLiteral | // FalseLiteral) ( ':' TypeName )? const node = this.NumberLiteral() || this.StringLiteral() || this.HexLiteral() || this.TrueLiteral() || this.FalseLiteral(); if (!node) return null; let kind = null; if (this.consume(':')) kind = this.assert(this.TypeName()); return { type: 'Literal', subtype: node.type, kind, value: node.value }; } NumberLiteral() { // NumberLiteral = HexNumber | DecimalNumber return this.HexNumber() || this.DecimalNumber(); } HexNumber() { // HexNumber = ( '-' )? '0x' [0-9a-fA-F]+ let value = this.match(/^-?0x[0-9a-fA-F]+/); if (!value) return null; this.assert(value.length <= Number(value[0] === '-') + 2 + 64); if (value.length === 67) this.assert(BigInt(value) >= -I256_SIGN); if (value[0] === '-') { value = BigInt(value) & U256_MAX; value = '0x' + value.toString(16); } return { type: 'HexNumber', value }; } DecimalNumber() { // DecimalNumber = ( '-' )? [0-9]+ let value = this.match(/^-?[0-9]+/); if (!value) return null; this.assert(value.length <= 78); if (value.length === 78) { const n = BigInt(value); this.assert(n >= -I256_SIGN && n <= U256_MAX); } if (value[0] === '-') { value = BigInt(value) & U256_MAX; value = '0x' + value.toString(16); return { type: 'HexNumber', value }; } return { type: 'DecimalNumber', value }; } StringLiteral() { // StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"' const value = this.match(/^"(?:[^"\r\n\\]|\\.)*"/); if (!value) return null; return { type: 'StringLiteral', value }; } HexLiteral() { // HexLiteral = // 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'') const value = this.match(/^hex(["'])(?:[0-9a-fA-F]{2})*\1/); if (!value) return null; return { type: 'HexLiteral', value }; } TrueLiteral() { // TrueLiteral = 'true' if (!this.keyword('true')) return null; return { type: 'BoolLiteral', value: 'true' }; } FalseLiteral() { // FalseLiteral = 'false' if (!this.keyword('false')) return null; return { type: 'BoolLiteral', value: 'false' }; } AnyIdentifier(replaceable = false) { // AnyIdentifier = MemberIdentifier | // CallDataIdentifier | // Identifier return this.MemberIdentifier() || this.CallDataIdentifier() || this.Identifier(replaceable); } MemberIdentifier() { // MemberIdentifier = Identifier ( ':' TypeName ) ? '->' Identifier if (!this.peek(/^[a-zA-Z_$][\w$.]*(?::[a-zA-Z_$][\w$.]*)?->/)) return null; const name = this.assert(this.Identifier()); let cast = null; if (this.consume(':')) cast = this.assert(this.TypeName(name)); this.expect('->'); const member = this.assert(this.Identifier()); return { type: 'MemberIdentifier', name, cast, member }; } DefaultIdentifier() { // DefaultIdentifier = '@' const value = this.match(/^@(?=\s*[,)])/); if (!value) return null; return { type: 'Identifier', value, replaceable: false }; } CallDataIdentifier() { // CallDataIdentifier = '&' ? 'calldata.' Identifier const match = this.match(/^&?calldata\./); if (!match) return null; const member = this.assert(this.Identifier()); return { type: 'CallDataIdentifier', member, ref: match[0] === '&' }; } Identifier(replaceable = false) { // Identifier = [a-zA-Z_$] [a-zA-Z_$0-9.]* const value = this.match(/^[a-zA-Z_$][\w$.]*/); if (!value) return null; return { type: 'Identifier', value, replaceable }; } parts(rx) { const matches = rx.exec(this.input); if (!matches) return null; this.eat(matches[0].length); return matches; } match(rx) { const matches = this.parts(rx); if (!matches) return null; return matches[0]; } eatChars(length) { for (let i = 0; i < length; i++) { const ch = this.input[i]; if (ch === '\n') { this.start = this.pos + 1; this.line++; } this.pos++; } if (length > 0) this.input = this.input.substring(length); } eatSpace() { let spaces = 0; for (let i = 0; i < this.input.length; i++) { const ch = this.input[i]; switch (ch) { case '\n': this.start = this.pos + 1; this.line++; // fallthrough case '\r': case '\t': case ' ': this.pos++; spaces++; continue; } break; } if (spaces > 0) this.input = this.input.substring(spaces); } eat(length) { this.eatChars(length); this.eatSpace(); return true; } save() { return [this.input, this.line, this.start, this.pos]; } restore(saved) { [this.input, this.line, this.start, this.pos] = saved; } peek(str) { if (str instanceof RegExp) return str.test(this.input); return this.input.startsWith(str); } consume(str) { if (!this.input.startsWith(str)) return false; return this.eat(str.length); } keyword(str) { if (!this.input.startsWith(str)) return false; if (str.length < this.input.length) { if (/[\w$.]/.test(this.input[str.length])) return false; } return this.eat(str.length); } expect(str) { this.assert(this.consume(str), `Expected "${str}"`); } currentLine() { const code = this.code; let pos = this.pos; while (pos < code.length && code[pos] !== '\n') pos++; return code.substring(this.start, pos); } } /* * Scope */ class Scope { constructor(node, parent = null, depth = 0) { this.node = node; this.parent = parent; this.depth = depth; this.consts = new Map(); this.structs = new Map(); this.macros = new Map(); this.funcs = new Map(); this.interfaces = new Map(); this.methods = new Map(); this.events = new Map(); this.errors = new Map(); this.vars = new Map(); this.calldata = null; this.depends = new Map(); this.data = []; this.immutable = []; } child(node) { return new Scope(node, this, this.depth + 1); } *scopes() { for (let scope = this; scope; scope = scope.parent) yield scope; } dependsOn(name, optional = false) { const func = this.findFunc(name); if (!func && !optional) throw new Error(`${name}() not available.`); if (func) { const scope = this.findCodeScope(); scope.depends.set(name, func); } return func; } addData(prefix, value) { const scope = this.findObjectScope(); const name = `__${prefix}_${scope.data.length}`; const data = DataValue(name, value); scope.data.push(data); return Literal(name); } findContractBlock() { for (const scope of this.scopes()) { if (scope.node.type === 'ObjectBlock') { if (scope.depth & 1) return scope.node; } } return null; } findObjectScope() { let root = this; for (const scope of this.scopes()) { if (scope.node.type === 'ObjectBlock') return scope; root = scope; } return root; } isDeployedObject() { if (this.node.type !== 'ObjectBlock') return false; return (this.depth & 1) === 0; } findCodeScope() { let root = this; for (const scope of this.scopes()) { if (scope.node.type === 'CodeBlock' || scope.node.type === 'ConstructorDefinition') { return scope; } root = scope; } return root; } findConst(name) { for (const scope of this.scopes()) { const result = scope.consts.get(name); if (result != null) return result; } return null; } findStruct(name) { for (const scope of this.scopes()) { const result = scope.structs.get(name); if (result != null) return result; } return null; } findMacro(name) { for (const scope of this.scopes()) { const result = scope.macros.get(name); if (result != null) return result; } return null; } findFunc(name) { for (const scope of this.scopes()) { const result = scope.funcs.get(name); if (result != null) return result; } return null; } findInterface(name) { for (const scope of this.scopes()) { const result = scope.interfaces.get(name); if (result != null) return result; } return null; } findConstructor(name) { const iface = this.findInterface(name); if (!iface) return null; return iface.ctor; } findDeclaration(name) { const parts = name.split('.'); if (parts.length !== 2) return null; const iface = this.findInterface(parts[0]); if (!iface) return null; return iface.map.get(parts[1]) || null; } findMethod(name) { for (const scope of this.scopes()) { const result = scope.methods.get(name); if (result != null) return result; } return null; } findEvent(name) { for (const scope of this.scopes()) { const result = scope.events.get(name); if (result != null) return result; } return null; } findError(name) { for (const scope of this.scopes()) { const result = scope.errors.get(name); if (result != null) return result; } return null; } findVar(name) { for (const scope of this.scopes()) { const result = scope.vars.get(name); if (result != null) return result; } return null; } findCalldata() { for (const scope of this.scopes()) { if (scope.calldata) return scope.calldata; } return null; } findMember(node) { assert(node.type === 'MemberIdentifier'); const name = node.name.value; const cast = node.cast ? node.cast.value : null; const member = node.member.value; const type = cast || this.findVar(name); if (type == null) throw new Error(`Referencing non-existent variable: ${name}`); const struct = this.findStruct(type); if (struct == null) throw new Error(`${name}:${type} is not a struct`); const mem = struct.map.get(member); if (mem == null) throw new Error(`Non-existent member ${name}->${member}`); return mem; } addConst(name, value) { if (this.vars.has(name)) throw new Error(`Variable already declared: ${name}`); if (this.consts.has(name)) throw new Error(`Constant already defined: ${name}`); this.consts.set(name, value); } addStruct(struct) { if (this.structs.has(struct.name)) throw new Error(`Duplicate struct name: ${struct.name}`); this.structs.set(struct.name, struct); } addMacro(node) { assert(node.type === 'MacroConstant' || node.type === 'MacroDefinition'); if (this.macros.has(node.name)) throw new Error(`Macro already defined: ${node.name}`); this.macros.set(node.name, node); } addInterface(iface) { const {name} = iface; if (this.interfaces.has(name)) throw new Error(`Interface ${name} already defined.`); this.interfaces.set(name, iface); } addMethod(method) { const {name} = method; if (this.methods.has(name)) throw new Error(`Method ${name} already defined.`); this.methods.set(name, method); } addEvent(event) { const {name} = event; if (this.events.has(name)) throw new Error(`Event ${name} already defined.`); this.events.set(name, event); } addError(error) { const {name} = error; if (this.errors.has(name)) throw new Error(`Error ${name} already defined.`); this.errors.set(name, error); } addVar(name, type) { if (this.vars.has(name)) throw new Error(`Variable already declared: ${name}`); if (this.consts.has(name)) throw new Error(`Constant already defined: ${name}`); this.vars.set(name, type); } undefMacro(name) { for (const scope of this.scopes()) { if (scope.macros.delete(name)) return true; } return false; } isLastNode(node) { if (this.node.type !== 'FunctionDefinition' && this.node.type !== 'MethodDefinition') { return false; } const body = this.node.block.body; return body[body.length - 1] === node; } } /* * Transformer */ class Transformer { constructor(root, options, hasher = null) { let versions = { solc: null, yulc: null }; let hardfork = 'london'; let level = 2; let debug = false; let macros = []; let builtins; if (options != null) { if (options.builtins != null) level = options.builtins + 1; if (options.level != null) level = options.level; if (options.debug != null) debug = options.debug; if (options.macros != null) macros = options.macros; if (options.hardfork != null) hardfork = options.hardfork; if (options.versions != null) versions = options.versions; assert((level >>> 0) === level); assert(typeof debug === 'boolean'); assert(Array.isArray(macros)); assert(typeof hardfork === 'string'); assert(hardforks[hardfork] != null, 'Unknown EVM version.'); assert(hardforks[hardfork] < 300000, 'Invalid EVM version.'); assert(versions && typeof versions === 'object'); assert(versions.solc == null || typeof versions.solc === 'string'); assert(versions.yulc == null || typeof versions.yulc === 'string'); } switch (level) { case 0: builtins = new Builtins(); break; case 1: builtins = Builtins.fromSupport(); break; case 2: builtins = Builtins.fromBuiltins(); break; default: throw new Error('Invalid builtin level.'); } if (macros.length > 0) builtins = builtins.clone().inject(macros); this.root = root; this.level = level; this.debug = debug; this.hardfork = hardfork; this.versions = versions; this.scope = builtins.scope(root); this.object = -1; this.subobject = -1; this.abi = new ABI(); this.deopt = new Set(['F']); this.hasher = hasher; } transform() { return this.rewrite(this.root); } generate(scope) { const nodes = []; for (const node of scope.depends.values()) { if (node.builtin) nodes.push(this.rewrite(node)); else nodes.push(node); } for (const node of scope.data) nodes.push(node); return nodes; } push(child) { assert(child.parent === this.scope); this.scope = child; } pop() { assert(this.scope.parent); this.scope = this.scope.parent; } _block(node) { const scope = this.scope; const body = []; for (const child of node.body) body.push(this.rewrite(child)); if (scope.depends.size > 0 || scope.data.length > 0) body.push(...this.generate(scope)); if (this.hasher && scope.isDeployedObject()) body.push(this.hasher.metadata); return Block(filter(body)); } block(node, scope) { this.push(scope); const body = this._block(node); this.pop(); return body; } params(node, scope) { const items = []; for (const [ident, kind] of node.items) { const name = this.rewrite(ident); const type = kind ? this.rewrite(kind) : null; scope.addVar(name.value, type ? type.value : 'u256'); if (type && !scope.findStruct(type.value)) items.push([name, type]); else items.push([name, null]); } return TypedIdentifierList(items); } prepare(node) { switch (node.type) { case 'StructDefinition': { const name = this.rewrite(node.name); const members = []; for (const member of node.members) members.push(this.prepare(member)); return { type: 'StructDefinition', name, members }; } case 'StructMember': { return { type: 'StructMember', kind: this.rewrite(node.kind), name: this.rewrite(node.name), value: this.rewrite(node.value) }; } case 'Interface': { return { type: 'Interface', name: this.rewrite(node.name), ctor: node.ctor ? this.prepare(node.ctor) : null, decls: node.decls.map(n => this.prepare(n)), comment: node.comment }; } case 'ConstructorDeclaration': case 'ConstructorDefinition': { return { type: 'ConstructorDeclaration', params: this.prepare(node.params), mutability: node.mutability, modifier: node.modifier, block: null, comment: node.comment }; } case 'MethodDeclaration': case 'MethodDefinition': { return { type: 'MethodDeclaration', name: this.rewrite(node.name), params: this.prepare(node.params), visibility: node.visibility, mutability: node.mutability, modifier: node.modifier, returns: this.prepare(node.returns), block: null, comment: node.comment }; } case 'MethodParams': { const items = []; for (const [type, name] of node.items) { if (name) items.push([type, this.rewrite(name)]); else items.push([type, null]); } return { type: 'MethodParams', items }; } case 'EventDeclaration': { return { type: 'EventDeclaration', name: this.rewrite(node.name), params: this.prepare(node.params), anonymous: node.anonymous, packed: node.packed, modifier: node.modifier, comment: node.comment }; } case 'EventParams': { const items = []; for (const [type, name, indexed] of node.items) { if (name) items.push([type, this.rewrite(name), indexed]); else items.push([type, null, indexed]); } return { type: 'EventParams', items }; } case 'ErrorDeclaration': { return { type: 'ErrorDeclaration', name: this.rewrite(node.name), params: this.prepare(node.params), comment: node.comment }; } case 'MemberIdentifier': { return { type: 'MemberIdentifier', name: this.rewrite(node.name), cast: node.cast ? this.rewrite(node.cast) : null, member: this.rewrite(node.member) }; } default: { throw new Error('unreachable'); } } } value(node) { const ident = this.rewrite(node); assert(ident.type === 'Identifier'); return ident.value; } rewrite(node) { const scope = this.scope; switch (node.type) { case 'Root': { const nodes = []; for (const child of node.nodes) nodes.push(this.rewrite(child)); if (!scope.parent && this.object < 0) { if (scope.depends.size > 0 || scope.data.length > 0) nodes.push(...this.generate(scope));