Solidity 官方文档中文版(一)

中文翻译文档:http://www.tryblockchain.org/Solidity-%E8%AF%AD%E8%A8%80%E4%BB%8B%E7%BB%8D.html

英文官方文档:http://solidity.readthedocs.io/en/develop/

Solidity是一种智能合约高级语言,运行在Ethereum虚拟机(EVM)之上。

Solidity与其它语言相关的特点?

它的语法接近于Javascript,是一种面向对象的语言。但作为一种真正意义上运行在网络上的去中心合约,它又有很多的不同,下面列举一些:

  • 以太坊底层是基于帐户,而非UTXO的,所以有一个特殊的Address的类型。用于定位用户,定位合约,定位合约的代码(合约本身也是一个帐户)。
  • 由于语言内嵌框架是支持支付的,所以提供了一些关键字,如payable,可以在语言层面直接支持支付,而且超级简单。
  • 存储是使用网络上的区块链,数据的每一个状态都可以永久存储,所以需要确定变量使用内存,还是区块链。
  • 运行环境是在去中心化的网络上,会比较强调合约或函数执行的调用的方式。因为原来一个简单的函数调用变为了一个网络上的节点中的代码执行,分布式的感觉。
  • 最后一个非常大的不同则是它的异常机制,一旦出现异常,所有的执行都将会被回撤,这主要是为了保证合约执行的原子性,以避免中间状态出现的数据不一致。

Hello Wolrd!

听起来高大上,其实入手玩起来也可以很简单:

pragma solidity ^0.4.0;

contract HelloWorld{
    uint balance;
    function update(uint amount) returns (address, uint){
        balance += amount;
        return (msg.sender, balance);
    }
}

通过读取参数输入的新值,并将之累加至合约的变量中,返回发送人的地址,和最终的累计值。

浏览器编译器Remix

使用无需安装的浏览器编译器Remix可以立即看到效果。打开后,如下图所示:

输入上述代码,点击Create按钮,就能在浏览器中创建能调用函数的按钮。在update按钮旁输入入参,点击按钮,就能执行函数调用并打印出函数返回的结果了。

备注

如果出现错误,可以等待浏览器资源加载完成,或强制刷新后再试。

Solidity的完整语法:

入门说明:

  • Solidity智能合约文件结构
  • 智能合约源文件的基本要素概览(Structure of a Contract)

值类型:

  • 类型
  • 布尔(Booleans)
  • 整型(Integer)
  • 地址(Address)
  • 字节数组(byte arrays)
  • 小数
  • 字符串(String literal)
  • 十六进制字面量
  • 枚举
  • 函数(Function Types)

引用类型:

  • 引用类型(Reference Types)
  • 数据位置(Data location)
  • 数组(Arrays)
  • 数据结构(struct)

杂项:

  • 映射/字典(mappings)
  • 左值运算符(Operators Involving LValues)
  • 类型间的转换(Conversions between Elementary Types)
  • 类型推断(Type Deduction)

单位:

  • 货币单位(Ether Units)
  • 时间单位(Time Units)

语言内置特性:

  • 特殊变量及函数(Special Variables and Functions)
  • 数学和加密函数(Mathematical and Cryptographic Functions)
  • 地址相关(Address Related)

进阶:

  • 入参和出参(Input Parameters and Output Parameters)
  • 控制结构
  • 函数调用(Function Calls)
  • 创建合约实例(Creating Contracts via new)
  • 表达式的执行顺序(Order of Evaluation of Expressions)
  • 赋值(Assignment)
  • 作用范围和声明(Scoping And Decarations)
  • 异常(Excepions)
  • 内联汇编(Inline Assembly)

合约详解:

  • 合约
  • 可见性或权限控制(Visibility And Accessors)
  • 访问函数(Accessor Functions)
  • 函数修改器(Function Modifiers)
  • 常状态变量(constant state variables)
  • 回退函数(fallback function)
  • 事件(Events)
  • 继承(Inheritance)
  • 接口(Abstract Contracts)

其它:

  • 库(Libraries)
  • 状态变量的存储模型(Layout of State Variables in Storage)
  • 内存变量的存局(Layout in Memory)

  • 调用数据的布局(Layout of CallData)

源文件映射

作为AST输出的一部分,编译器会提供AST某个节点以应的源代码的范围。这可以被用来做基于AST的静态代码错误分析,可以高亮本地变量,和他们对应使用的调试工具。

此外,编译器也可以生成字节码到生成指令源代码的范围映射。这对静态分析工具来说非常重要,它们在字节码级别分析,可以来在调试工具内显示对应代码位置,或支持断点操作。

上述的源代码映射都使用整数来引用源代码。


特殊特性(Esoteric Features)

在Solidity的类型系统里面有一些类型有一些在其它语言中没有的语法。其中之一就是函数类型。但依然,使用var时,可以把函数类型作为本地变量。

contract FunctionSelector {
  function select(bool useB, uint x) returns (uint z) {
    var f = a;
    if (useB) f = b;
    return f(x);
  }

  function a(uint x) returns (uint z) {
    return x * x;
  }

  function b(uint x) returns (uint z) {
    return 2 * x;
  }
}

可以对var赋值为不同的函数。


内部机制

内部机制 - 清理变量(Internals - Cleaning Up Variables)

当一个值占用的位数小于32字节时,那些没有用到的位必须被清除掉。Solidity编译器设计实现为,在任何可能受到潜在的残存数据带来的副作用之前,清理掉这些脏数据。比如,在向内存写入一个值前,不需要的字节位需要被清除掉,因为没有用到的内存位可能被用来计算哈希,或作为消息调用的发送的数据存储。同样的,在向storage中存储时,未用到的字节位需要被清理掉,否则这些脏数据会带来意想不到的事情。

另一方面,如果接下来的后述操作不会产生副作用,我们不会主动清理这些字节位。比如,由于任何非0的值被JUMP指令认为是true。在它作用JUMPI指令的条件前,我们在不会清理这个布尔值。

在上述设计准则之外,Solidity编译器会在输入数据加载到栈上后清理掉它们。

不同的类型,有不同的无效值的清理规则。

类型 有效值 无效值意味着
有n的成员的枚举类型 0到(n - 1) 异常(exception)
布尔 0或1 1
有符号整数 sign-extended word 当前静默的包装了结果,以后会以异常的形式抛出来
无符号整数 高位节是0 当前静默的包装了结果,以后会以异常的形式抛出来

内部机制 - 优化(Internals - The Optimizer)

Solidity是基于汇编优化的,所以它可以,同时也被其它编程语言所使用(译者注:其它语言编译为汇编)。编译器会在JUMPJUMPDEST处拆分基本的指令序列为一个个的基本块。在这些代码块内,所有的指令都被分析。所有的对栈,内存或存储的操作被记录成由指令及其参数组成的一个个表达式,这些表达式又会指向另一个表达式。核心目的是找到一些表达式在任何输入的情况下都恒等,然后将它们组合成一个表达式类。优化器首先尝试在一系列已知的表达式中,找出来一些全新的表达式。如果找不到,表达式通过一些简单的原则进行简化,比如 constant + constant = sum_of_constants 或 X * 1 = X。由于这一切是递归进行的,我们可以在第二项是一个更复杂的表达时,应用上述后续规则。对内存或存储的修改,存储的位置经常会被擦除,由此我们并不知道存的数据有什么不同:如果我们首先写入一个值x,再写入另一个值y,这两个都是输入变量,第二个写入时会覆盖第一个,所以我们实际在写入第二个值时,不知道第一个值是什么了。所以,如果一个简单的表达式x-y指向一个非0的常量,这样我们就能在操作y时知道x内存储的值。

在流程最后,我们知道哪一个表达式会在栈顶,并且有一系列的对内存或存储的修改。这些信息与基本的块存在一起以方便的用来连接他们。此外,关于栈,存储和内存配置的信息会传递到下一个块。如果我们知道所有JUMP和JUMPI指令的目标,我们可以构建程序的完整的控制流程图。如果有任何一个我们不知道目标的跳转(因为目标是通过输入参数进行计算的,所以原则上可能发生),我们必须擦除块知识的输入,因为他有可能是某个跳转的目的地(译者注:因为可能某个跳转在运行时会指向他,修改他的状态,所以他的推算状态是错误的)。如果某个JUMPI被发现他的条件是常量,它会被转化为一个无状态的跳转。

在最后一步,每个块中的代码都将重新生成。在某个块结束时,将生成栈上表达式的依赖树,不在这个树上的操作就被丢弃了。在我们原始代码中想要应用的对内存、存储想要的修改顺序的代码就生成出来了(被丢弃的修改被认为是完全不需要的),最终,生成了所有的需要在栈上存在的值。

这些步骤应用于每个基本的块,如果新生成的代码更小,将会替换现有的代码。如果一个块在分析期间在JUMPI处分裂,条件被证实为一个常量,JUMPI将可以基于常量值被替换掉,比如下述代码:

var x = 7;
data[7] = 9;
if (data[x] != x + 2)
  return 2;
else
  return 1;

简化的代码可以被编译为:

data[7] = 9;
return 1;

尽管上述代码在一开始有一个跳转指令。

调用数据的布局(Layout of CallData)

当Solidity合约被部署后,从某个帐户调用这个合约,需要输入的数据是需要符合the ABI specification, 中文翻译见这里: http://me.tryblockchain.org/Solidity-abi-abstraction.htmlABI规范需要参数被填充为多个32字节。内部的函数调用,则使用了不同的约定。


内存变量的布局(Layout in Memory)

Solidity预留了3个32字节大小的槽位:

  • 0-64:哈希方法的暂存空间(scratch space)
  • 64-96:当前已分配内存大小(也称空闲内存指针(free memory pointer))

暂存空间可在语句之间使用(如在内联编译时使用)

Solidity总是在空闲内存指针所在位置创建一个新对象,且对应的内存永远不会被释放(也许未来会改变这种做法)。

有一些在Solidity中的操作需要超过64字节的临时空间,这样就会超过预留的暂存空间。他们就将会分配到空闲内存指针所在的地方,但由于他们自身的特点,生命周期相对较短,且指针本身不能更新,内存也许会,也许不会被清零(zerod out)。因此,大家不应该认为空闲的内存一定已经是清零(zeroed out)的。


状态变量的存储模型(Layout of State Variables in Storage)

大小固定的变量(除了映射变长数组以外的所有类型)在存储(storage)中是依次连续从位置0开始排列的。如果多个变量占用的大小少于32字节,会尽可能的打包到单个storage槽位里,具体规则如下:

  • 在storage槽中第一项是按低位对齐存储(lower-order aligned)(译者注:意味着是大端序了,因为是按书写顺序。)。
  • 基本类型存储时仅占用其实际需要的字节。
  • 如果基本类型不能放入某个槽位余下的空间,它将被放入下一个槽位。
  • 结构体数组总是使用一个全新的槽位,并占用整个槽(但在结构体内或数组内的每个项仍遵从上述规则)

优化建议

当使用的元素占用少于32字节,你的合约的gas使用也许更高。这是因为EVM每次操作32字节。因此,如果元素比这小,EVM需要更多操作来从32字节减少到需要的大小。

因为编译器会将多个元素打包到一个storage槽位,这样就可以将多次读或写组合进一次操作中,只有在这时,通过缩减变量大小来优化存储结构才有意义。当操作函数参数和memory的变量时,因为编译器不会这样优化,所以没有上述的意义。

最后,为了方便EVM进行优化,尝试有意识排序storage的变量和结构体的成员,从而让他们能打包得更紧密。比如,按这样的顺序定义,uint128, uint128, uint256,而不是uint128, uint256, uint128。因为后一种会占用三个槽位。

非固定大小

结构体和数组里的元素按它们给定的顺序存储。

由于它们不可预知的大小。映射变长数组类型,使用Keccak-256哈希运算来找真正数据存储的起始位置。这些起始位置往往是完整的堆栈槽。

映射动态数组根据上述规则在位置p占用一个未满的槽位(对映射里嵌套映射,数组中嵌套数组的情况则递归应用上述规则)。对一个动态数组,位置p这个槽位存储数组的元素个数(字节数组和字符串例外,见下文)。而对于映射,这个槽位没有填充任何数据(但这是必要的,因为两个挨着的映射将会得到不同的哈希值)。数组的原始数据位置是keccak256(p);而映射类型的某个键k,它的数据存储位置则是keccak256(k . p),其中的.表示连接符号。如果定位到的值以是一个非基本类型,则继续运用上述规则,是基于keccak256(k . p)的新的偏移offset

bytesstring占用的字节大小如果足够小,会把其自身长度和原始数据存在当前的槽位。具体来说,如果数据最多31位长,高位存数据(左对齐),低位存储长度lenght * 2。如果再长一点,主槽位就只存lenght * 2 + 1。原始数据按普通规则存储在keccak256(slot)

所以对于接下来的代码片段:

pragma solidity ^0.4.4;

contract C {
    struct s { uint a; uint b; }
    uint x;
    mapping(uint => mapping(uint => s)) data;
}

按上面的代码来看,结构体从位置0开始,这里定义了一个结构体,但并没有对应的结构体变量,故不占用空间。uint x实际是uint256,单个占32字节,占用槽位0,所以映射data将从槽位1开始。

data[4][9].b的位置在keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1

有人在这里尝试直接读取区块链的存储值,https://github.com/ethereum/solidity/issues/1550


独立的汇编语言

上面介绍的在Solidity中嵌入的内联汇编语言也可以单独使用。实际上,它是被计划用来作为编译器的一种中间语言。在这个目的下,它尝试达到下述的目标:

  1. 使用它编写的代码要可读,即使代码是从Solidity编译得到的。
  2. 从汇编语言转为字节码应该尽可能的少坑。
  3. 控制流应该容易检测来帮助进行形式验证与优化。

为了达到第一条和最后一条的目标,Solidity汇编语言提供了高层级的组件比如,for循环,switch语句和函数调用。这样的话,可以不直接使用SWAP,DUP,JUMP,JUMPI语句,因为前两个有混淆的数据流,后两个有混淆的控制流。此外,函数形式的语句如mul(add(x, y), 7)比纯的指令码的形式7 y x add num更加可读。

第二个目标是通过引入一个绝对阶段来实现,该阶段只能以非常规则的方式去除较高级别的构造,并且仍允许检查生成的低级汇编代码。Solidity汇编语言提供的非原生的操作是用户定义的标识符的命名查找(函数名,变量名等),这些都遵循简单和常规的作用域规则,会清理栈上的局部变量。

作用域:一个标识符(标签,变量,函数,汇编)在定义的地方,均只有块级作用域(作用域会延伸到,所在块所嵌套的块)。跨函数边界访问局部变量是不合法的,即使可能在作用域内(译者注:这里可能说的是,函数内定义多个函数的情况,JavaScript有这种语法)。不允许shadowing。局部变量不能在定义前被访问,但标签,函数和汇编可以。汇编是非常特殊的块结构可以用来,如,返回运行时的代码,或创建合约。外部定义的汇编变量在子汇编内不可见。

如果控制流来到了块的结束,局部变量数匹配的pop指令会插入到栈底(译者注:移除局部变量,因为局部变量失效了)。无论何时引用局部变量,代码生成器需要知道其当前在堆栈中的相对位置,因此需要跟踪当前所谓的堆栈高度。由于所有的局部变量在块结束时会被移除,因此在进入块之前和之后的栈高应该是不变的,如果不是这样的,将会抛出一个警告。

我们为什么要使用高层级的构造器,比如switchfor和函数。

使用switchfor和函数,可以在不用jumpjumpi的情况下写出来复杂的代码。这会让分析控制流更加容易,也可以进行更多的形式验证及优化。

此外,如果手动使用jumps,计算栈高是非常复杂的。栈内所有的局部变量的位置必须是已知的,否则指向本地变量的引用,或者在块结束时自动删除局部变量都不会正常工作。脱机处理机制正确的在块内不可达的地方插入合适的操作以修正栈高来避免出现jump时非连续的控制流带来的栈高计算不准确的问题。

示例:

我们从一个例子来看一下Solidity到这种中间的脱机汇编结果。我们可以一起来考虑下下述Soldity程序的字节码:

contract C {
  function f(uint x) returns (uint y) {
    y = 1;
    for (uint i = 0; i < x; i++)
      y = 2 * y;
  }
}

它将生成下述的汇编内容:

{
  mstore(0x40, 0x60) // store the "free memory pointer"
  // function dispatcher
  switch div(calldataload(0), exp(2, 226))
  case 0xb3de648b {
    let (r) = f(calldataload(4))
    let ret := $allocate(0x20)
    mstore(ret, r)
    return(ret, 0x20)
  }
  default { revert(0, 0) }
  // memory allocator
  function $allocate(size) -> pos {
    pos := mload(0x40)
    mstore(0x40, add(pos, size))
  }
  // the contract function
  function f(x) -> y {
    y := 1
    for { let i := 0 } lt(i, x) { i := add(i, 1) } {
      y := mul(2, y)
    }
  }
}

在经过脱机汇编阶段,它会编译成下述的内容:

{
  mstore(0x40, 0x60)
  {
    let $0 := div(calldataload(0), exp(2, 226))
    jumpi($case1, eq($0, 0xb3de648b))
    jump($caseDefault)
    $case1:
    {
      // the function call - we put return label and arguments on the stack
      $ret1 calldataload(4) jump(f)
      // This is unreachable code. Opcodes are added that mirror the
      // effect of the function on the stack height: Arguments are
      // removed and return values are introduced.
      pop pop
      let r := 0
      $ret1: // the actual return point
      $ret2 0x20 jump($allocate)
      pop pop let ret := 0
      $ret2:
      mstore(ret, r)
      return(ret, 0x20)
      // although it is useless, the jump is automatically inserted,
      // since the desugaring process is a purely syntactic operation that
      // does not analyze control-flow
      jump($endswitch)
    }
    $caseDefault:
    {
      revert(0, 0)
      jump($endswitch)
    }
    $endswitch:
  }
  jump($afterFunction)
  allocate:
  {
    // we jump over the unreachable code that introduces the function arguments
    jump($start)
    let $retpos := 0 let size := 0
    $start:
    // output variables live in the same scope as the arguments and is
    // actually allocated.
    let pos := 0
    {
      pos := mload(0x40)
      mstore(0x40, add(pos, size))
    }
    // This code replaces the arguments by the return values and jumps back.
    swap1 pop swap1 jump
    // Again unreachable code that corrects stack height.
    0 0
  }
  f:
  {
    jump($start)
    let $retpos := 0 let x := 0
    $start:
    let y := 0
    {
      let i := 0
      $for_begin:
      jumpi($for_end, iszero(lt(i, x)))
      {
        y := mul(2, y)
      }
      $for_continue:
      { i := add(i, 1) }
      jump($for_begin)
      $for_end:
    } // Here, a pop instruction will be inserted for i
    swap1 pop swap1 jump
    0 0
  }
  $afterFunction:
  stop
}

汇编有下面四个阶段:

  1. 解析
  2. 脱汇编(移除switch,for和函数)
  3. 生成指令流
  4. 生成字节码

我们将简单的以步骤1到3指定步骤。更加详细的步骤将在后面说明。

解析、语法

解析的任务如下:

  • 将字节流转为符号流,去掉其中的C++风格的注释(一种特殊的源代码引用的注释,这里不打算深入讨论)。
  • 将符号流转为下述定义的语法结构的AST。
  • 注册块中定义的标识符,标注从哪里开始(根据AST节点的注解),变量可以被访问。

组合词典遵循由Solidity本身定义的词组。

空格用于分隔标记,它由空格,制表符和换行符组成。 注释是常规的JavaScript / C ++注释,并以与Whitespace相同的方式进行解释。

语法:

AssemblyBlock = '{' AssemblyItem* '}'
AssemblyItem =
    Identifier |
    AssemblyBlock |
    FunctionalAssemblyExpression |
    AssemblyLocalDefinition |
    FunctionalAssemblyAssignment |
    AssemblyAssignment |
    LabelDefinition |
    AssemblySwitch |
    AssemblyFunctionDefinition |
    AssemblyFor |
    'break' | 'continue' |
    SubAssembly | 'dataSize' '(' Identifier ')' |
    LinkerSymbol |
    'errorLabel' | 'bytecodeSize' |
    NumberLiteral | StringLiteral | HexLiteral
Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
FunctionalAssemblyExpression = Identifier '(' ( AssemblyItem ( ',' AssemblyItem )* )? ')'
AssemblyLocalDefinition = 'let' IdentifierOrList ':=' FunctionalAssemblyExpression
FunctionalAssemblyAssignment = IdentifierOrList ':=' FunctionalAssemblyExpression
IdentifierOrList = Identifier | '(' IdentifierList ')'
IdentifierList = Identifier ( ',' Identifier)*
AssemblyAssignment = '=:' Identifier
LabelDefinition = Identifier ':'
AssemblySwitch = 'switch' FunctionalAssemblyExpression AssemblyCase*
    ( 'default' AssemblyBlock )?
AssemblyCase = 'case' FunctionalAssemblyExpression AssemblyBlock
AssemblyFunctionDefinition = 'function' Identifier '(' IdentifierList? ')'
    ( '->' '(' IdentifierList ')' )? AssemblyBlock
AssemblyFor = 'for' ( AssemblyBlock | FunctionalAssemblyExpression)
    FunctionalAssemblyExpression ( AssemblyBlock | FunctionalAssemblyExpression) AssemblyBlock
SubAssembly = 'assembly' Identifier AssemblyBlock
LinkerSymbol = 'linkerSymbol' '(' StringLiteral ')'
NumberLiteral = HexNumber | DecimalNumber
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+

脱汇编

一个AST转换,移除其中的forswitch和函数构建。结果仍由同一个解析器,但它不确定使用什么构造。如果添加仅跳转到并且不继续的jumpdests,则添加有关堆栈内容的信息,除非没有局部变量访问到外部作用域或栈高度与上一条指令相同。伪代码如下:

desugar item: AST -> AST =
match item {
AssemblyFunctionDefinition('function' name '(' arg1, ..., argn ')' '->' ( '(' ret1, ..., retm ')' body) ->
  :
  {
    jump($<name>_start)
    let $retPC := 0 let argn := 0 ... let arg1 := 0
    $<name>_start:
    let ret1 := 0 ... let retm := 0
    { desugar(body) }
    swap and pop items so that only ret1, ... retm, $retPC are left on the stack
    jump
    0 (1 + n times) to compensate removal of arg1, ..., argn and $retPC
  }
AssemblyFor('for' { init } condition post body) ->
  {
    init // cannot be its own block because we want variable scope to extend into the body
    // find I such that there are no labels $forI_*
    $forI_begin:
    jumpi($forI_end, iszero(condition))
    { body }
    $forI_continue:
    { post }
    jump($forI_begin)
    $forI_end:
  }
'break' ->
  {
    // find nearest enclosing scope with label $forI_end
    pop all local variables that are defined at the current point
    but not at $forI_end
    jump($forI_end)
    0 (as many as variables were removed above)
  }
'continue' ->
  {
    // find nearest enclosing scope with label $forI_continue
    pop all local variables that are defined at the current point
    but not at $forI_continue
    jump($forI_continue)
    0 (as many as variables were removed above)
  }
AssemblySwitch(switch condition cases ( default: defaultBlock )? ) ->
  {
    // find I such that there is no $switchI* label or variable
    let $switchI_value := condition
    for each of cases match {
      case val: -> jumpi($switchI_caseJ, eq($switchI_value, val))
    }
    if default block present: ->
      { defaultBlock jump($switchI_end) }
    for each of cases match {
      case val: { body } -> $switchI_caseJ: { body jump($switchI_end) }
    }
    $switchI_end:
  }
FunctionalAssemblyExpression( identifier(arg1, arg2, ..., argn) ) ->
  {
    if identifier is function  with n args and m ret values ->
      {
        // find I such that $funcallI_* does not exist
        $funcallI_return argn  ... arg2 arg1 jump()
        pop (n + 1 times)
        if the current context is `let (id1, ..., idm) := f(...)` ->
          let id1 := 0 ... let idm := 0
          $funcallI_return:
        else ->
          0 (m times)
          $funcallI_return:
          turn the functional expression that leads to the function call
          into a statement stream
      }
    else -> desugar(children of node)
  }
default node ->
  desugar(children of node)
}

生成操作码流

在操作码流生成期间,我们在一个计数器中跟踪当前的栈高,所以通过名称访问栈的变量是可能的。栈高在会修改栈的操作码后或每一个标签后进行栈调整。当每一个新局部变量被引入时,它都会用当前的栈高进行注册。如果要访问一个变量(或者拷贝其值,或者对其赋值),会根据当前栈高与变量引入时的当时栈高的不同来选择合适的DUPSWAP指令。

伪代码:

codegen item: AST -> opcode_stream =
match item {
AssemblyBlock({ items }) ->
  join(codegen(item) for item in items)
  if last generated opcode has continuing control flow:
    POP for all local variables registered at the block (including variables
    introduced by labels)
    warn if the stack height at this point is not the same as at the start of the block
Identifier(id) ->
  lookup id in the syntactic stack of blocks
  match type of id
    Local Variable ->
      DUPi where i = 1 + stack_height - stack_height_of_identifier(id)
    Label ->
      // reference to be resolved during bytecode generation
      PUSHof label>
    SubAssembly ->
      PUSHof subassembly data>
FunctionalAssemblyExpression(id ( arguments ) ) ->
  join(codegen(arg) for arg in arguments.reversed())
  id (which has to be an opcode, might be a function name later)
AssemblyLocalDefinition(let (id1, ..., idn) := expr) ->
  register identifiers id1, ..., idn as locals in current block at current stack height
  codegen(expr) - assert that expr returns n items to the stack
FunctionalAssemblyAssignment((id1, ..., idn) := expr) ->
  lookup id1, ..., idn in the syntactic stack of blocks, assert that they are variables
  codegen(expr)
  for j = n, ..., i:
  SWAPi where i = 1 + stack_height - stack_height_of_identifier(idj)
  POP
AssemblyAssignment(=: id) ->
  look up id in the syntactic stack of blocks, assert that it is a variable
  SWAPi where i = 1 + stack_height - stack_height_of_identifier(id)
  POP
LabelDefinition(name:) ->
  JUMPDEST
NumberLiteral(num) ->
  PUSHas decimal and right-aligned>
HexLiteral(lit) ->
  PUSH32as hex and left-aligned>
StringLiteral(lit) ->
  PUSH328 encoded and left-aligned>
SubAssembly(assembly  block) ->
  append codegen(block) at the end of the code
dataSize() ->
  assert that  is a subassembly ->
  PUSH32of code generated from subassembly >
linkerSymbol() ->
  PUSH32 and append position to linker table
}

感谢您的支持


你可能感兴趣的:(区块链)