ターゲット
同じ Emela のプログラムはどのターゲットでも同じ観測可能な結果を返さなければなりません。そのため数値と文字列の意味をターゲットから独立に定めています。
ターゲットと Tier
Section titled “ターゲットと Tier”ターゲットは JS、WASM、C の3つです。
| ターゲット | Tier | 出力 |
|---|---|---|
| JS | 1 | JavaScript のモジュール(main.mjs)とランタイム |
| WASM | 1 | WebAssembly のモジュール |
| C | 2 | C のソース。cc でコンパイルして native の実行ファイルを作る |
Tier 2 のターゲットは、意味が Tier 1 と同じで、このページの規定をすべて満たし、同じテストスイートを通すことを条件にします。新しい機能は Tier 1 より遅れて入って構いません。
emela run --target js main.emel # 既定emela run --target c main.emel # cc で実行ファイルを作って動かす--target を省くと、Pome.toml の [build].target、それもなければ js になります(CLI)。
対象のホスト
Section titled “対象のホスト”- JS のターゲットはサーバーで動くホストを前提にします。Node 18 以降と、
node:のモジュールとfetchを持つ Deno、Bun です。 - ブラウザは 0.20 の対象にしません。platform のハンドラ(プラットフォーム)は、ブラウザでの動きを規定しません。
- C のターゲットの対象は macOS(arm64)と Linux(x86_64)で、C11 のコンパイラを前提にします。cc は、環境変数
EMELA_CCがあればそれを、なければ PATH のccを使います。Windows は後の版に回します。 - 外部関数の型を C の型にどう写すかは、外部関数 の対応表にあります。
| 項目 | 規定 |
|---|---|
| Int の範囲 | -2,147,483,648 から 2,147,483,647 |
| Int64 の範囲 | -2^63 から 2^63 - 1 |
| 整数のあふれ | 2の補数で巻き戻る。defect にはならない |
| 整数の除算 | 0の方向へ切り捨てる。剰余の符号は被除数に従う |
| 整数のゼロ除算 | defect |
| Float | IEEE 754 binary64。ゼロ除算は IEEE のとおり無限大か NaN になる。% は fmod(被除数の符号) |
| 型の変換 | 暗黙には行わない。Int64.from_int のような関数で明示する |
| Float から Int への変換 | Float.floor、ceil、round、truncate で明示する。結果が Int の範囲外か、NaN、±Infinity なら defect(巻き戻しも飽和もしない)。Float.round は 0.5 を 0 から遠い方へ丸める(2.5 は 3、-2.5 は -3) |
| Int64 から Int への変換 | Int64.to_int は下位32ビットに巻き戻す |
| ビットシフト | シフト量は Int64 の版でも Int。負なら defect。型の幅(32 か 64)以上なら全部のビットを押し出した値にし、幅で剰余を取らない |
| Float の等価 | IEEE 754 の比較に従う。NaN は自分自身とも等しくなく、0.0 == -0.0 |
| 数値の表示(Show) | Float は往復できる最短の桁数で表す。有限の整数値には .0 を付け、それ以外は 1e+21、-2.5e-8、Infinity、NaN の形。-0.0 は 0.0 と表示する。Int64 に接尾辞は付けない |
fn main() { max = 2147483647 dbg(max + 1) # -2147483648 dbg(-7 / 2) # -3 dbg(-7 % 2) # -1 dbg(1.0 / 0.0) # Infinity dbg(-7.5 % 2.0) # -1.5 dbg(Float.round(2.5)) # 3 dbg(Float.round(-2.5)) # -3 dbg(Int.checked_add(max, 1)) # None dbg((0 - max - 1) / -1) # -2147483648 big: Int64 = 4294967297 dbg(Int64.to_int(big)) # 1 ()}JS と C のどちらで動かしても、コメントのとおりの値が出ます。
- あふれを検出したいときは、
Int.checked_addのような関数を使います。 - 最小値を -1 で割ったとき(
-2147483648 / -1)も、あふれとして巻き戻ります。
- 比較の関数(
Int.compare、Float.compareなど)と、その上のmin、maxは、演算子の<と違って全順序で比べます。 Float.compareは NaN を一番大きい値とし、NaN どうしは Equal、0.0と-0.0も Equal とします。minとmaxは同じ値なら第1引数を返し、NaN を伝えません(Float.min(NaN, 1.0)は1.0)。absは最小値で巻き戻って最小値を返します。
- String は Unicode 文字の列で、内部の符号化は観測できません。
- 位置を数値で指定する API は持ちません。文字列をたどるときは
String.charsやString.splitなどの関数を使います。 String.lengthは見た目の文字数(書記素クラスタの数)を返します。String.length("👍")は 1 です。- 文字の型は持ちません。1文字は長さ1の String です。コードポイントを扱うときは
String.code_points(s) -> List[Int]とString.from_code_point(n) -> Option[String]を使います。 - 書記素クラスタの境界は Unicode の版によって変わります。ターゲットの間で結果をそろえるために、使う Unicode の版を固定します。どの版にするかは未決です。
- バイト数が要るときは
String.byte_sizeを使います。UTF-8 で符号化したときのバイト数を返します。 - 文字列の大小比較はコードポイントの辞書順です。
String.split(s, "")は書記素クラスタごとに分けます(String.charsと同じ)。String.split("", "")は[]、String.split("", ",")は[""]です。String.contains、starts_with、ends_with、splitの照合はコードポイントの列で行い、書記素の境界は見ません。String.from_code_point(n)は、nが Unicode のスカラー値(0〜10FFFF でサロゲートを除く)でなければNoneを返します。String.trimが取り除く空白は、Unicode の White_Space 特性の25文字で、表は固定します(U+FEFF と U+200B は含めません)。
fn main() { dbg(String.length("👍")) # 1 dbg(String.byte_size("é")) # 2 dbg(String.code_points("é")) # [233] dbg(String.from_code_point(55296)) # None(サロゲート) dbg(String.split("", ",")) # [""]}末尾呼び出し
Section titled “末尾呼び出し”自分自身への末尾呼び出しがスタックを消費しないことを、すべてのターゲットで保証します。suspend な操作を含む関数でも同じです。それ以外の再帰の深さの上限は、実装依存です。
末尾位置は次のとおりです。
- 関数の本体のブロックの最後の式
- 末尾位置にある if の両方の分岐、match の各腕の本体、ブロックの最後の式
- 末尾位置にある
&&と||の右辺(a && bはif a { b } else { False }、a || bはif a { True } else { b }と同じ)
次の中は末尾位置ではありません。
- 無名関数の本体(自分自身への呼び出しにならない)
escapeの対象の式(escape の腕が後に走りうる)withの本体(ハンドラの release が後に走る)
相互再帰の末尾呼び出しは保証しません。escape の対象と with の本体の中の、末尾呼び出しに見える自分自身への呼び出しは、linter が「保証されない」と警告します。
fn count(n: Int, acc: Int) -> Int { if n == 0 { acc } else { count(n - 1, acc + 1) }}
fn main() { dbg(count(1_000_000, 0)) # JS でも C でも 1000000}外部関数の境界
Section titled “外部関数の境界”外部関数の型と C の型の対応は 外部関数 にあります。ここでは境界での値の扱いを並べます。
- suspend fn の外部関数が Promise でない値を返したときは、解決した値として扱います。
- JS から受け取った文字列に対になっていないサロゲートが含まれるときは、境界で置換文字(U+FFFD)に直します。
- ランタイムは suspend fn の外部関数の最後の引数に
AbortSignalを足して渡し、待っている fiber が中断されると abort します。止めないと、結果を捨てても通信が続き、Node ではプロセスが終わりません。 - suspend fn の操作の実装の中で外部関数を待っているときも、resume の前に呼び出し元が中断されれば、実装を巻き戻して abort します。
- C に渡す文字列が U+0000 を含むときは、渡す前に defect になります。
- C から受け取った String の、UTF-8 として正しくない並びは置換文字に直します。NULL が返ったら defect です。
- 整数の幅をコンパイラは推し量りません。
emela.hの形はほかの API と同じく版によって変わることがあります。
C の suspend fn の外部関数は、引数の後ろに em_task *task を受け取り、完了を後から関数で知らせます。
- 完了の関数は
em_complete_int、em_complete_int64、em_complete_float、em_complete_bool、em_complete_string(mallocで確保した NUL 終端の UTF-8 を渡す)、em_complete_unit、em_complete_handle(@external type)です。 - ホスト側の誤りは
em_complete_defect(task, message)で知らせます。待っている fiber で defect になります。 - 完了の関数はどのスレッドからも呼べます。ほかの
em_の関数は、ランタイムのスレッド(外部関数の中と、ランタイムが呼ぶコールバックの中)からだけ呼べます。 - 待ちの登録は
em_after(ms, task, callback, ctx)、em_on_readable(fd, task, callback, ctx)、em_on_writable(fd, task, callback, ctx)の形です。コールバックはランタイムのスレッドで呼ばれ、ふつうはその中で完了の関数を呼びます。 - 待っている fiber が中断された後も、C の側は完了の関数をちょうど1回呼びます(値は捨てられます)。
ターゲットの注釈の検査
Section titled “ターゲットの注釈の検査”ビルドするターゲットの注釈がない外部関数は、エントリからたどれるときだけエラーになります(E0412)。
- ハンドラを with に書くと、その init と release から呼ぶ fn がたどれる側に入ります。操作の本体から呼ぶ fn は、その操作がどこかで呼ばれるか値として使われるときだけ入ります。同梱の
SystemConsoleが JS と C の注釈しか持たなくても、使わなければ困りません。 - ライブラリだけのプロジェクトではエントリの代わりに公開関数からたどれる外部関数を検査します。
- たどれないのに出力された外部関数(公開の fn から写されたもの)に注釈がなければ、呼ばれたときに defect になります。
規定しない点
Section titled “規定しない点”次の点は実装依存で、プログラムが依存してはいけません。
- 値のメモリ上の表現と、解放のタイミング
- Trait の実装方式(辞書渡しか、型ごとの複製か)
- 継続の実装方式
- 実行可能な fiber が複数あるときの実行順序
参考: 想定する実装方式
Section titled “参考: 想定する実装方式”この節は規定ではありません。
| 項目 | JS | WASM | C |
|---|---|---|---|
| 純粋な関数 | 普通の関数 | 普通の関数 | 普通の関数 |
| fn の操作 | ハンドラを引数で渡して直接呼ぶ | 同じ | 同じ |
| suspend fn に到達する関数 | ジェネレータに変換する | ステートマシンに変換する。Stack Switching が使えるようになれば差し替える | ステートマシンに変換する(WASM と同じ低い IR から出す) |
多相な R の関数(List.map の R(f)) | 元の関数は中断しない出力のままにし、中断しうる関数を渡す呼び出しのために「中断用の写し」(List.map$suspend8)を別に出力する(特殊化) | ジェネリクスと一緒に単相化する | 同じ |
| Async | ハンドラの値を持たないので、証拠の表に載せない。R が Async だけの main とテスト関数は引数なしで呼ぶ | 同じ | 同じ |
| エラー | 戻り値として返す | 同じ | 同じ |
| defect | 専用クラスの例外 | 戻り値で明示的に伝える(例外を使わない) | 同じ |
| メモリ管理 | ホストの GC | 線形メモリ上の参照カウント(Perceus 方式) | 参照カウント(Perceus 方式) |
| ランタイム | コンパイラが出力先に書き出す | 出力するモジュールにリンクする | C のソースとして出力と一緒にコンパイルする |