カナデKanade

3. 実行機(Interpreter)とランタイム

カナデ(Kanade) 言語の実行機(Interpreter)の動作とランタイムエラーの仕様です。

このドキュメントは、カナデ(Kanade)のランタイム(インタープリタ、VM、コンパイラのバックエンドなど)を実装したい開発者のための、言語に依存しないアーキテクチャと動作の仕様書です。カナデはハリ(Hari)と同じASTを使うので、ランタイムの規則もハリとまったく同じです。どの言語で実装するとしても、この仕様の規則を厳密に守ってください。

1. ランタイムのアーキテクチャと環境 (Environment)

ランタイムはASTをたどりながら状態を変えていきます。変数の状態と有効範囲はEnvironmentというデータ構造で管理します。

1.1 レキシカルスコープとクロージャ (Lexical Scoping & Closures)

  • クロージャの連鎖: ブロック、関数、ループに入るときは、現在のEnvironmentを親(parent)に持つ新しい子Environmentを作ってください。
  • 新しい範囲を開く場所: 関数・メソッドの呼び出し、繰り返し文(繰り返すたびに新しい範囲)、エラー処理のブロック(発生したら)が新しい範囲を開きます。もしとによって分けようのブロックは新しい範囲を開かないので、その中で最初に作った変数はブロックが終わったあとも見えます。繰り返しの中で最初に作った変数は、次の繰り返しや繰り返しが終わったあとにはありません。繰り返しのたびに新しく宣言するので、固定しようも毎回宣言し直せます。繰り返しの変数(1から3まで繰り返そう(『か』)の『か』、ごとに繰り返そうの項目)とエラー処理のブロックのエラー変数もその新しい範囲に宣言されるので、外に同じ名前の変数があればそれを変えずに隠し、範囲が終わると外のものがそのまま戻ります(外のものが定数でも、繰り返しの変数は定数ではありません)。範囲の繰り返しの始まりと終わりは、繰り返しの変数を作る前に計算します。トリーウォーク、バイトコード、ブラウザエンジンのすべてが同じ動きです。
  • 第一級オブジェクトの関数 (First-class Function): 関数は実行時に呼び出せる(Invokable)オブジェクトとして評価され、変数に入れたり引数として渡したりできます。このとき関数オブジェクトは、自分が定義された時点のEnvironmentを捕捉(Capture)して、クロージャを作らなければなりません。
  • 特殊な識別子外(OuterReference): 『外の名前』の形で書かれた場合、ランタイムは現在の環境(スコープ)を除き、親の環境から一番近い宣言を探して、その変数を参照したり変更したりしなければなりません。

1.2 関数の引数 (Parameters) とデフォルト値の評価

関数を呼び出すとき、ランタイムは次のように引数(Arguments)を引数用の環境変数に結び付けなければなりません。

  • 引数の渡し方 (Call by Object Reference): 関数に引数を渡すとき、ランタイムはオブジェクトの完全なコピーではなく、オブジェクトのメモリ参照(Reference)を引数用の環境変数に結び付けます。
    • 変更できる(Mutable)オブジェクトであるリストや辞書を引数として受け取り、関数の中でその属性を変更(『引数』の1番目を「値」にしよう)すると、元のオブジェクトも一緒に変わります。
    • 一方、文字列や数字のような変更できない(Immutable)オブジェクトは、属性を変更できません。そのため、引数の変数そのものに新しい値を再代入(『引数』を「新しい値」にしよう)しても、変わるのはローカル環境の結び付きだけで、外側の元の変数にはまったく影響しません。
  • 引数が足りないとき: 省略された引数のうちdefaultノード(式)を持つものは、そのデフォルト値を評価して埋めてください。
  • 必須の引数がないエラー: 省略された引数にdefaultフィールドがない(必須の引数)場合、ランタイムは引数を黙ってNullで埋めず、すぐにMissingArgumentError(またはArgumentError)を発生させなければなりません。
  • 引数が多すぎるエラー (Extra Arguments): 渡された引数の数が宣言された引数の数より多い場合、余った引数を黙って無視してはならず、すぐにArgumentError(またはTooManyArgumentsError)を発生させて厳密に守ってください。

2. 厳密な妥当性の検証とタイプの検査 (Strict Validations)

2.1 定数の不変性の検証 (Constant Enforcement)

  • 変数を宣言するとき、ASTのisConst: true(固定しよう)なら、ランタイムはその環境のマップに識別子を「定数」として記録しなければなりません。
  • 状態を変えるすべての操作: VariableDeclaration(ただし、環境にすでに識別子がある再代入の場合)だけでなく、CompoundAssignment(複合代入)、ListPushStatement(リストへの追加)など、変数やオブジェクトの値を変えるすべての操作ノードを評価するとき、対象の変数が定数として記録されていればConstantAssignmentErrorを発生させなければなりません。

2.2 動的なタイプの検査 (Runtime Type Checking)

  • 変数の宣言と再代入の単一の評価: パーサーは、変数の宣言と再代入をどちらもVariableDeclarationノードにまとめて生成します。ランタイムはこのノードを評価するとき、その識別子が現在のスコープ(環境)にすでにあるかを調べなければなりません。なければ新しい変数として宣言し、あれば既存の変数の値を更新(再代入)してください。
  • 宣言と状態変更の検査: 変数への代入時にtypeAnnotationがあれば、入ってくるvalueの実際の実行時のタイプを検査しなければなりません。変数が一度あるタイプで宣言されたら、再代入や複合代入(CompoundAssignment)で状態を変えるときにも、最初に宣言されたタイプの制約が守られ続けなければなりません。
  • ジェネリクス (Generics) のタイプ検査: typeAnnotationにジェネリクスの引数(例: 【(数字)リスト】)が含まれていれば、ランタイムはその変数に値が追加されたり代入されたりするときに、中の要素のタイプまで再帰的に検査(Type Reification)して安全性を保証しなければなりません。違反したときはTypeErrorを発生させてください。
  • Null(空っぽ)の例外的な許可: カナデのすべてのタイプは、既定でNullを許可する(Nullable-by-default)考え方です。そのため、あるタイプで厳密に宣言された変数でも、再代入されるvalueがNull(空っぽ)なら、タイプ検査は互換性があるものとして必ず通してください。
  • 関数の戻り値のタイプの検査: 関数の宣言に戻り値のタイプ(FunctionDeclaration.returnType、【数字】を返す)があれば、ランタイムは関数が返した値をそのタイプで検査し、合わなければTypeErrorを発生させなければなりません。戻り値のタイプは必須ではなく、省略すればどんな値でも返せます。Null(何も返さずに終わった関数を含む)はどのタイプでも通ります。
  • 【何でも】(Any Type): タイプが省略された場合や【何でも】と指定された場合、実行時のタイプ検査はすべての制約を無視して通してかまいません。
  • 暗黙の型変換の禁止 (No Implicit Casting): 文字列と数字を足す(「1」 + 2)などの演算で、ランタイムは暗黙の型変換を決して行ってはならず、すぐにTypeErrorを投げなければなりません。型変換は、明示的な組み込み関数(〈数字に〉()、〈文字列に〉())でだけ行われるべきです。
    • 例外(InputStatement): ただし、『変数』を【タイプ】で入力してもらおうの構文では、【タイプ】でそのものが明示的な型変換(Explicit Cast)の命令とみなされます。したがって、標準入力(文字列)を指定されたタイプに変換しても、暗黙の型変換の禁止の規則には反しません。

2.3 Nullableと既定の戻り値

  • 宣言だけの変数 (Nullable): isDeclarationOnly: true(準備しよう)で宣言された変数は、実行時のメモリにすぐNull(空っぽ)の値で初期化されなければなりません。つまり、カナデのすべてのタイプは、既定でNullを許可します(Nullable)。
  • 既定の戻り値 (Default Return): 関数やメソッドがReturnStatement(返そう)に出会わずにブロックの実行を終えたら、ランタイムは呼び出し元に自動的にNullを返さなければなりません。
  • トップレベルの戻り (Top-level Return): 関数の外(プログラムのトップレベル)でReturnStatementに出会ったら、プログラムはその場でエラーなしに終了しなければなりません(値は捨てます)。囲んでいる最後はいつもブロックは、終了する前に実行されます。インポートしたファイルのトップレベルなら、そのファイルのトップレベルのコードだけが終わり、ファイルは読み込まれたものとして扱います。

2.4 Null参照の安全性 (Null Safety)

  • 実行時の評価中にNull(空っぽ)の値のプロパティやインデックスにアクセスしようとしたり(MemberExpression)、関数として呼び出そうとしたり(CallExpression)した場合、ランタイムはすぐにNullReferenceErrorを発生させなければなりません。
  • 演算子のNull安全性と例外: CompoundAssignment、ListPushStatement、そしてBinaryExpression(算術、大小比較など)のオペランドのうち1つでもNullなら、ランタイムはすぐにNullReferenceError(またはTypeError)を発生させて、システムのクラッシュを防いでください。
    • ただし、等価比較(同じだ、違う)は例外です。Nullガード(Null Guard)のパターンを実現するため、等価比較の演算子を実行するとき、片方のオペランドがNullでもエラーを起こさず、普通に真偽値(真/偽)を評価して返さなければなりません。

3. オブジェクト指向システム (OOP) の詳細

3.1 インスタンス化とコンストラクタの規則 (Instantiation & Constructors)

  1. 抽象タイプの直接の生成の禁止: NewExpressionの対象がインターフェース(規定しよう)や抽象クラス(下設計しよう)の場合、インスタンス化できないので、すぐにInstantiationErrorを発生させなければなりません。
  2. メモリの割り当て: 有効なクラスのNewExpressionを評価するとき、独立したインスタンスオブジェクトを作ってください。
  3. 親の属性の継承: baseClass(親クラス)があれば、親の属性とメソッドを現在のオブジェクトにコピーするか、継承の連鎖(MRO)につなげなければなりません。
  4. コンストラクタの連鎖呼び出しの規則:
    • 子クラスにコンストラクタ(最初に作られる時)が宣言されていなければ、ランタイムは親クラスのコンストラクタを自動的に探して呼び出さなければなりません。
    • 子クラスでコンストラクタがオーバーライド(上書き)されている場合、ランタイムは親のコンストラクタを自動では呼び出しません。開発者が明示的に親の〈最初に作られる時〉()を呼び出す必要があります。

3.2 アクセス制御の検証 (Access Modifier Check)

  • private(隠そう): オブジェクトの外からの呼び出し(CallExpression / MemberExpression)を厳密に防ぎます。そのオブジェクトのクラスの中で定義されたメソッドから私の〈メソッド〉として呼び出すときだけ許可されます。(違反したときはAccessViolationErrorを発生させてください。)
  • protected(譲ろう): そのオブジェクトのクラスと、継承した子クラスの中でだけ呼び出しを許可します。

3.3 特殊な結び付き私(this)、親(super)、私たち(static)

  • 私(this): インスタンスメソッドを実行するときに作られるローカルなEnvironmentには、常に呼び出したインスタンスオブジェクトが私として結び付けられていなければなりません。
  • 親(super): SuperReferenceを評価するときは、私のオブジェクトの直接の親クラスの文脈を返して、オーバーライドされる前の元のメソッドを呼び出せるようにしてください。
  • 私たち(static): ASTでisStatic: trueと宣言されたメンバーは、インスタンスではなくクラスそのものに属します。クラスの中では『私たち』のキーワードでアクセスし、外ではクラス名で直接アクセスします。静的メソッドの中ではインスタンス(私)を参照できず、試みた場合はStaticContextErrorを投げなければなりません。

3.4 アップキャスト (Upcasting) とインターフェース (Interface)

  • Upcasting: 子のオブジェクトを親のタイプの引数や変数に代入することは、常に安全な互換とみなされ、タイプ検査を通らなければなりません。
  • InterfaceとAbstractの検証: インターフェースは、実行時にインスタンス化できない純粋な規約です。
    • インターフェースのタイプを引数に受け取る関数にオブジェクトを渡すとき、ランタイムはそのオブジェクトのクラスがそのインターフェースに従う(実装する)と宣言しているかを検証しなければなりません。
    • クラスの宣言の段階(評価の段階)で、インターフェースに従うと明記したクラスがインターフェースの必須メソッドの原型をすべて実装していなければ、ランタイム(またはコンパイラ)はすぐにInterfaceImplementationErrorを投げなければなりません。(抽象クラスを継承したときの、未実装の抽象メソッドの検証も同じように扱ってください。)

3.5 発展的なオブジェクト指向の機能

  • 抽象クラス (Abstract Classes): isAbstract: true(下設計しよう)で宣言されたクラスはインスタンス化できず、中に本体のない抽象メソッド(〈メソッド〉がなければならない ())を含められます。継承した子クラスは、必ずこの抽象メソッドをオーバーライドしなければなりません。
  • オーバーロードなし (No Overloading): 関数の名前は一つの場所(ファイルの一番外側、一つの関数の本体、一つのクラス)に一つだけ宣言できます。同じ場所に同じ名前の関数・メソッドをもう一度宣言すると、引数の数が違ってもパーサーがSyntaxError(SyntaxError.DuplicateFunction)を報告し、プログラムは実行されません。コンストラクタ(最初に作られる時)もクラスごとに一つで、二つ目はSyntaxError.DuplicateConstructorです。クラスの中では『私たち』のメソッド(静的)とオブジェクトのメソッドは別々に数えるので、同じ名前を使えます。子クラスが親のメソッドを同じ名前で宣言し直すオーバーライドは別の場所の宣言なので許可されます。いろいろな形の入力が必要なときは、デフォルト値のある引数か別の名前を使ってください。
  • 演算子のオーバーロード (Operator Overloading): クラスの中にマジックメソッドを宣言して、演算子の意味を決められます。左辺が先に決め、左辺が決めない算術演算だけ右辺に聞きます(下の「右から版」)。
    • 算術・大小比較: + 〈記号 足す〉、- 〈記号 引く〉、* 〈記号 掛ける〉、/ 〈記号 割る〉、% 〈記号 余り〉、> 〈記号 大きい〉、< 〈記号 小さい〉、>= 〈記号 以上〉、<= 〈記号 以下〉。左辺がオブジェクトで、そのクラス(親から受け継いだものも含む)にそのメソッドがあれば、ランタイムは基本の演算の代わりにAの〈記号 足す〉(B)を呼んだのとまったく同じように実行し、その戻り値を結果にします。普通のメソッド呼び出しなので、引数・戻り値の型の検査と呼び出しの深さの制限(RecursionError)がそのまま適用され、右辺が空っぽでもメソッドが受け取ります。『x』にBを足そう/『x』からBを引こうも同じ+/-で、大小比較が条件(もし、間繰り返そう)になるときは戻り値が真/偽でなければなりません(ConditionNotBoolean)。このメソッドがなければ、いつもどおりOperandTypeMismatch/NullOperandです。
    • 右から版: + - * / %は、左辺がそのメソッドを持つオブジェクトでなければ、右辺がオブジェクトでそのクラス(親も含む)に〈記号 右から足す〉、〈記号 右から引く〉、〈記号 右から掛ける〉、〈記号 右から割る〉、〈記号 右から余り〉があるとき、Bの〈記号 右から掛ける〉(A)のように左辺を引数にして呼びます(3 * ベクトル)。呼び出しの規則は上と同じです。大小比較には右から版がありません(3 < BはB > 3と書けばいいです)。
    • 等価比較: 〈記号 同じだ〉一つがAとBが同じだ(==)とAとBが違う(!=)をまとめて決めます。探し方と呼び出しは上とまったく同じで(親から受け継いだものも含み、引数・戻り値の型を検査)、参照の比較の代わりにAの〈記号 同じだ〉(B)の結果を使います。違うは真/偽の結果を反転し、何も返さなければ(空っぽ)同じではないとみなします。
  • 動的なリフレクション (Dynamic Reflection): メソッドの呼び出しで、静的な名前(〈メソッド名〉)の代わりに変数や式(〈「文字列」〉または〈『変数』〉)が山括弧の中に書かれたら、ランタイムはその式を文字列として評価したあと、オブジェクトの中でその文字列と一致する名前のメソッドを動的に探して(Dynamic Dispatch)実行しなければなりません。メソッドが見つからなければMethodNotFoundErrorを発生させてください。
  • タイプ検査の演算(一種だ): 実行時に、あるオブジェクトが特定のクラスやインターフェースのタイプかを確かめるため、instanceofの役割のAがBの一種だの演算に対応しています。継承の階層とインターフェースの実装の有無をどちらも調べて、真/偽を返してください。
  • プロパティアクセサ (Property Getters/Setters): 変数を宣言するときにgetterやsetterのブロックがある場合、そのプロパティを読むときは内部の変数をそのまま返す代わりにgetterのブロックを評価して、その結果を返さなければなりません。反対に値を代入するときは、setter.paramの識別子に代入する値を結び付けてからsetter.bodyのブロックを実行して、処理してください。

4. 制御フロー (Control Flow) と例外処理

4.1 スイッチ文の貫通 (Fallthrough) と既定 (Default) の検証

  • スイッチの分岐の中でFallthroughStatement(次に続けよう)に出会ったら、breakのフラグを無視して、ソースコード上ですぐ下に宣言された次のcaseのブロックを、条件(テスト)の評価なしにすぐ続けて実行しなければなりません。
  • 一致するcaseがないとき (Default): 入ってきた値に一致するcaseがまったくなければ、ランタイムはdefault(残りは:)のブロックがある場合にそれを実行します。defaultのブロックがなければ何も実行せず、次の文へ進みます(エラーではありません)。

4.2 例外処理 (Exception / Try-Catch-Finally)

  • ランタイムは、例外が発生したらコールスタックをすぐにほどき(Unwinding)始めなければなりません。
  • TryStatement(とりあえずやってみよう)のブロックを実行している途中でエラーが発生したら、ランタイムは登録されたhandlers(CatchClauseの配列)を宣言された順に調べます。
    • 特定の例外の絞り込み: 発生したエラーオブジェクトのタイプがCatchClauseのcatchTypeと一致する(アップキャストの互換を含む)か、catchTypeがnull(発生したら)の場合、そのブロックの引数にエラーを結び付けて実行を再開します。一致した最初のブロックだけを実行して、残りは無視してください。
    • CatchClauseがない、または合わない場合: エラー処理のブロック(handlers)がそもそもない、または発生したエラーと一致するタイプのブロックが1つもない場合、ランタイムはエラーを無視してはならず、FinallyClauseが終わった直後に上の呼び出しスタックへそのエラーを必ず再び投げなければなりません(Re-throw)。
  • 既知の制限: 処理されなかったランタイムエラーを報告するとき、何行目で起きたかは知らせません(構文エラーだけが行と文字の位置を知らせます)。エラーの種類と文言で探してください。
  • FinallyClause(最後はいつも)がある場合は、エラーが発生しても正常に終了しても、中にReturnStatementがあっても、必ずコールスタックを抜ける直前にそのブロックを実行するよう保証しなければなりません。

4.3 モジュールとインポート (Module / Import)

  • ImportStatementを評価するとき、ランタイムはモジュールの書き方で種類を決めます。文字列(「ファイル」)は、常にプロジェクトからの相対パスのローカルファイルです。タイプリテラル(【名前】)は、標準ライブラリ、ランタイムと一緒に配るパッケージ、またはインストールしたサードパーティのパッケージです。どれであっても、コードを評価して独立したEnvironmentに読み込まなければなりません。
  • 重複と循環の防止 (Circular Import): モジュールのローダーは必ずキャッシュの仕組みを使って、同じモジュールが2回評価されないようにしてください。モジュールAが評価の途中でモジュールBを呼び、BがまたAを呼ぶ循環参照が起きたときは、ランタイムは無限ループに陥らず、その時点までに評価が終わったAの部分的な状態(Partial Environment/Object)をすぐに返して、Bの評価を続けなければなりません。
  • 持ってきたものの統合: ASTのimportsの属性がnull(全部持ってくる)なら、読み込んだモジュールのすべての公開識別子を現在のファイルのグローバルなEnvironmentに統合します。識別子の一覧が指定されていれば(["関数名", "変数名"])、明記された識別子だけを正確に選んで統合しなければならず、存在しない項目を要求されたらすぐにImportErrorを発生させてください。
  • インポートなしで使えるものと標準ライブラリ: インポートなしで使えるのは、文法の動詞(出力・入力など)と基本タイプの型変換の関数(5.5)だけです。数学のようなそのほかの機能はすべて標準ライブラリ(std)で、"builtin"のモジュールとしてインポートしなければなりません。標準ライブラリのモジュール名と関数名は言語ごとに違います(ハリの[수학]と<올림>、カナデの【数学】と〈切り上げ〉)。実装は、言語に依存しないID(math.ceil)に言語ごとの名前を付けて提供してください。
  • 言語の決定: インポートされたコードは自分の言語で実行します。ローカルファイル(「ファイル」)は拡張子(.hrハリ、.kndカナデ)で言語が決まるので、ハリのファイルとカナデのファイルが互いに持ってこられます。パッケージのフォルダはインポートする側の言語のエントリポイント(<モジュール>/<言語>/index.<拡張子>)を読み込み、その言語のエントリポイントがなければImportErrorを発生させてください。インポートされたモジュールも、自分の言語の標準ライブラリ(5.5の関数を含む)を使えなければなりません。
  • モジュールの範囲 (Module Scope): 持ってきた関数・メソッド・コンストラクタの本体は、自分のモジュールの名前空間で関数を探さなければなりません。同じモジュールが宣言したほかの関数(補助の関数)と、そのモジュールが自分で持ってきたもの(ほかのパッケージ、標準ライブラリの関数)は、持ってくる側が別に持ってこなくても呼び出せます。持ってくる側に同じ名前の関数があっても、お互いを隠してはなりません。持ってくる側から見えるのは、importsが要求した識別子だけです。モジュールの一番上の変数と文も、モジュールのものです。モジュールは最初に持ってきたときに1回だけ読み込まれて、一番上のコードが1回実行され(もう一度持ってきても同じ状態)、持ってきた関数はその変数を読んだり変えたりできます。持ってくる側の変数は、モジュールの関数の中からは見えません(ReferenceError)。モジュールが宣言したクラスとインターフェースのうち、持ってくる側に同じ名前がないものは、自分の名前で登録されるので、モジュールの関数が作れます。すでに同じ名前が別のモジュールやプログラム自身のものとしてある場合は、ImportErrorを発生させなければなりません(持ってくるときに〈名前〉を〈別名〉にで別の名前を付ければ避けられます)。
  • サードパーティのパッケージ: 名前がgitのパスの形(【github.com/owner/repo】)なら、パッケージマネージャー(hana add)が、プロジェクトのhana.json(最小バージョン)とhana-lock.json(選んだバージョンとコミット)に従ってダウンロードしたフォルダを指します。インストールされていなければImportError(インストールの案内)で、ランタイムは実行中にパッケージをダウンロードしません。
  • ファイルシステムのないランタイム: ブラウザのプレイグラウンドのようにファイルシステムがない環境は、標準ライブラリのインポートだけに対応し、ファイルとパッケージのインポートはエラーとして知らせてください。存在しないモジュールや項目はImportErrorです。

4.4 繰り返しの制御 (Loop Control)

  • BreakStatement(繰り返しを終わろう): 実行中にこの文に出会ったら、ランタイムは一番内側(Inner-most)にある繰り返し文(WhileLoop、ForEachLoop、ForRangeStatement)の制御の流れをすぐに抜けなければなりません。
    • 注意: カナデのスイッチ(SwitchStatement)は、各caseが終わると自動でブロックを抜けるように設計されているので、スイッチ文の中でBreakStatementを使うとスイッチを無視して、その外側を囲んでいる繰り返し文を抜けます。
    • IllegalBreakError: 繰り返し文の中ではない最上位や単純な関数のブロックの中でBreakStatementが実行された場合、ランタイムは捕まえられていない(Uncaught)ループの中断とみなして、すぐにIllegalBreakError(またはSyntaxError)を発生させなければなりません。
  • ForRangeStatementの評価の方向: 1から10までのような数の範囲の繰り返し文を評価するとき、終了値は必ず含まれます(Inclusive)(例: 1から3までは1、2、3を実行)。
    • もし開始値が終了値より大きければ(例: 10から1まで)、ランタイムはすぐに終了せず、逆順(-1ずつ減らす)で繰り返しを実行しなければなりません。

4.5 呼び出しの深さの制限 (Call Depth)

  • 関数・メソッドの呼び出しとインスタンスの生成が重なって積まれる深さに、上限(10000階層)を設けなければなりません。上限を超えたら、ホストのコールスタックが壊れるのに任せず、プログラムがとりあえずやってみようで捕まえられるRecursionErrorを発生させてください。

5. 組み込みのリテラルと演算子 (Built-in Literals & Operators)

5.1 リストと辞書

  • リスト (ListLiteral): 実行時の評価で、大きさが動的に変わるリスト(Dynamic Array)のオブジェクトとして作られなければなりません。
    • インデックスアクセス(1番目の値): ランタイムは、人にやさしい1始まり(1-based)のインデックスを使います(例: 1番目はリストの最初の要素です)。
      • オフセットの変換: コンパイラやインタープリタが内部で0始まりのインデックスを使うホスト言語(C、Rust、Pythonなど)で書かれているなら、実行時にプロパティを評価するとき、必ずアクセスするインデックスの値 - 1にオフセットを変換して処理してください。
      • 境界の検査 (Bounds Check): リストの長さを超えたインデックスにアクセスしたり代入したりしようとした場合、黙って無視せず、すぐにIndexOutOfBoundsErrorを発生させなければなりません。
    • 長さの取得(長さ): 『リスト』の『長さ』を評価するとき、リストの現在の大きさを整数で返してください。
    • ネイティブなリストの操作(追加/取り出し): オブジェクト指向のメソッドの代わりに、次のような言語のネイティブな構文でリストを操作します。
      • 追加(ListPushStatement): 『リスト』(の前に/の後に)値を追加しようを評価するとき、リストオブジェクトの先頭(0番のインデックス、Shift)または末尾(最後、Append)に要素を入れてください。
      • 取り出し(ListPopStatement / ListPopExpression): 『リスト』(の前から/の後から)取り出そう(文)または取り出した値(式)を評価するとき、指定された位置の要素を取り除きます。(空のリストの場合は、すぐにIndexOutOfBoundsErrorを発生させてください。)
    • 組み込みのメソッド: 次のメソッドにも対応します。
      • 〈空にする〉(): リストのすべての要素を取り除いて、長さを0にします。
  • 辞書 (DictLiteral): 実行時の評価で、文字列などハッシュできるキーを値に対応させるハッシュテーブル(Hash Map / Dictionary)のオブジェクトとして作られなければなりません。
    • キーへのアクセス: 辞書オブジェクトにないキーを調べようとした場合、すぐにKeyErrorを発生させてください。
    • キーの確認 (〈含むか確認〉(キー)): 辞書にそのキーがあれば真、なければ偽を返します(文字列の〈含むか確認〉と同じ名前)。辞書のメソッドはこれ一つだけで、ほかの名前はMethodNotFoundErrorです。
    • 巡回: ForEachLoop(ごとに繰り返そう)で辞書を回すと、繰り返すたびにキーが出てきます。順序は数字のキー(小さいものから)、文字列のキー(コードポイントの辞書順)、偽、真です。繰り返しは始めたときのキーの一覧を回すので、繰り返しの途中でキーを足してもその繰り返しには出てきません。

5.2 論理演算の短絡評価 (Short-circuit Evaluation)

  • かつ(Logical AND): 左辺 かつ 右辺を評価するとき、ランタイムは左辺を先に評価し、その結果が偽(False)なら右辺を決して評価せず、すぐに偽を返さなければなりません。(右辺の関数呼び出しや実行時のエラーは起きません。)
  • または(Logical OR): 左辺 または 右辺を評価するとき、ランタイムは左辺を先に評価し、その結果が真(True)なら右辺を決して評価せず、すぐに真を返さなければなりません。
  • 条件のタイプ: もしと間繰り返そうの条件、そしてかつ/またはの両側のオペランドは、真か偽でなければなりません。数字(0を含む)、文字列、リスト、空っぽのようなほかの値は、偽として扱わずTypeErrorを発生させなければなりません。(短絡評価で評価されない右辺は検査しません。)

5.3 数学演算の例外 (DivideByZeroError)

  • 0で割る、および余り: 割り算(/)と余り(%)の演算子を評価するとき、右辺(分母)の実行時の値が0(0、0.0)の場合、ランタイムはホストシステムのクラッシュや無限大(Infinity)/NaNを返すことを許さず、すぐにMathError(またはDivideByZeroError)の例外を発生させて、コールスタックをほどいてください。

5.4 文字列 (String)

  • 文字列 (String): リスト(List)と同じインデックスアクセス(1番目)と長さの取得(長さ)の構文に対応します。
    • インデックスアクセス: 1始まりのインデックスを同じように適用し、範囲を超えたらIndexOutOfBoundsErrorを発生させます。
    • 戻り値のタイプ: インデックスアクセスの結果は、長さ1の【文字列】の値です(別の文字(Char)のタイプはありません)。
    • 不変性 (Immutability): 文字列は変更できない(Immutable)値です。MemberExpressionによるインデックス位置への再代入(『名前』の1番目を「金」にしよう)は許可されず、試みた場合はImmutableAssignmentError(またはTypeError)を発生させなければなりません。
    • 長さの取得: 『文字列』の『長さ』を評価するとき、文字の数を整数で返してください。
    • Unicodeと正規化 (NFC/Codepoint): 文字列のインデックスと長さの測定は、常にUnicodeのコードポイント(Codepoint)を基準に計算します。ソースコードとすべての入力文字列はNFC(合成形の正規化)済みとみなして、ホスト言語ごとのばらつきを防がなければなりません。
    • 文字列の巡回: ForEachLoop(ごとに繰り返そう)で巡回でき、繰り返しのたびに長さ1の文字列が返されます。
  • 文字列の連結(+)と複合代入: 異なるタイプの間の+演算は禁止されますが、文字列と文字列の間の+演算は許可され、つなげた新しい文字列を返します。また、複合代入の演算である『文字列変数』に「値」を足そう(CompoundAssignmentノード、operator: "+=")を使った場合、既存の文字列のインスタンスを変更するのではなく、つなげた新しい文字列を作って、対象の変数やプロパティに再代入する形で正しく動かなければなりません。
  • 文字列の比較(<、>、==): 文字列どうしの比較演算子は、Unicodeの辞書順(Lexicographical)を基準にします。
  • 組み込みのメソッド: すべての文字列のインスタンスは、次のような組み込み(Built-in)メソッドの呼び出しに標準で対応していなければなりません。
    • 〈切り取り〉(開始, 終了): 部分文字列を返します。数の範囲の繰り返し文と同じく1始まりで、終了インデックスの文字まで含めて(Inclusive)切り取ります。
    • 〈入れ替え〉(元の文字列, 新しい文字列): 文字列の中で見つかるすべて(All)の「元の文字列」を「新しい文字列」に置き換えて、新しい文字列を返します。
    • 〈含むか確認〉(検索語): 含まれているかを論理値で返してください。
    • 〈分割〉(区切り文字): 区切り文字を基準に文字列を分けて【リスト】のオブジェクトで返します。区切り文字が元の文字列の中になければ、元の文字列全体を唯一の要素とする長さ1のリストを返さなければなりません。

5.5 グローバルな組み込み関数 (Global Built-in Functions)

すべてのランタイム環境は、別のインポートなしで次のグローバルなユーティリティ関数を標準で提供しなければなりません。

  • 型変換の関数:
    • 〈数字に〉(値): 文字列やほかのタイプの値を実数または整数に変換して返します。変換できない値の場合はConversionErrorを発生させてください。
    • 〈文字列に〉(値): 数字や論理値などの任意の値を文字列(文法的なシリアライズ)に変換して返します。
  • ASCII/Unicodeの変換の関数:
    • 〈コードに〉(文字列): 長さ1の文字列を受け取り、対応するUnicodeのコードポイント(整数)を返します。(長さが1でなければエラーを発生させてください。)
    • 〈文字に〉(数字): コードポイント(整数)を受け取り、対応するUnicodeの文字(長さ1の文字列)を返します。

5.6 演算の順序 (Operator Precedence)

  • 算術演算子は、普通の数学の順序に従います。*、/、%が+、-より先に結び付き、同じ段階どうしは左から結び付きます(2 + 3 * 4は14、100 / 10 / 5は2)。括弧は常に最優先です。
  • 比較(同じだ、大きいなど)とかつ/またはは、算術をすべて計算したあとに付く別の構造なので、この順序には含まれません。パーサーはこの順序をASTのBinaryExpressionの入れ子で表し、ランタイムは木をそのまま評価すればかまいません。

6. 標準入出力 (Standard I/O)

6.1 出力 (Print / PrintInline)

  • PrintStatement(出力しよう): ランタイムは、評価された値を文字列に変換してホストの標準出力のストリーム(またはUIのコンソール)に書き、最後に必ず改行(Newline、\n)を加えたあと、バッファを空にして(Flush)ください。
  • タイプ名を値として使うとき(【名前】): その名前の変数があればその値、なくてその名前の設計(クラス)があれば設計そのもの、どちらでもなければ(【数字】のような基本タイプ)名前そのままの文字列です。設計そのものを文字列に変えると、そのオブジェクトと同じ形([動物 オブジェクト])になります。
  • PrintInlineStatement(続けて出力しよう): ランタイムは値を標準出力に書きますが、改行の記号は加えません。ただし、入力のプロンプトなどに使われることがあるので、必ずすぐに出力バッファを空にして(Flush)、画面に表示されるように保証しなければなりません。

6.2 入力 (Input)

  • 抽象化された入力ソース (I/O Provider): InputStatement(入力してもらおう)の実行時、ランタイムは、ホスト環境の標準入力(stdin)、ブラウザのprompt()、あるいはGUIの入力ダイアログなど、環境に合った同期的な入力ストリームを待たなければなりません(Blocking)。
  • 許可されるタイプの制約: 入力を受け取るときに指定できるタイプ(typeAnnotation)は、必ず基本タイプ(【文字列】、【数字】、【論理】)に限られなければなりません。【自動車】のようなカスタムクラスや複合タイプを指定した場合、ランタイムはすぐにUnsupportedInputTypeErrorを発生させてください。
  • 既定のタイプ (Default String): タイプが省略された構文(『変数』を入力してもらおう)を評価するときは、既定で【文字列】のタイプとして扱います。
  • 厳密な変換のエラー (Strict Conversion): 文字列として入ってきた入力値を、指定されたタイプに合わせて解析してください。指定されたタイプに変換できない値(例: 【数字】を期待したのに「あいう」が入力された)の場合、ランタイムは暗黙の処理や勝手な再入力のループを行わず、すぐにInputConversionErrorを投げなければなりません。失敗の処理は、言語の中のとりあえずやってみよう(Try-Catch)の構文で開発者が直接制御するように強制します。
プレイグラウンド
結果
コードを書いて「実行」ボタンを押してください。