TaPL読書録 #10

前回:

hkdnet.hatenablog.com

番号がやたらとんでるのは、勉強会はつつがなく開催されていたのですが僕が書くのをめんどくさがっていたら書かれなかったという回がたくさんあるからです。

今回は chap13最初-13.3まで。勉強会の slack があるのですが、TaPLチャンネルではなぜか Rust の話しかしておらず、当日の様子が全く振り返れず謎です。


いままで純粋な言語機能しかなかったが、今回からは純粋でないものを扱う。chap13では特に参照について話す。

p.120 オブジェクト の話があるが、「副作用を及ぼす関数のかたまり」として定義しているような印象を受けた

p.121 ぶらさがり参照 = dangling reference

「明示的な解放操作のもとでは、型安全性の達成が極めて困難になる」

p.122

非解釈の集合Lをストアでの位置の集合とし

ここでいう「非解釈の」は、集合の要素の並びとか型とかそういうのは気にしないぜ、くらいの意味合いで言っていると思われる。

中段

...がストアμを変更せずに返すことに注意されたい。関数適用は、それ自体は何の副作用も起こさない。

t の操作はすべて純粋な操作であり、ここでは副作用とはμに対する変更として表されている。

脚注

例えばストアについて、位置nがFloatを保持しているという事実は、位置n+4の方について有用な情報を何も与えない。」

それをプログラマに任せているという話であり、人間はよく自分の足を撃つのであった……。


次回 chap 13 終わりまで。

オフラインリアルタイムどう書く E29 の誤回答 (Ruby)

タイトルどおり爆死しました

yhpg.doorkeeper.jp

問題 : http://nabetani.sakura.ne.jp/hena/orde30sumt/
実装リンク集 : https://qiita.com/Nabetani/items/725e09cc5913a8569c04

以下考えたこと。

ざっくりいうと、最終行の必要な幅からどんどん上にいくにつれて必要な幅がたされていく。最終的に、その値がいきつくのが1行目なので、開始終了の幅の分だけとってきたら合計するだけ。

と思っていたのだけどそれでやると40ケース以上おちまして。

だめなパターンがこんな感じ。

f:id:hkdnet:20190203152545p:plain

あー、これで足すやつどうやって調べようかなーと思っていたら時間切れ。

あとこれは「面白い判定」をそんなに真面目にやってないので、試してないけど並びによってはたまたま面白い数が並んでしまう(例えば上図なら3と5のところ)と合算値が合わなくなる。残念賞ーーー!!

メモ化使いつつ計算するほうほうで再実装するか迷ってるんだけど、書いても無難になりそうなのでスキップ。久々に手ひどく負けたのでした……。

github.com

gowrtr で名前付き戻り値を扱う

先日リリースされた gowrtr が非常に便利でめちゃくちゃ助かってます。buf に自分で fmt.Fprintf とかしてたのが懐かしいです。
お気に入りポイントは、immutable なつくりになっているところと、goimports とかをかけた状態で出力できるところです。マジでいい

moznion.hatenadiary.com

特に goimports をかけてくれるおかげでパッケージの import 文とかはすげえ適当に書いておいて後からツールに削ってもらうというのが可能になります。makeファイルに後処理書かなくて済んでうれしい。

さて、そんな go-wrtr で func の戻り値を定義しようとすると *FuncSignature に対して AddReturnTypes とすることになります。

package main

import (
    "fmt"

    "github.com/moznion/gowrtr/generator"
)

func main() {
    g := generator.NewRoot()

    sig := generator.NewFuncSignature("foo")

    sig = sig.AddReturnTypes("int32", "error")

    g = g.AddStatements(
        generator.NewFunc(
            nil,
            sig,
            generator.NewReturnStatement("1, nil"),
        ),
    )

    src, err := g.Gofmt("-s").Goimports().Generate(0)
    if err != nil {
        panic(err)
    }
    fmt.Print(src)
}
$  go run tekitou.go
func foo() (int32, error) {
        return 1, nil
}

しかし自動生成するときには名前付き戻り値を使いたくなったりします。具体的にはライブラリの関数をラップする関数を書くとき、途中でエラーになったから早期リターンしたいが、各戻り値のデフォルト値を毎回書くのがダルいとかです。

// Foo() (int32, string, err) のようなシグネチャだとして
func WrapFoo() (int32, string, error) {
        err := preprocess()
        if err != nil {
                return 0, "", err // ここでデフォルト値を書くのがめんどい
        }
        return Foo()
}

func WrapFoo() (i int32, s string, err error) {
        err = preprocess()
        if err != nil {
                return // こうしたい
        }
        return Foo()
}

しかし、AddReturnTypes は string しか受け付けません。これはどうするのかというと AddReturnTypes にそのまま文字列で渡しちゃうのがよさそうです。

package main

import (
    "fmt"

    "github.com/moznion/gowrtr/generator"
)

func main() {
    g := generator.NewRoot()

    sig := generator.NewFuncSignature("foo")

    sig = sig.AddReturnTypes("i int32", "err error") // 名前も書いちゃう

    g = g.AddStatements(
        generator.NewFunc(
            nil,
            sig,
            generator.NewReturnStatement("1, nil"),
        ),
    )

    src, err := g.Gofmt("-s").Goimports().Generate(0)
    if err != nil {
        panic(err)
    }
    fmt.Print(src)
}

と、ここまで書いたところで気づいたのですが、実は多値を返さずに1つしか return しないときは生成時にエラーになっちゃいますね……。多値のときにお試しください。1


2019-01-21 00:54 ライブラリの名前が間違ってたので直しました(ハイフンとった)


  1. パッチ書いてみる予定

gocode での補完をやめるが vim-go は使う

どうも vim 界隈でも lsp の機運が高まっており gocode ではなく golsp などを使うことを推奨されているっぽい。

一方で今はもうすでに vim-go に手が馴染んでいるわけだが、と思って腰が重かったのだけど、補完部分だけを golsp にすることができた(気がする)

github.com

雰囲気で gocode とか使ってそうなところを消している。

実際書いてるとこんな感じに golsp が vim から立ち上がってるのがわかるのでまあよさそう。

$ pstree -s golsp
-+= 00001 root /sbin/launchd
 \-+= 01328 hkdnet tmux
   \-+= 29150 hkdnet -zsh
     \-+= 45875 hkdnet vim
       \--= 46034 hkdnet golsp -mode stdio

なお最近は GoLand で書いている

低レイヤを知りたい人のための Cコンパイラを Rust で作っている

hkdnet.hatenablog.com

↑の続きというわけで Rust 版を作っている

github.com

Rust 化して気づいたのは Vec なる tokens を consume していくような形にすると毎回食べ終わった Vec を返す必要がありそこそこだるいということだった。これは Vec は固定的なものであるというように捉えて index だけ返すようにしたほうがいいかもしれない。

また、Node について binary operator を考えると当然 Node<Node, Node> のようなものにならざるを得ないのだが1、この場合に Node が再帰的に定義されてしまうため、sizeof(Node) が一意に定まらないという問題があることがわかった。この場合に Rust 的には Box をもつようにして heap 領域に確保したメモリへのポインタをもつことで stack として確保すべき領域を決定的にしているということがわかった。おもしろい。確かにCで書くときも *Node みたいなメンバ宣言にするしなあ。

Rust 特有のアレコレについて悩みながらもやっている。特にCが書けるようになりたいわけでもないので2このまま Rust 版を育てるかもしれない。


  1. なお実際には NodeType にもたせている

  2. 別に読めればいい

『低レイヤを知りたい人のための Cコンパイラ作成入門』感想

低レイヤを知りたい人のための Cコンパイラ作成入門

インターネット上で公開されている範囲については実装が終わりました。

github.com

内容について

非常によかったです。わかりやすかったです。僕は技術書の場合、だいたい冒頭だけ読んで、実装してみて、本の内容と比較するという読み方をしています。この本については、ある機能を実装するにあたって、「このへんは今はやらなくていいや」と割り切るのが上手いと感じました。実装していると「これもやらなきゃじゃん」「こうなってるほうがいいよなー」みたいな気持ちがどんどん生まれますが、実際にを読んでみると「これは後ででいいです」とバッサリ。これはCコンパイラを複数回作った経験とコンピューターサイエンスに関する体系的な知識があるがゆえにできているのかなーと思っています。

このコンテンツを読む前に期待している点としては、「言語実装に関する理解が深まること」と「アセンブリ機械語を生成するフェイズの知識が得られること」の2点でした。やってみた感じとしては、前者は特に深まったなーと思っています。tokenize, parse というフェイズごとに何をしているのかという点や実際のバイナリをつくるまでの過程というのが理解できました。

一方で後者は、いまだによくわかりません。吐くアセンブリの意味を正しく理解できているかというとあやしいし……。これはどちらかというと文章としての問題ではなく、読み手としてアセンブリと仲良くなる努力がまだ足りてないのかなーと思っています。どうにかしないとなー。いい教材みたいなのがあると助かります。fizzbuzzみたいなのを自分で書いてみるか。

これから

上記のアセンブリと仲良くなるっていうのと、あとはCで書くのがダルいので他言語で実装しなおすかもしれません。あるいはもうちょい自力ですすめるか。あと解析フェイズの理解が進んだせいで TaPL の言語実装が適当すぎることに気づいたのでそっちを直したくもなっています。どうしよう。

動いているようす

こんな感じになります。ちゃんと動いてますね。中間生成物のアセンブリもチラ見せ。

$ make test
bin/hkdcc -test
OK
./test.sh
0; => 0
42; => 42
1+2; => 3
1+10+2; => 13
3-1; => 2
23-12; => 11
3 - 1; => 2
70+2*5*5; => 120
4/2; => 2
5+6*7; => 47
5*(9-6); => 15
(3+5)/2; => 4
1; 2; => 2
a = 1; => 1
a = 1; a; => 1
a = 1; a + 1; => 2
a = b = 1; a + b; => 2
1 == 1; => 1
a = 1; 1 == a; => 1
a = 2; 1 == a; => 0
a = 2; a == 1; => 0
a = 2; a == a; => 1
a = 1 == 1; a; => 1
a = 0 == 1; a; => 0
1 != 1; => 0
2 != 1; => 1
OK
$ cat tmp.s
.intel_syntax noprefix
.global _main
_main:
  push rbp
  mov rbp, rsp
  sub rsp, 208
  push 2
  push 1
  pop rax
  pop rdi
  cmp rdi, rax
  setne al
  movzx rax, al
  push rax
  pop rax
  mov rsp, rbp
  pop rbp
  ret

signal.Notify したチャンネルを close すると死ぬ(可能性がある)

TL; DR

signal.Stop しよう

https://golang.org/pkg/os/signal/#Stop

検証

ダメな例

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    close(c)

    fmt.Println("C-c plz")
    time.Sleep(10 * time.Second)
}
$ go run main.go
C-c plz
^Cpanic: send on closed channel

goroutine 5 [running]:
os/signal.process(0x10dbc60, 0xc00002aa30)
        /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal.go:227 +0x163
os/signal.loop()
        /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal_unix.go:23 +0x52
created by os/signal.init.0
        /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal_unix.go:29 +0x41
exit status 2

ある channel がクローズ済かは送信者はわからないので仕方がない。

大丈夫な例

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    signal.Stop(c) // signal.Stop を追加した
    close(c)

    fmt.Println("C-c plz")
    time.Sleep(10 * time.Second)
}
$ go run main.go
C-c plz
^Csignal: interrupt

OK

背景

graceful restart 的なことをするときに、シグナルでハンドリングをしていた。のだが、場合によっては即座に死んでほしくないパターンがあった。API通信相手がメンテ中だとわかっている場合とかは即死すると即死→メンテ→即死→メンテの無限ループって怖くね?状態になる。

しょうがないのでメンテのときはある程度待ってから死ぬようにしてみるか、と思って書いているわけだが、シグナルハンドリングが終わったあとの息が長くなるとリソースの解放漏れが気になってきた。そこでさくっと close しようとしたら、これ死ぬんじゃね?って思って調べて今に至る。

ここまで書いた後でさっさと死んで起動直後にメンテ中か確認すれば特に悩む必要なかったなって思った。おしまい。