Nim 仕様精読メモ (未完)
チュートリアルで出てこなかった内容メモ
DEFs
nim で runtime と言えば compile-time execution と code execution の両方を指す
compile-time にも一部の制約を除いてほぼほぼフルの nim を使えるので
データの格納される場所を location といい、 variable は location につけられるタグと考える。 location と variable にはそれぞれ型がついて、 variable につくのが静的型。 location は動的型を持っていて、これらは一致するか sub/super type の関係になる
expression は value か location を返すが、 location を返すものを特に l-value と呼ぶ
static error は syntax / semantic analysis の途中に起こったエラーを指す (macro 展開中のエラーとかは runtime error ってことだネ)
'checked' runtime error は runtime にコンパイラが発見したエラーで、これを exception で処理するか死ぬかは実装依存 ('check' を無効にする pragma もある)
'unchecked' runtime error はコンパイラが発見できないエラーで、未定義動作になる。 unsafe な操作や check の無効化をしない限りは起こらない
semantic analysis 中に値を決定できる式が constant expression で、副作用を持ったり結果が l-value になることはない。 compile-time execution の結果が semantic analysis に必要な場合もあるので、これらは並行して行われる (semantic analysis 中に適宜 compile-time execution が起動される)
LEX
nim のソースファイルは utf-8、改行コードはどれでもok
(インデント自体はトークンではなく、先頭のトークンにつく属性として解析される (そうすると先読みを1トークンで済ませられるため、らしい))
構文解析フェーズでは構文規則に特殊な終端記号を使う: IND{>} は直感的にはブロックの始まり。具体的には、パーサーが内部的に持っている「インデントの深さ」を管理するスタックの先頭より深いインデントにマッチする。これ自身もスタックにインデントを push する。 DED は直感的にはブロックの終わり。具体的には空のトークン列にマッチして、スタックを pop する。 IND{=} はスタックトップと同じ深さのインデントにマッチして、スタックを操作しない。
文字、文字列リテラルの途中以外で # を書けば確実にコメントになる
(コメントしか書かれていない行が連続した場合は、それら全体を一つのコメントとして扱う。コメントの先頭の行だけはコメント以外のトークンを含んでいても良い。改行はコメントの内容の一部として扱う)
documentation comment (##) は構文規則に含まれていて、したがって ast にも含まれる。普通のコメントと違って書ける場所には制限がある
#, ## の直後に [ を書くと、対応する ]#, ]## までがすべてコメントになる。ネストもちゃんと解析される
識別子に使えるのは文字・数字・アンスコだが、先頭は文字 (たぶんアンスコもダメ) で、アンスコを二連続以上使うこともできない。今は unicode 文字もすべて「文字」として扱い、識別子の一部に使えるが、将来的には unicode 文字の一部は「記号」として演算子用に使う可能性がある
識別子に使えない予約語が色々ある。将来の拡張用に確保されているものも含む (リスト略)。xor とか shr とかが予約語なのすげえな…と思ったけど、そうか演算子として中置するためか。ふーむ
識別子・予約語は先頭の文字だけが case-sensitive で、残りの文字は insensitive。それどころかアンスコも無視される。snake_case で書かれたライブラリと camelCase で書かれたライブラリを同時に使っても気持ちよく使えるため。これ天才だな
個々のリテラルのフォーマットにはそんなに興味ないのでちょい飛ばし
string
triple-quoted str
raw str
generalized raw str
character ... 1バイトしか入らないのでunicode文字は専用のクラスを使うこと
numerical
.=, :, :: オペレーターは予約されているので自由に使えない
*: は特別に *: として解釈されるので hoge*:int と書くことができる こんな特別ルール構文規則で吸収すれば…とか思ったけど、字句解析フェーズがかっちり分かれてるとこうせざるを得ないか
{..} は空のアノテーション {. .}ではなく { .. } になる
SYN
^ から始まるオペレーターだけが右結合で、それ以外は全て左結合
unary operator は binary より常に結合が強いが、suffix (index, .field など) よりは弱い @ から始まる unary operator は sigil-like と呼ばれて、さらに suffix よりも強い (@hoge.fuga は (@hoge).fuga)
binary operator は一文字目によって優先度が決まる ただしキーワード系 (div など) はキーワード全体で優先度が決まる
= で終わり、始まりが比較系の記号 (< や ! ) でないオペレーターは assignment operator と呼ばれ、ほかの binary operator たちより優先度が低い
->, =>, ~> で終わるオペレーターは arrow-like と呼ばれ、assignmentよりさらに優先度が低い
あるオペレーターが unary かどうかは手前の空白によっても左右される (echo $a は echo $ a にはならないが、 echo$a はその限りではない、という話っぽい)
引数リストとタプルの区別も同じく空白でお気持ち表明できる (hoge (a, b) と hoge(a, b))
構文規則ちゃんとみてみる
module は ; または同じインデントレベルの改行で区切られた stmt の列 (この時点で stmt を区切る改行は ; で相互に代用可能とわかる)
終端記号の COMMENT? はdocumentation commentがきてもいいよという意味なのかな
カンマ、セミコロン、コロンの後ろには自由にコメントを付けられるっぽい。
colcom と colon が完全に被ってるの謎だけど、とりあえずcolcomもcolonに読み替えて読み進めてみる
〜〜これ検索なしで読むのきついのであとでpcで読む〜〜
EVAL
つねに左から右、内から外に評価されるよ
代入も左辺の左辺値を先に評価するので一貫しているよ
ただし「左から右の評価」が適用されるのはノーマライズ、具体的にはテンプレートの展開とキーワード引数の並べ替え、が終わったあとなのでこれらが使われている場合は必ずしも見た目の順番と一致するわけではないよ。この方法だと実装が圧倒的に単純になるのでこうなってるよ
CONSTEXPRs
constant とは constant expression の値
constant expression とは expr の中でも以下のものだけで構成されたもの
・literals
・ builtin ops
・すでに宣言された constant, compile-time vars
・すでに宣言された macro, template
・すでに宣言された、compile-time varsを変更する以外の副作用を持たないproc
「すでに宣言された」という前置きが付くことで、上から下に一周舐めればプログラムの意味が確定して嬉しい、みたいなことが書いてあった
constant expression の中でもコードブロックが使えて、上の条件を満たす限りは手続き的な (変数を更新したりする) コードを書くこともできる
ブロックの外でも、無理やり compile-time に実行されるプログラムを書くことはできるっぽいが、もちろん推奨されてはいない
・変数に {.compileTime.} pragma をつけるとグローバル compile-time var が作れるっぽい
・static: の下にブロックを書くとコンパイル時にそこに書かれたプログラムを起動できるっぽい なんかechoとかも使えてるけど、この中では副作用も許されるんかな…?
compile-time execution (たぶん上の例の static: のとこかな…) では「現状」以下のものが使えない
・method
・closure iterators
・cast 演算子
・ref / ptr
・FFI
TYPEs
すべての式は semantic analysis 中に型が確定する
型クラスがいくつかある
・ordinal types (整数、bool、文字、enum、subrangeと、ordinal type の distinct type)
・floatingpoint types
・string type
・structured types
・ref / ptr types
・procedual type
・gennric type
この「型クラス」はhaskell的な意味ではなく単純に型のカテゴリという話っぽい。
ordinal types は離散かつ順序が定義できる。最大・最小が必ずあって、オーバーフローは (either 静的 or 動的に) 検出されてエラーになる
uint, uint64 は実装上の都合で ordinal types ではないが、今後仲間入りさせたい。uint はオーバーフローやアンダーフローでエラーにならない
int, uint はシステムのワードサイズに合った大きさの整数型、一番頻繁に使われるべき (パフォーマンス上、かな?)
定数は明記されなければ大きさによって適宜int32とかint64とか判定される
signed integer をオペランドとして unsigned 流の計算をする特殊なオペレーターがあるが、これらはunsignedに専用の型がなかった時代の言語のための後方互換なので、今は普通にunsigned型を使えば良い (たいてい後ろに % をつけると unsigned として計算するオペレーターになる、+% など)
integer types は桁が足りなくなるとよしなに widening conversion (より大きい型にキャスト) される。narrowing conversion は原則行われないが、ビット数指定なしの int はもしワードサイズを超える大きさに widening されていた場合、ワードサイズに収まる大きさになったタイミングで narrowing される (パフォーマンスのため)
subrange types は上下限がチェックされる ordinal types または floatingpoint type。base type と互いに行き来ができるが、範囲外を代入しようとするとエラー。可能なら静的にエラーを検出する
subrange に NaN を含めることはできない
subrange にしてもメモリの節約はできない
int と同様、 float のほかに精度を指定した floatXX がある。オペレーションは ieee (多分 754) 準拠と規定されている
ieee で規定されたコーナーケース 5 種類: invalid (log(-1)など), divByZero, overflow, underflow, inexact (無限小数)。今の実装では overflow / underflow 時に例外が投げられる。nanChecks, infChecks pragma で他の例外もエラーにできる。今の実装では divByZero 時も OverflowError が投げられるので注意
bool 型は 1 バイト
char 型も 1 バイト、したがってunicode char などは表現不可。この制約を利用して arraychar, int, set(char) などを最適化している unicode module に Rune 型というのがあるので、 unicode 1 文字を扱いたい場合はそちらを使う
enum 型は具体値を指定することもできるが、昇順になっていなければエラー
type Hoge = enum
foo = 1, bar = 800
ただし連番になっていない場合 (hole がある、と表現するみたい) は inc, dec, succ などが定義されず、 ordinal type ではなくなるので注意
$ したときにどう表示されて欲しいかを指定することもできる
type Hoge =
hoge = (1, "HOGE") // 値と文字列表現を指定
fuga = "FUGA" // 文字列表現を指定
piyo = 5 // 値を指定
pure pragma の意味はよくわからなかった
string は内部的にはヌル終端されているが、 len メソッドでは末尾のヌルは数えないし、添字でアクセスもできない
cstring にキャストすると末尾のヌルにも自由にアクセスできるようになる。string がもともと内部的にヌル終端されているお陰で、この変換はゼロコストでできる (ポインタを読み替えるだけ)。
$ メソッドがあらゆる型に対して定義されていて、その型のオブジェクトをいい感じの文字列に変換する。再定義もできる。echoには文字列以外が渡ってきたら勝手に$で変換する機能が備わっている。ただし言語自体にimplicit conversionが備わっているわけではないので、他の場面では明示的に変換する必要がある
文字列は添字で文字にアクセスできる
case の条件に直接文字列を書くことができる (配列はできない)
文字列にunicodeや任意のバイト列を格納することはできるが、添字アクセスはあくまで1バイトのcharなので注意。unicode 文字列をいい感じにバラしたい場合は unicode ライブラリの runes イテレーターに投げる
cstring の c は "compatible"。ターゲット言語ネイティブの文字列型だが、ターゲット言語が C とは限らないので。C backeng の場合、ヌル終端されたcharの配列として振る舞う。
cstring の添字アクセスには境界チェックがないので unsafe (未定義動作の可能性がある)。
cstring を受け取る関数に string を投げると暗黙に変換されるが、これも unsafe: nim の gc は cstring を root に登録しないので gc されてしまう可能性がある (ただしコンサバgcなのでほとんどのケースでは問題ない)。問題のあるケースに遭遇した場合は、 GC _ref, GC _unref メソッドで明示的に管理することができる
cstring も $ で nimstring に戻せる
array の要素は全て同じ型、長さは constant expression で指定する必要がある
array の添字は ordinal type なら何でも良い
seq の添字は常に 0-origin
@ が array to seq の変換なので @.. が実質 seq コンストラクタのように使える。コンストラクタを使わずに newSeq 関数で直接空の seq をアロケートすることもできる array も seq も、最小・最大の添字がそれぞれ low, high, 長さが len 関数で取れる
seq には add (&), pop で末尾に要素を追加・削除できる
array の境界チェックはコンパイラオプション --boundChecks: off で外すことができる
array コンストラクタではキーワード引数のように添字が使える。enum でもよい
[6: "hoge", "fuga"]
添字のない要素が混在する場合は、直前の添字+1に格納される
openarray は関数の引数専用の型で、任意の長さの array や seq を受け取れる。もとの array にインデックスの範囲が指定されていたとしても 0-origin になることに注意。len, low, high は通常の seq と同様に使える
実装効率の都合から openarray はネストできない
varargs は openarray に似ているが、最終引数に書くと可変長引数になる。関数の末尾の引数たちが暗黙に配列でくくられるとみなしてよい
型が揃っていなかった場合は暗黙に変換される。変換に使う関数を指定することもできる
proc hoge(args: varargs[string, $]) = ...
直接 array コンストラクタが書かれた場合はそれがそのまま渡る
hoge(["hoge", "fuga"])
"コンストラクタ" と書かれているので、変数に代入された配列とかseqは渡せないのかな。そのうち実験してみよう
型を typed と書く (varargs[typed, fn]) と、配列が渡ってきても勝手に展開せず、変換に通す。この振る舞いは、たとえば echo を実装するために必要
echo([1, 2, 3]) # echo([[1, 2, 3]]) になる
UncheckedArray[T] は境界値チェックをしない配列。C で構造体の末尾に arr[] を置く「アレ」に翻訳される。TにGC 管轄の型を書くと壊れるが、現状静的にエラーを出すことはしていない (将来的には書けるようにしたいので、書かせない方向の努力はしていない)。
アロケートの方法が登場しなかったので使い方は謎。まあ普通の用途では seq を使えばよさそうな感じはする。
タプルのコンストラクタはケツカンマok。これを使うと 1 要素のタプルとカッコで囲んだ式を区別できる: (1,)
ref object のコンストラクタを呼ぶと暗黙に system.new が呼ばれて、ヒープにメモリがとられる
of は java の instanceof のようにオブジェクトの型の検査にも使える。T = ref object of S な場合も T of S は true になる
variants は「フィールドの形式が同じなら」後から型を変えることができる
code:hoge.nim
type
Hoge = object
case kind: 1..3
of 1:
intVal: int
of 2, 3:
floatVal: float
Hoge(kind:2, floatVal: 1.0).kind = 3 # ok
フィールドの形式が変わるような mutation をしたい場合は新しい variant object で自身のメモリを上書きする
code:hoge.nim
var hoge = Hoge(kind: 1, intVal: 1)
hoge[] = Hoge(kind: 2, floatVal: 1.0)
variants のコンストラクタで各フィールドの値を初期化する場合、種類 (discriminator field, ここでは kind) は constant expression でないとエラー。ただし、次の場合はコードを解析して種類を確定させようと頑張ってくれる
・immutable な値に対する case 分岐内で、その immutable な値を kind として使っている場合 (mutable だと case 分岐が確定してからコンストラクタが呼ばれるまでの間に変更される可能性があるのでダメという話かな。immutableであること自体は静的にわかるので、kindが特定の範囲に収まることも静的にわかる)
code:hoge.nim
let hogeKind = pickAKind() # immutable
var obj : Hoge
case hogeKind
of 2..3:
# hogeKind は 2 か 3 に決まっている
obj = Hoge(kind: hogeKind, floatVal: 1.0)
else:
discard
・subrange 型の値を kind に使っていて、特定の範囲内に収まることが型レベルで保証できる場合
code:hoge.nim
let
hogeKind : range2..3 = pickAKind() var
obj = Hoge(kind: hogeKind, floatVal: 1.0)
これめちゃ賢いなあ
set は cint と相互に変換できる
code:hoge.nim
type
Flag {.size: sizeof(cint).} = enum
Hoge, Fuga, Piyo
echo castcint({ Hoge, Fuga }) # => 5 size pragma は enum の範囲が cint のビット数に収まることを保証するのかな。説明がなかったので謎