Skip to content

ターゲット

同じ Emela のプログラムはどのターゲットでも同じ観測可能な結果を返さなければなりません。そのため数値と文字列の意味をターゲットから独立に定めています。

ターゲットは JS、WASM、C の3つです。

ターゲットTier出力
JS1JavaScript のモジュール(main.mjs)とランタイム
WASM1WebAssembly のモジュール
C2C のソース。cc でコンパイルして native の実行ファイルを作る

Tier 2 のターゲットは、意味が Tier 1 と同じで、このページの規定をすべて満たし、同じテストスイートを通すことを条件にします。新しい機能は Tier 1 より遅れて入って構いません。

Terminal window
emela run --target js main.emel # 既定
emela run --target c main.emel # cc で実行ファイルを作って動かす

--target を省くと、Pome.toml の [build].target、それもなければ js になります(CLI)。

  • 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
FloatIEEE 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("", ",")) # [""]
}

自分自身への末尾呼び出しがスタックを消費しないことを、すべてのターゲットで保証します。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
}

外部関数の型と 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回呼びます(値は捨てられます)。

ビルドするターゲットの注釈がない外部関数は、エントリからたどれるときだけエラーになります(E0412)。

  • ハンドラを with に書くと、その init と release から呼ぶ fn がたどれる側に入ります。操作の本体から呼ぶ fn は、その操作がどこかで呼ばれるか値として使われるときだけ入ります。同梱の SystemConsole が JS と C の注釈しか持たなくても、使わなければ困りません。
  • ライブラリだけのプロジェクトではエントリの代わりに公開関数からたどれる外部関数を検査します。
  • たどれないのに出力された外部関数(公開の fn から写されたもの)に注釈がなければ、呼ばれたときに defect になります。

次の点は実装依存で、プログラムが依存してはいけません。

  • 値のメモリ上の表現と、解放のタイミング
  • Trait の実装方式(辞書渡しか、型ごとの複製か)
  • 継続の実装方式
  • 実行可能な fiber が複数あるときの実行順序

この節は規定ではありません。

項目JSWASMC
純粋な関数普通の関数普通の関数普通の関数
fn の操作ハンドラを引数で渡して直接呼ぶ同じ同じ
suspend fn に到達する関数ジェネレータに変換するステートマシンに変換する。Stack Switching が使えるようになれば差し替えるステートマシンに変換する(WASM と同じ低い IR から出す)
多相な R の関数(List.map の R(f))元の関数は中断しない出力のままにし、中断しうる関数を渡す呼び出しのために「中断用の写し」(List.map$suspend8)を別に出力する(特殊化)ジェネリクスと一緒に単相化する同じ
Asyncハンドラの値を持たないので、証拠の表に載せない。R が Async だけの main とテスト関数は引数なしで呼ぶ同じ同じ
エラー戻り値として返す同じ同じ
defect専用クラスの例外戻り値で明示的に伝える(例外を使わない)同じ
メモリ管理ホストの GC線形メモリ上の参照カウント(Perceus 方式)参照カウント(Perceus 方式)
ランタイムコンパイラが出力先に書き出す出力するモジュールにリンクするC のソースとして出力と一緒にコンパイルする