構文仕様
本文書では、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 | — | メタ型キーワード |
true | Bool | ブール真値 |
false | Bool | ブール偽値 |
void | Void | Void リテラル(Unit 値)。小文字の void は値リテラル;大文字の Void は型名(§1.4.3 参照)。 |
1.4.2 コンストラクタ式
以下のコンストラクタはパターンマッチングと式の文脈でパーサに識別される:
| コンストラクタ | 所属型 | 説明 |
|---|---|---|
some(T) | Option | Option 値バリアントコンストラクタ |
ok(T) | Result | Result 成功バリアント |
err(E) | Result | Result エラーバリアント |
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()を使用すること:yaoxiangempty = 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
| Lambda2.2 演算子の優先順位
| 優先順位 | 演算子 | 結合性 |
|---|---|---|
| 1 | () [] . ? | 左から右 |
| 2 | as | 左から右 |
| 3 | 単項前置 ! - + | 右から左 |
| 4 | * / % | 左から右 |
| 5 | + - | 左から右 |
| 6 | .. | 左から右 |
| 7 | << >> | 左から右 |
| 8 | & | ^ | 左から右 |
| 9 | == != < > <= >= | 左から右 |
| 10 | and or | 左から右 |
| 11 | if...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 §構築形式)。位置引数は名前付き引数の前に置かなければならない;順序指定された引数は名前付き引数の中で任意の順序に配置できる:
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 '.' Identifier2.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' TypeExpr2.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
| OrPattern2.9 ブロック式
Block ::= '{' Stmt* Expr? '}'文の終端規則:Stmt 間の区切りと改行の挙動(
;による明示的な区切り、改行による終端、継続行の例外、行頭の(/[はマージしない)は RFC-038 で定義される。
2.9.1 { の3つの形態
式位置における { はちょうど3つの解釈を持ち、内容によって一度に判定される:
| 形態 | 記法 | 型 | 例 |
|---|---|---|---|
| 空ブロック | {} | Void | x: Void = {} |
| 辞書リテラル | { "k": v, ... } | Dict(K, V) | d = { "a": 1 } |
| ブロック | { Stmt* Expr? } | 末尾式の型 | y = { 1 + 1 } |
判定順序:
{}(内容なし)→ 空ブロック、値Void- 最初の要素が
String ':' Exprのキー・値ペア → 辞書リテラル - それ以外 → ブロック、値は末尾式によって与えられる
なぜ
{}は空辞書ではないか:空辞書の「空」は自己記述できない(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は直接値を与える
// 通常の {} ブロック:末尾式が値を与える
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 が関数であることを宣言するので、{ ... } は関数本体。
{ ... } を即座に評価したい場合は、ターゲット型を書けばよい(新しい文法は不要):
// ブロック値:即座に評価
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) は「次のパラメータ」ではなく、その関数そのものでなければならない。
// カリー化: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 の型パラメータ形式を含む):
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? ')' '=>' Block2.11 エラー伝播演算子
ErrorPropagate ::= Expr '?'? 演算子は後置演算子であり、優先順位は . と同位。Result(T, E) 型に対して:
Ok(v)の場合は値vを取り出して実行を継続Err(e)の場合はエラーを上位に伝播する(return Err(e))
process: (data: Data) -> Result(Data, Error) = {
validated = validate(data)? // 成功時は値を取り出し、失敗時は上位に伝播
transform(validated)
}2.12 範囲式
RangeExpr ::= Expr '..' Expr ('..' Expr)?.. は範囲値を作成する(Range は一級値であり、構文糖ではない)。
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, 2step セマンティクス:
a..b..cのcはステップ。c = 0のリテラルはコンパイル時に拒否される;動的cはランタイムでゼロチェックされる(E6001 ファミリー;エラーシステム実装後は Result に格上げ)。c < 0は合法、区間方向は符号に応じて反転する(10..0..(-2)は降順)。
2.13 ref 式
RefExpr ::= 'ref' Exprref は共有所有を作成する。コンパイラは自動的に Rc(単一タスク)または Arc(タスク横断)を選択し、ユーザーは実装詳細を気にする必要がない。
data = ref heavy_data
spawn { use(data) } // タスク横断:コンパイラが自動的に Arc を選択2.14 unsafe 式
UnsafeExpr ::= 'unsafe' Blockunsafe ブロックは不透明型の定義と生ポインタの操作に使用される。 return を使用して型定義を上位スコープに返す。
セマンティクス:
unsafe {}内で型を定義し、生ポインタを操作できる- 返された型は
unsafe {}の外で使用可能 - 型のフィールドアクセスには unsafe 権限が必要
// unsafe ブロック内で不透明型を定義
SqliteDb = unsafe {
SqliteDb: Type = {
handle: *Void // 生ポインタ
}
return SqliteDb
}
// SqliteDb は unsafe ブロック外で使用可能
db = sqlite3_open("test.db")2.15 スコープ
基本規則:
- 各
{}ブロックはスコープを作成する - 内側スコープは外側スコープの変数にアクセスできる
- 外側スコープは内側スコープの変数にアクセスできない
- 変数宣言は「代入優先」原則に従う
// ブロックスコープ
{
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
| SpawnStmt3.2 変数宣言
LetStmt ::= ('mut')? Identifier (':' TypeExpr)? '=' Expr3.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 を参照。
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 活性分析の構造的切断に参加する(飛び出したイテレーションは逆辺活性の導出に参加しない)
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)
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 Block3.9 for 文
ForStmt ::= 'for' 'mut'? Identifier 'in' Expr Block3.9.1 セマンティクス:各イテレーションは新しい値をバインドする
YaoXiang の for ループセマンティクスは従来の言語とは異なる:各イテレーションは同じ変数を変更するのではなく、新しい値をバインドする。
// 例: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 | 可変 | ループ本体内でバインディングを変更できる |
// 合法:各イテレーションで新しい値をバインドするため、変更不要
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 ループ変数は外側スコープの変数と同名にできない:
// エラー: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 がバインディングセマンティクスを採用するのは:
より自然なセマンティクスに適合:自然言語では「集合の各要素 x について」は各 x が独立した存在であることを意味する。YaoXiang の
for i in 1..5は「1 から 5 の各 i について」と読まれ、各イテレーションの i は完全に新しいバインディングであり、人類の直感的な理解と一致する。偶発的な変更を回避 デフォルトで不変なバインディングセマンティクスは、ループ本体内でループ変数を偶発的に変更できない。複雑なループ本体のどこかで誤って
i = ...と書いて追跡困難なバグを引き起こす心配がない。高性能ソリューションへの近道 イテレーション間で変数を再利用する必要がある場合(例:アキュムレータ、キャッシュ)、
for mutを使用するだけで可変バインディングモードに切り替えられる。これは暗黙の共有状態よりも明確で、構文で明示的に意図を表現し、ランタイム動作に隠すのではない。
3.10 spawn 文
SpawnBlock ::= '(' Pattern (',' Pattern)* ')' '=' 'spawn' '{' Expr (',' Expr)* '}'
SpawnFor ::= Identifier '=' 'spawn' 'for' 'mut'? Identifier 'in' Expr '{' Expr '}'
SpawnStmt ::= SpawnBlock | SpawnForspawn ブロック:明示的に並行領域を宣言し、ブロック内の式が並行実行される。
(result_a, result_b) = spawn {
parse(fetch("url1")),
parse(fetch("url2"))
}spawn ループ:データ並列ループ。
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が参照する外側変数も同様にキャプチャされる
t1 = 1 + 1
t2 = 2 + 2
result = spawn {
return t1 + t2 // t1/t2 値キャプチャ、result == 6
}3.11 プログラム入口とトップレベル文
ソースファイルはコンパイラの視点では2つの役割を持つ。yaoxiang.toml の有無によって決定される:
| 役割 | 判定 | プログラム主体 |
|---|---|---|
| Script | 単一ファイル直接実行、yaoxiang.toml なし | トップレベル文(記述順に実行);main は通常のバインディング |
| Bin | yaoxiang.toml が存在する | main 関数;トップレベルに実行可能文は許されない |
Script:トップレベル文がプログラム
manifest がない場合、ファイルは「スクリプト」であり、トップレベル文が記述順に実行される:
use std.io
io.println("hello") // 直接実行
x: Int = { 42 } // トップレベルバインディング:ランタイム初期化
io.println(x) // 42このモードでは main は特殊ではない——通常のバインディングに過ぎない。main を実行したい場合は明示的に呼び出す必要がある:
use std.io
main: () -> Void = { io.println("only runs if called") }
main() // ← この行を必ず書くなぜ
mainを自動呼び出ししないのか? トップレベル文は既にプログラム本体である。ここで暗黙的にmainを呼び出すと、明示的にmain()と書いたスクリプトは2回実行される。2つの規則は共存できないため、Script では「トップレベル文」のみが実行入口となる。
Bin:main が入口
manifest がある場合、ファイルは「実行可能ターゲット」であり、この場合:
mainを定義しなければならず、それは関数でなければならない(シグネチャはゼロ引数呼び出し可能でなければならない)- トップレベルに実行可能文は許されない——プログラム本体は
mainである
main: () -> Void = {
print("hello")
}main の欠落や main が関数でない場合はどちらもコンパイルエラー(前者:main がない場合、すべての関数は到達不能;後者:値バインディングは呼び出されない)。
ライブラリファイル:他のファイルから
useされるファイル、または[lib].path/[exports]が指すファイルはmainを要求しない——プログラム入口ではない。
トップレベルバインディングの初期化
トップレベルバインディングの初期化値はランタイムで評価され、コンパイル時定数である必要はない:
answer: Int = { 42 } // ブロック値
inc: (Int) -> Int = (x) => x + 1
computed: Int = inc(41) // 関数呼び出し初期化は依存順序で実行され、記述順序とは無関係:
derived: Int = base * 3 // 後で宣言される base を参照
base: Int = 7 // 先に初期化(トポロジカルソート)循環依存はコンパイルエラー(環上の名前が列挙される):
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,
}