julc
Version:
yul with some solidioms
2,429 lines (1,894 loc) • 210 kB
JavaScript
/*!
* 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));