。bWasm文本格式使用S-表达式描述模块,源自Lisp语言,使用了大量圆括号,特别适合描述类似抽象语法树的树形结构。
    Wasm文本格式和二进制格式基本是一致的,除了表现形式不同以外,在结构上,两种格式还有几个较大的不同之处。
    1、二进制是以段(Section)为单位组织数据的,文本格式则是以域(Field)为单位组织内容,域相当于二进制段中的项目,但不一定会连续出现,WAT编译器会把同类型的域收集起来,合并成二进制段。
    2、在二进制格式中,除了自定义段,其他段必须按照ID递增的顺序排列,文本格式中的域则没有这么严格的限制,不过导入域必须出现在函数域、表域、内存域和全局域之前。
    3、文本格式的域和二进制的段基本是一一对应的,但是有两种情况例外,第一种是文本格式没有单独的代码域、只有函数域。WAT编译器会将函数域收集起来,分别生成函数段和代码段,第二种是文本格式没有自定义域,每办法描述自定义段。
    4、文本格式提供了多种内联写法。例如:函数域、表域、内存域、全局域可以内联导入或导出域,表域可以内联元素域,内存域可以内联数据域,函数域和导入域可以内联类型域。这些内联写法只是“语法糖”,WAT编译器会妥善处理。

    类型域
    类型域用于定义函数类型,下面例子定义了一个接收两个i32类型参数、返回1个i32类型结果的函数类型。

    1. (module
    2. (type (func (param i32) (param i32) (result i32)))
    3. )

    圆括号是WAT语言主要的分隔符,module、type、func、param、result等是WAT语言关键字(以小些字母开头)
    可以给函数类型分配一个标识符(以$符开头),换句话说就是给它取个名字,这样在其他地方通过调用名字来引用函数类型,不必直接使用索引,多个参可以写在同一个param里,多个返回值可以写在同一个result块里。

    1. (module
    2. (type $ft1 (func (param i32 i32) (result i32)))
    3. (type $ft2 (func (param f64) (result f64 f64)))
    4. )

    导入和导出域
    Wasm模块可以导入或者导出函数、表、内存和全局变量这4中类型元素,因此导入和导出域也支持这4种类型。
    下面代码为导入域的写法例子:

    1. (module
    2. (type $ft1 (func (param i32 i32) (result i32)))
    3. (import "env" "f1" (func $f1 (type $ft1)))
    4. (import "env" "t1" (table $t 1 8 funcref))
    5. (import "env" "m1" (memory $m 4 16))
    6. (import "env" "g1" (global $g1 i32)) ;; immutable
    7. (import "env" "g2" (global $g2 (mut i32))) (;; immutable ;;)
    8. )

    导入域中需要指明模块名、导入元素名,以及导入元素的具体类型。模块名和元素名用字符串指定,双引号分隔。导入域可以附带一个标识符,这样可以在后面通过名字引入被导入的元素
    两种注释方式:
    ;; xxxxx
    (;; xxxxx ;;)
    如果某类型只被使用一次,也可以把它内联进导入域中

    1. (module
    2. (import "env" "f1"
    3. (func $f1
    4. (param i32 i32) (result i32) ;;inline function type
    5. )
    6. )
    7. )

    导出域只须指定导出名和元素索引,更好的做法是通过标识符引用元素,实际索引交给WAT编译器取计算,导出名在整个模块内必须是唯一的,下面例子为导出域的写法

    1. (module
    2. (export "f1" (func $f1))
    3. (export "f2" (func $f2))
    4. (export "t1" (table $t))
    5. (export "m1" (memory $m))
    6. (export "g1" (global $g1))
    7. (export "g2" (global $g2))
    8. )

    导入域和导出域可以内联在函数、表、内存、全局域中。
    下面为导入域内联写法

    1. (module
    2. (type $ft1 (func (param i32 i32) (result i32)))
    3. (func $f1 (import "env" "f1") (type $ft1))
    4. (table $t1 (import "env" "t") 1 8 funcref)
    5. (memory $m1 (import "env" "m") 4 16)
    6. (global $g1 (import "env" "g1") i32)
    7. (global $g2 (import "env" "g2") (mut i32))
    8. )

    下面为导出域内联写法

    1. (module
    2. (func $f (export "f1") ... )
    3. (table $t (export "t") ... )
    4. (memory $m (export "m") ... )
    5. (global $g (export "g1") ... )
    6. )

    函数域
    函数域定义函数的类型和局部变量,并给出函数的指令,WAT编译器会把函数域拆开,把类型索引存在函数段中,把局部变量信息和字节码放在代码段中、下面例子展示了函数域写法。

    (module
      (type $ft1 (func (param i32 i32) (result i32)))
      (func $add (type $ft1)
        (local i64 i64)
    
        (i64.add (local.get 2) (local.get 3)) (drop)
        (i32.add (local.get 0) (local.get 1))
      )
    )
    

    函数参数的本质上也是局部变量,同函数域里定义的局部变量一起构成了函数局部变量空间,索引从0开始递增。
    以下为内联类型定义,可有助于提高代码的可读性。

    (module
      (func $add (param $a i32) (param $b i32) (result i32)
        (local $c i64) (local $d i64)
    
        (i64.add (local.get $c) (local.get $d)) (drop)
        (i32.add (local.get $a) (local.get $b))
      )
    )
    

    表域和元素域
    模块最多只能导入或定义一张表,所以表域最多只能出现一次,但元素域可以出现很多次。表域可以出现多次,表域需要描述表的类型,包括限制和元素类型(目前只能为funcref),元素域可以指定若干个函数索引,以及第一个索引的表内偏移量。

    (module
      (func $f1) (func $f2) (func $f3)
      (table 10 20 funcref)
      (elem (offset (i32.const 5)) $f1 $f2 $f3)
    )
    

    表和内存偏移量以及全局变量的初始值是通过常量指令指定的,表域中也可以内联一个元素域,但使用这种方式无法指定表的限制,也无法指定元素的表内的偏移量(只能从0开始)。

    (module
      (func $f1) (func $f2) (func $f3)
      (table funcref
        (elem $f1 $f2 $f3)
      )
    )
    

    内存域和数据域
    和表相似,模块最多只能导入或定义一块内存,所以内存域最多也只能出现一次,数据域则可以出现多次,内存域需要描述内存的类型(即页数上下限),数据域需要指定内存的偏移量和初始数据。

    (module
      (memory 4 16)
      (data (offset (i32.const 100)) "Hello, ")
      (data (offset (i32.const 108)) "World!\n")
    )
    

    内存初始数据是以字符串形式指定的,除了普通字符,还可以使用转义序列在字符串中嵌入回车换行等特殊符号、十六进制编码的任意字节,以及Unicode代码点。
    和表域相似,内存域中也可以内联一个数据域,无法指定内存页数、也无法指定内存的偏移量(只能从0开始),另外数据域中的数据还可以写成多个字符串。
    下面例子为数据域的内联写法:

    (module
      (memory
        (data "Hello, " "World!\n")
      )
    )
    

    全局域
    全局域定义全局变量,需要描述全局变量的类型和可变性,并给定初始值。和其他元素一样,全局域也可以指定标识符,这样就可以在变量指令中使用全局变量的名字而非索引。

    (module
      (global $g1 (mut i32) (i32.const 100))
      (global $g2 (mut i32) (i32.const 100))
      (global $g3 f32 (f32.const 3.14))
      (global $g4 f64 (f64.const 2.71))
      (func
        (global.get $g1) (global.set $g2)
      )
    )
    

    起始域
    只须指定一个起始函数名或索引。

    (module
      (func $main ... )
      (start $main)
    )