nom

wasm-interp

前回やったWABTが供給してる、wasm-interpの使いかたが解らなかったので調べてみた。 名前からインタープリタって想像が付くんだけど、定義した関数を実行する方法が不明だったのだ。普通はWEBで実行しちゃうんで、良きに図らえって亊で、資料がなかなか見付からず。

あちこちを漁っていたら、 WABT: The WebAssembly Binary Toolkitを使ってみる こういうヒントが出てた。

(module
 (func $fib (param $n i32) (result i32)
       (if (i32.lt_s (local.get $n) (i32.const 2))
           (return (local.get $n)))
       (i32.add
        (call $fib (i32.sub (local.get $n) (i32.const 1)))
        (call $fib (i32.sub (local.get $n) (i32.const 2))) ))

;; (export "fib" (func $fib))
 (func (export "run") (result i32)
       i32.const 10
       call $fib)
)

exportしたものが連られて実行されるみたい。だから、runって名前で外に放出しつつ、実体は、引数をスタックに積んで関数をコール。こんなトリック思いつかないよ。

少しschemeを扱っている気分を出すために、このfib.watをfib.scmにリンクしとく。そして、fib.scmをemacsから呼べば、emacsはscheme-modeになる。

ついでに、Makefileも作っておく。そうすれば編集してから実行のmakeで代用出来る。

run:
        wat2wasm fib.wat
        wasm-interp fib.wasm --run-all-exports

コンパイルしてから、実行。実体はexportsしてねだ。

-*- mode: compilation; default-directory: "/tmp/t/" -*-
Compilation started at Wed Mar  10 07:31:02

make -k
wat2wasm fib.wat
wasm-interp fib.wasm --run-all-exports
run() => i32:55

外部に見せる名前を先程はrunにしたけど、今度はそれっぽくmainにして、スタックに積む数を25に変更。したら、main() => i32:75025 になった。

それから、多少の知見として、コメントは ; はエラーになる。;; はOK。手続名前($fib)とか引数の$nは、$を前置するのが約束っぽい。忘れると、そんなの知らんと文句を言われる。

lrwxrwxrwx 1 sakae sakae   7 Mar 10 06:07 fib.scm -> fib.wat
-rw-r--r-- 1 sakae sakae  74 Mar 10 07:39 fib.wasm
-rw-r--r-- 1 sakae sakae 358 Mar 10 07:39 fib.wat
-rw-r--r-- 1 sakae sakae  63 Mar 10 07:02 Makefile

fib.wasmは極小で小気味がよい。前回やったrustから作るのは、1M以上のサイズでげんなり。

小さいなりにもexeみたいに、一人前の構造を持っている。曰く最初はヘッダーだ。

sakae@deb:/tmp/t$ wasm-objdump -h fib.wasm

fib.wasm:       file format wasm 0x1

Sections:

     Type start=0x0000000a end=0x00000014 (size=0x0000000a) count: 2
 Function start=0x00000016 end=0x00000019 (size=0x00000003) count: 2
   Export start=0x0000001b end=0x00000023 (size=0x00000008) count: 1
     Code start=0x00000025 end=0x0000004a (size=0x00000025) count: 2

そして、逆アセンブル。

sakae@deb:/tmp/t$ wasm-objdump -d fib.wasm

fib.wasm:       file format wasm 0x1

Code Disassembly:

000027 func[0]:
 000028: 20 00                      | local.get 0
 00002a: 41 02                      | i32.const 2
 00002c: 48                         | i32.lt_s
 00002d: 04 40                      | if
 00002f: 20 00                      |   local.get 0
 000031: 0f                         |   return
 000032: 0b                         | end
 000033: 20 00                      | local.get 0
 000035: 41 01                      | i32.const 1
 000037: 6b                         | i32.sub
 000038: 10 00                      | call 0
 00003a: 20 00                      | local.get 0
 00003c: 41 02                      | i32.const 2
 00003e: 6b                         | i32.sub
 00003f: 10 00                      | call 0
 000041: 6a                         | i32.add
 000042: 0b                         | end
000044 func[1] <main>:
 000045: 41 14                      | i32.const 20
 000047: 10 00                      | call 0
 000049: 0b                         | end

もっと人間が読めるように、ならば

sakae@deb:/tmp/t$ wasm-decompile fib.wasm
function f_a(a:int):int {
  if (a < 2) { return a }
  return f_a(a - 1) + f_a(a - 2);
}

export function main():int {
  return f_a(20)
}

これって、javascript語なのかな。きっとそうなのだろう。WEB屋さんの常識だもの。ああ、WEB屋さんと言えども裸のjavascriptは使わないで、ラッパーを被せているのか。 【2022年最新】JavaScriptのフレームワーク6選を初心者向けに比較!

でも、逆アセしたコードを見ると、オイラーにはforthに見えるんだけど、どうよ。

sakae@deb:/tmp/t$ gforth
Gforth 0.7.3, Copyright (C) 1995-2008 Free Software Foundation, Inc.
Gforth comes with ABSOLUTELY NO WARRANTY; for details type `license'
Type `bye' to exit
10 3 - . 7  ok

そういう眼でみると、 (i32.sub (local.get $n) (i32.const 1)) なんてのは、まどろっこしいな。まあ、forth言語をS式に置き換えると、こうなります、って亊だね。

paser

markdownからhtmlへの変換器を調べていた時、nomって言う木箱を眼にしてたんだ。どんなものかREADME.mdを見ると、nom is a parser combinators library written in Rust. こんな説明があったので、敬遠してたのさ。

変換器の核になるぐらいだから、きっと凄い物なんだろう。

unix 方面

変換と言ったら、sed,perl,ruby,pythonと色々な言語が利用出来る。その核心は正規表現だ。それ以外だと、ずっと難しくなり、yaccとかbisonに(f)lexを組合せるのが標準みたい。

昔rubyってどうなってるのと調べていて、bisonとflexを入れてからコンパイルするんだなんて亊をやってたな。先月出たやつで調べてみると、鬼のようだった。

sakae@deb:/tmp$ wc ruby-3.1.1/parse.y
 14052  40142 342886 ruby-3.1.1/parse.y

bisonとflexで自作パーサーを作る

言語を作る!bisonとflexを使ってみた

haskell 方面

度々登場してる、パーサー・コンビネーターって何よ?

Parser Combinator とは、Parser を入力として、新しい Parser を出力するような高階関数のこと。複数の単純な Parser を組み合わせて、複雑な Parser を構築する。この文脈では、Parser とは、文字列を入力として受け取り、何か構造 (parse tree やパースの終端位置等) を出力する。

あどけない話 for haskell

Haskell 構文解析 超入門

rust 方面

rust方面ではどうなってるか、調べてみる。

Parser tooling

nomってのが一番人気、続いてはcombineってのが人気みたい。combileはhaskellのparsecに影響を受けたってはっきり言ってるから、恐れをなして、二番手なのかな。

rust nom

まあ、長いものに巻かれろってのもあるし、ぼんくらなオイラーでも見付けだせたので、少しやっかいになってみる。例によって資料探し。

Rust: nom によるパーサー実装 nom=5

Parser combinator nom 入門 good

Rust製のパーサコンビネータnom v6.0.0を解剖する

Nom Tutorial for linux mount

中には、全て自前で何とかしちゃう猛者な方もおられるぞ。 Rust でつくるインタプリタ

例題の電卓

前回見付ておいた RustとNomで電卓を作る をトレースしてみる。習うより慣れろですから。

   Compiling lexical-core v0.4.3
error[E0308]: mismatched types
   --> /home/sakae/.cargo/registry/src/github.com-1ecc6299db9ec823/lexical-core-
0.4.3/src/atof/algorithm/bigcomp.rs:242:55
    |
242 |     let nlz = den.leading_zeros().wrapping_sub(wlz) & (u32::BITS - 1);
    |                                                       ^^^^^^^^^^^^^^^ expe
cted `usize`, found `u32`
  :

gitで取ってきたものをコンパイルすると、早速のエラー。でメッセージをみると、nomを動かすための木箱の所でエラー。依存が過ぎると、こういう亊が平気で発生するんだね。

エラー内容は、オイラーも経験してるからよく分る。近頃のrustは、ひたすら、ご安全にって方向を目指しているな。昔は警告と言う緩いやつだったぞ。世知辛い世の中になったものだ。

sakae@pen:~/rust-calc$ cargo tree
rust_calc v0.1.0 (/home/sakae/rust-calc)
└── nom v5.0.0
    ├── lexical-core v0.4.3
    │   ├── cfg-if v0.1.9
    │   ├── ryu v1.0.0
    │   ├── stackvector v1.0.6
    │   │   └── unreachable v1.0.0
    │   │       └── void v1.0.2
    │   │   [build-dependencies]
    │   │   └── rustc_version v0.2.3
    │   │       └── semver v0.9.0
    │   │           └── semver-parser v0.7.0
    │   └── static_assertions v0.3.4
    │   [build-dependencies]
    │   └── rustc_version v0.2.3 (*)
    └── memchr v2.2.1
    [build-dependencies]
    └── version_check v0.1.5

Cargo.tomlには、nom="5"と指定されたけど、これの版数をあげてみる。 nomの版数来歴を調べて、ゴハンの一番新しいやつにした。

sakae@pen:~/rust-calc$ cargo tree
rust_calc v0.1.0 (/home/sakae/rust-calc)
└── nom v5.1.2
    ├── lexical-core v0.7.6
    │   ├── arrayvec v0.5.2
    │   ├── bitflags v1.3.2
    │   ├── cfg-if v1.0.0
    │   ├── ryu v1.0.0
    │   └── static_assertions v1.1.0
    └── memchr v2.2.1
    [build-dependencies]
    └── version_check v0.9.4

nomが依存してるlexical-coreが整理整頓されたって亊だね。この依存木箱は勝手にアップデートされるんで、常に爆弾を抱えているって亊になる。

1+3
ok:4
3 * 4
ok:3

構文エラー

構文エラー
3*4
ok:12

ちょっと実行してみた。後は、解説を読んで理解せいとな。

じゃ、nomを最新にしたらどうよ

sakae@pen:/tmp/rust-calc$ cargo tree
rust_calc v0.1.0 (/tmp/rust-calc)
└── nom v7.1.0
    ├── memchr v2.4.1
    └── minimal-lexical v0.2.1
    [build-dependencies]
    └── version_check v0.9.4

エラーになった。nomの仕様が変ったんだね。センシティブなんだな。怖いな。でも、依存を減らそうと頑張っているね。

   Compiling rust_calc v0.1.0 (/tmp/rust-calc)
error[E0308]: mismatched types
  --> src/calc.rs:9:5
   |
8  |   pub fn expr_eval(s: &str) -> Result<i32, Err<(&str, ErrorKind)>> {
   |                                ----------------------------------- expected `Result<i32, nom::Err<(&str, nom::error::ErrorKind)>>` because of return type
9  | /     parser::expr_parser(s)
10 | |         .map(|(_, expr)| expr.eval())
   | |_____________________________________^ expected tuple, found struct `nom::error::Error`
   |
   = note: expected enum `Result<_, nom::Err<(&str, nom::error::ErrorKind)>>`
              found enum `Result<_, nom::Err<nom::error::Error<&str>>>`

便利さの裏には不便が隠れている。

キャッシュレスって騒いでいるけど、停電したりネットが落るだけで買物も出来無くなる。いつもにこにこ現金払いがオイラーには安心です。

もう一つ例があった。Rustのパーサコンビネータライブラリnom 5.0を使ってみた nom のシャワーを沢山浴びよう。

nom example

nomをgitから取り寄せると、exampleが付いてくる。 それにどんな版数がサポートされてるか容易に確認出来る。

sakae@pen:~/nom$ git tag
  :
5.0.0
5.0.0-alpha1
5.0.0-alpha2
5.0.0-beta1
5.0.0-beta2
5.0.0-beta3
5.0.1
5.1.0
5.1.1
5.1.2
6.0.0
 :

CHANGELOG.mdを見るのも良い亊。5版数になって大幅に改善、6版数でも7版でも同様って亊で、まるでLinuxみたいな進行状況。追従するのは大変。まあ、それだけ改善が施されているって亊だとポジテブに理解してあげよう。

で、exampleは 開発者自らが用意した、宣伝材料だ。多分に、nomは凄いだろうって亊をアッピールする例になってるはず。これは見ない訳にはいかない。

毎日携帯に屆く宣伝メールは、頑無視してもいいけどね。携帯乗り換えて、1年になるけど、メアドを知ってるのは携帯会社だけ。そこから屆くメールなんてCMだけだろ。

exampleに美味しそうな、 s_expression.rs なんてのが掲載されてた。これはもう馴染なんで試してみる鹿。場合によっては変更もしてみたいので、これを取出してきて実験環境を作る。

ob$ cargo new se
     Created binary (application) `se` package
ob$ cd se
ob$ cp /home/sakae/nom/examples/s_expression.rs  src/main.rs
ob$ vi Cargo.toml     ;; add   nom = "7.1.0"
ob$ cargo b
   Compiling se v0.1.0 (/tmp/se)
error[E0601]: `main` function not found in crate `se`
   --> src/main.rs:1:1
    |
1   | / //! In this example we build an [S-expression](https://en.wikipedia.org/
wiki/S-expression)
2   | | //! parser and tiny [lisp](https://en.wikipedia.org/wiki/Lisp_(programmi
ng_language)) interpreter.
3   | | //! Lisp is a simple type of language made up of Atoms and Lists, formin
g easily parsable trees.
4   | |
...   |
377 | |   );
378 | | }
    | |_^ consider adding a `main` function to `src/main.rs`

For more information about this error, try `rustc --explain E0601`.
error: could not compile `se` due to previous error

378行目ってファイルの最後。そこまでスキャンしてもmainが見付からない。目視すると、ちゃんと定義されてるんだけどな。なんてこったい。動かない例を載せておいて、修整して下さいって?

昔rubyを盛んにいじっていた時、こういうエラーに遭遇したぞ。ファイルの最後まで調べたけど、有効な文が検出出来ませんでしたって奴ね。

大体、こういうエラーは、文字列を囲むダブルクォートが閉じていなくて、ひたすら文字列と思っているうちにEOFになりました。とか、関数の括弧が閉じていないとかの不注意が原因。

ファイルをひたすら切り詰めて、不注意の場所を探したものだ。今はもっとスマートな方法って有るのかな?

ひたすら切り刻んでみたら。冒頭付近にあるプラグマが邪魔してた。セメントで固めたじゃなくて、コメントで封じ込めたよ。

//  #![cfg(feature = "alloc")]

use nom::{
  branch::alt,
   :

これで動いた。

ob$ cargo r
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/se`
"((if (= (+ 3 (/ 9 3))
         (* 2 3))
     *
     /)
  456 123)"
evaled gives us: Ok(Constant(Num(56088)))

S式をパースして、実行してるんだね。超ミニなschemeを示してくれている。

ifのtest句が#t,#fかによって、かけ算するか割り算するか決定。で、今の場合なら (* 456 123)が全体の結果ですって亊。素晴しい。

で、頂けないのは、mainを見つけ出せないエラー。もしやと思ってCargo.tomlを見たら

[[example]]
name = "s_expression"
path = "examples/s_expression.rs"
required-features = ["alloc"]

正しい使いかたが示されていた。野生児はこれだから困るな。

combine

hoge v0.1.0 (/tmp/hoge)
└── combine v4.6.3
    ├── bytes v1.1.0
    └── memchr v2.4.1
extern crate combine;
use combine::parser::char::{letter, space};
use combine::{many1, sep_by, Parser};

fn main() {
    let word = many1(letter());

    let mut parser = sep_by(word, space())
                   .map(|mut words: Vec<String>| words.pop());
    let result = parser.parse("Pick-up that word!");
    println!("{:?}", result);
}

wordってのは、文字が繋ったものですって定義から始まるんだな。そして、そのワードはスペースで区切られていますって言う解析器械を組み立てる。後は解析機械に依頼。

Ok((Some("Pick"), "-up that word!"))

左側は、それを満したもの。右側は、つっかかった所って亊か。


This year's Index

Home