Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ブロックにラベルを付けて指定したブロックから脱出できるようにする #840

Open
takejohn opened this issue Nov 4, 2024 · 30 comments · May be fixed by #859
Open
Assignees
Labels
enhancement New feature or request

Comments

@takejohn
Copy link
Contributor

takejohn commented Nov 4, 2024

#276 (comment) で言及されている、Common Lispでいうreturn-fromのように、ブロックにラベルを付けて指定されたブロックから脱出できる構文を追加したい。

前提issue

以下のissueが解決されることが前提

構文案

  • ラベルを付けたいブロック(eval, if, match, loop, for, each)の前に#のような記号, ラベル名, :を前置する。(#label: eval { ... })
    • 記号を前置するのはreturn文で値とラベルを区別したり解析を楽にするため
    • :はいらないかも(#label eval { ... })?
  • ラベルは変数と別の名前空間(ラベルと変数で同じ名前が使用されてもエラーが出ないよう)にするが、予約語の使用を禁止する。
    • ただし、fn, eval, if, match, loopは例外で、定義はできないが参照はできる。
  • ラベルfn, eval, if, match, loopはそれぞれ最も内側の関数、eval式、if式、match式、ループ構文(loop, for, eachを含む)を指す。
  • ラベルが省略されたreturn vreturn #fn vcontinuecontinue #loopbreakbreak #loopと同等に扱う。
  • return文で脱出されたブロックの評価値はreturn文に指定された値となる。
  • break文で脱出されたブロックの評価値はnullとなる。
  • continueの参照するラベルがループ構文を指していないと文法エラーになる。
#label: loop {
    break #label
}

@f() {
    let a = eval {
        return #eval 1
    }
    return a + 1
}
<: f() // => 2

@g() #label: {
    [1, 3, -2].map(@(v) {
        if v < 0 { return #label null } else { `{v}` }
    })
}
<: g() // => null
@takejohn takejohn self-assigned this Nov 4, 2024
@takejohn takejohn added the enhancement New feature or request label Nov 6, 2024
@takejohn
Copy link
Contributor Author

takejohn commented Nov 8, 2024

関数の中から外へジャンプするのは静的に整合性を解析できないから実装できなさそう

@f() {
    return #label null // どこの#label?
}

#label: eval {
    f()
}

returnは関数から値を返す機能だけにして、関数にもラベルを付けられないようにし、
breakが値を返せるようにしたい

let a = eval {
    break #eval 1
}

繰り返し処理も式にしたら使うことがあるかも?

let a = [1, 3, -2]
let b = each let e, a {
    if (b < 0) break e
}
<: b // => -2

@FineArchs
Copy link
Member

breakでループ式から値を返せるというのは面白いですね。

ラベル付きreturnの静的な整合性解析は、恐らく出来ないこともないとは思います。
大雑把に言えば、関数型の返り値の情報にラベル付きreturnの情報も含め、ラベル付きreturnのある関数を呼び出す関数はその情報を引き継ぐ、というような実装が可能ではないかと考えています。
ただ、それをするのならそのようなreturnはむしろthrowと呼ぶべきですね。

@takejohn
Copy link
Contributor Author

#90 があったわ

@takejohn
Copy link
Contributor Author

takejohn commented Nov 18, 2024

ループ構文を式にして、breakが値を返すようにすると

for 1 break + 1

for 1 {
    break +1
}

なのか

(for 1 { break }) + 1

なのかわからない問題……

@FineArchs
Copy link
Member

今のパーサだと

(for 1 { break }) + 1

で読まれそう?そういう仕様にしちゃってもいいかも知れない

@FineArchs
Copy link
Member

いや先読みするからfor 1 { break +1 }の方になるか

@takejohn
Copy link
Contributor Author

if false 1 + 1

(if false { 1 }) + 1じゃなくてif false { 1 + 1 }だからfor 1 { break +1 }でよさそう

ひょっとするとif falsefor 1を前置演算子として見て、どの演算子よりも弱いレベル(||の下)とするといい感じに実装できたり……?

@takejohn
Copy link
Contributor Author

do break while false
//       ^
// このwhileはdo-while式
// do { break } while false

do break while false break while false
//       ^
// このwhileはbreakが返すwhile式
// do { break (while false { break })} while false 

これはパースが地獄になりそう

@takejohn
Copy link
Contributor Author

もうbreak文の構文は

Break = "break" ["#" IDENT Expr]

にするか
ラベルがついているときは式が必須になる

@FineArchs
Copy link
Member

逆に式を返すならラベル必須にもなる?

@takejohn
Copy link
Contributor Author

takejohn commented Nov 19, 2024

逆に式を返すならラベル必須にもなる?

そうですね

とか考えてたらやっぱりbreakは値を返さないようにしてreturnを値を返すbreakとしても使えるようにするのがいいかもしれない……

@salano-ym
Copy link
Member

do-while自体whileの前置後置が混在して曖昧さが避けにくいので{}を必須にするという手もあると思います。

@takejohn
Copy link
Contributor Author

do-while自体whileの前置後置が混在して曖昧さが避けにくいので{}を必須にするという手もあると思います。

その場合if false 1 + 1なども曖昧性をなくしたいのでparseBlockOrStatementparseBlockに置き換えたいですね
if, match, each, for, do-while, whileに影響が出て動かなくなるコードが多そうだけど文法が分かりやすくなると思います

@takejohn takejohn linked a pull request Nov 20, 2024 that will close this issue
@FineArchs
Copy link
Member

今更で申し訳ないんですが、文法の厳格化はユーザー離れを引き起こすイメージがあるんですよね。なので個人的には慎重になりたい。
スペース区切りの廃止( #402 )にもちょこちょこ悲しみの声が聞こえていて、このまま文解釈の一意化に突っ走っていいのか、パースの優先順位を定めて解決するならそちらの方がいいんじゃないか、という気持ちがあります。

@takejohn
Copy link
Contributor Author

波括弧の必須化やbreakが値を返す構文の実装は見送るか

@FineArchs
Copy link
Member

breakが返す値には括弧必須にするとか…?

@FineArchs
Copy link
Member

FineArchs commented Nov 24, 2024

パースの優先順位を定めて解決するならそちらの方がいいんじゃないか

この視点で言うなら、「breakまたはbreak #labelに続くExprの塊を返す」という仕様に決めてしまって特に小細工せず実装するのがいいです

@salano-ym
Copy link
Member

break #labelに関しては:の有無で区別が付くので構文上の曖昧さは無いと思います。

@salano-ym
Copy link
Member

ただdo-whileについては構文の曖昧さから優先順位だけでは意図しないor分かりにくいエラーになるリスクが高いように思うのでそこの配慮は必要かと思います

@FineArchs
Copy link
Member

FineArchs commented Nov 24, 2024

そういうのはLinterやFormatterを作ってそっちに任せるべきだと思います。
厳格さは好む人と好まない人がいるので

@salano-ym
Copy link
Member

do-whileと式化したwhileの相性が悪さが構文の曖昧さ(≠柔軟さ)の原因になっていて、構文エラーになっても原因が分かりにくく、今後の言語拡張の障害になる可能性もあるのでそのまま実装はしたくないです。

do break #label while cond

と書いたときに表示されるエラーがunexpected token: NewLineだと、構文について詳しくないとどう改善すべきか分かりません。
while式や値を取るbreakは一般的ではないので、これを「breakに渡すwhile式になっていて本体が無いのが原因」と気付くのは難しいと思います。

そういうのはLinterやFormatterを作ってそっちに任せるべきだと思います。

aiscriptの用途的に外部ツールの使用を期待するのがいいとはあまり思いません。

@takejohn
Copy link
Contributor Author

do-whileについては0.19.0になかった構文だし括弧必須化しても影響は大きくない気がする

@FineArchs
Copy link
Member

do-whileについては0.19.0になかった構文だし括弧必須化しても影響は大きくない気がする

確かに

@takejohn
Copy link
Contributor Author

ループ構文を式にするのはやめて、
breakで値を返せるのはもとより式であるevalとif、matchだけで十分かも

do break while false
// do { break } while false

#label: eval { do break #label eval { while false break } while false }
// #label: eval { do { break #label (eval { while false break }) } while false }

@FineArchs
Copy link
Member

evalは覚えてないですが、ifとmatchはそもそもbreakに反応しないはずです(if xx breakのような書き方ができなくて不便になるため)

@takejohn
Copy link
Contributor Author

evalは覚えてないですが、ifとmatchはそもそもbreakに反応しないはずです(if xx breakのような書き方ができなくて不便になるため)

分かりづらくてすみません
もともとevalとif、matchが式であることと、
新しい機能としてこれらの式にbreakで値を返せるようにしたいってことです

@FineArchs
Copy link
Member

そうすると後でループ式も値を返せるようにしたくなった時に

let v = while (true) {
  if cond break value`
}

のようなことができなくなって困ると思います。
値を返せるようにするのであれば別の表現にしたいです。

@takejohn
Copy link
Contributor Author

takejohn commented Dec 11, 2024

そうすると後でループ式も値を返せるようにしたくなった時に

let v = while (true) {
  if cond break value`
}

のようなことができなくなって困ると思います。 値を返せるようにするのであれば別の表現にしたいです。

私の案ではeval式からのbreakはラベルの指定が必須なので、その問題はないと思います

@takejohn
Copy link
Contributor Author

現状の構文案をまとめるとこんな感じです。
考慮漏れなどがあれば指摘お願いいたします。

  • ラベルを付けたいブロック(eval, if, match, loop, while, do-while, for, each)の前に#のような記号, ラベル名, :を前置する。
    例:
    #label: eval { null }
  • ラベルは変数と別の名前空間(ラベルと変数で同じ名前が使用されてもエラーが出ないよう)にするが、予約語の使用を禁止する。
  • ラベルが省略されたcontinue文やbreak文は最も内側の繰り返し処理(loop, while, do-while, for, each)から脱出する。そのような繰り返し処理スコープの中でなければ文法エラーになる。
    例:
    while true { break }
    文法エラーとなる例:
    eval { break } // unlabeled break must be inside for / each / while / do-while / loop
  • ラベルが指定されたcontinue文やbreak文は同じ名前のラベルがついたeval, if, match, loop, while, do-while, for, eachのうち、最も近い(内側の)ものから脱出する。
  • 値が指定されていないbreak文で脱出されたeval, if, matchの評価値はnullとなる。
  • continueの参照するラベルがループ構文を指していないと文法エラーになる。
  • eval, if, matchから脱出するbreak文には値を指定できるようにする。それら以外の構文からのbreak文に値が指定されている場合、文法エラーとする。
    例:
    #label1: eval { break #label1 1 }
    
    #label2: eval {
        while true {
            if true break #label2 1
        }
    }
    文法エラーとなる例:
    while true { break 1 } // break corresponding to statement cannot include value

このissueの説明に書いた以下の内容は実装が複雑になりそうだったので抜きました。

  • ラベルfn, eval, if, match, loopはそれぞれ最も内側の関数、eval式、if式、match式、ループ構文(loop, for, eachを含む)を指す。
  • ラベルが省略されたreturn vreturn #fn vcontinuecontinue #loopbreakbreak #loopと同等に扱う。

@takejohn
Copy link
Contributor Author

追加:

  • #とラベル名の間の空白は禁止される。
    文法エラーとなる例:
    # label: eval { null }
    #label: eval { break # label }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants