Skip to content

構文仕様 ​

本文書では、YaoXiang プログラミング言語の構文仕様を定義する。字句構造、構文規則、演算子の優先順位を含む。


第一章:字句構造 ​

1.1 ソースファイル ​

YaoXiang ソースファイルは UTF-8 エンコーディングを使用しなければならない。ソースファイルの拡張子は通常 .yx である。

1.2 トークンの分類 ​

種類説明例
識別子文字またはアンダースコアで始まるx, _private, my_var
キーワード言語で事前定義された予約語Type, pub, use
リテラル固定値42, "hello", true
演算子演算記号+, -, *, /
区切り記号構文の区切り(, ), {, }, ,

1.3 キーワード ​

YaoXiang はごく少数のキーワードを定義する:

pub    use    spawn
ref    mut    if     else
else   match  while  for    return
break  continue as     in     unsafe

これらのキーワードはいかなる文脈でも特別な意味を持ち、識別子として使用することはできない。

1.4 予約語 ​

YaoXiang の「予約語」は3層に分かれ、それぞれパーサ(parser)と型検査器(type checker)が異なる段階で識別する:

1.4.1 リテラル予約語 ​

パーサが独立したトークンとして持つリテラル識別子。通常の識別子としては使用できない:

識別子所属型説明
Type—メタ型キーワード
trueBoolブール真値
falseBoolブール偽値
voidVoidVoid リテラル(Unit 値)。小文字の void は値リテラル;大文字の Void は型名(§1.4.3 参照)。

1.4.2 コンストラクタ式 ​

以下のコンストラクタはパターンマッチングと式の文脈でパーサに識別される:

コンストラクタ所属型説明
some(T)OptionOption 値バリアントコンストラクタ
ok(T)ResultResult 成功バリアント
err(E)ResultResult エラーバリアント

1.4.3 組み込み型名 ​

以下の型名は型検査器に事前登録されており、インポートなしでも型位置で使用できる。パーサはこれらを通常の識別子として扱う——予約語ではなく、局所バインディングによるシャドウイングが可能(推奨されない)。

型名論理的対応説明
Void⊤(真/Unit)ゼロフィールドの積型。居留者はちょうど1つ(void リテラル、§1.4.1 参照)
Never⊥(偽/空型)ゼロバリアント和型、居留者ゼロ。Never 型の値を生成する式は存在しない。Never <: T はすべての T に対して成立(爆発原理)。
Int—符号付き整数
Float—浮動小数点数
Bool—ブール値:true / false
Char—Unicode 文字
String—文字列

1.5 識別子 ​

識別子は文字またはアンダースコアで始まり、後続の文字は文字、数字、アンダースコアのいずれかである。識別子は大文字小文字を区別する。

特殊な識別子:

  • _ はプレースホルダとして使用され、値を無視することを示す
  • アンダースコア始まりの識別子はプライベートメンバーを示す

1.6 リテラル ​

1.6.1 整数 ​

Decimal     ::= [0-9][0-9_]*
Octal       ::= 0o[0-7][0-7_]*
Hex         ::= 0x[0-9a-fA-F][0-9a-fA-F_]*
Binary      ::= 0b[01][01_]*

1.6.2 浮動小数点数 ​

Float       ::= [0-9][0-9_]* '.' [0-9][0-9_]* ([eE][+-]?[0-9][0-9_]*)?

1.6.3 文字列 ​

String      ::= '"' ([^"\\] | EscapeSequence)* '"'
Escape      ::= '\\' ([nrt'"\\] | UnicodeEscape)
Unicode     ::= 'u' '{' HexDigit+ '}'

1.6.4 コレクション ​

List        ::= '[' Expr (',' Expr)* ']'
Dict        ::= '{' String ':' Expr (',' String ':' Expr)* '}'
Array       ::= '[' Expr (',' Expr)* ']'   // ターゲット型注釈が Array(T, N) の場合、リテラルは固定長配列に落ちる

辞書リテラルは少なくとも1つのキー・値ペアを要求する:{} は空辞書ではない——これは空ブロック(値 Void、 §2.9 参照)である。空の辞書にはコンストラクタ dict.new() を使用すること:

yaoxiang
empty = dict.new()                  // ✅ 空辞書
d = { "a": 1 }                       // ✅ 辞書リテラル
wrong = {}                           // ❌ これは辞書ではなく、空ブロック(Void)

判定は内容による:Dict 文法は少なくとも1つの String ':' Expr を要求し、{} には判定材料がないため、ブロック構造のゼロ形態を取る。非空形態は内容が自己記述する({ "k": v } はキー・値ペアを持つ → 辞書)——これは f = { 5 } が関数ではなく Int 値であることと同源である:型は内容によって決まる。

Set にはリテラル文法もランタイム表現もない——集合型は計画中であり、ニーズが現れた時点で Dict のパターンに従って完成させる(std.set + HeapValue::Set)。List/Dict リテラルの落下先はコンテキスト型注釈によって決定される:素のリテラルと List(T) 注釈は拡張可能リストに落ちる;Array(T, N) 注釈がリテラルに直接作用する場合は固定長配列に落ちる。暗黙の List→Array 変換は禁止。

Array リテラルセマンティクス:

  • 要素数は N と等しくなければならない。等しくない場合はコンパイル時 E1002;空リテラルに非ゼロ N を組み合わせた場合も拒否される
  • 各要素の型は T と互換性がなければならない。互換性がない場合はコンパイル時 E1002
  • N の文法形式:整数リテラルのみ(負可)または定数名;複合式(例:2+1)は解析時に拒否される
  • N がシンボル定数(関数の const パラメータ、例:Array(Int, n))の場合、個数検査は精化型フェーズまで延期される
  • v1 のネストされた配列リテラル(Array(Array(Int,2),2) = [[1,2],[3,4]])はコンパイル時に拒否される。階層ごとに明示的に構築する必要がある。再帰的な落下の対応は今後のバージョンに持ち越される

1.6.5 リスト内包表記 ​

ListComp    ::= '[' Expr 'for' Identifier 'in' Expr (',' Expr)* ('if' Expr)? ']'

行動の厳格化(移行記録):イテレーション変数の文法自体は 'for' Identifier 'in' であるが、旧実装では pattern が完全な pratt 解析を行っていた——'in' が中置演算子として登録された後、x は in items を membership 式として飲み込んでしまう。修正後は非識別子 pattern は直接解析に失敗し、旧実装のように _ にフォールバック(静かにエラーを吸収)することはなくなった。影響範囲:以前 [x for (a, b) in pairs] のような書き方ができていたが、現在はエラーになる——この形式には定義された挙動が一度もなかった(変数は常に _ だった)。厳格化の方向は正しく、意味的な移行コストはない。

1.6.6 メンバーシップ検査 ​

Membership  ::= Expr 'in' Expr

in は二項関係演算子であり、Bool を返す——ヒットすれば true、ヒットしなければ false で、エラーにはならない。意味の分割:[] は存在をアサートして値を取り出す(失敗はエラー)、in は存在するかを問う(未ヒットは正常な false)。右オペランドの範囲:List / Array / Dict(キーセット) / Tuple / String(部分文字列) / Range(区間)。in は一級ホーア述語であり、精化型フェーズでコンパイル時に証明可能な命題の基底となる。(Set は右オペランドのリストから除外——Set にはランタイム表現がない、§1.6.4 参照)

1.7 コメント ​

// 単一行コメント

/* 複数行コメント
   複数行に渡ることができる */

1.8 インデント規則 ​

コードは4つのスペースでインデントしなければならず、Tab 文字の使用は禁止。これは強制的な構文規則である。


第二章:構文規則 ​

2.1 式の分類 ​

Expr        ::= Literal
              | Identifier
              | FnCall
              | MemberAccess
              | IndexAccess
              | UnaryOp
              | BinaryOp
              | TypeCast
              | RangeExpr
              | ErrorPropagate
              | RefExpr
              | IfExpr
              | MatchExpr
              | Block
              | Lambda

2.2 演算子の優先順位 ​

優先順位演算子結合性
1() [] . ?左から右
2as左から右
3単項前置 ! - +右から左
4* / %左から右
5+ -左から右
6..左から右
7<< >>左から右
8& | ^左から右
9== != < > <= >=左から右
10and or左から右
11if...else右から左
12= += -= *= /=右から左

単項前置演算子(! -+)は強く結合する:呼び出しとメンバーアクセスより低く、すべての二項演算子より高い。したがって !a == b ≡ (!a) == b(Zig 風のセマンティクス);! は純粋な単項演算であり、短絡制御フローには参加せず、and/or キーワード(短絡)とは直交する(RFC-010 による権威ある定義)。

Range の結合力:.. の結合力は (6, 7)——左 6 は加算(7)より低く、右 7 は加算を吸収するが同位の .. は吸収しない。変更前後の対比:

式変更前(レベル 1、右結合)変更後((6,7)、左結合)
x in 1..10x in 1..10(in の右オペランドレベル 4、.. のレベル 1 では吸収できず、実際には解析不可)x in (1..10)——区間全体が in の右オペランド
0..n+2(0..n)+2(右結合の落とし穴:上限が食われ、for ループは直接 E3004)0..(n+2)——上限は算術式
a == b..ca == (b..c)(.. のレベル 1 < == のレベル 3、本来は全体)a == (b..c)——セマンティクス不変、.. は比較レベルより依然高い
1..2*3(1..2)*31..(2*3)——上限は算術式
a..b..ca..(b..c)(右結合の連鎖、意味のない Range の入れ子)(a..b)..c——step 形式(c はステップ)

純効果:複合上限 for i in 0..n+2 が「解析成功だが E3004」から「直接使用可能」に;x in 1..10 が「解析不可」から「区間チェック」に;a..b..c が「無意味な入れ子」から「step 成分」に。レベル 6 は +(レベル 5)と <<(レベル 7)の間に位置し、数学的慣例による:区間は強く結合する構造であり、上限は当然完全な算術式である。

2.3 関数呼び出し ​

FnCall      ::= Expr '(' ArgList? ')'
ArgList     ::= Expr (',' Expr)* (',' NamedArg)* | NamedArg (',' NamedArg)*
NamedArg    ::= Identifier '=' Expr

名前付き引数は name = value(RFC-010 §関数定義、RFC-011 §構築形式)。位置引数は名前付き引数の前に置かなければならない;順序指定された引数は名前付き引数の中で任意の順序に配置できる:

yaoxiang
add: (a: Int, b: Int) -> Int = a + b

add(3, 5)          // 位置的
add(a = 3, b = 5)  // 名前付き
add(b = 5, a = 3)  // 順序自由
add(3, b = 5)      // 混在、位置が前

名前付き引数の名前が間違っている場合は E1014、同じ仮引数が位置と名前の両方で指定された場合は E1015、個数が一致しない場合は E1010(RFC-013)。

2.4 メンバーアクセス ​

MemberAccess::= Expr '.' Identifier

2.5 インデックスアクセス ​

IndexAccess ::= Expr '[' Expr ']'

3層のセマンティクス(RFC-011b):a[i] は a の型によって分岐する——① 組み込みコンテナ(List/Vec/Array/Dict/Tuple)はネイティブインデックス命令を経由(高速パス);② Index インターフェースを実装したユーザー型はその index メソッドにディスパッチされる(型本体内の Index(Grid, Int, Float) インスタンス化 + Grid.index メソッド);③ RFC-004 の f[0] 位置バインディングはバインディング宣言にのみ存在し、本文法を経由しない。多次元インデックス a[0, 1] のキーはタプルとしてパックされる。未実装 Index の他の型は型レベル拒否される。

2.6 型キャスト ​

TypeCast    ::= Expr 'as' TypeExpr

2.7 条件式 ​

IfExpr      ::= 'if' Expr Block ('else' 'if' Expr Block)* ('else' Block)?

2.8 パターンマッチング ​

MatchExpr   ::= 'match' Expr '{' MatchArm+ '}'
MatchArm    ::= Pattern ('|' Pattern)* ('if' Expr)? '=>' Expr ','
Pattern     ::= Literal
              | Identifier
              | Wildcard
              | StructPattern
              | TuplePattern
              | EnumPattern
              | OrPattern

2.9 ブロック式 ​

Block       ::= '{' Stmt* Expr? '}'

文の終端規則:Stmt 間の区切りと改行の挙動(; による明示的な区切り、改行による終端、継続行の例外、行頭の (/[ はマージしない)は RFC-038 で定義される。

2.9.1 { の3つの形態 ​

式位置における { はちょうど3つの解釈を持ち、内容によって一度に判定される:

形態記法型例
空ブロック{}Voidx: Void = {}
辞書リテラル{ "k": v, ... }Dict(K, V)d = { "a": 1 }
ブロック{ Stmt* Expr? }末尾式の型y = { 1 + 1 }

判定順序:

  1. {}(内容なし)→ 空ブロック、値 Void
  2. 最初の要素が String ':' Expr のキー・値ペア → 辞書リテラル
  3. それ以外 → ブロック、値は末尾式によって与えられる

なぜ {} は空辞書ではないか:空辞書の「空」は自己記述できない(Dict(K, V) にもなり空ブロックにもなり得る)、辞書文法 §1.6.4 は少なくとも1つのキー・値ペアを要求する。判定材料がない場合はブロック構造のゼロ形態を取る:これは unsafe {} / spawn {} と一致し、特例を導入しない。空辞書には dict.new() を使用すること。

なぜ関数には注釈が必要か:f = { stmt } は値(末尾式の型)であり、関数ではない。関数を定義したい場合は Fn 注釈を明示する:f: () -> Int = { 5 }。これは辞書と同じ原則である:型は内容によって決まる、注釈の存在によって決まるわけではない。(この規則は RFC-010a 付録 D を参照。)

統一セマンティクス:すべての {} ブロックの値は末尾式によって与えられ、return は Never 型の非ローカルな終了である。

ブロック型値の出口空ブロック {}
通常の {}末尾式Void
unsafe {}末尾式Void
spawn {}末尾式Void

核心原則(詳細は RFC-010a 参照):

  • ブロックの値 = 末尾式(最後の式)、唯一の出口、例外なし
  • 末尾が代入文の場合、ブロックの値は Void;Void を望むなら明示的に Void と書く
  • return は最も近い関数境界を終了する(あらゆるブロックを貫通し、ブロックに「返さない」)、型 Never;Never <: T は任意の型に対して成立(爆発原理)、したがって任意の戻り型の位置に現れることができる
  • 式形式 = expr は直接値を与える
yaoxiang
// 通常の {} ブロック:末尾式が値を与える
result = {
    x = compute()
    x                // ブロックの値
}

// unsafe {} ブロック:末尾式が型定義を与える
SqliteDb = unsafe {
    SqliteDb: Type = {
        handle: *Void
    }
    SqliteDb         // ブロックの値
}

// spawn {} ブロック:末尾式が結果を与える
(a, b) = spawn {
    result1 = fetch("url1"),
    result2 = fetch("url2")
    (result1, result2)   // ブロックの値
}

// return:ブロックを貫通し、関数を終了
f: (n: Int) -> Int = {
    if n < 0 {
        return 0     // if と関数本体を貫通し、関数を終了
    }
    n * 2            // 末尾式
}

name = { ... } は関数かブロック値か? ​

name = { ... } は関数定義(RFC-007「空パラメータ最簡」)にも、ブロック値のバインディングにもなり得る。注釈優先、デフォルトは関数によって裁定する(RFC-010a 付録D):

ケース結果例
値が Lambda(=>)関数f = () => 5 → f() = 5
注釈が関数型関数f: () -> Int = { 5 } → f() = 5
注釈が非関数型ブロック値x: Int = { 5 } → x = 5
注釈なし関数f = { 5 } → f() = 5

注釈は型そのもの:x: Int = ... は x が Int であることを宣言するので、{ ... } は Int に評価される; f: () -> Int = ... は f が関数であることを宣言するので、{ ... } は関数本体。

{ ... } を即座に評価したい場合は、ターゲット型を書けばよい(新しい文法は不要):

yaoxiang
// ブロック値:即座に評価
x: Int = {
    y = 5
    y            // x = 5
}

// 関数:注釈なし、デフォルト
f = { 5 }        // f() = 5

ネストされた関数型:カリー化か関数を返すか? ​

-> の右側のネストされた関数型には2つの読み方があり、括弧で区別する(RFC-004):

書き方意味呼び出し
(a: Int) -> (b: Int) -> Intカリー化f(1)(2)
(a: Int) -> ((b: Int) -> Int)関数を返すg(1) で関数を得る

根拠:注釈は型そのもの。g: (a: Int) -> ((b: Int) -> Int) は g(1) : (b: Int) -> Int を宣言するので、 g(1) は「次のパラメータ」ではなく、その関数そのものでなければならない。

yaoxiang
// カリー化:2 段階のパラメータを逐次提供
add: (a: Int) -> (b: Int) -> Int = { a + b }
add(1)(2)        // → 3

// 関数を返す:外側の1段階のパラメータ、返されるのが関数
adder: (n: Int) -> ((x: Int) -> Int) = (x) => x + n
adder(10)(5)     // → 15
h = adder(10)    // 返された関数は変数に保存したり、渡したりできる

括弧なしのネストされた Fn はすべてカリー化(RFC-011 の型パラメータ形式を含む):

yaoxiang
identity: (T: Type) -> (x: T) -> T = (x) => x   // カリー化
identity(5)      // → 5

型検査:括弧は戻り型を宣言する。 body はその型を生成しなければならない。 f: () -> (() -> Int) = { 7 } は誤り——() -> Int を期待するが、実際には Int が得られる(E1002)。

2.10 Lambda 式 ​

Lambda      ::= '(' ParamList? ')' '=>' Expr
            |  '(' ParamList? ')' '=>' Block

2.11 エラー伝播演算子 ​

ErrorPropagate ::= Expr '?'

? 演算子は後置演算子であり、優先順位は . と同位。Result(T, E) 型に対して:

  • Ok(v) の場合は値 v を取り出して実行を継続
  • Err(e) の場合はエラーを上位に伝播する(return Err(e))
yaoxiang
process: (data: Data) -> Result(Data, Error) = {
    validated = validate(data)?     // 成功時は値を取り出し、失敗時は上位に伝播
    transform(validated)
}

2.12 範囲式 ​

RangeExpr   ::= Expr '..' Expr ('..' Expr)?

.. は範囲値を作成する(Range は一級値であり、構文糖ではない)。

yaoxiang
for i in 0..10 { print(i) }
slice = array[0..5]

// Range は値:バインディング、受け渡し、メンバー判定
r = 1..10
assert.assert(5 in r, "membership")
for i in r { print(i) }

// step 形式(第三成分、デフォルト 1)
for i in 0..10..2 { print(i) }  // 0, 2, 4, 6, 8
for i in 10..0..(-2) { print(i) }  // 10, 8, 6, 4, 2

step セマンティクス:a..b..c の c はステップ。c = 0 のリテラルはコンパイル時に拒否される;動的 c はランタイムでゼロチェックされる(E6001 ファミリー;エラーシステム実装後は Result に格上げ)。 c < 0 は合法、区間方向は符号に応じて反転する(10..0..(-2) は降順)。

2.13 ref 式 ​

RefExpr     ::= 'ref' Expr

ref は共有所有を作成する。コンパイラは自動的に Rc(単一タスク)または Arc(タスク横断)を選択し、ユーザーは実装詳細を気にする必要がない。

yaoxiang
data = ref heavy_data
spawn { use(data) }   // タスク横断:コンパイラが自動的に Arc を選択

2.14 unsafe 式 ​

UnsafeExpr  ::= 'unsafe' Block

unsafe ブロックは不透明型の定義と生ポインタの操作に使用される。 return を使用して型定義を上位スコープに返す。

セマンティクス:

  • unsafe {} 内で型を定義し、生ポインタを操作できる
  • 返された型は unsafe {} の外で使用可能
  • 型のフィールドアクセスには unsafe 権限が必要
yaoxiang
// unsafe ブロック内で不透明型を定義
SqliteDb = unsafe {
    SqliteDb: Type = {
        handle: *Void  // 生ポインタ
    }
    return SqliteDb
}

// SqliteDb は unsafe ブロック外で使用可能
db = sqlite3_open("test.db")

2.15 スコープ ​

基本規則:

  • 各 {} ブロックはスコープを作成する
  • 内側スコープは外側スコープの変数にアクセスできる
  • 外側スコープは内側スコープの変数にアクセスできない
  • 変数宣言は「代入優先」原則に従う
yaoxiang
// ブロックスコープ
{
    x = 10
    // x はこのスコープ内で可視
}
// x はこのスコープ外では不可視

// 関数スコープ
add: (a: Int, b: Int) -> Int = {
    result = a + b
    return result
}
// result は関数の外では不可視

変数宣言とシャドウィング:

  • x = value:スコープチェーンを遡って x を検索し、見つかれば代入、見つからなければ新規宣言
  • mut x = value:明示的な新しい可変宣言、外側と同名は禁止
  • 同じスコープ内で同じ名前を複数回宣言することはできない

詳細定義:スコープの完全な規則、変数宣言、シャドウィングのメカニズムについては モジュールシステム仕様 を参照。


第三章:文 ​

3.1 文の分類 ​

Stmt        ::= LetStmt
              | ExprStmt
              | ReturnStmt
              | BreakStmt
              | ContinueStmt
              | IfStmt
              | MatchStmt
              | WhileStmt
              | ForStmt
              | SpawnStmt

3.2 変数宣言 ​

LetStmt     ::= ('mut')? Identifier (':' TypeExpr)? '=' Expr

3.3 return 文 ​

ReturnStmt  ::= 'return' Expr?

セマンティクス:return は非ローカルな終了であり、最も近い関数境界を終了する(if / while / for / match / 素のブロック / spawn / unsafe を含むあらゆるブロックを貫通する)、値を呼び出し元に渡す。ブロックに「返さない」。

型:return e : Never(e : T)。Never <: T' は任意の T' に対して成立(爆発原理、 型システム §2.2 参照)、したがって return は任意の戻り型の位置に現れることができ、追加の規則制約は不要。

ブロック評価との関係:ブロックの値は常に末尾式である(§2.9 参照)。{ return n } はブロックとして値が n、型 Never;同時に return の作用は関数を終了すること。2つの事象が同時に成立し、爆発原理によって共存する。

return と末尾式は共同で「早期リターン」を成立させ、return が関数を特に指す追加規則は不要である——RFC-010a を参照。

yaoxiang
factorial: (n: Int) -> Int = {
    if n <= 1 {
        return 1          // if を貫通し、関数を終了(型 Never)
    }
    n * factorial(n - 1)  // 末尾式 = ブロックの値
}

3.4 break 文 ​

BreakStmt   ::= 'break'

セマンティクス:含まれている最も内側の while/for ループを直ちに終了し、制御フローをそのループ本体の後へ移動する。

  • 直近の1層のみを出る:break は常にそれを含む最も内側のループに作用する。ネストされたループで複数層を一度に飛び出す必要がある場合は、内側ループを関数として抽出して return で返すか、フラグを使用する(break/continue にはラベルがない;将来ループラベルを導入する場合は、ループ宣言側の構文に従って RFC プロセスを経る。証明パイプラインの多出口設計と一括して決定する)
  • ループ本体内に限定:break は while/for ループ本体内(本体内にネストされたブロック/if/match を含む)にのみ出現でき、ループ外に出現した場合はコンパイルエラー(E1102 'break' outside of a loop)
  • 停止性証明に影響しない:break は停止性の議論に参加しない——測度を提供せず、測度の減少ステップを構成しない;ループの停止義務は break とは無関係であり、精化型によってトリガーされる(型システム §8.4 参照)
  • 借用セマンティクス:break の制御フロー辺は RFC-009a の逆 BFS 活性分析の構造的切断に参加する(飛び出したイテレーションは逆辺活性の導出に参加しない)
yaoxiang
mut i = 0
while i < 10 {
    i = i + 1
    if i == 3 {
        break              // 制御フローがループの後へ移動、i == 3
    }
}

// ネストされたループ:break は内側のみを終了
while j < 3 {
    while k < 10 {
        if k == 2 { break }    // 内側ループのみを終了
    }
    j = j + 1                  // 外側の各イテレーションでここまで実行される
}

3.5 continue 文 ​

ContinueStmt::= 'continue'

セマンティクス:現在のイテレーションの残りの文をスキップし、含まれている最も内側のループの次のラウンドに直接入る—— while は条件の再評価へ戻り、for は次の要素を取り出す。

  • 直近の1層のみに作用:break と同様、ラベルなし
  • ループ本体内に限定:ループ外に出現した場合はコンパイルエラー(E1102)
yaoxiang
mut sum = 0
mut n = 0
while n < 5 {
    n = n + 1
    if n == 3 {
        continue           // 下の累算をスキップ、n == 3 は集計されない
    }
    sum = sum + n
}
// sum == 12(1 + 2 + 4 + 5)

3.6 if 文 ​

IfStmt      ::= 'if' Expr Block ('else' 'if' Expr Block)* ('else' Block)?

3.7 match 文 ​

MatchStmt   ::= 'match' Expr '{' MatchArm+ '}'

3.8 while 文 ​

WhileStmt   ::= 'while' Expr Block

3.9 for 文 ​

ForStmt     ::= 'for' 'mut'? Identifier 'in' Expr Block

3.9.1 セマンティクス:各イテレーションは新しい値をバインドする ​

YaoXiang の for ループセマンティクスは従来の言語とは異なる:各イテレーションは同じ変数を変更するのではなく、新しい値をバインドする。

yaoxiang
// 例:for i in 1..5
for i in 1..5 {
    print(i)
}

実行プロセス:

イテレーションループ変数の動作
1回目新しいバインディング i = 1 を作成、ループ本体実行、1 を出力
2回目新しいバインディング i = 2 を作成(以前のバインディングは破棄済み)、ループ本体実行、2 を出力
3回目新しいバインディング i = 3 を作成、ループ本体実行、3 を出力
4回目新しいバインディング i = 4 を作成、ループ本体実行、4 を出力
終了ループ本体終了、バインディング破棄

重要な点:各イテレーション終了後、そのイテレーションで作成されたバインディングは破棄される。次のイテレーションは完全に新しいバインディングであり、前のイテレーションのバインディングとは一切関係がない。

3.9.2 for と for mut の違い ​

構文ループ変数の可変性説明
for i in 1..5不変ループ本体内でバインディングを変更できない
for mut i in 1..5可変ループ本体内でバインディングを変更できる
yaoxiang
// 合法:各イテレーションで新しい値をバインドするため、変更不要
for i in 1..5 {
    print(i)  // i の値を読み取る
}

// エラー:不変バインディング、変更不可
for i in 1..5 {
    i = i + 1  // エラー:不変バインディングを変更できない
}

// 合法:for mut を使用すればバインディングの変更が許可される
for mut i in 1..5 {
    i = i + 1  // 変更が許可される
}

3.9.3 シャドウィング検査 ​

YaoXiang は変数のシャドウィングを禁止する。for ループ変数は外側スコープの変数と同名にできない:

yaoxiang
// エラー:i は既に外部で宣言されている
i = 10
for i in 1..5 {
    print(i)
}

// 正しい:異なる変数名を使用
i = 10
for j in 1..5 {
    print(j)
}

この規則はすべてのコードブロックに適用される。詳細は 4.3 シャドウィング規則 を参照。

3.9.4 他の言語との比較 ​

言語for ループ変数のセマンティクス
YaoXiang各イテレーションで新しい値をバインド
Rust同じ変数を変更(mut が必要)
Python同じ変数を変更(mut 不要)
C/C++同じ変数を変更(ポインタまたは参照が必要)

設計理由:YaoXiang がバインディングセマンティクスを採用するのは:

  1. より自然なセマンティクスに適合:自然言語では「集合の各要素 x について」は各 x が独立した存在であることを意味する。YaoXiang の for i in 1..5 は「1 から 5 の各 i について」と読まれ、各イテレーションの i は完全に新しいバインディングであり、人類の直感的な理解と一致する。

  2. 偶発的な変更を回避 デフォルトで不変なバインディングセマンティクスは、ループ本体内でループ変数を偶発的に変更できない。複雑なループ本体のどこかで誤って i = ... と書いて追跡困難なバグを引き起こす心配がない。

  3. 高性能ソリューションへの近道 イテレーション間で変数を再利用する必要がある場合(例:アキュムレータ、キャッシュ)、for mut を使用するだけで可変バインディングモードに切り替えられる。これは暗黙の共有状態よりも明確で、構文で明示的に意図を表現し、ランタイム動作に隠すのではない。

3.10 spawn 文 ​

SpawnBlock  ::= '(' Pattern (',' Pattern)* ')' '=' 'spawn' '{' Expr (',' Expr)* '}'
SpawnFor    ::= Identifier '=' 'spawn' 'for' 'mut'? Identifier 'in' Expr '{' Expr '}'
SpawnStmt   ::= SpawnBlock | SpawnFor

spawn ブロック:明示的に並行領域を宣言し、ブロック内の式が並行実行される。

yaoxiang
(result_a, result_b) = spawn {
    parse(fetch("url1")),
    parse(fetch("url2"))
}

spawn ループ:データ並列ループ。

yaoxiang
results = spawn for item in items {
    process(item)
}

spawn ブロックは外側変数をキャプチャする(RFC-024 §2.3、値キャプチャセマンティクス):

  • ブロック本体が外側変数を参照する = Move 値キャプチャ:値は spawn 作成時点でクロージャ環境にスナップショットされ、ブロック本体は env 経由で読み取る(LoadUpvalue)
  • プリミティブ(Int/Float/Bool/Char)は値をコピーし、外側変数は影響を受けない
  • ハンドル型(Struct/String/List など)のスナップショット = ハンドルコピー、基層オブジェクトを共有;Embedded ランタイム(デフォルト)は同一スレッド同一ヒープ、ハンドルは有効
  • 複数タスク間の共有には明示的な ref が必要(§2.13、コンパイラが自動的に Rc/Arc を選択)
  • ブロック内の return が参照する外側変数も同様にキャプチャされる
yaoxiang
t1 = 1 + 1
t2 = 2 + 2
result = spawn {
    return t1 + t2    // t1/t2 値キャプチャ、result == 6
}

3.11 プログラム入口とトップレベル文 ​

ソースファイルはコンパイラの視点では2つの役割を持つ。yaoxiang.toml の有無によって決定される:

役割判定プログラム主体
Script単一ファイル直接実行、yaoxiang.toml なしトップレベル文(記述順に実行);main は通常のバインディング
Binyaoxiang.toml が存在するmain 関数;トップレベルに実行可能文は許されない

Script:トップレベル文がプログラム ​

manifest がない場合、ファイルは「スクリプト」であり、トップレベル文が記述順に実行される:

yaoxiang
use std.io
io.println("hello")          // 直接実行
x: Int = { 42 }              // トップレベルバインディング:ランタイム初期化
io.println(x)                // 42

このモードでは main は特殊ではない——通常のバインディングに過ぎない。main を実行したい場合は明示的に呼び出す必要がある:

yaoxiang
use std.io
main: () -> Void = { io.println("only runs if called") }

main()                       // ← この行を必ず書く

なぜ main を自動呼び出ししないのか? トップレベル文は既にプログラム本体である。ここで暗黙的に main を呼び出すと、明示的に main() と書いたスクリプトは2回実行される。2つの規則は共存できないため、Script では「トップレベル文」のみが実行入口となる。

Bin:main が入口 ​

manifest がある場合、ファイルは「実行可能ターゲット」であり、この場合:

  • main を定義しなければならず、それは関数でなければならない(シグネチャはゼロ引数呼び出し可能でなければならない)
  • トップレベルに実行可能文は許されない——プログラム本体は main である
yaoxiang
main: () -> Void = {
    print("hello")
}

main の欠落や main が関数でない場合はどちらもコンパイルエラー(前者:main がない場合、すべての関数は到達不能;後者:値バインディングは呼び出されない)。

ライブラリファイル:他のファイルから use されるファイル、または [lib].path / [exports] が指すファイルは main を要求しない——プログラム入口ではない。

トップレベルバインディングの初期化 ​

トップレベルバインディングの初期化値はランタイムで評価され、コンパイル時定数である必要はない:

yaoxiang
answer: Int = { 42 }              // ブロック値
inc: (Int) -> Int = (x) => x + 1
computed: Int = inc(41)           // 関数呼び出し

初期化は依存順序で実行され、記述順序とは無関係:

yaoxiang
derived: Int = base * 3           // 後で宣言される base を参照
base: Int = 7                     // 先に初期化(トポロジカルソート)

循環依存はコンパイルエラー(環上の名前が列挙される):

yaoxiang
a: Int = b + 1
b: Int = a + 1                    // エラー:a → b → a

設計根拠:RFC-029f(ファイル役割モデル)、RFC-010a 付録D(ブロックバインディング裁定)。


付録:構文早見表 ​

A.1 制御フロー ​

if Expr Block (else if Expr Block)* (else Block)?
match Expr { MatchArm+ }
while Expr Block
for 'mut'? Identifier 'in' Expr Block
break | continue          // ループ本体内のみ(§3.4 / §3.5)

A.2 エラー処理 ​

Expr '?'              // エラー伝播(Result 型)

A.3 match 構文 ​

match value {
    pattern1 => expr1,
    pattern2 if guard => expr2,
    _ => default_expr,
}